diff options
Diffstat (limited to 'spec/ruby/core')
2558 files changed, 147686 insertions, 0 deletions
diff --git a/spec/ruby/core/argf/argf_spec.rb b/spec/ruby/core/argf/argf_spec.rb new file mode 100644 index 0000000000..af67170b98 --- /dev/null +++ b/spec/ruby/core/argf/argf_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' + +describe "ARGF" do + it "is extended by the Enumerable module" do + ARGF.should be_kind_of(Enumerable) + end + + it "is an instance of ARGF.class" do + ARGF.should be_an_instance_of(ARGF.class) + end +end diff --git a/spec/ruby/core/argf/argv_spec.rb b/spec/ruby/core/argf/argv_spec.rb new file mode 100644 index 0000000000..eab03c450f --- /dev/null +++ b/spec/ruby/core/argf/argv_spec.rb @@ -0,0 +1,19 @@ +require_relative '../../spec_helper' + +describe "ARGF.argv" do + before :each do + @file1 = fixture __FILE__, "file1.txt" + @file2 = fixture __FILE__, "file2.txt" + end + + it "returns ARGV for the initial ARGF" do + ARGF.argv.should equal ARGV + end + + it "returns the remaining arguments to treat" do + argf [@file1, @file2] do + # @file1 is stored in current file + @argf.argv.should == [@file2] + end + end +end diff --git a/spec/ruby/core/argf/binmode_spec.rb b/spec/ruby/core/argf/binmode_spec.rb new file mode 100644 index 0000000000..e083a30a27 --- /dev/null +++ b/spec/ruby/core/argf/binmode_spec.rb @@ -0,0 +1,43 @@ +require_relative '../../spec_helper' + +describe "ARGF.binmode" do + before :each do + @file1 = fixture __FILE__, "file1.txt" + @file2 = fixture __FILE__, "file2.txt" + @bin_file = fixture __FILE__, "bin_file.txt" + end + + it "returns self" do + argf [@bin_file] do + @argf.binmode.should equal @argf + end + end + + platform_is :windows do + it "puts reading into binmode" do + argf [@bin_file, @bin_file] do + @argf.gets.should == "test\n" + @argf.binmode + @argf.gets.should == "test\r\n" + end + end + + it "puts all subsequent streams reading through ARGF into binmode" do + argf [@bin_file, @bin_file] do + @argf.binmode + @argf.gets.should == "test\r\n" + @argf.gets.should == "test\r\n" + end + end + end + + it "sets the file's encoding to BINARY" do + argf [@bin_file, @file1] do + @argf.binmode + @argf.should.binmode? + @argf.gets.encoding.should == Encoding::BINARY + @argf.skip + @argf.read.encoding.should == Encoding::BINARY + end + end +end diff --git a/spec/ruby/core/argf/close_spec.rb b/spec/ruby/core/argf/close_spec.rb new file mode 100644 index 0000000000..d4d6a51e72 --- /dev/null +++ b/spec/ruby/core/argf/close_spec.rb @@ -0,0 +1,35 @@ +require_relative '../../spec_helper' + +describe "ARGF.close" do + before :each do + @file1_name = fixture __FILE__, "file1.txt" + @file2_name = fixture __FILE__, "file2.txt" + end + + it "closes the current open stream" do + argf [@file1_name, @file2_name] do + io = @argf.to_io + @argf.close + io.closed?.should be_true + end + end + + it "returns self" do + argf [@file1_name, @file2_name] do + @argf.close.should equal(@argf) + end + end + + it "doesn't raise an IOError if called on a closed stream" do + argf [@file1_name] do + -> { @argf.close }.should_not raise_error + -> { @argf.close }.should_not raise_error + end + end +end + +describe "ARGF.close" do + it "does not close STDIN" do + ruby_exe("ARGV.replace(['-']); ARGF.close; print ARGF.closed?").should == "false" + end +end diff --git a/spec/ruby/core/argf/closed_spec.rb b/spec/ruby/core/argf/closed_spec.rb new file mode 100644 index 0000000000..e2dd6134e5 --- /dev/null +++ b/spec/ruby/core/argf/closed_spec.rb @@ -0,0 +1,18 @@ +require_relative '../../spec_helper' + +describe "ARGF.closed?" do + before :each do + @file1_name = fixture __FILE__, "file1.txt" + @file2_name = fixture __FILE__, "file2.txt" + end + + it "returns true if the current stream has been closed" do + argf [@file1_name, @file2_name] do + stream = @argf.to_io + stream.close + + @argf.closed?.should be_true + stream.reopen(@argf.filename, 'r') + end + end +end diff --git a/spec/ruby/core/argf/each_byte_spec.rb b/spec/ruby/core/argf/each_byte_spec.rb new file mode 100644 index 0000000000..c5cce9f250 --- /dev/null +++ b/spec/ruby/core/argf/each_byte_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/each_byte' + +describe "ARGF.each_byte" do + it_behaves_like :argf_each_byte, :each_byte +end diff --git a/spec/ruby/core/argf/each_char_spec.rb b/spec/ruby/core/argf/each_char_spec.rb new file mode 100644 index 0000000000..724e5e6e3e --- /dev/null +++ b/spec/ruby/core/argf/each_char_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/each_char' + +describe "ARGF.each_char" do + it_behaves_like :argf_each_char, :each_char +end diff --git a/spec/ruby/core/argf/each_codepoint_spec.rb b/spec/ruby/core/argf/each_codepoint_spec.rb new file mode 100644 index 0000000000..0bf8bf9764 --- /dev/null +++ b/spec/ruby/core/argf/each_codepoint_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/each_codepoint' + +describe "ARGF.each_codepoint" do + it_behaves_like :argf_each_codepoint, :each_codepoint +end diff --git a/spec/ruby/core/argf/each_line_spec.rb b/spec/ruby/core/argf/each_line_spec.rb new file mode 100644 index 0000000000..52a7e5c411 --- /dev/null +++ b/spec/ruby/core/argf/each_line_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/each_line' + +describe "ARGF.each_line" do + it_behaves_like :argf_each_line, :each_line +end diff --git a/spec/ruby/core/argf/each_spec.rb b/spec/ruby/core/argf/each_spec.rb new file mode 100644 index 0000000000..5742ba43bd --- /dev/null +++ b/spec/ruby/core/argf/each_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/each_line' + +describe "ARGF.each" do + it_behaves_like :argf_each_line, :each +end diff --git a/spec/ruby/core/argf/eof_spec.rb b/spec/ruby/core/argf/eof_spec.rb new file mode 100644 index 0000000000..518f6e566e --- /dev/null +++ b/spec/ruby/core/argf/eof_spec.rb @@ -0,0 +1,10 @@ +require_relative '../../spec_helper' +require_relative 'shared/eof' + +describe "ARGF.eof" do + it_behaves_like :argf_eof, :eof +end + +describe "ARGF.eof?" do + it_behaves_like :argf_eof, :eof? +end diff --git a/spec/ruby/core/argf/file_spec.rb b/spec/ruby/core/argf/file_spec.rb new file mode 100644 index 0000000000..df8552d457 --- /dev/null +++ b/spec/ruby/core/argf/file_spec.rb @@ -0,0 +1,21 @@ +require_relative '../../spec_helper' + +describe "ARGF.file" do + before :each do + @file1 = fixture __FILE__, "file1.txt" + @file2 = fixture __FILE__, "file2.txt" + end + + # NOTE: this test assumes that fixtures files have two lines each + it "returns the current file object on each file" do + argf [@file1, @file2] do + result = [] + # returns first current file even when not yet open + result << @argf.file.path + result << @argf.file.path while @argf.gets + # returns last current file even when closed + result << @argf.file.path + result.should == [@file1, @file1, @file1, @file2, @file2, @file2] + end + end +end diff --git a/spec/ruby/core/argf/filename_spec.rb b/spec/ruby/core/argf/filename_spec.rb new file mode 100644 index 0000000000..7c0446269d --- /dev/null +++ b/spec/ruby/core/argf/filename_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/filename' + +describe "ARGF.filename" do + it_behaves_like :argf_filename, :filename +end diff --git a/spec/ruby/core/argf/fileno_spec.rb b/spec/ruby/core/argf/fileno_spec.rb new file mode 100644 index 0000000000..29d50c3582 --- /dev/null +++ b/spec/ruby/core/argf/fileno_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/fileno' + +describe "ARGF.fileno" do + it_behaves_like :argf_fileno, :fileno +end diff --git a/spec/ruby/core/argf/fixtures/bin_file.txt b/spec/ruby/core/argf/fixtures/bin_file.txt new file mode 100644 index 0000000000..2545e9037e --- /dev/null +++ b/spec/ruby/core/argf/fixtures/bin_file.txt @@ -0,0 +1,2 @@ +test
+test
diff --git a/spec/ruby/core/argf/fixtures/file1.txt b/spec/ruby/core/argf/fixtures/file1.txt new file mode 100644 index 0000000000..1c89bfbd82 --- /dev/null +++ b/spec/ruby/core/argf/fixtures/file1.txt @@ -0,0 +1,2 @@ +file1.1 +file1.2 diff --git a/spec/ruby/core/argf/fixtures/file2.txt b/spec/ruby/core/argf/fixtures/file2.txt new file mode 100644 index 0000000000..62e8dba00b --- /dev/null +++ b/spec/ruby/core/argf/fixtures/file2.txt @@ -0,0 +1,2 @@ +line2.1 +line2.2 diff --git a/spec/ruby/core/argf/fixtures/filename.rb b/spec/ruby/core/argf/fixtures/filename.rb new file mode 100644 index 0000000000..599c97dd57 --- /dev/null +++ b/spec/ruby/core/argf/fixtures/filename.rb @@ -0,0 +1,3 @@ +puts $FILENAME while ARGF.gets +# returns last current file even when closed +puts $FILENAME diff --git a/spec/ruby/core/argf/fixtures/lineno.rb b/spec/ruby/core/argf/fixtures/lineno.rb new file mode 100644 index 0000000000..079cc92e8e --- /dev/null +++ b/spec/ruby/core/argf/fixtures/lineno.rb @@ -0,0 +1,5 @@ +puts $. +ARGF.gets +puts $. +ARGF.gets +puts $. diff --git a/spec/ruby/core/argf/fixtures/rewind.rb b/spec/ruby/core/argf/fixtures/rewind.rb new file mode 100644 index 0000000000..90a334cd89 --- /dev/null +++ b/spec/ruby/core/argf/fixtures/rewind.rb @@ -0,0 +1,5 @@ +puts ARGF.lineno +ARGF.gets +puts ARGF.lineno +ARGF.rewind +puts ARGF.lineno diff --git a/spec/ruby/core/argf/fixtures/stdin.txt b/spec/ruby/core/argf/fixtures/stdin.txt new file mode 100644 index 0000000000..063cdcb1d4 --- /dev/null +++ b/spec/ruby/core/argf/fixtures/stdin.txt @@ -0,0 +1,2 @@ +stdin.1 +stdin.2 diff --git a/spec/ruby/core/argf/getc_spec.rb b/spec/ruby/core/argf/getc_spec.rb new file mode 100644 index 0000000000..dc5de9b7df --- /dev/null +++ b/spec/ruby/core/argf/getc_spec.rb @@ -0,0 +1,20 @@ +require_relative '../../spec_helper' +require_relative 'shared/getc' + +describe "ARGF.getc" do + it_behaves_like :argf_getc, :getc +end + +describe "ARGF.getc" do + before :each do + @file1 = fixture __FILE__, "file1.txt" + @file2 = fixture __FILE__, "file2.txt" + end + + it "returns nil when end of stream reached" do + argf [@file1, @file2] do + @argf.read + @argf.getc.should == nil + end + end +end diff --git a/spec/ruby/core/argf/gets_spec.rb b/spec/ruby/core/argf/gets_spec.rb new file mode 100644 index 0000000000..cc7673b190 --- /dev/null +++ b/spec/ruby/core/argf/gets_spec.rb @@ -0,0 +1,49 @@ +require_relative '../../spec_helper' +require_relative 'shared/gets' + +describe "ARGF.gets" do + it_behaves_like :argf_gets, :gets +end + +describe "ARGF.gets" do + it_behaves_like :argf_gets_inplace_edit, :gets +end + +describe "ARGF.gets" do + before :each do + @file1_name = fixture __FILE__, "file1.txt" + @file2_name = fixture __FILE__, "file2.txt" + + @file1 = File.readlines @file1_name + @file2 = File.readlines @file2_name + end + + it "returns nil when reaching end of files" do + argf [@file1_name, @file2_name] do + total = @file1.size + @file2.size + total.times { @argf.gets } + @argf.gets.should == nil + end + end + + before :each do + @external = Encoding.default_external + @internal = Encoding.default_internal + + Encoding.default_external = Encoding::UTF_8 + Encoding.default_internal = nil + end + + after :each do + Encoding.default_external = @external + Encoding.default_internal = @internal + end + + it "reads the contents of the file with default encoding" do + Encoding.default_external = Encoding::US_ASCII + argf [@file1_name, @file2_name] do + @argf.gets.encoding.should == Encoding::US_ASCII + end + end + +end diff --git a/spec/ruby/core/argf/lineno_spec.rb b/spec/ruby/core/argf/lineno_spec.rb new file mode 100644 index 0000000000..72a108c187 --- /dev/null +++ b/spec/ruby/core/argf/lineno_spec.rb @@ -0,0 +1,30 @@ +require_relative '../../spec_helper' + +describe "ARGF.lineno" do + before :each do + @file1 = fixture __FILE__, "file1.txt" + @file2 = fixture __FILE__, "file2.txt" + end + + # NOTE: this test assumes that fixtures files have two lines each + # TODO: break this into four specs + it "returns the current line number on each file" do + argf [@file1, @file2] do + @argf.lineno = 0 + @argf.gets + @argf.lineno.should == 1 + @argf.gets + @argf.lineno.should == 2 + @argf.gets + @argf.lineno.should == 3 + @argf.gets + @argf.lineno.should == 4 + end + end + + it "aliases to $." do + script = fixture __FILE__, "lineno.rb" + out = ruby_exe(script, args: [@file1, @file2]) + out.should == "0\n1\n2\n" + end +end diff --git a/spec/ruby/core/argf/path_spec.rb b/spec/ruby/core/argf/path_spec.rb new file mode 100644 index 0000000000..7120f7d0e3 --- /dev/null +++ b/spec/ruby/core/argf/path_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/filename' + +describe "ARGF.path" do + it_behaves_like :argf_filename, :path +end diff --git a/spec/ruby/core/argf/pos_spec.rb b/spec/ruby/core/argf/pos_spec.rb new file mode 100644 index 0000000000..fb3f25b945 --- /dev/null +++ b/spec/ruby/core/argf/pos_spec.rb @@ -0,0 +1,38 @@ +require_relative '../../spec_helper' +require_relative 'shared/pos' + +describe "ARGF.pos" do + it_behaves_like :argf_pos, :pos +end + +describe "ARGF.pos=" do + before :each do + @file1_name = fixture __FILE__, "file1.txt" + @file2_name = fixture __FILE__, "file2.txt" + + @file1 = File.readlines @file1_name + @file2 = File.readlines @file2_name + end + + # NOTE: this test assumes that fixtures files have two lines each + it "sets the correct position in files" do + argf [@file1_name, @file2_name] do + @argf.pos = @file1.first.size + @argf.gets.should == @file1.last + @argf.pos = 0 + @argf.gets.should == @file1.first + + # finish reading file1 + @argf.gets + + @argf.gets + @argf.pos = 1 + @argf.gets.should == @file2.first[1..-1] + + @argf.pos = @file2.first.size + @file2.last.size - 1 + @argf.gets.should == @file2.last[-1,1] + @argf.pos = 1000 + @argf.read.should == "" + end + end +end diff --git a/spec/ruby/core/argf/read_nonblock_spec.rb b/spec/ruby/core/argf/read_nonblock_spec.rb new file mode 100644 index 0000000000..804a459a62 --- /dev/null +++ b/spec/ruby/core/argf/read_nonblock_spec.rb @@ -0,0 +1,80 @@ +require_relative '../../spec_helper' +require_relative 'shared/read' + +platform_is_not :windows do + describe 'ARGF.read_nonblock' do + it_behaves_like :argf_read, :read_nonblock + + before do + @file1_name = fixture(__FILE__, 'file1.txt') + @file2_name = fixture(__FILE__, 'file2.txt') + + @file1 = File.read(@file1_name) + @file2 = File.read(@file2_name) + + @chunk1 = File.read(@file1_name, 4) + @chunk2 = File.read(@file2_name, 4) + end + + it 'reads up to the given amount of bytes' do + argf [@file1_name] do + @argf.read_nonblock(4).should == @chunk1 + end + end + + describe 'when using multiple files' do + it 'reads up to the given amount of bytes from the first file' do + argf [@file1_name, @file2_name] do + @argf.read_nonblock(4).should == @chunk1 + end + end + + it 'returns an empty String when reading after having read the first file in its entirety' do + argf [@file1_name, @file2_name] do + @argf.read_nonblock(File.size(@file1_name)).should == @file1 + @argf.read_nonblock(4).should == '' + end + end + end + + it 'reads up to the given bytes from STDIN' do + stdin = ruby_exe('print ARGF.read_nonblock(4)', :args => "< #{@file1_name}") + + stdin.should == @chunk1 + end + + it 'reads up to the given bytes from a file when a file and STDIN are present' do + stdin = ruby_exe("print ARGF.read_nonblock(4)", :args => "#{@file1_name} - < #{@file2_name}") + + stdin.should == @chunk1 + end + + context "with STDIN" do + before do + @r, @w = IO.pipe + @stdin = $stdin + $stdin = @r + end + + after do + $stdin = @stdin + @w.close + @r.close unless @r.closed? + end + + it 'raises IO::EAGAINWaitReadable when empty' do + argf ['-'] do + -> { + @argf.read_nonblock(4) + }.should raise_error(IO::EAGAINWaitReadable) + end + end + + it 'returns :wait_readable when the :exception is set to false' do + argf ['-'] do + @argf.read_nonblock(4, nil, exception: false).should == :wait_readable + end + end + end + end +end diff --git a/spec/ruby/core/argf/read_spec.rb b/spec/ruby/core/argf/read_spec.rb new file mode 100644 index 0000000000..bbeef95456 --- /dev/null +++ b/spec/ruby/core/argf/read_spec.rb @@ -0,0 +1,85 @@ +require_relative '../../spec_helper' +require_relative 'shared/read' + +describe "ARGF.read" do + it_behaves_like :argf_read, :read + + before :each do + @file1_name = fixture __FILE__, "file1.txt" + @file2_name = fixture __FILE__, "file2.txt" + @stdin_name = fixture __FILE__, "stdin.txt" + + @file1 = File.read @file1_name + @file2 = File.read @file2_name + @stdin = File.read @stdin_name + end + + it "reads the contents of a file" do + argf [@file1_name] do + @argf.read().should == @file1 + end + end + + it "treats first nil argument as no length limit" do + argf [@file1_name] do + @argf.read(nil).should == @file1 + end + end + + it "reads the contents of two files" do + argf [@file1_name, @file2_name] do + @argf.read.should == @file1 + @file2 + end + end + + it "reads the contents of one file and some characters from the second" do + argf [@file1_name, @file2_name] do + len = @file1.size + (@file2.size / 2) + @argf.read(len).should == (@file1 + @file2)[0,len] + end + end + + it "reads across two files consecutively" do + argf [@file1_name, @file2_name] do + @argf.read(@file1.size - 2).should == @file1[0..-3] + @argf.read(2+5).should == @file1[-2..-1] + @file2[0,5] + end + end + + it "reads the contents of stdin" do + stdin = ruby_exe("print ARGF.read", args: "< #{@stdin_name}") + stdin.should == @stdin + end + + it "reads the contents of one file and stdin" do + stdin = ruby_exe("print ARGF.read", args: "#{@file1_name} - < #{@stdin_name}") + stdin.should == @file1 + @stdin + end + + it "reads the contents of the same file twice" do + argf [@file1_name, @file1_name] do + @argf.read.should == @file1 + @file1 + end + end + + + before :each do + @external = Encoding.default_external + @internal = Encoding.default_internal + + Encoding.default_external = Encoding::UTF_8 + Encoding.default_internal = nil + end + + after :each do + Encoding.default_external = @external + Encoding.default_internal = @internal + end + + it "reads the contents of the file with default encoding" do + Encoding.default_external = Encoding::US_ASCII + argf [@file1_name, @file2_name] do + @argf.read.encoding.should == Encoding::US_ASCII + end + end +end diff --git a/spec/ruby/core/argf/readchar_spec.rb b/spec/ruby/core/argf/readchar_spec.rb new file mode 100644 index 0000000000..4eca2efcf7 --- /dev/null +++ b/spec/ruby/core/argf/readchar_spec.rb @@ -0,0 +1,19 @@ +require_relative '../../spec_helper' +require_relative 'shared/getc' + +describe "ARGF.getc" do + it_behaves_like :argf_getc, :readchar +end + +describe "ARGF.readchar" do + before :each do + @file1 = fixture __FILE__, "file1.txt" + @file2 = fixture __FILE__, "file2.txt" + end + + it "raises EOFError when end of stream reached" do + argf [@file1, @file2] do + -> { while @argf.readchar; end }.should raise_error(EOFError) + end + end +end diff --git a/spec/ruby/core/argf/readline_spec.rb b/spec/ruby/core/argf/readline_spec.rb new file mode 100644 index 0000000000..db53c499e9 --- /dev/null +++ b/spec/ruby/core/argf/readline_spec.rb @@ -0,0 +1,23 @@ +require_relative '../../spec_helper' +require_relative 'shared/gets' + +describe "ARGF.readline" do + it_behaves_like :argf_gets, :readline +end + +describe "ARGF.readline" do + it_behaves_like :argf_gets_inplace_edit, :readline +end + +describe "ARGF.readline" do + before :each do + @file1 = fixture __FILE__, "file1.txt" + @file2 = fixture __FILE__, "file2.txt" + end + + it "raises an EOFError when reaching end of files" do + argf [@file1, @file2] do + -> { while @argf.readline; end }.should raise_error(EOFError) + end + end +end diff --git a/spec/ruby/core/argf/readlines_spec.rb b/spec/ruby/core/argf/readlines_spec.rb new file mode 100644 index 0000000000..30be936dab --- /dev/null +++ b/spec/ruby/core/argf/readlines_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/readlines' + +describe "ARGF.readlines" do + it_behaves_like :argf_readlines, :readlines +end diff --git a/spec/ruby/core/argf/readpartial_spec.rb b/spec/ruby/core/argf/readpartial_spec.rb new file mode 100644 index 0000000000..ea4301f25c --- /dev/null +++ b/spec/ruby/core/argf/readpartial_spec.rb @@ -0,0 +1,75 @@ +require_relative '../../spec_helper' +require_relative 'shared/read' + +describe "ARGF.readpartial" do + it_behaves_like :argf_read, :readpartial + + before :each do + @file1_name = fixture __FILE__, "file1.txt" + @file2_name = fixture __FILE__, "file2.txt" + @stdin_name = fixture __FILE__, "stdin.txt" + + @file1 = File.read @file1_name + @file2 = File.read @file2_name + @stdin = File.read @stdin_name + end + + it "raises an ArgumentError if called without a maximum read length" do + argf [@file1_name] do + -> { @argf.readpartial }.should raise_error(ArgumentError) + end + end + + it "reads maximum number of bytes from one file at a time" do + argf [@file1_name, @file2_name] do + len = @file1.size + @file2.size + @argf.readpartial(len).should == @file1 + end + end + + it "clears output buffer even if EOFError is raised because @argf is at end" do + begin + output = +"to be cleared" + + argf [@file1_name] do + @argf.read + @argf.readpartial(1, output) + end + rescue EOFError + output.should == "" + end + end + + it "reads maximum number of bytes from one file at a time" do + argf [@file1_name, @file2_name] do + len = @file1.size + @file2.size + @argf.readpartial(len).should == @file1 + end + end + + it "returns an empty string if EOFError is raised while reading any but the last file" do + argf [@file1_name, @file2_name] do + @argf.readpartial(@file1.size) + @argf.readpartial(1).should == "" + end + end + + it "raises an EOFError if the exception was raised while reading the last file" do + argf [@file1_name, @file2_name] do + @argf.readpartial(@file1.size) + @argf.readpartial(1) + @argf.readpartial(@file2.size) + -> { @argf.readpartial(1) }.should raise_error(EOFError) + -> { @argf.readpartial(1) }.should raise_error(EOFError) + end + end + + it "raises an EOFError if the exception was raised while reading STDIN" do + ruby_str = <<-STR + print ARGF.readpartial(#{@stdin.size}) + ARGF.readpartial(1) rescue print $!.class + STR + stdin = ruby_exe(ruby_str, args: "< #{@stdin_name}") + stdin.should == @stdin + "EOFError" + end +end diff --git a/spec/ruby/core/argf/rewind_spec.rb b/spec/ruby/core/argf/rewind_spec.rb new file mode 100644 index 0000000000..b29f0b75b7 --- /dev/null +++ b/spec/ruby/core/argf/rewind_spec.rb @@ -0,0 +1,39 @@ +require_relative '../../spec_helper' + +describe "ARGF.rewind" do + before :each do + @file1_name = fixture __FILE__, "file1.txt" + @file2_name = fixture __FILE__, "file2.txt" + + @file1 = File.readlines @file1_name + @file2 = File.readlines @file2_name + end + + # NOTE: this test assumes that fixtures files have two lines each + it "goes back to beginning of current file" do + argf [@file1_name, @file2_name] do + @argf.gets + @argf.rewind + @argf.gets.should == @file1.first + + @argf.gets # finish reading file1 + + @argf.gets + @argf.rewind + @argf.gets.should == @file2.first + end + end + + it "resets ARGF.lineno to 0" do + script = fixture __FILE__, "rewind.rb" + out = ruby_exe(script, args: [@file1_name, @file2_name]) + out.should == "0\n1\n0\n" + end + + it "raises an ArgumentError when end of stream reached" do + argf [@file1_name, @file2_name] do + @argf.read + -> { @argf.rewind }.should raise_error(ArgumentError) + end + end +end diff --git a/spec/ruby/core/argf/seek_spec.rb b/spec/ruby/core/argf/seek_spec.rb new file mode 100644 index 0000000000..2b73bd46fb --- /dev/null +++ b/spec/ruby/core/argf/seek_spec.rb @@ -0,0 +1,63 @@ +require_relative '../../spec_helper' + +describe "ARGF.seek" do + before :each do + @file1_name = fixture __FILE__, "file1.txt" + @file2_name = fixture __FILE__, "file2.txt" + + @file1 = File.readlines @file1_name + @file2 = File.readlines @file2_name + end + + it "sets the absolute position relative to beginning of file" do + argf [@file1_name, @file2_name] do + @argf.seek 2 + @argf.gets.should == @file1.first[2..-1] + @argf.seek @file1.first.size + @argf.gets.should == @file1.last + @argf.seek 0, IO::SEEK_END + @argf.gets.should == @file2.first + end + end + + it "sets the position relative to current position in file" do + argf [@file1_name, @file2_name] do + @argf.seek(0, IO::SEEK_CUR) + @argf.gets.should == @file1.first + @argf.seek(-@file1.first.size+2, IO::SEEK_CUR) + @argf.gets.should == @file1.first[2..-1] + @argf.seek(1, IO::SEEK_CUR) + @argf.gets.should == @file1.last[1..-1] + @argf.seek(3, IO::SEEK_CUR) + @argf.gets.should == @file2.first + @argf.seek(@file1.last.size, IO::SEEK_CUR) + @argf.gets.should == nil + end + end + + it "sets the absolute position relative to end of file" do + argf [@file1_name, @file2_name] do + @argf.seek(-@file1.first.size-@file1.last.size, IO::SEEK_END) + @argf.gets.should == @file1.first + @argf.seek(-6, IO::SEEK_END) + @argf.gets.should == @file1.last[-6..-1] + @argf.seek(-4, IO::SEEK_END) + @argf.gets.should == @file1.last[4..-1] + @argf.gets.should == @file2.first + @argf.seek(-6, IO::SEEK_END) + @argf.gets.should == @file2.last[-6..-1] + end + end +end + +describe "ARGF.seek" do + before :each do + @file1_name = fixture __FILE__, "file1.txt" + end + + it "takes at least one argument (offset)" do + argf [@file1_name] do + -> { @argf.seek }.should raise_error(ArgumentError) + end + end +end diff --git a/spec/ruby/core/argf/set_encoding_spec.rb b/spec/ruby/core/argf/set_encoding_spec.rb new file mode 100644 index 0000000000..a871e084b6 --- /dev/null +++ b/spec/ruby/core/argf/set_encoding_spec.rb @@ -0,0 +1,41 @@ +require_relative '../../spec_helper' + +describe "ARGF.set_encoding" do + before :each do + @file = fixture __FILE__, "file1.txt" + end + + it "sets the external encoding when passed an encoding instance" do + argf [@file] do + @argf.set_encoding(Encoding::US_ASCII) + @argf.external_encoding.should == Encoding::US_ASCII + @argf.gets.encoding.should == Encoding::US_ASCII + end + end + + it "sets the external encoding when passed an encoding name" do + argf [@file] do + @argf.set_encoding("us-ascii") + @argf.external_encoding.should == Encoding::US_ASCII + @argf.gets.encoding.should == Encoding::US_ASCII + end + end + + it "sets the external, internal encoding when passed two encoding instances" do + argf [@file] do + @argf.set_encoding(Encoding::US_ASCII, Encoding::EUC_JP) + @argf.external_encoding.should == Encoding::US_ASCII + @argf.internal_encoding.should == Encoding::EUC_JP + @argf.gets.encoding.should == Encoding::EUC_JP + end + end + + it "sets the external, internal encoding when passed 'ext:int' String" do + argf [@file] do + @argf.set_encoding("us-ascii:euc-jp") + @argf.external_encoding.should == Encoding::US_ASCII + @argf.internal_encoding.should == Encoding::EUC_JP + @argf.gets.encoding.should == Encoding::EUC_JP + end + end +end diff --git a/spec/ruby/core/argf/shared/each_byte.rb b/spec/ruby/core/argf/shared/each_byte.rb new file mode 100644 index 0000000000..6b1dc1dae2 --- /dev/null +++ b/spec/ruby/core/argf/shared/each_byte.rb @@ -0,0 +1,58 @@ +describe :argf_each_byte, shared: true do + before :each do + @file1_name = fixture __FILE__, "file1.txt" + @file2_name = fixture __FILE__, "file2.txt" + + @bytes = [] + File.read(@file1_name).each_byte { |b| @bytes << b } + File.read(@file2_name).each_byte { |b| @bytes << b } + end + + it "yields each byte of all streams to the passed block" do + argf [@file1_name, @file2_name] do + bytes = [] + @argf.send(@method) { |b| bytes << b } + bytes.should == @bytes + end + end + + it "returns self when passed a block" do + argf [@file1_name, @file2_name] do + @argf.send(@method) {}.should equal(@argf) + end + end + + it "returns an Enumerator when passed no block" do + argf [@file1_name, @file2_name] do + enum = @argf.send(@method) + enum.should be_an_instance_of(Enumerator) + + bytes = [] + enum.each { |b| bytes << b } + bytes.should == @bytes + end + end + + describe "when no block is given" do + it "returns an Enumerator" do + argf [@file1_name, @file2_name] do + enum = @argf.send(@method) + enum.should be_an_instance_of(Enumerator) + + bytes = [] + enum.each { |b| bytes << b } + bytes.should == @bytes + end + end + + describe "returned Enumerator" do + describe "size" do + it "should return nil" do + argf [@file1_name, @file2_name] do + @argf.send(@method).size.should == nil + end + end + end + end + end +end diff --git a/spec/ruby/core/argf/shared/each_char.rb b/spec/ruby/core/argf/shared/each_char.rb new file mode 100644 index 0000000000..9e333ecc5b --- /dev/null +++ b/spec/ruby/core/argf/shared/each_char.rb @@ -0,0 +1,58 @@ +describe :argf_each_char, shared: true do + before :each do + @file1_name = fixture __FILE__, "file1.txt" + @file2_name = fixture __FILE__, "file2.txt" + + @chars = [] + File.read(@file1_name).each_char { |c| @chars << c } + File.read(@file2_name).each_char { |c| @chars << c } + end + + it "yields each char of all streams to the passed block" do + argf [@file1_name, @file2_name] do + chars = [] + @argf.send(@method) { |c| chars << c } + chars.should == @chars + end + end + + it "returns self when passed a block" do + argf [@file1_name, @file2_name] do + @argf.send(@method) {}.should equal(@argf) + end + end + + it "returns an Enumerator when passed no block" do + argf [@file1_name, @file2_name] do + enum = @argf.send(@method) + enum.should be_an_instance_of(Enumerator) + + chars = [] + enum.each { |c| chars << c } + chars.should == @chars + end + end + + describe "when no block is given" do + it "returns an Enumerator" do + argf [@file1_name, @file2_name] do + enum = @argf.send(@method) + enum.should be_an_instance_of(Enumerator) + + chars = [] + enum.each { |c| chars << c } + chars.should == @chars + end + end + + describe "returned Enumerator" do + describe "size" do + it "should return nil" do + argf [@file1_name, @file2_name] do + @argf.send(@method).size.should == nil + end + end + end + end + end +end diff --git a/spec/ruby/core/argf/shared/each_codepoint.rb b/spec/ruby/core/argf/shared/each_codepoint.rb new file mode 100644 index 0000000000..e2a2dfff46 --- /dev/null +++ b/spec/ruby/core/argf/shared/each_codepoint.rb @@ -0,0 +1,58 @@ +describe :argf_each_codepoint, shared: true do + before :each do + file1_name = fixture __FILE__, "file1.txt" + file2_name = fixture __FILE__, "file2.txt" + @filenames = [file1_name, file2_name] + + @codepoints = File.read(file1_name).codepoints + @codepoints.concat File.read(file2_name).codepoints + end + + it "is a public method" do + argf @filenames do + @argf.public_methods(false).should include(@method) + end + end + + it "does not require arguments" do + argf @filenames do + @argf.method(@method).arity.should == 0 + end + end + + it "returns self when passed a block" do + argf @filenames do + @argf.send(@method) {}.should equal(@argf) + end + end + + it "returns an Enumerator when passed no block" do + argf @filenames do + @argf.send(@method).should be_an_instance_of(Enumerator) + end + end + + it "yields each codepoint of all streams" do + argf @filenames do + @argf.send(@method).to_a.should == @codepoints + end + end + + describe "when no block is given" do + it "returns an Enumerator" do + argf @filenames do + @argf.send(@method).should be_an_instance_of(Enumerator) + end + end + + describe "returned Enumerator" do + describe "size" do + it "should return nil" do + argf @filenames do + @argf.send(@method).size.should == nil + end + end + end + end + end +end diff --git a/spec/ruby/core/argf/shared/each_line.rb b/spec/ruby/core/argf/shared/each_line.rb new file mode 100644 index 0000000000..c0ef77dc54 --- /dev/null +++ b/spec/ruby/core/argf/shared/each_line.rb @@ -0,0 +1,62 @@ +describe :argf_each_line, shared: true do + before :each do + @file1_name = fixture __FILE__, "file1.txt" + @file2_name = fixture __FILE__, "file2.txt" + + @lines = File.readlines @file1_name + @lines += File.readlines @file2_name + end + + it "is a public method" do + argf [@file1_name, @file2_name] do + @argf.public_methods(false).should include(@method) + end + end + + it "requires multiple arguments" do + argf [@file1_name, @file2_name] do + @argf.method(@method).arity.should < 0 + end + end + + it "reads each line of files" do + argf [@file1_name, @file2_name] do + lines = [] + @argf.send(@method) { |b| lines << b } + lines.should == @lines + end + end + + it "returns self when passed a block" do + argf [@file1_name, @file2_name] do + @argf.send(@method) {}.should equal(@argf) + end + end + + describe "with a separator" do + it "yields each separated section of all streams" do + argf [@file1_name, @file2_name] do + @argf.send(@method, '.').to_a.should == + (File.readlines(@file1_name, '.') + File.readlines(@file2_name, '.')) + end + end + end + + describe "when no block is given" do + it "returns an Enumerator" do + argf [@file1_name, @file2_name] do + @argf.send(@method).should be_an_instance_of(Enumerator) + end + end + + describe "returned Enumerator" do + describe "size" do + it "should return nil" do + argf [@file1_name, @file2_name] do + @argf.send(@method).size.should == nil + end + end + end + end + end +end diff --git a/spec/ruby/core/argf/shared/eof.rb b/spec/ruby/core/argf/shared/eof.rb new file mode 100644 index 0000000000..0e684f943f --- /dev/null +++ b/spec/ruby/core/argf/shared/eof.rb @@ -0,0 +1,24 @@ +describe :argf_eof, shared: true do + before :each do + @file1 = fixture __FILE__, "file1.txt" + @file2 = fixture __FILE__, "file2.txt" + end + + # NOTE: this test assumes that fixtures files have two lines each + it "returns true when reaching the end of a file" do + argf [@file1, @file2] do + result = [] + while @argf.gets + result << @argf.send(@method) + end + result.should == [false, true, false, true] + end + end + + it "raises IOError when called on a closed stream" do + argf [@file1] do + @argf.read + -> { @argf.send(@method) }.should raise_error(IOError) + end + end +end diff --git a/spec/ruby/core/argf/shared/filename.rb b/spec/ruby/core/argf/shared/filename.rb new file mode 100644 index 0000000000..f47c673dc0 --- /dev/null +++ b/spec/ruby/core/argf/shared/filename.rb @@ -0,0 +1,28 @@ +describe :argf_filename, shared: true do + before :each do + @file1 = fixture __FILE__, "file1.txt" + @file2 = fixture __FILE__, "file2.txt" + end + + # NOTE: this test assumes that fixtures files have two lines each + it "returns the current file name on each file" do + argf [@file1, @file2] do + result = [] + # returns first current file even when not yet open + result << @argf.send(@method) + result << @argf.send(@method) while @argf.gets + # returns last current file even when closed + result << @argf.send(@method) + + result.map! { |f| File.expand_path(f) } + result.should == [@file1, @file1, @file1, @file2, @file2, @file2] + end + end + + # NOTE: this test assumes that fixtures files have two lines each + it "sets the $FILENAME global variable with the current file name on each file" do + script = fixture __FILE__, "filename.rb" + out = ruby_exe(script, args: [@file1, @file2]) + out.should == "#{@file1}\n#{@file1}\n#{@file2}\n#{@file2}\n#{@file2}\n" + end +end diff --git a/spec/ruby/core/argf/shared/fileno.rb b/spec/ruby/core/argf/shared/fileno.rb new file mode 100644 index 0000000000..4350d43747 --- /dev/null +++ b/spec/ruby/core/argf/shared/fileno.rb @@ -0,0 +1,24 @@ +describe :argf_fileno, shared: true do + before :each do + @file1 = fixture __FILE__, "file1.txt" + @file2 = fixture __FILE__, "file2.txt" + end + + # NOTE: this test assumes that fixtures files have two lines each + it "returns the current file number on each file" do + argf [@file1, @file2] do + result = [] + # returns first current file even when not yet open + result << @argf.send(@method) while @argf.gets + # returns last current file even when closed + result.map { |d| d.class }.should == [Integer, Integer, Integer, Integer] + end + end + + it "raises an ArgumentError when called on a closed stream" do + argf [@file1] do + @argf.read + -> { @argf.send(@method) }.should raise_error(ArgumentError) + end + end +end diff --git a/spec/ruby/core/argf/shared/getc.rb b/spec/ruby/core/argf/shared/getc.rb new file mode 100644 index 0000000000..d63372d9d7 --- /dev/null +++ b/spec/ruby/core/argf/shared/getc.rb @@ -0,0 +1,17 @@ +describe :argf_getc, shared: true do + before :each do + @file1 = fixture __FILE__, "file1.txt" + @file2 = fixture __FILE__, "file2.txt" + + @chars = File.read @file1 + @chars += File.read @file2 + end + + it "reads each char of files" do + argf [@file1, @file2] do + chars = +"" + @chars.size.times { chars << @argf.send(@method) } + chars.should == @chars + end + end +end diff --git a/spec/ruby/core/argf/shared/gets.rb b/spec/ruby/core/argf/shared/gets.rb new file mode 100644 index 0000000000..160d24c27b --- /dev/null +++ b/spec/ruby/core/argf/shared/gets.rb @@ -0,0 +1,99 @@ +describe :argf_gets, shared: true do + before :each do + @file1_name = fixture __FILE__, "file1.txt" + @file2_name = fixture __FILE__, "file2.txt" + @stdin_name = fixture __FILE__, "stdin.txt" + + @file1 = File.readlines @file1_name + @file2 = File.readlines @file2_name + @stdin = File.read @stdin_name + end + + it "reads one line of a file" do + argf [@file1_name] do + @argf.send(@method).should == @file1.first + end + end + + it "reads all lines of a file" do + argf [@file1_name] do + lines = [] + @file1.size.times { lines << @argf.send(@method) } + lines.should == @file1 + end + end + + it "reads all lines of stdin" do + total = @stdin.count $/ + stdin = ruby_exe( + "#{total}.times { print ARGF.send(#{@method.inspect}) }", + args: "< #{@stdin_name}") + stdin.should == @stdin + end + + it "reads all lines of two files" do + argf [@file1_name, @file2_name] do + total = @file1.size + @file2.size + lines = [] + total.times { lines << @argf.send(@method) } + lines.should == @file1 + @file2 + end + end + + it "sets $_ global variable with each line read" do + argf [@file1_name, @file2_name] do + total = @file1.size + @file2.size + total.times do + line = @argf.send(@method) + $_.should == line + end + end + end +end + +describe :argf_gets_inplace_edit, shared: true do + before :each do + @file1_name = fixture __FILE__, "file1.txt" + @file2_name = fixture __FILE__, "file2.txt" + + @tmp1_name = tmp "file1.txt" + @tmp2_name = tmp "file2.txt" + + @tmp1_name_bak = @tmp1_name + ".bak" + @tmp2_name_bak = @tmp2_name + ".bak" + + cp @file1_name, @tmp1_name + cp @file2_name, @tmp2_name + + method = "ARGF.send(#{@method.inspect})" + @code = "begin while line = #{method} do puts 'x' end rescue EOFError; end" + end + + after :each do + rm_r @tmp1_name, @tmp2_name, @tmp1_name_bak, @tmp2_name_bak + end + + # -i with no backup extension is not supported on Windows + platform_is_not :windows do + it "modifies the files when in place edit mode is on" do + ruby_exe(@code, + options: "-i", + args: "#{@tmp1_name} #{@tmp2_name}") + + File.read(@tmp1_name).should == "x\nx\n" + File.read(@tmp2_name).should == "x\nx\n" + end + end + + it "modifies and backups two files when in place edit mode is on" do + ruby_exe(@code, + options: "-i.bak", + args: "#{@tmp1_name} #{@tmp2_name}") + + File.read(@tmp1_name).should == "x\nx\n" + File.read(@tmp2_name).should == "x\nx\n" + + File.read(@tmp1_name_bak).should == "file1.1\nfile1.2\n" + File.read(@tmp2_name_bak).should == "line2.1\nline2.2\n" + end +end diff --git a/spec/ruby/core/argf/shared/pos.rb b/spec/ruby/core/argf/shared/pos.rb new file mode 100644 index 0000000000..9836d5f1e4 --- /dev/null +++ b/spec/ruby/core/argf/shared/pos.rb @@ -0,0 +1,31 @@ +describe :argf_pos, shared: true do + before :each do + @file1 = fixture __FILE__, "file1.txt" + @file2 = fixture __FILE__, "file2.txt" + end + + it "gives the correct position for each read operation" do + argf [@file1, @file2] do + size1 = File.size(@file1) + size2 = File.size(@file2) + + @argf.read(2) + @argf.send(@method).should == 2 + @argf.read(size1-2) + @argf.send(@method).should == size1 + @argf.read(6) + @argf.send(@method).should == 6 + @argf.rewind + @argf.send(@method).should == 0 + @argf.read(size2) + @argf.send(@method).should == size2 + end + end + + it "raises an ArgumentError when called on a closed stream" do + argf [@file1] do + @argf.read + -> { @argf.send(@method) }.should raise_error(ArgumentError) + end + end +end diff --git a/spec/ruby/core/argf/shared/read.rb b/spec/ruby/core/argf/shared/read.rb new file mode 100644 index 0000000000..e76d022139 --- /dev/null +++ b/spec/ruby/core/argf/shared/read.rb @@ -0,0 +1,58 @@ +describe :argf_read, shared: true do + before :each do + @file1_name = fixture __FILE__, "file1.txt" + @stdin_name = fixture __FILE__, "stdin.txt" + + @file1 = File.read @file1_name + @stdin = File.read @stdin_name + end + + it "treats second nil argument as no output buffer" do + argf [@file1_name] do + @argf.send(@method, @file1.size, nil).should == @file1 + end + end + + it "treats second argument as an output buffer" do + argf [@file1_name] do + buffer = +"" + @argf.send(@method, @file1.size, buffer) + buffer.should == @file1 + end + end + + it "clears output buffer before appending to it" do + argf [@file1_name] do + buffer = +"to be cleared" + @argf.send(@method, @file1.size, buffer) + buffer.should == @file1 + end + end + + it "reads a number of bytes from the first file" do + argf [@file1_name] do + @argf.send(@method, 5).should == @file1[0, 5] + end + end + + it "reads from a single file consecutively" do + argf [@file1_name] do + @argf.send(@method, 1).should == @file1[0, 1] + @argf.send(@method, 2).should == @file1[1, 2] + @argf.send(@method, 3).should == @file1[3, 3] + end + end + + it "reads a number of bytes from stdin" do + stdin = ruby_exe("print ARGF.#{@method}(10)", :args => "< #{@stdin_name}") + stdin.should == @stdin[0, 10] + end + + platform_is_not :windows do + it "reads the contents of a special device file" do + argf ['/dev/zero'] do + @argf.send(@method, 100).should == "\000" * 100 + end + end + end +end diff --git a/spec/ruby/core/argf/shared/readlines.rb b/spec/ruby/core/argf/shared/readlines.rb new file mode 100644 index 0000000000..505fa94acb --- /dev/null +++ b/spec/ruby/core/argf/shared/readlines.rb @@ -0,0 +1,22 @@ +describe :argf_readlines, shared: true do + before :each do + @file1 = fixture __FILE__, "file1.txt" + @file2 = fixture __FILE__, "file2.txt" + + @lines = File.readlines(@file1) + @lines += File.readlines(@file2) + end + + it "reads all lines of all files" do + argf [@file1, @file2] do + @argf.send(@method).should == @lines + end + end + + it "returns an empty Array when end of stream reached" do + argf [@file1, @file2] do + @argf.read + @argf.send(@method).should == [] + end + end +end diff --git a/spec/ruby/core/argf/skip_spec.rb b/spec/ruby/core/argf/skip_spec.rb new file mode 100644 index 0000000000..0181801c2d --- /dev/null +++ b/spec/ruby/core/argf/skip_spec.rb @@ -0,0 +1,42 @@ +require_relative '../../spec_helper' + +describe "ARGF.skip" do + before :each do + @file1_name = fixture __FILE__, "file1.txt" + @file2_name = fixture __FILE__, "file2.txt" + + @file2 = File.readlines @file2_name + end + + it "skips the current file" do + argf [@file1_name, @file2_name] do + @argf.read(1) + @argf.skip + @argf.gets.should == @file2.first + end + end + + it "has no effect when called twice in a row" do + argf [@file1_name, @file2_name] do + @argf.read(1) + @argf.skip + @argf.skip + @argf.gets.should == @file2.first + end + end + + it "has no effect at end of stream" do + argf [@file1_name, @file2_name] do + @argf.read + @argf.skip + @argf.gets.should == nil + end + end + + # This bypasses argf helper because the helper will call argf.file + # which as a side-effect calls argf.file which will initialize + # internals of ARGF enough for this to work. + it "has no effect when nothing has been processed yet" do + -> { ARGF.class.new(@file1_name).skip }.should_not raise_error + end +end diff --git a/spec/ruby/core/argf/tell_spec.rb b/spec/ruby/core/argf/tell_spec.rb new file mode 100644 index 0000000000..16d9f29920 --- /dev/null +++ b/spec/ruby/core/argf/tell_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/pos' + +describe "ARGF.tell" do + it_behaves_like :argf_pos, :tell +end diff --git a/spec/ruby/core/argf/to_a_spec.rb b/spec/ruby/core/argf/to_a_spec.rb new file mode 100644 index 0000000000..b17a93db33 --- /dev/null +++ b/spec/ruby/core/argf/to_a_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/readlines' + +describe "ARGF.to_a" do + it_behaves_like :argf_readlines, :to_a +end diff --git a/spec/ruby/core/argf/to_i_spec.rb b/spec/ruby/core/argf/to_i_spec.rb new file mode 100644 index 0000000000..2183de6cd4 --- /dev/null +++ b/spec/ruby/core/argf/to_i_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/fileno' + +describe "ARGF.to_i" do + it_behaves_like :argf_fileno, :to_i +end diff --git a/spec/ruby/core/argf/to_io_spec.rb b/spec/ruby/core/argf/to_io_spec.rb new file mode 100644 index 0000000000..062383d291 --- /dev/null +++ b/spec/ruby/core/argf/to_io_spec.rb @@ -0,0 +1,23 @@ +require_relative '../../spec_helper' + +describe "ARGF.to_io" do + before :each do + @file1 = fixture __FILE__, "file1.txt" + @file2 = fixture __FILE__, "file2.txt" + end + + # NOTE: this test assumes that fixtures files have two lines each + it "returns the IO of the current file" do + argf [@file1, @file2] do + result = [] + 4.times do + @argf.gets + result << @argf.to_io + end + + result.each { |io| io.should be_kind_of(IO) } + result[0].should == result[1] + result[2].should == result[3] + end + end +end diff --git a/spec/ruby/core/argf/to_s_spec.rb b/spec/ruby/core/argf/to_s_spec.rb new file mode 100644 index 0000000000..3f505898f4 --- /dev/null +++ b/spec/ruby/core/argf/to_s_spec.rb @@ -0,0 +1,14 @@ +require_relative '../../spec_helper' + +describe "ARGF.to_s" do + before :each do + @file1 = fixture __FILE__, "file1.txt" + @file2 = fixture __FILE__, "file2.txt" + end + + it "returns 'ARGF'" do + argf [@file1, @file2] do + @argf.to_s.should == "ARGF" + end + end +end diff --git a/spec/ruby/core/array/all_spec.rb b/spec/ruby/core/array/all_spec.rb new file mode 100644 index 0000000000..680e8c26fa --- /dev/null +++ b/spec/ruby/core/array/all_spec.rb @@ -0,0 +1,13 @@ +require_relative '../../spec_helper' +require_relative 'shared/iterable_and_tolerating_size_increasing' + +describe "Array#all?" do + @value_to_return = -> _ { true } + it_behaves_like :array_iterable_and_tolerating_size_increasing, :all? + + it "ignores the block if there is an argument" do + -> { + ['bar', 'foobar'].all?(/bar/) { false }.should == true + }.should complain(/given block not used/) + end +end diff --git a/spec/ruby/core/array/allocate_spec.rb b/spec/ruby/core/array/allocate_spec.rb new file mode 100644 index 0000000000..04f7c0d0ad --- /dev/null +++ b/spec/ruby/core/array/allocate_spec.rb @@ -0,0 +1,19 @@ +require_relative '../../spec_helper' + +describe "Array.allocate" do + it "returns an instance of Array" do + ary = Array.allocate + ary.should be_an_instance_of(Array) + end + + it "returns a fully-formed instance of Array" do + ary = Array.allocate + ary.size.should == 0 + ary << 1 + ary.should == [1] + end + + it "does not accept any arguments" do + -> { Array.allocate(1) }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/array/any_spec.rb b/spec/ruby/core/array/any_spec.rb new file mode 100644 index 0000000000..b51ce62f0f --- /dev/null +++ b/spec/ruby/core/array/any_spec.rb @@ -0,0 +1,49 @@ +require_relative '../../spec_helper' +require_relative 'shared/iterable_and_tolerating_size_increasing' + +describe "Array#any?" do + describe 'with no block given (a default block of { |x| x } is implicit)' do + it "is false if the array is empty" do + empty_array = [] + empty_array.should_not.any? + end + + it "is false if the array is not empty, but all the members of the array are falsy" do + falsy_array = [false, nil, false] + falsy_array.should_not.any? + end + + it "is true if the array has any truthy members" do + not_empty_array = ['anything', nil] + not_empty_array.should.any? + end + end + + describe 'with a block given' do + @value_to_return = -> _ { false } + it_behaves_like :array_iterable_and_tolerating_size_increasing, :any? + + it 'is false if the array is empty' do + empty_array = [] + empty_array.any? {|v| 1 == 1 }.should == false + end + + it 'is true if the block returns true for any member of the array' do + array_with_members = [false, false, true, false] + array_with_members.any? {|v| v == true }.should == true + end + + it 'is false if the block returns false for all members of the array' do + array_with_members = [false, false, true, false] + array_with_members.any? {|v| v == 42 }.should == false + end + end + + describe 'when given a pattern argument' do + it "ignores the block if there is an argument" do + -> { + ['bar', 'foobar'].any?(/bar/) { false }.should == true + }.should complain(/given block not used/) + end + end +end diff --git a/spec/ruby/core/array/append_spec.rb b/spec/ruby/core/array/append_spec.rb new file mode 100644 index 0000000000..c12473dc07 --- /dev/null +++ b/spec/ruby/core/array/append_spec.rb @@ -0,0 +1,40 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/push' + +describe "Array#<<" do + it "pushes the object onto the end of the array" do + ([ 1, 2 ] << "c" << "d" << [ 3, 4 ]).should == [1, 2, "c", "d", [3, 4]] + end + + it "returns self to allow chaining" do + a = [] + b = a + (a << 1).should equal(b) + (a << 2 << 3).should equal(b) + end + + it "correctly resizes the Array" do + a = [] + a.size.should == 0 + a << :foo + a.size.should == 1 + a << :bar << :baz + a.size.should == 3 + + a = [1, 2, 3] + a.shift + a.shift + a.shift + a << :foo + a.should == [:foo] + end + + it "raises a FrozenError on a frozen array" do + -> { ArraySpecs.frozen_array << 5 }.should raise_error(FrozenError) + end +end + +describe "Array#append" do + it_behaves_like :array_push, :append +end diff --git a/spec/ruby/core/array/array_spec.rb b/spec/ruby/core/array/array_spec.rb new file mode 100644 index 0000000000..855f17348f --- /dev/null +++ b/spec/ruby/core/array/array_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' + +describe "Array" do + it "includes Enumerable" do + Array.include?(Enumerable).should == true + end +end diff --git a/spec/ruby/core/array/assoc_spec.rb b/spec/ruby/core/array/assoc_spec.rb new file mode 100644 index 0000000000..f0be3de795 --- /dev/null +++ b/spec/ruby/core/array/assoc_spec.rb @@ -0,0 +1,52 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Array#assoc" do + it "returns the first array whose 1st item is == obj or nil" do + s1 = ["colors", "red", "blue", "green"] + s2 = [:letters, "a", "b", "c"] + s3 = [4] + s4 = ["colors", "cyan", "yellow", "magenta"] + s5 = [:letters, "a", "i", "u"] + s_nil = [nil, nil] + a = [s1, s2, s3, s4, s5, s_nil] + a.assoc(s1.first).should equal(s1) + a.assoc(s2.first).should equal(s2) + a.assoc(s3.first).should equal(s3) + a.assoc(s4.first).should equal(s1) + a.assoc(s5.first).should equal(s2) + a.assoc(s_nil.first).should equal(s_nil) + a.assoc(4).should equal(s3) + a.assoc("key not in array").should be_nil + end + + it "calls == on first element of each array" do + key1 = 'it' + key2 = mock('key2') + items = [['not it', 1], [ArraySpecs::AssocKey.new, 2], ['na', 3]] + + items.assoc(key1).should equal(items[1]) + items.assoc(key2).should be_nil + end + + it "ignores any non-Array elements" do + [1, 2, 3].assoc(2).should be_nil + s1 = [4] + s2 = [5, 4, 3] + a = ["foo", [], s1, s2, nil, []] + a.assoc(s1.first).should equal(s1) + a.assoc(s2.first).should equal(s2) + end + + it "calls to_ary on non-array elements" do + s1 = [1, 2] + s2 = ArraySpecs::ArrayConvertible.new(2, 3) + a = [s1, s2] + + s1.should_not_receive(:to_ary) + a.assoc(s1.first).should equal(s1) + + a.assoc(2).should == [2, 3] + s2.called.should equal(:to_ary) + end +end diff --git a/spec/ruby/core/array/at_spec.rb b/spec/ruby/core/array/at_spec.rb new file mode 100644 index 0000000000..8bc789fef7 --- /dev/null +++ b/spec/ruby/core/array/at_spec.rb @@ -0,0 +1,56 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Array#at" do + it "returns the (n+1)'th element for the passed index n" do + a = [1, 2, 3, 4, 5, 6] + a.at(0).should == 1 + a.at(1).should == 2 + a.at(5).should == 6 + end + + it "returns nil if the given index is greater than or equal to the array's length" do + a = [1, 2, 3, 4, 5, 6] + a.at(6).should == nil + a.at(7).should == nil + end + + it "returns the (-n)'th element from the last, for the given negative index n" do + a = [1, 2, 3, 4, 5, 6] + a.at(-1).should == 6 + a.at(-2).should == 5 + a.at(-6).should == 1 + end + + it "returns nil if the given index is less than -len, where len is length of the array" do + a = [1, 2, 3, 4, 5, 6] + a.at(-7).should == nil + a.at(-8).should == nil + end + + it "does not extend the array unless the given index is out of range" do + a = [1, 2, 3, 4, 5, 6] + a.length.should == 6 + a.at(100) + a.length.should == 6 + a.at(-100) + a.length.should == 6 + end + + it "tries to convert the passed argument to an Integer using #to_int" do + a = ["a", "b", "c"] + a.at(0.5).should == "a" + + obj = mock('to_int') + obj.should_receive(:to_int).and_return(2) + a.at(obj).should == "c" + end + + it "raises a TypeError when the passed argument can't be coerced to Integer" do + -> { [].at("cat") }.should raise_error(TypeError) + end + + it "raises an ArgumentError when 2 or more arguments are passed" do + -> { [:a, :b].at(0,1) }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/array/bsearch_index_spec.rb b/spec/ruby/core/array/bsearch_index_spec.rb new file mode 100644 index 0000000000..94d85b37f3 --- /dev/null +++ b/spec/ruby/core/array/bsearch_index_spec.rb @@ -0,0 +1,81 @@ +require_relative '../../spec_helper' +require_relative '../enumerable/shared/enumeratorized' + +describe "Array#bsearch_index" do + context "when not passed a block" do + before :each do + @enum = [1, 2, 42, 100, 666].bsearch_index + end + + it "returns an Enumerator" do + @enum.should be_an_instance_of(Enumerator) + end + + it "returns an Enumerator with unknown size" do + @enum.size.should be_nil + end + + it "returns index of element when block condition is satisfied" do + @enum.each { |x| x >= 33 }.should == 2 + end + end + + it "raises a TypeError when block returns a String" do + -> { [1, 2, 3].bsearch_index { "not ok" } }.should raise_error(TypeError) + end + + it "returns nil when block is empty" do + [1, 2, 3].bsearch_index {}.should be_nil + end + + context "minimum mode" do + before :each do + @array = [0, 4, 7, 10, 12] + end + + it "returns index of first element which satisfies the block" do + @array.bsearch_index { |x| x >= 4 }.should == 1 + @array.bsearch_index { |x| x >= 6 }.should == 2 + @array.bsearch_index { |x| x >= -1 }.should == 0 + end + + it "returns nil when block condition is never satisfied" do + @array.bsearch_index { false }.should be_nil + @array.bsearch_index { |x| x >= 100 }.should be_nil + end + end + + context "find any mode" do + before :each do + @array = [0, 4, 7, 10, 12] + end + + it "returns the index of any matched elements where element is between 4 <= x < 8" do + [1, 2].should include(@array.bsearch_index { |x| 1 - x / 4 }) + end + + it "returns the index of any matched elements where element is between 8 <= x < 10" do + @array.bsearch_index { |x| 4 - x / 2 }.should be_nil + end + + it "returns nil when block never returns 0" do + @array.bsearch_index { |x| 1 }.should be_nil + @array.bsearch_index { |x| -1 }.should be_nil + end + + context "magnitude does not effect the result" do + it "returns the index of any matched elements where element is between 4n <= xn < 8n" do + [1, 2].should include(@array.bsearch_index { |x| (1 - x / 4) * (2**100) }) + end + + it "returns nil when block never returns 0" do + @array.bsearch_index { |x| 1 * (2**100) }.should be_nil + @array.bsearch_index { |x| (-1) * (2**100) }.should be_nil + end + + it "handles values from Integer#coerce" do + [1, 2].should include(@array.bsearch_index { |x| (2**100).coerce((1 - x / 4) * (2**100)).first }) + end + end + end +end diff --git a/spec/ruby/core/array/bsearch_spec.rb b/spec/ruby/core/array/bsearch_spec.rb new file mode 100644 index 0000000000..8fa6245dbf --- /dev/null +++ b/spec/ruby/core/array/bsearch_spec.rb @@ -0,0 +1,84 @@ +require_relative '../../spec_helper' +require_relative '../enumerable/shared/enumeratorized' + +describe "Array#bsearch" do + it "returns an Enumerator when not passed a block" do + [1].bsearch.should be_an_instance_of(Enumerator) + end + + it_behaves_like :enumeratorized_with_unknown_size, :bsearch, [1,2,3] + + it "raises a TypeError if the block returns an Object" do + -> { [1].bsearch { Object.new } }.should raise_error(TypeError) + end + + it "raises a TypeError if the block returns a String" do + -> { [1].bsearch { "1" } }.should raise_error(TypeError) + end + + context "with a block returning true or false" do + it "returns nil if the block returns false for every element" do + [0, 1, 2, 3].bsearch { |x| x > 3 }.should be_nil + end + + it "returns nil if the block returns nil for every element" do + [0, 1, 2, 3].bsearch { |x| nil }.should be_nil + end + + it "returns element at zero if the block returns true for every element" do + [0, 1, 2, 3].bsearch { |x| x < 4 }.should == 0 + + end + + it "returns the element at the smallest index for which block returns true" do + [0, 1, 3, 4].bsearch { |x| x >= 2 }.should == 3 + [0, 1, 3, 4].bsearch { |x| x >= 1 }.should == 1 + end + end + + context "with a block returning negative, zero, positive numbers" do + it "returns nil if the block returns less than zero for every element" do + [0, 1, 2, 3].bsearch { |x| x <=> 5 }.should be_nil + end + + it "returns nil if the block returns greater than zero for every element" do + [0, 1, 2, 3].bsearch { |x| x <=> -1 }.should be_nil + + end + + it "returns nil if the block never returns zero" do + [0, 1, 3, 4].bsearch { |x| x <=> 2 }.should be_nil + end + + it "accepts (+/-)Float::INFINITY from the block" do + [0, 1, 3, 4].bsearch { |x| Float::INFINITY }.should be_nil + [0, 1, 3, 4].bsearch { |x| -Float::INFINITY }.should be_nil + end + + it "returns an element at an index for which block returns 0.0" do + result = [0, 1, 2, 3, 4].bsearch { |x| x < 2 ? 1.0 : x > 2 ? -1.0 : 0.0 } + result.should == 2 + end + + it "returns an element at an index for which block returns 0" do + result = [0, 1, 2, 3, 4].bsearch { |x| x < 1 ? 1 : x > 3 ? -1 : 0 } + [1, 2].should include(result) + end + end + + context "with a block that calls break" do + it "returns nil if break is called without a value" do + ['a', 'b', 'c'].bsearch { |v| break }.should be_nil + end + + it "returns nil if break is called with a nil value" do + ['a', 'b', 'c'].bsearch { |v| break nil }.should be_nil + end + + it "returns object if break is called with an object" do + ['a', 'b', 'c'].bsearch { |v| break 1234 }.should == 1234 + ['a', 'b', 'c'].bsearch { |v| break 'hi' }.should == 'hi' + ['a', 'b', 'c'].bsearch { |v| break [42] }.should == [42] + end + end +end diff --git a/spec/ruby/core/array/clear_spec.rb b/spec/ruby/core/array/clear_spec.rb new file mode 100644 index 0000000000..81ba56e01e --- /dev/null +++ b/spec/ruby/core/array/clear_spec.rb @@ -0,0 +1,32 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Array#clear" do + it "removes all elements" do + a = [1, 2, 3, 4] + a.clear.should equal(a) + a.should == [] + end + + it "returns self" do + a = [1] + a.should equal a.clear + end + + it "leaves the Array empty" do + a = [1] + a.clear + a.should.empty? + a.size.should == 0 + end + + it "does not accept any arguments" do + -> { [1].clear(true) }.should raise_error(ArgumentError) + end + + it "raises a FrozenError on a frozen array" do + a = [1] + a.freeze + -> { a.clear }.should raise_error(FrozenError) + end +end diff --git a/spec/ruby/core/array/clone_spec.rb b/spec/ruby/core/array/clone_spec.rb new file mode 100644 index 0000000000..e22a6c6d53 --- /dev/null +++ b/spec/ruby/core/array/clone_spec.rb @@ -0,0 +1,31 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/clone' + +describe "Array#clone" do + it_behaves_like :array_clone, :clone + + it "copies frozen status from the original" do + a = [1, 2, 3, 4] + b = [1, 2, 3, 4] + a.freeze + aa = a.clone + bb = b.clone + + aa.should.frozen? + bb.should_not.frozen? + end + + it "copies singleton methods" do + a = [1, 2, 3, 4] + b = [1, 2, 3, 4] + def a.a_singleton_method; end + aa = a.clone + bb = b.clone + + a.respond_to?(:a_singleton_method).should be_true + b.respond_to?(:a_singleton_method).should be_false + aa.respond_to?(:a_singleton_method).should be_true + bb.respond_to?(:a_singleton_method).should be_false + end +end diff --git a/spec/ruby/core/array/collect_spec.rb b/spec/ruby/core/array/collect_spec.rb new file mode 100644 index 0000000000..0ad4c283b1 --- /dev/null +++ b/spec/ruby/core/array/collect_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/collect' + +describe "Array#collect" do + it_behaves_like :array_collect, :collect +end + +describe "Array#collect!" do + it_behaves_like :array_collect_b, :collect! +end diff --git a/spec/ruby/core/array/combination_spec.rb b/spec/ruby/core/array/combination_spec.rb new file mode 100644 index 0000000000..f16d6f98fc --- /dev/null +++ b/spec/ruby/core/array/combination_spec.rb @@ -0,0 +1,74 @@ +require_relative '../../spec_helper' + +describe "Array#combination" do + before :each do + @array = [1, 2, 3, 4] + end + + it "returns an enumerator when no block is provided" do + @array.combination(2).should be_an_instance_of(Enumerator) + end + + it "returns self when a block is given" do + @array.combination(2){}.should equal(@array) + end + + it "yields nothing for out of bounds length and return self" do + @array.combination(5).to_a.should == [] + @array.combination(-1).to_a.should == [] + end + + it "yields the expected combinations" do + @array.combination(3).to_a.sort.should == [[1,2,3],[1,2,4],[1,3,4],[2,3,4]] + end + + it "yields nothing if the argument is out of bounds" do + @array.combination(-1).to_a.should == [] + @array.combination(5).to_a.should == [] + end + + it "yields a copy of self if the argument is the size of the receiver" do + r = @array.combination(4).to_a + r.should == [@array] + r[0].should_not equal(@array) + end + + it "yields [] when length is 0" do + @array.combination(0).to_a.should == [[]] # one combination of length 0 + [].combination(0).to_a.should == [[]] # one combination of length 0 + end + + it "yields a partition consisting of only singletons" do + @array.combination(1).to_a.sort.should == [[1],[2],[3],[4]] + end + + it "generates from a defensive copy, ignoring mutations" do + accum = [] + @array.combination(2) do |x| + accum << x + @array[0] = 1 + end + accum.should == [[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]] + end + + describe "when no block is given" do + describe "returned Enumerator" do + describe "size" do + it "returns 0 when the number of combinations is < 0" do + @array.combination(-1).size.should == 0 + [].combination(-2).size.should == 0 + end + it "returns the binomial coefficient between the array size the number of combinations" do + @array.combination(5).size.should == 0 + @array.combination(4).size.should == 1 + @array.combination(3).size.should == 4 + @array.combination(2).size.should == 6 + @array.combination(1).size.should == 4 + @array.combination(0).size.should == 1 + [].combination(0).size.should == 1 + [].combination(1).size.should == 0 + end + end + end + end +end diff --git a/spec/ruby/core/array/compact_spec.rb b/spec/ruby/core/array/compact_spec.rb new file mode 100644 index 0000000000..83b3fa2a89 --- /dev/null +++ b/spec/ruby/core/array/compact_spec.rb @@ -0,0 +1,51 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Array#compact" do + it "returns a copy of array with all nil elements removed" do + a = [1, 2, 4] + a.compact.should == [1, 2, 4] + a = [1, nil, 2, 4] + a.compact.should == [1, 2, 4] + a = [1, 2, 4, nil] + a.compact.should == [1, 2, 4] + a = [nil, 1, 2, 4] + a.compact.should == [1, 2, 4] + end + + it "does not return self" do + a = [1, 2, 3] + a.compact.should_not equal(a) + end + + it "does not return subclass instance for Array subclasses" do + ArraySpecs::MyArray[1, 2, 3, nil].compact.should be_an_instance_of(Array) + end +end + +describe "Array#compact!" do + it "removes all nil elements" do + a = ['a', nil, 'b', false, 'c'] + a.compact!.should equal(a) + a.should == ["a", "b", false, "c"] + a = [nil, 'a', 'b', false, 'c'] + a.compact!.should equal(a) + a.should == ["a", "b", false, "c"] + a = ['a', 'b', false, 'c', nil] + a.compact!.should equal(a) + a.should == ["a", "b", false, "c"] + end + + it "returns self if some nil elements are removed" do + a = ['a', nil, 'b', false, 'c'] + a.compact!.should equal a + end + + it "returns nil if there are no nil elements to remove" do + [1, 2, false, 3].compact!.should == nil + end + + it "raises a FrozenError on a frozen array" do + -> { ArraySpecs.frozen_array.compact! }.should raise_error(FrozenError) + end +end diff --git a/spec/ruby/core/array/comparison_spec.rb b/spec/ruby/core/array/comparison_spec.rb new file mode 100644 index 0000000000..5d1c3265f1 --- /dev/null +++ b/spec/ruby/core/array/comparison_spec.rb @@ -0,0 +1,97 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Array#<=>" do + it "calls <=> left to right and return first non-0 result" do + [-1, +1, nil, "foobar"].each do |result| + lhs = Array.new(3) { mock("#{result}") } + rhs = Array.new(3) { mock("#{result}") } + + lhs[0].should_receive(:<=>).with(rhs[0]).and_return(0) + lhs[1].should_receive(:<=>).with(rhs[1]).and_return(result) + lhs[2].should_not_receive(:<=>) + + (lhs <=> rhs).should == result + end + end + + it "returns 0 if the arrays are equal" do + ([] <=> []).should == 0 + ([1, 2, 3, 4, 5, 6] <=> [1, 2, 3, 4, 5.0, 6.0]).should == 0 + end + + it "returns -1 if the array is shorter than the other array" do + ([] <=> [1]).should == -1 + ([1, 1] <=> [1, 1, 1]).should == -1 + end + + it "returns +1 if the array is longer than the other array" do + ([1] <=> []).should == +1 + ([1, 1, 1] <=> [1, 1]).should == +1 + end + + it "returns -1 if the arrays have same length and a pair of corresponding elements returns -1 for <=>" do + eq_l = mock('an object equal to the other') + eq_r = mock('an object equal to the other') + eq_l.should_receive(:<=>).with(eq_r).any_number_of_times.and_return(0) + + less = mock('less than the other') + greater = mock('greater then the other') + less.should_receive(:<=>).with(greater).any_number_of_times.and_return(-1) + + rest = mock('an rest element of the arrays') + rest.should_receive(:<=>).with(rest).any_number_of_times.and_return(0) + lhs = [eq_l, eq_l, less, rest] + rhs = [eq_r, eq_r, greater, rest] + + (lhs <=> rhs).should == -1 + end + + it "returns +1 if the arrays have same length and a pair of corresponding elements returns +1 for <=>" do + eq_l = mock('an object equal to the other') + eq_r = mock('an object equal to the other') + eq_l.should_receive(:<=>).with(eq_r).any_number_of_times.and_return(0) + + greater = mock('greater then the other') + less = mock('less than the other') + greater.should_receive(:<=>).with(less).any_number_of_times.and_return(+1) + + rest = mock('an rest element of the arrays') + rest.should_receive(:<=>).with(rest).any_number_of_times.and_return(0) + lhs = [eq_l, eq_l, greater, rest] + rhs = [eq_r, eq_r, less, rest] + + (lhs <=> rhs).should == +1 + end + + it "properly handles recursive arrays" do + empty = ArraySpecs.empty_recursive_array + (empty <=> empty).should == 0 + (empty <=> []).should == 1 + ([] <=> empty).should == -1 + + (ArraySpecs.recursive_array <=> []).should == 1 + ([] <=> ArraySpecs.recursive_array).should == -1 + + (ArraySpecs.recursive_array <=> ArraySpecs.empty_recursive_array).should == nil + + array = ArraySpecs.recursive_array + (array <=> array).should == 0 + end + + it "tries to convert the passed argument to an Array using #to_ary" do + obj = mock('to_ary') + obj.stub!(:to_ary).and_return([1, 2, 3]) + ([4, 5] <=> obj).should == ([4, 5] <=> obj.to_ary) + end + + it "does not call #to_ary on Array subclasses" do + obj = ArraySpecs::ToAryArray[5, 6, 7] + obj.should_not_receive(:to_ary) + ([5, 6, 7] <=> obj).should == 0 + end + + it "returns nil when the argument is not array-like" do + ([] <=> false).should be_nil + end +end diff --git a/spec/ruby/core/array/concat_spec.rb b/spec/ruby/core/array/concat_spec.rb new file mode 100644 index 0000000000..f3cab9c17c --- /dev/null +++ b/spec/ruby/core/array/concat_spec.rb @@ -0,0 +1,74 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Array#concat" do + it "returns the array itself" do + ary = [1,2,3] + ary.concat([4,5,6]).equal?(ary).should be_true + end + + it "appends the elements in the other array" do + ary = [1, 2, 3] + ary.concat([9, 10, 11]).should equal(ary) + ary.should == [1, 2, 3, 9, 10, 11] + ary.concat([]) + ary.should == [1, 2, 3, 9, 10, 11] + end + + it "does not loop endlessly when argument is self" do + ary = ["x", "y"] + ary.concat(ary).should == ["x", "y", "x", "y"] + end + + it "tries to convert the passed argument to an Array using #to_ary" do + obj = mock('to_ary') + obj.should_receive(:to_ary).and_return(["x", "y"]) + [4, 5, 6].concat(obj).should == [4, 5, 6, "x", "y"] + end + + it "does not call #to_ary on Array subclasses" do + obj = ArraySpecs::ToAryArray[5, 6, 7] + obj.should_not_receive(:to_ary) + [].concat(obj).should == [5, 6, 7] + end + + it "raises a FrozenError when Array is frozen and modification occurs" do + -> { ArraySpecs.frozen_array.concat [1] }.should raise_error(FrozenError) + end + + # see [ruby-core:23666] + it "raises a FrozenError when Array is frozen and no modification occurs" do + -> { ArraySpecs.frozen_array.concat([]) }.should raise_error(FrozenError) + end + + it "appends elements to an Array with enough capacity that has been shifted" do + ary = [1, 2, 3, 4, 5] + 2.times { ary.shift } + 2.times { ary.pop } + ary.concat([5, 6]).should == [3, 5, 6] + end + + it "appends elements to an Array without enough capacity that has been shifted" do + ary = [1, 2, 3, 4] + 3.times { ary.shift } + ary.concat([5, 6]).should == [4, 5, 6] + end + + it "takes multiple arguments" do + ary = [1, 2] + ary.concat [3, 4] + ary.should == [1, 2, 3, 4] + end + + it "concatenates the initial value when given arguments contain 2 self" do + ary = [1, 2] + ary.concat ary, ary + ary.should == [1, 2, 1, 2, 1, 2] + end + + it "returns self when given no arguments" do + ary = [1, 2] + ary.concat.should equal(ary) + ary.should == [1, 2] + end +end diff --git a/spec/ruby/core/array/constructor_spec.rb b/spec/ruby/core/array/constructor_spec.rb new file mode 100644 index 0000000000..6f36074c45 --- /dev/null +++ b/spec/ruby/core/array/constructor_spec.rb @@ -0,0 +1,24 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Array.[]" do + it "returns a new array populated with the given elements" do + obj = Object.new + Array.[](5, true, nil, 'a', "Ruby", obj).should == [5, true, nil, "a", "Ruby", obj] + + a = ArraySpecs::MyArray.[](5, true, nil, 'a', "Ruby", obj) + a.should be_an_instance_of(ArraySpecs::MyArray) + a.inspect.should == [5, true, nil, "a", "Ruby", obj].inspect + end +end + +describe "Array[]" do + it "is a synonym for .[]" do + obj = Object.new + Array[5, true, nil, 'a', "Ruby", obj].should == Array.[](5, true, nil, "a", "Ruby", obj) + + a = ArraySpecs::MyArray[5, true, nil, 'a', "Ruby", obj] + a.should be_an_instance_of(ArraySpecs::MyArray) + a.inspect.should == [5, true, nil, "a", "Ruby", obj].inspect + end +end diff --git a/spec/ruby/core/array/count_spec.rb b/spec/ruby/core/array/count_spec.rb new file mode 100644 index 0000000000..e778233c16 --- /dev/null +++ b/spec/ruby/core/array/count_spec.rb @@ -0,0 +1,26 @@ +require_relative '../../spec_helper' +require_relative 'shared/iterable_and_tolerating_size_increasing' + +describe "Array#count" do + it "returns the number of elements" do + [:a, :b, :c].count.should == 3 + end + + it "returns the number of elements that equal the argument" do + [:a, :b, :b, :c].count(:b).should == 2 + end + + it "returns the number of element for which the block evaluates to true" do + [:a, :b, :c].count { |s| s != :b }.should == 2 + end + + it "ignores the block if there is an argument" do + -> { + [:a, :b, :b, :c].count(:b) { |e| e.size > 10 }.should == 2 + }.should complain(/given block not used/) + end + + context "when a block argument given" do + it_behaves_like :array_iterable_and_tolerating_size_increasing, :count + end +end diff --git a/spec/ruby/core/array/cycle_spec.rb b/spec/ruby/core/array/cycle_spec.rb new file mode 100644 index 0000000000..7219b49883 --- /dev/null +++ b/spec/ruby/core/array/cycle_spec.rb @@ -0,0 +1,101 @@ +require_relative '../../spec_helper' +require_relative '../enumerable/shared/enumeratorized' + +describe "Array#cycle" do + before :each do + ScratchPad.record [] + + @array = [1, 2, 3] + @prc = -> x { ScratchPad << x } + end + + it "does not yield and returns nil when the array is empty and passed value is an integer" do + [].cycle(6, &@prc).should be_nil + ScratchPad.recorded.should == [] + end + + it "does not yield and returns nil when the array is empty and passed value is nil" do + [].cycle(nil, &@prc).should be_nil + ScratchPad.recorded.should == [] + end + + it "does not yield and returns nil when passed 0" do + @array.cycle(0, &@prc).should be_nil + ScratchPad.recorded.should == [] + end + + it "iterates the array 'count' times yielding each item to the block" do + @array.cycle(2, &@prc) + ScratchPad.recorded.should == [1, 2, 3, 1, 2, 3] + end + + it "iterates indefinitely when not passed a count" do + @array.cycle do |x| + ScratchPad << x + break if ScratchPad.recorded.size > 7 + end + ScratchPad.recorded.should == [1, 2, 3, 1, 2, 3, 1, 2] + end + + it "iterates indefinitely when passed nil" do + @array.cycle(nil) do |x| + ScratchPad << x + break if ScratchPad.recorded.size > 7 + end + ScratchPad.recorded.should == [1, 2, 3, 1, 2, 3, 1, 2] + end + + it "does not rescue StopIteration when not passed a count" do + -> do + @array.cycle { raise StopIteration } + end.should raise_error(StopIteration) + end + + it "does not rescue StopIteration when passed a count" do + -> do + @array.cycle(3) { raise StopIteration } + end.should raise_error(StopIteration) + end + + it "iterates the array Integer(count) times when passed a Float count" do + @array.cycle(2.7, &@prc) + ScratchPad.recorded.should == [1, 2, 3, 1, 2, 3] + end + + it "calls #to_int to convert count to an Integer" do + count = mock("cycle count 2") + count.should_receive(:to_int).and_return(2) + + @array.cycle(count, &@prc) + ScratchPad.recorded.should == [1, 2, 3, 1, 2, 3] + end + + it "raises a TypeError if #to_int does not return an Integer" do + count = mock("cycle count 2") + count.should_receive(:to_int).and_return("2") + + -> { @array.cycle(count, &@prc) }.should raise_error(TypeError) + end + + it "raises a TypeError if passed a String" do + -> { @array.cycle("4") { } }.should raise_error(TypeError) + end + + it "raises a TypeError if passed an Object" do + -> { @array.cycle(mock("cycle count")) { } }.should raise_error(TypeError) + end + + it "raises a TypeError if passed true" do + -> { @array.cycle(true) { } }.should raise_error(TypeError) + end + + it "raises a TypeError if passed false" do + -> { @array.cycle(false) { } }.should raise_error(TypeError) + end + + before :all do + @object = [1, 2, 3, 4] + @empty_object = [] + end + it_should_behave_like :enumeratorized_with_cycle_size +end diff --git a/spec/ruby/core/array/deconstruct_spec.rb b/spec/ruby/core/array/deconstruct_spec.rb new file mode 100644 index 0000000000..ad67abe47b --- /dev/null +++ b/spec/ruby/core/array/deconstruct_spec.rb @@ -0,0 +1,9 @@ +require_relative '../../spec_helper' + +describe "Array#deconstruct" do + it "returns self" do + array = [1] + + array.deconstruct.should equal array + end +end diff --git a/spec/ruby/core/array/delete_at_spec.rb b/spec/ruby/core/array/delete_at_spec.rb new file mode 100644 index 0000000000..80ec643702 --- /dev/null +++ b/spec/ruby/core/array/delete_at_spec.rb @@ -0,0 +1,41 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Array#delete_at" do + it "removes the element at the specified index" do + a = [1, 2, 3, 4] + a.delete_at(2) + a.should == [1, 2, 4] + a.delete_at(-1) + a.should == [1, 2] + end + + it "returns the removed element at the specified index" do + a = [1, 2, 3, 4] + a.delete_at(2).should == 3 + a.delete_at(-1).should == 4 + end + + it "returns nil and makes no modification if the index is out of range" do + a = [1, 2] + a.delete_at(3).should == nil + a.should == [1, 2] + a.delete_at(-3).should == nil + a.should == [1, 2] + end + + it "tries to convert the passed argument to an Integer using #to_int" do + obj = mock('to_int') + obj.should_receive(:to_int).and_return(-1) + [1, 2].delete_at(obj).should == 2 + end + + it "accepts negative indices" do + a = [1, 2] + a.delete_at(-2).should == 1 + end + + it "raises a FrozenError on a frozen array" do + -> { [1,2,3].freeze.delete_at(0) }.should raise_error(FrozenError) + end +end diff --git a/spec/ruby/core/array/delete_if_spec.rb b/spec/ruby/core/array/delete_if_spec.rb new file mode 100644 index 0000000000..10972eee0e --- /dev/null +++ b/spec/ruby/core/array/delete_if_spec.rb @@ -0,0 +1,82 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/enumeratorize' +require_relative 'shared/delete_if' +require_relative 'shared/iterable_and_tolerating_size_increasing' +require_relative '../enumerable/shared/enumeratorized' + +describe "Array#delete_if" do + before do + @a = [ "a", "b", "c" ] + end + + it "removes each element for which block returns true" do + @a = [ "a", "b", "c" ] + @a.delete_if { |x| x >= "b" } + @a.should == ["a"] + end + + it "returns self" do + @a.delete_if{ true }.equal?(@a).should be_true + end + + it_behaves_like :enumeratorize, :delete_if + + it "returns self when called on an Array emptied with #shift" do + array = [1] + array.shift + array.delete_if { |x| true }.should equal(array) + end + + it "returns an Enumerator if no block given, and the enumerator can modify the original array" do + enum = @a.delete_if + enum.should be_an_instance_of(Enumerator) + @a.should_not be_empty + enum.each { true } + @a.should be_empty + end + + it "returns an Enumerator if no block given, and the array is frozen" do + @a.freeze.delete_if.should be_an_instance_of(Enumerator) + end + + it "raises a FrozenError on a frozen array" do + -> { ArraySpecs.frozen_array.delete_if {} }.should raise_error(FrozenError) + end + + it "raises a FrozenError on an empty frozen array" do + -> { ArraySpecs.empty_frozen_array.delete_if {} }.should raise_error(FrozenError) + end + + it "does not truncate the array is the block raises an exception" do + a = [1, 2, 3] + begin + a.delete_if { raise StandardError, 'Oops' } + rescue + end + + a.should == [1, 2, 3] + end + + it "only removes elements for which the block returns true, keeping the element which raised an error." do + a = [1, 2, 3, 4] + begin + a.delete_if do |e| + case e + when 2 then true + when 3 then raise StandardError, 'Oops' + else false + end + end + rescue StandardError + end + + a.should == [1, 3, 4] + end + + it_behaves_like :enumeratorized_with_origin_size, :delete_if, [1,2,3] + it_behaves_like :delete_if, :delete_if + + @value_to_return = -> _ { false } + it_behaves_like :array_iterable_and_tolerating_size_increasing, :delete_if +end diff --git a/spec/ruby/core/array/delete_spec.rb b/spec/ruby/core/array/delete_spec.rb new file mode 100644 index 0000000000..dddbbe6bd3 --- /dev/null +++ b/spec/ruby/core/array/delete_spec.rb @@ -0,0 +1,46 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Array#delete" do + it "removes elements that are #== to object" do + x = mock('delete') + def x.==(other) 3 == other end + + a = [1, 2, 3, x, 4, 3, 5, x] + a.delete mock('not contained') + a.should == [1, 2, 3, x, 4, 3, 5, x] + + a.delete 3 + a.should == [1, 2, 4, 5] + end + + it "calculates equality correctly for reference values" do + a = ["foo", "bar", "foo", "quux", "foo"] + a.delete "foo" + a.should == ["bar","quux"] + end + + it "returns object or nil if no elements match object" do + [1, 2, 4, 5].delete(1).should == 1 + [1, 2, 4, 5].delete(3).should == nil + end + + it "may be given a block that is executed if no element matches object" do + [1].delete(1) {:not_found}.should == 1 + [].delete('a') {:not_found}.should == :not_found + end + + it "returns nil if the array is empty due to a shift" do + a = [1] + a.shift + a.delete(nil).should == nil + end + + it "returns nil on a frozen array if a modification does not take place" do + [1, 2, 3].freeze.delete(0).should == nil + end + + it "raises a FrozenError on a frozen array" do + -> { [1, 2, 3].freeze.delete(1) }.should raise_error(FrozenError) + end +end diff --git a/spec/ruby/core/array/difference_spec.rb b/spec/ruby/core/array/difference_spec.rb new file mode 100644 index 0000000000..9f7d4c4a1a --- /dev/null +++ b/spec/ruby/core/array/difference_spec.rb @@ -0,0 +1,22 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/difference' + +describe "Array#difference" do + it_behaves_like :array_binary_difference, :difference + + it "returns a copy when called without any parameter" do + x = [1, 2, 3, 2] + x.difference.should == x + x.difference.should_not equal x + end + + it "does not return subclass instances for Array subclasses" do + ArraySpecs::MyArray[1, 2, 3].difference.should be_an_instance_of(Array) + end + + it "accepts multiple arguments" do + x = [1, 2, 3, 1] + x.difference([], [0, 1], [3, 4], [3]).should == [2] + end +end diff --git a/spec/ruby/core/array/dig_spec.rb b/spec/ruby/core/array/dig_spec.rb new file mode 100644 index 0000000000..f2d8ff47fd --- /dev/null +++ b/spec/ruby/core/array/dig_spec.rb @@ -0,0 +1,52 @@ +require_relative '../../spec_helper' + +describe "Array#dig" do + + it "returns #at with one arg" do + ['a'].dig(0).should == 'a' + ['a'].dig(1).should be_nil + end + + it "recurses array elements" do + a = [ [ 1, [2, '3'] ] ] + a.dig(0, 0).should == 1 + a.dig(0, 1, 1).should == '3' + a.dig(0, -1, 0).should == 2 + end + + it "returns the nested value specified if the sequence includes a key" do + a = [42, { foo: :bar }] + a.dig(1, :foo).should == :bar + end + + it "raises a TypeError for a non-numeric index" do + -> { + ['a'].dig(:first) + }.should raise_error(TypeError) + end + + it "raises a TypeError if any intermediate step does not respond to #dig" do + a = [1, 2] + -> { + a.dig(0, 1) + }.should raise_error(TypeError) + end + + it "raises an ArgumentError if no arguments provided" do + -> { + [10].dig() + }.should raise_error(ArgumentError) + end + + it "returns nil if any intermediate step is nil" do + a = [[1, [2, 3]]] + a.dig(1, 2, 3).should == nil + end + + it "calls #dig on the result of #at with the remaining arguments" do + h = [[nil, [nil, nil, 42]]] + h[0].should_receive(:dig).with(1, 2).and_return(42) + h.dig(0, 1, 2).should == 42 + end + +end diff --git a/spec/ruby/core/array/drop_spec.rb b/spec/ruby/core/array/drop_spec.rb new file mode 100644 index 0000000000..5926c291b8 --- /dev/null +++ b/spec/ruby/core/array/drop_spec.rb @@ -0,0 +1,56 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Array#drop" do + it "removes the specified number of elements from the start of the array" do + [1, 2, 3, 4, 5].drop(2).should == [3, 4, 5] + end + + it "raises an ArgumentError if the number of elements specified is negative" do + -> { [1, 2].drop(-3) }.should raise_error(ArgumentError) + end + + it "returns an empty Array if all elements are dropped" do + [1, 2].drop(2).should == [] + end + + it "returns an empty Array when called on an empty Array" do + [].drop(0).should == [] + end + + it "does not remove any elements when passed zero" do + [1, 2].drop(0).should == [1, 2] + end + + it "returns an empty Array if more elements than exist are dropped" do + [1, 2].drop(3).should == [] + end + + it 'acts correctly after a shift' do + ary = [nil, 1, 2] + ary.shift + ary.drop(1).should == [2] + end + + it "tries to convert the passed argument to an Integer using #to_int" do + obj = mock("to_int") + obj.should_receive(:to_int).and_return(2) + + [1, 2, 3].drop(obj).should == [3] + end + + it "raises a TypeError when the passed argument can't be coerced to Integer" do + -> { [1, 2].drop("cat") }.should raise_error(TypeError) + end + + it "raises a TypeError when the passed argument isn't an integer and #to_int returns non-Integer" do + obj = mock("to_int") + obj.should_receive(:to_int).and_return("cat") + + -> { [1, 2].drop(obj) }.should raise_error(TypeError) + end + + it 'returns a Array instance for Array subclasses' do + ArraySpecs::MyArray[1, 2, 3, 4, 5].drop(1).should be_an_instance_of(Array) + end +end diff --git a/spec/ruby/core/array/drop_while_spec.rb b/spec/ruby/core/array/drop_while_spec.rb new file mode 100644 index 0000000000..bd46e8b882 --- /dev/null +++ b/spec/ruby/core/array/drop_while_spec.rb @@ -0,0 +1,24 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/iterable_and_tolerating_size_increasing' + +describe "Array#drop_while" do + @value_to_return = -> _ { true } + it_behaves_like :array_iterable_and_tolerating_size_increasing, :drop_while + + it "removes elements from the start of the array while the block evaluates to true" do + [1, 2, 3, 4].drop_while { |n| n < 4 }.should == [4] + end + + it "removes elements from the start of the array until the block returns nil" do + [1, 2, 3, nil, 5].drop_while { |n| n }.should == [nil, 5] + end + + it "removes elements from the start of the array until the block returns false" do + [1, 2, 3, false, 5].drop_while { |n| n }.should == [false, 5] + end + + it 'returns a Array instance for Array subclasses' do + ArraySpecs::MyArray[1, 2, 3, 4, 5].drop_while { |n| n < 4 }.should be_an_instance_of(Array) + end +end diff --git a/spec/ruby/core/array/dup_spec.rb b/spec/ruby/core/array/dup_spec.rb new file mode 100644 index 0000000000..17f467d5fc --- /dev/null +++ b/spec/ruby/core/array/dup_spec.rb @@ -0,0 +1,31 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/clone' + +describe "Array#dup" do + it_behaves_like :array_clone, :dup # FIX: no, clone and dup are not alike + + it "does not copy frozen status from the original" do + a = [1, 2, 3, 4] + b = [1, 2, 3, 4] + a.freeze + aa = a.dup + bb = b.dup + + aa.frozen?.should be_false + bb.frozen?.should be_false + end + + it "does not copy singleton methods" do + a = [1, 2, 3, 4] + b = [1, 2, 3, 4] + def a.a_singleton_method; end + aa = a.dup + bb = b.dup + + a.respond_to?(:a_singleton_method).should be_true + b.respond_to?(:a_singleton_method).should be_false + aa.respond_to?(:a_singleton_method).should be_false + bb.respond_to?(:a_singleton_method).should be_false + end +end diff --git a/spec/ruby/core/array/each_index_spec.rb b/spec/ruby/core/array/each_index_spec.rb new file mode 100644 index 0000000000..3a4bca9251 --- /dev/null +++ b/spec/ruby/core/array/each_index_spec.rb @@ -0,0 +1,58 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/enumeratorize' +require_relative '../enumerable/shared/enumeratorized' + +# Modifying a collection while the contents are being iterated +# gives undefined behavior. See +# https://blade.ruby-lang.org/ruby-core/23633 + +describe "Array#each_index" do + before :each do + ScratchPad.record [] + end + + it "passes the index of each element to the block" do + a = ['a', 'b', 'c', 'd'] + a.each_index { |i| ScratchPad << i } + ScratchPad.recorded.should == [0, 1, 2, 3] + end + + it "returns self" do + a = [:a, :b, :c] + a.each_index { |i| }.should equal(a) + end + + it "is not confused by removing elements from the front" do + a = [1, 2, 3] + + a.shift + ScratchPad.record [] + a.each_index { |i| ScratchPad << i } + ScratchPad.recorded.should == [0, 1] + + a.shift + ScratchPad.record [] + a.each_index { |i| ScratchPad << i } + ScratchPad.recorded.should == [0] + end + + it_behaves_like :enumeratorize, :each_index + it_behaves_like :enumeratorized_with_origin_size, :each_index, [1,2,3] +end + +describe "Array#each_index" do + it "tolerates increasing an array size during iteration" do + array = [:a, :b, :c] + ScratchPad.record [] + i = 0 + + array.each_index do |index| + ScratchPad << index + array << i if i < 100 + i += 1 + end + + ScratchPad.recorded.should == (0..102).to_a # element indices + end +end diff --git a/spec/ruby/core/array/each_spec.rb b/spec/ruby/core/array/each_spec.rb new file mode 100644 index 0000000000..f4b5b758d0 --- /dev/null +++ b/spec/ruby/core/array/each_spec.rb @@ -0,0 +1,82 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/enumeratorize' +require_relative 'shared/iterable_and_tolerating_size_increasing' +require_relative '../enumerable/shared/enumeratorized' + +# Mutating the array while it is being iterated is discouraged as it can result in confusing behavior. +# Yet a Ruby implementation must not crash in such a case, and following the simple CRuby behavior makes sense. +# CRuby simply reads the array storage and checks the size for every iteration; +# like `i = 0; while i < size; yield self[i]; i += 1; end` + +describe "Array#each" do + it "yields each element to the block" do + a = [] + x = [1, 2, 3] + x.each { |item| a << item }.should equal(x) + a.should == [1, 2, 3] + end + + it "yields each element to the block even if the array is changed during iteration" do + a = [1, 2, 3, 4, 5] + iterated = [] + a.each { |x| iterated << x; a << x+5 if x.even? } + iterated.should == [1, 2, 3, 4, 5, 7, 9] + end + + it "yields only elements that are still in the array" do + a = [0, 1, 2, 3, 4] + iterated = [] + a.each { |x| iterated << x; a.pop if x.even? } + iterated.should == [0, 1, 2] + end + + it "yields elements based on an internal index" do + a = [0, 1, 2, 3, 4] + iterated = [] + a.each { |x| iterated << x; a.shift if x.even? } + iterated.should == [0, 2, 4] + end + + it "yields the same element multiple times if inserting while iterating" do + a = [1, 2] + iterated = [] + a.each { |x| iterated << x; a.unshift(0) if a.size == 2 } + iterated.should == [1, 1, 2] + end + + it "yields each element to a block that takes multiple arguments" do + a = [[1, 2], :a, [3, 4]] + b = [] + + a.each { |x, y| b << x } + b.should == [1, :a, 3] + + b = [] + a.each { |x, y| b << y } + b.should == [2, nil, 4] + end + + it "yields elements added to the end of the array by the block" do + a = [2] + iterated = [] + a.each { |x| iterated << x; x.times { a << 0 } } + + iterated.should == [2, 0, 0] + end + + it "does not yield elements deleted from the end of the array" do + a = [2, 3, 1] + iterated = [] + a.each { |x| iterated << x; a.delete_at(2) if x == 3 } + + iterated.should == [2, 3] + end + + it_behaves_like :enumeratorize, :each + it_behaves_like :enumeratorized_with_origin_size, :each, [1,2,3] +end + +describe "Array#each" do + it_behaves_like :array_iterable_and_tolerating_size_increasing, :each +end diff --git a/spec/ruby/core/array/element_reference_spec.rb b/spec/ruby/core/array/element_reference_spec.rb new file mode 100644 index 0000000000..31e5578a09 --- /dev/null +++ b/spec/ruby/core/array/element_reference_spec.rb @@ -0,0 +1,50 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/slice' + +describe "Array#[]" do + it_behaves_like :array_slice, :[] +end + +describe "Array.[]" do + it "[] should return a new array populated with the given elements" do + array = Array[1, 'a', nil] + array[0].should == 1 + array[1].should == 'a' + array[2].should == nil + end + + it "when applied to a literal nested array, unpacks its elements into the containing array" do + Array[1, 2, *[3, 4, 5]].should == [1, 2, 3, 4, 5] + end + + it "when applied to a nested referenced array, unpacks its elements into the containing array" do + splatted_array = Array[3, 4, 5] + Array[1, 2, *splatted_array].should == [1, 2, 3, 4, 5] + end + + it "can unpack 2 or more nested referenced array" do + splatted_array = Array[3, 4, 5] + splatted_array2 = Array[6, 7, 8] + Array[1, 2, *splatted_array, *splatted_array2].should == [1, 2, 3, 4, 5, 6, 7, 8] + end + + it "constructs a nested Hash for tailing key-value pairs" do + Array[1, 2, 3 => 4, 5 => 6].should == [1, 2, { 3 => 4, 5 => 6 }] + end + + describe "with a subclass of Array" do + before :each do + ScratchPad.clear + end + + it "returns an instance of the subclass" do + ArraySpecs::MyArray[1, 2, 3].should be_an_instance_of(ArraySpecs::MyArray) + end + + it "does not call #initialize on the subclass instance" do + ArraySpecs::MyArray[1, 2, 3].should == [1, 2, 3] + ScratchPad.recorded.should be_nil + end + end +end diff --git a/spec/ruby/core/array/element_set_spec.rb b/spec/ruby/core/array/element_set_spec.rb new file mode 100644 index 0000000000..df5ca9582e --- /dev/null +++ b/spec/ruby/core/array/element_set_spec.rb @@ -0,0 +1,537 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Array#[]=" do + it "sets the value of the element at index" do + a = [1, 2, 3, 4] + a[2] = 5 + a[-1] = 6 + a[5] = 3 + a.should == [1, 2, 5, 6, nil, 3] + + a = [] + a[4] = "e" + a.should == [nil, nil, nil, nil, "e"] + a[3] = "d" + a.should == [nil, nil, nil, "d", "e"] + a[0] = "a" + a.should == ["a", nil, nil, "d", "e"] + a[-3] = "C" + a.should == ["a", nil, "C", "d", "e"] + a[-1] = "E" + a.should == ["a", nil, "C", "d", "E"] + a[-5] = "A" + a.should == ["A", nil, "C", "d", "E"] + a[5] = "f" + a.should == ["A", nil, "C", "d", "E", "f"] + a[1] = [] + a.should == ["A", [], "C", "d", "E", "f"] + a[-1] = nil + a.should == ["A", [], "C", "d", "E", nil] + end + + it "sets the section defined by [start,length] to other" do + a = [1, 2, 3, 4, 5, 6] + a[0, 1] = 2 + a[3, 2] = ['a', 'b', 'c', 'd'] + a.should == [2, 2, 3, "a", "b", "c", "d", 6] + end + + it "replaces the section defined by [start,length] with the given values" do + a = [1, 2, 3, 4, 5, 6] + a[3, 2] = 'a', 'b', 'c', 'd' + a.should == [1, 2, 3, "a", "b", "c", "d", 6] + end + + it "just sets the section defined by [start,length] to other even if other is nil" do + a = ['a', 'b', 'c', 'd', 'e'] + a[1, 3] = nil + a.should == ["a", nil, "e"] + end + + it "returns nil if the rhs is nil" do + a = [1, 2, 3] + (a[1, 3] = nil).should == nil + (a[1..3] = nil).should == nil + end + + it "sets the section defined by range to other" do + a = [6, 5, 4, 3, 2, 1] + a[1...2] = 9 + a[3..6] = [6, 6, 6] + a.should == [6, 9, 4, 6, 6, 6] + end + + it "replaces the section defined by range with the given values" do + a = [6, 5, 4, 3, 2, 1] + a[3..6] = :a, :b, :c + a.should == [6, 5, 4, :a, :b, :c] + end + + it "just sets the section defined by range to other even if other is nil" do + a = [1, 2, 3, 4, 5] + a[0..1] = nil + a.should == [nil, 3, 4, 5] + end + + it 'expands and nil-pads the array if section assigned by range is outside array boundaries' do + a = ['a'] + a[3..4] = ['b', 'c'] + a.should == ['a', nil, nil, 'b', 'c'] + end + + it "calls to_int on its start and length arguments" do + obj = mock('to_int') + obj.stub!(:to_int).and_return(2) + + a = [1, 2, 3, 4] + a[obj, 0] = [9] + a.should == [1, 2, 9, 3, 4] + a[obj, obj] = [] + a.should == [1, 2, 4] + a[obj] = -1 + a.should == [1, 2, -1] + end + + it "checks frozen before attempting to coerce arguments" do + a = [1,2,3,4].freeze + -> {a[:foo] = 1}.should raise_error(FrozenError) + -> {a[:foo, :bar] = 1}.should raise_error(FrozenError) + end + + it "sets elements in the range arguments when passed ranges" do + ary = [1, 2, 3] + rhs = [nil, [], ["x"], ["x", "y"]] + (0 .. ary.size + 2).each do |a| + (a .. ary.size + 3).each do |b| + rhs.each do |c| + ary1 = ary.dup + ary1[a .. b] = c + ary2 = ary.dup + ary2[a, 1 + b-a] = c + ary1.should == ary2 + + ary1 = ary.dup + ary1[a ... b] = c + ary2 = ary.dup + ary2[a, b-a] = c + ary1.should == ary2 + end + end + end + end + + it "inserts the given elements with [range] which the range is zero-width" do + ary = [1, 2, 3] + ary[1...1] = 0 + ary.should == [1, 0, 2, 3] + ary[1...1] = [5] + ary.should == [1, 5, 0, 2, 3] + ary[1...1] = :a, :b, :c + ary.should == [1, :a, :b, :c, 5, 0, 2, 3] + end + + it "inserts the given elements with [start, length] which length is zero" do + ary = [1, 2, 3] + ary[1, 0] = 0 + ary.should == [1, 0, 2, 3] + ary[1, 0] = [5] + ary.should == [1, 5, 0, 2, 3] + ary[1, 0] = :a, :b, :c + ary.should == [1, :a, :b, :c, 5, 0, 2, 3] + end + + # Now we only have to test cases where the start, length interface would + # have raise an exception because of negative size + it "inserts the given elements with [range] which the range has negative width" do + ary = [1, 2, 3] + ary[1..0] = 0 + ary.should == [1, 0, 2, 3] + ary[1..0] = [4, 3] + ary.should == [1, 4, 3, 0, 2, 3] + ary[1..0] = :a, :b, :c + ary.should == [1, :a, :b, :c, 4, 3, 0, 2, 3] + end + + it "just inserts nil if the section defined by range is zero-width and the rhs is nil" do + ary = [1, 2, 3] + ary[1...1] = nil + ary.should == [1, nil, 2, 3] + end + + it "just inserts nil if the section defined by range has negative width and the rhs is nil" do + ary = [1, 2, 3] + ary[1..0] = nil + ary.should == [1, nil, 2, 3] + end + + it "does nothing if the section defined by range is zero-width and the rhs is an empty array" do + ary = [1, 2, 3] + ary[1...1] = [] + ary.should == [1, 2, 3] + end + + it "does nothing if the section defined by range has negative width and the rhs is an empty array" do + ary = [1, 2, 3, 4, 5] + ary[1...0] = [] + ary.should == [1, 2, 3, 4, 5] + ary[-2..2] = [] + ary.should == [1, 2, 3, 4, 5] + end + + it "tries to convert Range elements to Integers using #to_int with [m..n] and [m...n]" do + from = mock('from') + to = mock('to') + + # So we can construct a range out of them... + def from.<=>(o) 0 end + def to.<=>(o) 0 end + + def from.to_int() 1 end + def to.to_int() -2 end + + a = [1, 2, 3, 4] + + a[from .. to] = ["a", "b", "c"] + a.should == [1, "a", "b", "c", 4] + + a[to .. from] = ["x"] + a.should == [1, "a", "b", "x", "c", 4] + -> { a["a" .. "b"] = [] }.should raise_error(TypeError) + -> { a[from .. "b"] = [] }.should raise_error(TypeError) + end + + it "raises an IndexError when passed indexes out of bounds" do + a = [1, 2, 3, 4] + -> { a[-5] = "" }.should raise_error(IndexError) + -> { a[-5, -1] = "" }.should raise_error(IndexError) + -> { a[-5, 0] = "" }.should raise_error(IndexError) + -> { a[-5, 1] = "" }.should raise_error(IndexError) + -> { a[-5, 2] = "" }.should raise_error(IndexError) + -> { a[-5, 10] = "" }.should raise_error(IndexError) + + -> { a[-5..-5] = "" }.should raise_error(RangeError) + -> { a[-5...-5] = "" }.should raise_error(RangeError) + -> { a[-5..-4] = "" }.should raise_error(RangeError) + -> { a[-5...-4] = "" }.should raise_error(RangeError) + -> { a[-5..10] = "" }.should raise_error(RangeError) + -> { a[-5...10] = "" }.should raise_error(RangeError) + + # ok + a[0..-9] = [1] + a.should == [1, 1, 2, 3, 4] + end + + it "calls to_ary on its rhs argument for multi-element sets" do + obj = mock('to_ary') + def obj.to_ary() [1, 2, 3] end + ary = [1, 2] + ary[0, 0] = obj + ary.should == [1, 2, 3, 1, 2] + ary[1, 10] = obj + ary.should == [1, 1, 2, 3] + end + + it "does not call to_ary on rhs array subclasses for multi-element sets" do + ary = [] + ary[0, 0] = ArraySpecs::ToAryArray[5, 6, 7] + ary.should == [5, 6, 7] + end + + it "raises a FrozenError on a frozen array" do + -> { ArraySpecs.frozen_array[0, 0] = [] }.should raise_error(FrozenError) + end +end + +describe "Array#[]= with [index]" do + it "returns value assigned if idx is inside array" do + a = [1, 2, 3, 4, 5] + (a[3] = 6).should == 6 + end + + it "returns value assigned if idx is right beyond right array boundary" do + a = [1, 2, 3, 4, 5] + (a[5] = 6).should == 6 + end + + it "returns value assigned if idx far beyond right array boundary" do + a = [1, 2, 3, 4, 5] + (a[10] = 6).should == 6 + end + + it "sets the value of the element at index" do + a = [1, 2, 3, 4] + a[2] = 5 + a[-1] = 6 + a[5] = 3 + a.should == [1, 2, 5, 6, nil, 3] + end + + it "sets the value of the element if it is right beyond the array boundary" do + a = [1, 2, 3, 4] + a[4] = 8 + a.should == [1, 2, 3, 4, 8] + end + +end + +describe "Array#[]= with [index, count]" do + it "returns non-array value if non-array value assigned" do + a = [1, 2, 3, 4, 5] + (a[2, 3] = 10).should == 10 + end + + it "returns array if array assigned" do + a = [1, 2, 3, 4, 5] + (a[2, 3] = [4, 5]).should == [4, 5] + end + + it "accepts a frozen String literal as RHS" do + a = ['a', 'b', 'c'] + a[0, 2] = 'd'.freeze + a.should == ['d', 'c'] + end + + it "just sets the section defined by [start,length] to nil even if the rhs is nil" do + a = ['a', 'b', 'c', 'd', 'e'] + a[1, 3] = nil + a.should == ["a", nil, "e"] + end + + it "just sets the section defined by [start,length] to nil if negative index within bounds, cnt > 0 and the rhs is nil" do + a = ['a', 'b', 'c', 'd', 'e'] + a[-3, 2] = nil + a.should == ["a", "b", nil, "e"] + end + + it "replaces the section defined by [start,length] to other" do + a = [1, 2, 3, 4, 5, 6] + a[0, 1] = 2 + a[3, 2] = ['a', 'b', 'c', 'd'] + a.should == [2, 2, 3, "a", "b", "c", "d", 6] + end + + it "replaces the section to other if idx < 0 and cnt > 0" do + a = [1, 2, 3, 4, 5, 6] + a[-3, 2] = ["x", "y", "z"] + a.should == [1, 2, 3, "x", "y", "z", 6] + end + + it "replaces the section to other even if cnt spanning beyond the array boundary" do + a = [1, 2, 3, 4, 5] + a[-1, 3] = [7, 8] + a.should == [1, 2, 3, 4, 7, 8] + end + + it "pads the Array with nils if the span is past the end" do + a = [1, 2, 3, 4, 5] + a[10, 1] = [1] + a.should == [1, 2, 3, 4, 5, nil, nil, nil, nil, nil, 1] + + b = [1, 2, 3, 4, 5] + b[10, 0] = [1] + a.should == [1, 2, 3, 4, 5, nil, nil, nil, nil, nil, 1] + + c = [1, 2, 3, 4, 5] + c[10, 0] = [] + c.should == [1, 2, 3, 4, 5, nil, nil, nil, nil, nil] + end + + it "inserts other section in place defined by idx" do + a = [1, 2, 3, 4, 5] + a[3, 0] = [7, 8] + a.should == [1, 2, 3, 7, 8, 4, 5] + + b = [1, 2, 3, 4, 5] + b[1, 0] = b + b.should == [1, 1, 2, 3, 4, 5, 2, 3, 4, 5] + end + + it "raises an IndexError when passed start and negative length" do + a = [1, 2, 3, 4] + -> { a[-2, -1] = "" }.should raise_error(IndexError) + -> { a[0, -1] = "" }.should raise_error(IndexError) + -> { a[2, -1] = "" }.should raise_error(IndexError) + -> { a[4, -1] = "" }.should raise_error(IndexError) + -> { a[10, -1] = "" }.should raise_error(IndexError) + -> { [1, 2, 3, 4, 5][2, -1] = [7, 8] }.should raise_error(IndexError) + end +end + +describe "Array#[]= with [m..n]" do + it "returns non-array value if non-array value assigned" do + a = [1, 2, 3, 4, 5] + (a[2..4] = 10).should == 10 + (a.[]=(2..4, 10)).should == 10 + end + + it "returns array if array assigned" do + a = [1, 2, 3, 4, 5] + (a[2..4] = [7, 8]).should == [7, 8] + (a.[]=(2..4, [7, 8])).should == [7, 8] + end + + it "just sets the section defined by range to nil even if the rhs is nil" do + a = [1, 2, 3, 4, 5] + a[0..1] = nil + a.should == [nil, 3, 4, 5] + end + + it "just sets the section defined by range to nil if m and n < 0 and the rhs is nil" do + a = [1, 2, 3, 4, 5] + a[-3..-2] = nil + a.should == [1, 2, nil, 5] + end + + it "replaces the section defined by range" do + a = [6, 5, 4, 3, 2, 1] + a[1...2] = 9 + a[3..6] = [6, 6, 6] + a.should == [6, 9, 4, 6, 6, 6] + end + + it "replaces the section if m and n < 0" do + a = [1, 2, 3, 4, 5] + a[-3..-2] = [7, 8, 9] + a.should == [1, 2, 7, 8, 9, 5] + end + + it "replaces the section if m < 0 and n > 0" do + a = [1, 2, 3, 4, 5] + a[-4..3] = [8] + a.should == [1, 8, 5] + end + + it "inserts the other section at m if m > n" do + a = [1, 2, 3, 4, 5] + a[3..1] = [8] + a.should == [1, 2, 3, 8, 4, 5] + end + + it "inserts at the end if m > the array size" do + a = [1, 2, 3] + a[3..3] = [4] + a.should == [1, 2, 3, 4] + a[5..7] = [6] + a.should == [1, 2, 3, 4, nil, 6] + end + + describe "Range subclasses" do + before :each do + @range_incl = ArraySpecs::MyRange.new(1, 2) + @range_excl = ArraySpecs::MyRange.new(-3, -1, true) + end + + it "accepts Range subclasses" do + a = [1, 2, 3, 4] + + a[@range_incl] = ["a", "b"] + a.should == [1, "a", "b", 4] + a[@range_excl] = ["A", "B"] + a.should == [1, "A", "B", 4] + end + + it "returns non-array value if non-array value assigned" do + a = [1, 2, 3, 4, 5] + (a[@range_incl] = 10).should == 10 + (a.[]=(@range_incl, 10)).should == 10 + end + + it "returns array if array assigned" do + a = [1, 2, 3, 4, 5] + (a[@range_incl] = [7, 8]).should == [7, 8] + a.[]=(@range_incl, [7, 8]).should == [7, 8] + end + end +end + +describe "Array#[]= with [m..]" do + it "just sets the section defined by range to nil even if the rhs is nil" do + a = [1, 2, 3, 4, 5] + a[eval("(2..)")] = nil + a.should == [1, 2, nil] + end + + it "just sets the section defined by range to nil if m and n < 0 and the rhs is nil" do + a = [1, 2, 3, 4, 5] + a[eval("(-3..)")] = nil + a.should == [1, 2, nil] + end + + it "replaces the section defined by range" do + a = [6, 5, 4, 3, 2, 1] + a[eval("(3...)")] = 9 + a.should == [6, 5, 4, 9] + a[eval("(2..)")] = [7, 7, 7] + a.should == [6, 5, 7, 7, 7] + end + + it "replaces the section if m and n < 0" do + a = [1, 2, 3, 4, 5] + a[eval("(-3..)")] = [7, 8, 9] + a.should == [1, 2, 7, 8, 9] + end + + it "inserts at the end if m > the array size" do + a = [1, 2, 3] + a[eval("(3..)")] = [4] + a.should == [1, 2, 3, 4] + a[eval("(5..)")] = [6] + a.should == [1, 2, 3, 4, nil, 6] + end +end + +describe "Array#[]= with [..n] and [...n]" do + it "just sets the section defined by range to nil even if the rhs is nil" do + a = [1, 2, 3, 4, 5] + a[(..2)] = nil + a.should == [nil, 4, 5] + a[(...2)] = nil + a.should == [nil, 5] + end + + it "just sets the section defined by range to nil if n < 0 and the rhs is nil" do + a = [1, 2, 3, 4, 5] + a[(..-3)] = nil + a.should == [nil, 4, 5] + a[(...-1)] = [nil, 5] + end + + it "replaces the section defined by range" do + a = [6, 5, 4, 3, 2, 1] + a[(...3)] = 9 + a.should == [9, 3, 2, 1] + a[(..2)] = [7, 7, 7, 7, 7] + a.should == [7, 7, 7, 7, 7, 1] + end + + it "replaces the section if n < 0" do + a = [1, 2, 3, 4, 5] + a[(..-2)] = [7, 8, 9] + a.should == [7, 8, 9, 5] + end + + it "replaces everything if n > the array size" do + a = [1, 2, 3] + a[(...7)] = [4] + a.should == [4] + end + + it "inserts at the beginning if n < negative the array size" do + a = [1, 2, 3] + a[(..-7)] = [4] + a.should == [4, 1, 2, 3] + a[(...-10)] = [6] + a.should == [6, 4, 1, 2, 3] + end +end + +describe "Array#[] after a shift" do + it "works for insertion" do + a = [1,2] + a.shift + a.shift + a[0,0] = [3,4] + a.should == [3,4] + end +end diff --git a/spec/ruby/core/array/empty_spec.rb b/spec/ruby/core/array/empty_spec.rb new file mode 100644 index 0000000000..f70b1b6ebe --- /dev/null +++ b/spec/ruby/core/array/empty_spec.rb @@ -0,0 +1,10 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Array#empty?" do + it "returns true if the array has no elements" do + [].should.empty? + [1].should_not.empty? + [1, 2].should_not.empty? + end +end diff --git a/spec/ruby/core/array/eql_spec.rb b/spec/ruby/core/array/eql_spec.rb new file mode 100644 index 0000000000..8565b94c60 --- /dev/null +++ b/spec/ruby/core/array/eql_spec.rb @@ -0,0 +1,19 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/eql' + +describe "Array#eql?" do + it_behaves_like :array_eql, :eql? + + it "returns false if any corresponding elements are not #eql?" do + [1, 2, 3, 4].should_not eql([1, 2, 3, 4.0]) + end + + it "returns false if other is not a kind of Array" do + obj = mock("array eql?") + obj.should_not_receive(:to_ary) + obj.should_not_receive(:eql?) + + [1, 2, 3].should_not eql(obj) + end +end diff --git a/spec/ruby/core/array/equal_value_spec.rb b/spec/ruby/core/array/equal_value_spec.rb new file mode 100644 index 0000000000..a82e07b218 --- /dev/null +++ b/spec/ruby/core/array/equal_value_spec.rb @@ -0,0 +1,51 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/eql' + +describe "Array#==" do + it_behaves_like :array_eql, :== + + it "compares with an equivalent Array-like object using #to_ary" do + obj = mock('array-like') + obj.should_receive(:respond_to?).at_least(1).with(:to_ary).and_return(true) + obj.should_receive(:==).with([1]).at_least(1).and_return(true) + + ([1] == obj).should be_true + ([[1]] == [obj]).should be_true + ([[[1], 3], 2] == [[obj, 3], 2]).should be_true + + # recursive arrays + arr1 = [[1]] + arr1 << arr1 + arr2 = [obj] + arr2 << arr2 + (arr1 == arr2).should be_true + (arr2 == arr1).should be_true + end + + it "returns false if any corresponding elements are not #==" do + a = ["a", "b", "c"] + b = ["a", "b", "not equal value"] + a.should_not == b + + c = mock("c") + c.should_receive(:==).and_return(false) + ["a", "b", c].should_not == a + end + + it "returns true if corresponding elements are #==" do + [].should == [] + ["a", "c", 7].should == ["a", "c", 7] + + [1, 2, 3].should == [1.0, 2.0, 3.0] + + obj = mock('5') + obj.should_receive(:==).and_return(true) + [obj].should == [5] + end + + # See https://bugs.ruby-lang.org/issues/1720 + it "returns true for [NaN] == [NaN] because Array#== first checks with #equal? and NaN.equal?(NaN) is true" do + [Float::NAN].should == [Float::NAN] + end +end diff --git a/spec/ruby/core/array/fetch_spec.rb b/spec/ruby/core/array/fetch_spec.rb new file mode 100644 index 0000000000..b81c0b48d7 --- /dev/null +++ b/spec/ruby/core/array/fetch_spec.rb @@ -0,0 +1,55 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Array#fetch" do + it "returns the element at the passed index" do + [1, 2, 3].fetch(1).should == 2 + [nil].fetch(0).should == nil + end + + it "counts negative indices backwards from end" do + [1, 2, 3, 4].fetch(-1).should == 4 + end + + it "raises an IndexError if there is no element at index" do + -> { [1, 2, 3].fetch(3) }.should raise_error(IndexError) + -> { [1, 2, 3].fetch(-4) }.should raise_error(IndexError) + -> { [].fetch(0) }.should raise_error(IndexError) + end + + it "returns default if there is no element at index if passed a default value" do + [1, 2, 3].fetch(5, :not_found).should == :not_found + [1, 2, 3].fetch(5, nil).should == nil + [1, 2, 3].fetch(-4, :not_found).should == :not_found + [nil].fetch(0, :not_found).should == nil + end + + it "returns the value of block if there is no element at index if passed a block" do + [1, 2, 3].fetch(9) { |i| i * i }.should == 81 + [1, 2, 3].fetch(-9) { |i| i * i }.should == 81 + end + + it "passes the original index argument object to the block, not the converted Integer" do + o = mock('5') + def o.to_int(); 5; end + + [1, 2, 3].fetch(o) { |i| i }.should equal(o) + end + + it "gives precedence to the default block over the default argument" do + -> { + @result = [1, 2, 3].fetch(9, :foo) { |i| i * i } + }.should complain(/block supersedes default value argument/) + @result.should == 81 + end + + it "tries to convert the passed argument to an Integer using #to_int" do + obj = mock('to_int') + obj.should_receive(:to_int).and_return(2) + ["a", "b", "c"].fetch(obj).should == "c" + end + + it "raises a TypeError when the passed argument can't be coerced to Integer" do + -> { [].fetch("cat") }.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/array/fetch_values_spec.rb b/spec/ruby/core/array/fetch_values_spec.rb new file mode 100644 index 0000000000..cf377b3b71 --- /dev/null +++ b/spec/ruby/core/array/fetch_values_spec.rb @@ -0,0 +1,55 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Array#fetch_values" do + before :each do + @array = [:a, :b, :c] + end + + ruby_version_is "3.4" do + describe "with matched indexes" do + it "returns the values for indexes" do + @array.fetch_values(0).should == [:a] + @array.fetch_values(0, 2).should == [:a, :c] + @array.fetch_values(-1).should == [:c] + end + + it "returns the values for indexes ordered in the order of the requested indexes" do + @array.fetch_values(2, 0).should == [:c, :a] + end + end + + describe "with unmatched indexes" do + it "raises a index error if no block is provided" do + -> { @array.fetch_values(0, 1, 44) }.should raise_error(IndexError, "index 44 outside of array bounds: -3...3") + end + + it "returns the default value from block" do + @array.fetch_values(44) { |index| "`#{index}' is not found" }.should == ["`44' is not found"] + @array.fetch_values(0, 44) { |index| "`#{index}' is not found" }.should == [:a, "`44' is not found"] + end + end + + describe "without keys" do + it "returns an empty Array" do + @array.fetch_values.should == [] + end + end + + it "tries to convert the passed argument to an Integer using #to_int" do + obj = mock('to_int') + obj.should_receive(:to_int).and_return(2) + @array.fetch_values(obj).should == [:c] + end + + it "does not support a Range object as argument" do + -> { + @array.fetch_values(1..2) + }.should raise_error(TypeError, "no implicit conversion of Range into Integer") + end + + it "raises a TypeError when the passed argument can't be coerced to Integer" do + -> { [].fetch_values("cat") }.should raise_error(TypeError, "no implicit conversion of String into Integer") + end + end +end diff --git a/spec/ruby/core/array/fill_spec.rb b/spec/ruby/core/array/fill_spec.rb new file mode 100644 index 0000000000..2c3b5d9e84 --- /dev/null +++ b/spec/ruby/core/array/fill_spec.rb @@ -0,0 +1,374 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Array#fill" do + before :all do + @never_passed = -> i do + raise ExpectationNotMetError, "the control path should not pass here" + end + end + + it "returns self" do + ary = [1, 2, 3] + ary.fill(:a).should equal(ary) + end + + it "is destructive" do + ary = [1, 2, 3] + ary.fill(:a) + ary.should == [:a, :a, :a] + end + + it "does not replicate the filler" do + ary = [1, 2, 3, 4] + str = +"x" + ary.fill(str).should == [str, str, str, str] + str << "y" + ary.should == [str, str, str, str] + ary[0].should equal(str) + ary[1].should equal(str) + ary[2].should equal(str) + ary[3].should equal(str) + end + + it "replaces all elements in the array with the filler if not given a index nor a length" do + ary = ['a', 'b', 'c', 'duh'] + ary.fill(8).should == [8, 8, 8, 8] + + str = "x" + ary.fill(str).should == [str, str, str, str] + end + + it "replaces all elements with the value of block (index given to block)" do + [nil, nil, nil, nil].fill { |i| i * 2 }.should == [0, 2, 4, 6] + end + + it "raises a FrozenError on a frozen array" do + -> { ArraySpecs.frozen_array.fill('x') }.should raise_error(FrozenError) + end + + it "raises a FrozenError on an empty frozen array" do + -> { ArraySpecs.empty_frozen_array.fill('x') }.should raise_error(FrozenError) + end + + it "raises an ArgumentError if 4 or more arguments are passed when no block given" do + [].fill('a').should == [] + [].fill('a', 1).should == [] + [].fill('a', 1, 2).should == [nil, 'a', 'a'] + -> { [].fill('a', 1, 2, true) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if no argument passed and no block given" do + -> { [].fill }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if 3 or more arguments are passed when a block given" do + [].fill() {|i|}.should == [] + [].fill(1) {|i|}.should == [] + [].fill(1, 2) {|i|}.should == [nil, nil, nil] + -> { [].fill(1, 2, true) {|i|} }.should raise_error(ArgumentError) + end + + it "does not truncate the array is the block raises an exception" do + a = [1, 2, 3] + begin + a.fill { raise StandardError, 'Oops' } + rescue + end + + a.should == [1, 2, 3] + end + + it "only changes elements before error is raised, keeping the element which raised an error." do + a = [1, 2, 3, 4] + begin + a.fill do |i| + case i + when 0 then -1 + when 1 then -2 + when 2 then raise StandardError, 'Oops' + else 0 + end + end + rescue StandardError + end + + a.should == [-1, -2, 3, 4] + end + + it "tolerates increasing an array size during iteration" do + array = [:a, :b, :c] + ScratchPad.record [] + i = 0 + + array.fill do |index| + ScratchPad << index + array << i if i < 100 + i++ + index + end + + ScratchPad.recorded.should == [0, 1, 2] + end +end + +describe "Array#fill with (filler, index, length)" do + it "replaces length elements beginning with the index with the filler if given an index and a length" do + ary = [1, 2, 3, 4, 5, 6] + ary.fill('x', 2, 3).should == [1, 2, 'x', 'x', 'x', 6] + end + + it "replaces length elements beginning with the index with the value of block" do + [true, false, true, false, true, false, true].fill(1, 4) { |i| i + 3 }.should == [true, 4, 5, 6, 7, false, true] + end + + it "replaces all elements after the index if given an index and no length" do + ary = [1, 2, 3] + ary.fill('x', 1).should == [1, 'x', 'x'] + ary.fill(1){|i| i*2}.should == [1, 2, 4] + end + + it "replaces all elements after the index if given an index and nil as a length" do + a = [1, 2, 3] + a.fill('x', 1, nil).should == [1, 'x', 'x'] + a.fill(1, nil){|i| i*2}.should == [1, 2, 4] + a.fill('y', nil).should == ['y', 'y', 'y'] + end + + it "replaces the last (-n) elements if given an index n which is negative and no length" do + a = [1, 2, 3, 4, 5] + a.fill('x', -2).should == [1, 2, 3, 'x', 'x'] + a.fill(-2){|i| i.to_s}.should == [1, 2, 3, '3', '4'] + end + + it "replaces the last (-n) elements if given an index n which is negative and nil as a length" do + a = [1, 2, 3, 4, 5] + a.fill('x', -2, nil).should == [1, 2, 3, 'x', 'x'] + a.fill(-2, nil){|i| i.to_s}.should == [1, 2, 3, '3', '4'] + end + + it "makes no modifications if given an index greater than end and no length" do + [1, 2, 3, 4, 5].fill('a', 5).should == [1, 2, 3, 4, 5] + [1, 2, 3, 4, 5].fill(5, &@never_passed).should == [1, 2, 3, 4, 5] + end + + it "makes no modifications if given an index greater than end and nil as a length" do + [1, 2, 3, 4, 5].fill('a', 5, nil).should == [1, 2, 3, 4, 5] + [1, 2, 3, 4, 5].fill(5, nil, &@never_passed).should == [1, 2, 3, 4, 5] + end + + it "replaces length elements beginning with start index if given an index >= 0 and a length >= 0" do + [1, 2, 3, 4, 5].fill('a', 2, 0).should == [1, 2, 3, 4, 5] + [1, 2, 3, 4, 5].fill('a', 2, 2).should == [1, 2, "a", "a", 5] + + [1, 2, 3, 4, 5].fill(2, 0, &@never_passed).should == [1, 2, 3, 4, 5] + [1, 2, 3, 4, 5].fill(2, 2){|i| i*2}.should == [1, 2, 4, 6, 5] + end + + it "increases the Array size when necessary" do + a = [1, 2, 3] + a.size.should == 3 + a.fill 'a', 0, 10 + a.size.should == 10 + end + + it "pads between the last element and the index with nil if given an index which is greater than size of the array" do + [1, 2, 3, 4, 5].fill('a', 8, 5).should == [1, 2, 3, 4, 5, nil, nil, nil, 'a', 'a', 'a', 'a', 'a'] + [1, 2, 3, 4, 5].fill(8, 5){|i| 'a'}.should == [1, 2, 3, 4, 5, nil, nil, nil, 'a', 'a', 'a', 'a', 'a'] + end + + it "replaces length elements beginning with the (-n)th if given an index n < 0 and a length > 0" do + [1, 2, 3, 4, 5].fill('a', -2, 2).should == [1, 2, 3, "a", "a"] + [1, 2, 3, 4, 5].fill('a', -2, 4).should == [1, 2, 3, "a", "a", "a", "a"] + + [1, 2, 3, 4, 5].fill(-2, 2){|i| 'a'}.should == [1, 2, 3, "a", "a"] + [1, 2, 3, 4, 5].fill(-2, 4){|i| 'a'}.should == [1, 2, 3, "a", "a", "a", "a"] + end + + it "starts at 0 if the negative index is before the start of the array" do + [1, 2, 3, 4, 5].fill('a', -25, 3).should == ['a', 'a', 'a', 4, 5] + [1, 2, 3, 4, 5].fill('a', -10, 10).should == %w|a a a a a a a a a a| + + [1, 2, 3, 4, 5].fill(-25, 3){|i| 'a'}.should == ['a', 'a', 'a', 4, 5] + [1, 2, 3, 4, 5].fill(-10, 10){|i| 'a'}.should == %w|a a a a a a a a a a| + end + + it "makes no modifications if the given length <= 0" do + [1, 2, 3, 4, 5].fill('a', 2, 0).should == [1, 2, 3, 4, 5] + [1, 2, 3, 4, 5].fill('a', -2, 0).should == [1, 2, 3, 4, 5] + + [1, 2, 3, 4, 5].fill('a', 2, -2).should == [1, 2, 3, 4, 5] + [1, 2, 3, 4, 5].fill('a', -2, -2).should == [1, 2, 3, 4, 5] + + [1, 2, 3, 4, 5].fill(2, 0, &@never_passed).should == [1, 2, 3, 4, 5] + [1, 2, 3, 4, 5].fill(-2, 0, &@never_passed).should == [1, 2, 3, 4, 5] + + [1, 2, 3, 4, 5].fill(2, -2, &@never_passed).should == [1, 2, 3, 4, 5] + [1, 2, 3, 4, 5].fill(-2, -2, &@never_passed).should == [1, 2, 3, 4, 5] + end + + # See: https://blade.ruby-lang.org/ruby-core/17481 + it "does not raise an exception if the given length is negative and its absolute value does not exceed the index" do + [1, 2, 3, 4].fill('a', 3, -1).should == [1, 2, 3, 4] + [1, 2, 3, 4].fill('a', 3, -2).should == [1, 2, 3, 4] + [1, 2, 3, 4].fill('a', 3, -3).should == [1, 2, 3, 4] + + [1, 2, 3, 4].fill(3, -1, &@never_passed).should == [1, 2, 3, 4] + [1, 2, 3, 4].fill(3, -2, &@never_passed).should == [1, 2, 3, 4] + [1, 2, 3, 4].fill(3, -3, &@never_passed).should == [1, 2, 3, 4] + end + + it "does not raise an exception even if the given length is negative and its absolute value exceeds the index" do + [1, 2, 3, 4].fill('a', 3, -4).should == [1, 2, 3, 4] + [1, 2, 3, 4].fill('a', 3, -5).should == [1, 2, 3, 4] + [1, 2, 3, 4].fill('a', 3, -10000).should == [1, 2, 3, 4] + + [1, 2, 3, 4].fill(3, -4, &@never_passed).should == [1, 2, 3, 4] + [1, 2, 3, 4].fill(3, -5, &@never_passed).should == [1, 2, 3, 4] + [1, 2, 3, 4].fill(3, -10000, &@never_passed).should == [1, 2, 3, 4] + end + + it "tries to convert the second and third arguments to Integers using #to_int" do + obj = mock('to_int') + obj.should_receive(:to_int).and_return(2, 2) + filler = mock('filler') + filler.should_not_receive(:to_int) + [1, 2, 3, 4, 5].fill(filler, obj, obj).should == [1, 2, filler, filler, 5] + end + + it "raises a TypeError if the index is not numeric" do + -> { [].fill 'a', true }.should raise_error(TypeError) + + obj = mock('nonnumeric') + -> { [].fill('a', obj) }.should raise_error(TypeError) + end + + it "raises a TypeError when the length is not numeric" do + -> { [1, 2, 3].fill("x", 1, "foo") }.should raise_error(TypeError, /no implicit conversion of String into Integer/) + -> { [1, 2, 3].fill("x", 1, :"foo") }.should raise_error(TypeError, /no implicit conversion of Symbol into Integer/) + -> { [1, 2, 3].fill("x", 1, Object.new) }.should raise_error(TypeError, /no implicit conversion of Object into Integer/) + end + + not_supported_on :opal do + it "raises an ArgumentError or RangeError for too-large sizes" do + error_types = [RangeError, ArgumentError] + arr = [1, 2, 3] + -> { arr.fill(10, 1, fixnum_max) }.should raise_error { |err| error_types.should include(err.class) } + -> { arr.fill(10, 1, bignum_value) }.should raise_error(RangeError) + end + end +end + +describe "Array#fill with (filler, range)" do + it "replaces elements in range with object" do + [1, 2, 3, 4, 5, 6].fill(8, 0..3).should == [8, 8, 8, 8, 5, 6] + [1, 2, 3, 4, 5, 6].fill(8, 0...3).should == [8, 8, 8, 4, 5, 6] + [1, 2, 3, 4, 5, 6].fill('x', 4..6).should == [1, 2, 3, 4, 'x', 'x', 'x'] + [1, 2, 3, 4, 5, 6].fill('x', 4...6).should == [1, 2, 3, 4, 'x', 'x'] + [1, 2, 3, 4, 5, 6].fill('x', -2..-1).should == [1, 2, 3, 4, 'x', 'x'] + [1, 2, 3, 4, 5, 6].fill('x', -2...-1).should == [1, 2, 3, 4, 'x', 6] + [1, 2, 3, 4, 5, 6].fill('x', -2...-2).should == [1, 2, 3, 4, 5, 6] + [1, 2, 3, 4, 5, 6].fill('x', -2..-2).should == [1, 2, 3, 4, 'x', 6] + [1, 2, 3, 4, 5, 6].fill('x', -2..0).should == [1, 2, 3, 4, 5, 6] + [1, 2, 3, 4, 5, 6].fill('x', 0...0).should == [1, 2, 3, 4, 5, 6] + [1, 2, 3, 4, 5, 6].fill('x', 1..1).should == [1, 'x', 3, 4, 5, 6] + end + + it "replaces all elements in range with the value of block" do + [1, 1, 1, 1, 1, 1].fill(1..6) { |i| i + 1 }.should == [1, 2, 3, 4, 5, 6, 7] + end + + it "increases the Array size when necessary" do + [1, 2, 3].fill('x', 1..6).should == [1, 'x', 'x', 'x', 'x', 'x', 'x'] + [1, 2, 3].fill(1..6){|i| i+1}.should == [1, 2, 3, 4, 5, 6, 7] + end + + it "raises a TypeError with range and length argument" do + -> { [].fill('x', 0 .. 2, 5) }.should raise_error(TypeError) + end + + it "replaces elements between the (-m)th to the last and the (n+1)th from the first if given an range m..n where m < 0 and n >= 0" do + [1, 2, 3, 4, 5, 6].fill('x', -4..4).should == [1, 2, 'x', 'x', 'x', 6] + [1, 2, 3, 4, 5, 6].fill('x', -4...4).should == [1, 2, 'x', 'x', 5, 6] + + [1, 2, 3, 4, 5, 6].fill(-4..4){|i| (i+1).to_s}.should == [1, 2, '3', '4', '5', 6] + [1, 2, 3, 4, 5, 6].fill(-4...4){|i| (i+1).to_s}.should == [1, 2, '3', '4', 5, 6] + end + + it "replaces elements between the (-m)th and (-n)th to the last if given an range m..n where m < 0 and n < 0" do + [1, 2, 3, 4, 5, 6].fill('x', -4..-2).should == [1, 2, 'x', 'x', 'x', 6] + [1, 2, 3, 4, 5, 6].fill('x', -4...-2).should == [1, 2, 'x', 'x', 5, 6] + + [1, 2, 3, 4, 5, 6].fill(-4..-2){|i| (i+1).to_s}.should == [1, 2, '3', '4', '5', 6] + [1, 2, 3, 4, 5, 6].fill(-4...-2){|i| (i+1).to_s}.should == [1, 2, '3', '4', 5, 6] + end + + it "replaces elements between the (m+1)th from the first and (-n)th to the last if given an range m..n where m >= 0 and n < 0" do + [1, 2, 3, 4, 5, 6].fill('x', 2..-2).should == [1, 2, 'x', 'x', 'x', 6] + [1, 2, 3, 4, 5, 6].fill('x', 2...-2).should == [1, 2, 'x', 'x', 5, 6] + + [1, 2, 3, 4, 5, 6].fill(2..-2){|i| (i+1).to_s}.should == [1, 2, '3', '4', '5', 6] + [1, 2, 3, 4, 5, 6].fill(2...-2){|i| (i+1).to_s}.should == [1, 2, '3', '4', 5, 6] + end + + it "makes no modifications if given an range which implies a section of zero width" do + [1, 2, 3, 4, 5, 6].fill('x', 2...2).should == [1, 2, 3, 4, 5, 6] + [1, 2, 3, 4, 5, 6].fill('x', -4...2).should == [1, 2, 3, 4, 5, 6] + [1, 2, 3, 4, 5, 6].fill('x', -4...-4).should == [1, 2, 3, 4, 5, 6] + [1, 2, 3, 4, 5, 6].fill('x', 2...-4).should == [1, 2, 3, 4, 5, 6] + + [1, 2, 3, 4, 5, 6].fill(2...2, &@never_passed).should == [1, 2, 3, 4, 5, 6] + [1, 2, 3, 4, 5, 6].fill(-4...2, &@never_passed).should == [1, 2, 3, 4, 5, 6] + [1, 2, 3, 4, 5, 6].fill(-4...-4, &@never_passed).should == [1, 2, 3, 4, 5, 6] + [1, 2, 3, 4, 5, 6].fill(2...-4, &@never_passed).should == [1, 2, 3, 4, 5, 6] + end + + it "makes no modifications if given an range which implies a section of negative width" do + [1, 2, 3, 4, 5, 6].fill('x', 2..1).should == [1, 2, 3, 4, 5, 6] + [1, 2, 3, 4, 5, 6].fill('x', -4..1).should == [1, 2, 3, 4, 5, 6] + [1, 2, 3, 4, 5, 6].fill('x', -2..-4).should == [1, 2, 3, 4, 5, 6] + [1, 2, 3, 4, 5, 6].fill('x', 2..-5).should == [1, 2, 3, 4, 5, 6] + + [1, 2, 3, 4, 5, 6].fill(2..1, &@never_passed).should == [1, 2, 3, 4, 5, 6] + [1, 2, 3, 4, 5, 6].fill(-4..1, &@never_passed).should == [1, 2, 3, 4, 5, 6] + [1, 2, 3, 4, 5, 6].fill(-2..-4, &@never_passed).should == [1, 2, 3, 4, 5, 6] + [1, 2, 3, 4, 5, 6].fill(2..-5, &@never_passed).should == [1, 2, 3, 4, 5, 6] + end + + it "raises an exception if some of the given range lies before the first of the array" do + -> { [1, 2, 3].fill('x', -5..-3) }.should raise_error(RangeError) + -> { [1, 2, 3].fill('x', -5...-3) }.should raise_error(RangeError) + -> { [1, 2, 3].fill('x', -5..-4) }.should raise_error(RangeError) + + -> { [1, 2, 3].fill(-5..-3, &@never_passed) }.should raise_error(RangeError) + -> { [1, 2, 3].fill(-5...-3, &@never_passed) }.should raise_error(RangeError) + -> { [1, 2, 3].fill(-5..-4, &@never_passed) }.should raise_error(RangeError) + end + + it "tries to convert the start and end of the passed range to Integers using #to_int" do + obj = mock('to_int') + def obj.<=>(rhs); rhs == self ? 0 : nil end + obj.should_receive(:to_int).twice.and_return(2) + filler = mock('filler') + filler.should_not_receive(:to_int) + [1, 2, 3, 4, 5].fill(filler, obj..obj).should == [1, 2, filler, 4, 5] + end + + it "raises a TypeError if the start or end of the passed range is not numeric" do + obj = mock('nonnumeric') + def obj.<=>(rhs); rhs == self ? 0 : nil end + -> { [].fill('a', obj..obj) }.should raise_error(TypeError) + end + + it "works with endless ranges" do + [1, 2, 3, 4].fill('x', eval("(1..)")).should == [1, 'x', 'x', 'x'] + [1, 2, 3, 4].fill('x', eval("(3...)")).should == [1, 2, 3, 'x'] + [1, 2, 3, 4].fill(eval("(1..)")) { |x| x + 2 }.should == [1, 3, 4, 5] + [1, 2, 3, 4].fill(eval("(3...)")) { |x| x + 2 }.should == [1, 2, 3, 5] + end + + it "works with beginless ranges" do + [1, 2, 3, 4].fill('x', (..2)).should == ["x", "x", "x", 4] + [1, 2, 3, 4].fill((...2)) { |x| x + 2 }.should == [2, 3, 3, 4] + end +end diff --git a/spec/ruby/core/array/filter_spec.rb b/spec/ruby/core/array/filter_spec.rb new file mode 100644 index 0000000000..156ad14f9c --- /dev/null +++ b/spec/ruby/core/array/filter_spec.rb @@ -0,0 +1,14 @@ +require_relative '../../spec_helper' +require_relative 'shared/select' + +describe "Array#filter" do + it_behaves_like :array_select, :filter +end + +describe "Array#filter!" do + it "returns nil if no changes were made in the array" do + [1, 2, 3].filter! { true }.should be_nil + end + + it_behaves_like :keep_if, :filter! +end diff --git a/spec/ruby/core/array/find_index_spec.rb b/spec/ruby/core/array/find_index_spec.rb new file mode 100644 index 0000000000..759472024a --- /dev/null +++ b/spec/ruby/core/array/find_index_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/index' + +describe "Array#find_index" do + it_behaves_like :array_index, :find_index +end diff --git a/spec/ruby/core/array/first_spec.rb b/spec/ruby/core/array/first_spec.rb new file mode 100644 index 0000000000..66eeba6565 --- /dev/null +++ b/spec/ruby/core/array/first_spec.rb @@ -0,0 +1,93 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Array#first" do + it "returns the first element" do + %w{a b c}.first.should == 'a' + [nil].first.should == nil + end + + it "returns nil if self is empty" do + [].first.should == nil + end + + it "returns the first count elements if given a count" do + [true, false, true, nil, false].first(2).should == [true, false] + end + + it "returns an empty array when passed count on an empty array" do + [].first(0).should == [] + [].first(1).should == [] + [].first(2).should == [] + end + + it "returns an empty array when passed count == 0" do + [1, 2, 3, 4, 5].first(0).should == [] + end + + it "returns an array containing the first element when passed count == 1" do + [1, 2, 3, 4, 5].first(1).should == [1] + end + + it "raises an ArgumentError when count is negative" do + -> { [1, 2].first(-1) }.should raise_error(ArgumentError) + end + + it "raises a RangeError when count is a Bignum" do + -> { [].first(bignum_value) }.should raise_error(RangeError) + end + + it "returns the entire array when count > length" do + [1, 2, 3, 4, 5, 9].first(10).should == [1, 2, 3, 4, 5, 9] + end + + it "returns an array which is independent to the original when passed count" do + ary = [1, 2, 3, 4, 5] + ary.first(0).replace([1,2]) + ary.should == [1, 2, 3, 4, 5] + ary.first(1).replace([1,2]) + ary.should == [1, 2, 3, 4, 5] + ary.first(6).replace([1,2]) + ary.should == [1, 2, 3, 4, 5] + end + + it "properly handles recursive arrays" do + empty = ArraySpecs.empty_recursive_array + empty.first.should equal(empty) + + ary = ArraySpecs.head_recursive_array + ary.first.should equal(ary) + end + + it "tries to convert the passed argument to an Integer using #to_int" do + obj = mock('to_int') + obj.should_receive(:to_int).and_return(2) + [1, 2, 3, 4, 5].first(obj).should == [1, 2] + end + + it "raises a TypeError if the passed argument is not numeric" do + -> { [1,2].first(nil) }.should raise_error(TypeError) + -> { [1,2].first("a") }.should raise_error(TypeError) + + obj = mock("nonnumeric") + -> { [1,2].first(obj) }.should raise_error(TypeError) + end + + it "does not return subclass instance when passed count on Array subclasses" do + ArraySpecs::MyArray[].first(0).should be_an_instance_of(Array) + ArraySpecs::MyArray[].first(2).should be_an_instance_of(Array) + ArraySpecs::MyArray[1, 2, 3].first(0).should be_an_instance_of(Array) + ArraySpecs::MyArray[1, 2, 3].first(1).should be_an_instance_of(Array) + ArraySpecs::MyArray[1, 2, 3].first(2).should be_an_instance_of(Array) + end + + it "is not destructive" do + a = [1, 2, 3] + a.first + a.should == [1, 2, 3] + a.first(2) + a.should == [1, 2, 3] + a.first(3) + a.should == [1, 2, 3] + end +end diff --git a/spec/ruby/core/array/fixtures/classes.rb b/spec/ruby/core/array/fixtures/classes.rb new file mode 100644 index 0000000000..05283c0f74 --- /dev/null +++ b/spec/ruby/core/array/fixtures/classes.rb @@ -0,0 +1,592 @@ +class Object + # This helper is defined here rather than in MSpec because + # it is only used in #pack specs. + def pack_format(count=nil, repeat=nil) + format = instance_variable_get(:@method) + format += count.to_s unless format == 'P' || format == 'p' + format *= repeat if repeat + format.dup # because it may then become tainted + end +end + +module ArraySpecs + SampleRange = 0..1000 + SampleCount = 1000 + + def self.frozen_array + [1,2,3].freeze + end + + def self.empty_frozen_array + [].freeze + end + + def self.recursive_array + a = [1, 'two', 3.0] + 5.times { a << a } + a + end + + def self.head_recursive_array + a = [] + 5.times { a << a } + a << 1 << 'two' << 3.0 + a + end + + def self.empty_recursive_array + a = [] + a << a + a + end + + # Chi squared critical values for tests with n degrees of freedom at 99% confidence. + # Values obtained from NIST Engineering Statistic Handbook at + # https://www.itl.nist.gov/div898/handbook/eda/section3/eda3674.htm + + CHI_SQUARED_CRITICAL_VALUES = [ + 0, + 6.635, 9.210, 11.345, 13.277, 15.086, 16.812, 18.475, 20.090, 21.666, 23.209, + 24.725, 26.217, 27.688, 29.141, 30.578, 32.000, 33.409, 34.805, 36.191, 37.566, + 38.932, 40.289, 41.638, 42.980, 44.314, 45.642, 46.963, 48.278, 49.588, 50.892, + 52.191, 53.486, 54.776, 56.061, 57.342, 58.619, 59.893, 61.162, 62.428, 63.691, + 64.950, 66.206, 67.459, 68.710, 69.957, 71.201, 72.443, 73.683, 74.919, 76.154, + 77.386, 78.616, 79.843, 81.069, 82.292, 83.513, 84.733, 85.950, 87.166, 88.379, + 89.591, 90.802, 92.010, 93.217, 94.422, 95.626, 96.828, 98.028, 99.228, 100.425, + 101.621, 102.816, 104.010, 105.202, 106.393, 107.583, 108.771, 109.958, 111.144, 112.329, + 113.512, 114.695, 115.876, 117.057, 118.236, 119.414, 120.591, 121.767, 122.942, 124.116, + 125.289, 126.462, 127.633, 128.803, 129.973, 131.141, 132.309, 133.476, 134.642, 135.807, + ] + + def self.measure_sample_fairness(size, samples, iters) + ary = Array.new(size) { |x| x } + expected = iters.fdiv size + (samples).times do |i| + chi_results = [] + 3.times do + counts = Array.new(size, 0) + iters.times do + x = ary.sample(samples)[i] + counts[x] += 1 + end + chi_squared = counts.sum {|count| (count - expected) ** 2} / expected + chi_results << chi_squared + break if chi_squared <= CHI_SQUARED_CRITICAL_VALUES[size] + end + + chi_results.min.should <= CHI_SQUARED_CRITICAL_VALUES[size] + end + end + + def self.measure_sample_fairness_large_sample_size(size, samples, iters) + ary = Array.new(size) { |x| x } + counts = Array.new(size, 0) + expected = (iters * samples).fdiv size + iters.times do + ary.sample(samples).each do |sample| + counts[sample] += 1 + end + end + chi_squared = counts.sum {|count| (count - expected) ** 2} / expected + + # Chi squared critical values for tests with 4 degrees of freedom + # Values obtained from NIST Engineering Statistic Handbook at + # https://www.itl.nist.gov/div898/handbook/eda/section3/eda3674.htm + + chi_squared.should <= CHI_SQUARED_CRITICAL_VALUES[size] + end + + class MyArray < Array + # The #initialize method has a different signature than Array to help + # catch places in the specs that do not assert the #initialize is not + # called when Array methods make new instances. + def initialize(a, b) + self << a << b + ScratchPad.record :my_array_initialize + end + end + + class Sexp < Array + def initialize(*args) + super(args) + end + end + + # TODO: replace specs that use this with #should_not_receive(:to_ary) + # expectations on regular objects (e.g. Array instances). + class ToAryArray < Array + def to_ary() ["to_ary", "was", "called!"] end + end + + class MyRange < Range; end + + class AssocKey + def ==(other); other == 'it'; end + end + + class D + def <=>(obj) + return 4 <=> obj unless obj.class == D + 0 + end + end + + class SubArray < Array + def initialize(*args) + ScratchPad.record args + end + end + + class ArrayConvertible + attr_accessor :called + def initialize(*values, &block) + @values = values; + end + + def to_a + self.called = :to_a + @values + end + + def to_ary + self.called = :to_ary + @values + end + end + + class ArrayMethodMissing + def initialize(*values, &block) + @values = values; + end + + def method_missing(name, *args) + @values + end + end + + class SortSame + def <=>(other); 0; end + def ==(other); true; end + end + + class UFOSceptic + def <=>(other); raise "N-uh, UFO:s do not exist!"; end + end + + class MockForCompared + @@count = 0 + @@compared = false + def initialize + @@compared = false + @order = (@@count += 1) + end + def <=>(rhs) + @@compared = true + return rhs.order <=> self.order + end + def self.compared? + @@compared + end + + protected + attr_accessor :order + end + + class ComparableWithInteger + include Comparable + def initialize(num) + @num = num + end + + def <=>(fixnum) + @num <=> fixnum + end + end + + class Uncomparable + def <=>(obj) + nil + end + end + + def self.universal_pack_object + obj = mock("string float int".freeze) + obj.stub!(:to_int).and_return(1) + obj.stub!(:to_str).and_return("1") + obj.stub!(:to_f).and_return(1.0) + obj + end + + LargeArray = [ + "test_create_table_with_force_true_does_not_drop_nonexisting_table", + "test_add_table", + "assert_difference", + "assert_operator", + "instance_variables", + "class", + "instance_variable_get", + "__class__", + "expects", + "assert_no_difference", + "name", + "assert_blank", + "assert_not_same", + "is_a?", + "test_add_table_with_decimals", + "test_create_table_with_timestamps_should_create_datetime_columns", + "assert_present", + "assert_no_match", + "__instance_of__", + "assert_deprecated", + "assert", + "assert_throws", + "kind_of?", + "try", + "__instance_variable_get__", + "object_id", + "timeout", + "instance_variable_set", + "assert_nothing_thrown", + "__instance_variable_set__", + "copy_object", + "test_create_table_with_timestamps_should_create_datetime_columns_with_options", + "assert_not_deprecated", + "assert_in_delta", + "id", + "copy_metaclass", + "test_create_table_without_a_block", + "dup", + "assert_not_nil", + "send", + "__instance_variables__", + "to_sql", + "mock", + "assert_send", + "instance_variable_defined?", + "clone", + "require", + "test_migrator", + "__instance_variable_defined_eh__", + "frozen?", + "test_add_column_not_null_with_default", + "freeze", + "test_migrator_one_up", + "test_migrator_one_down", + "singleton_methods", + "method_exists?", + "create_fixtures", + "test_migrator_one_up_one_down", + "test_native_decimal_insert_manual_vs_automatic", + "instance_exec", + "__is_a__", + "test_migrator_double_up", + "stub", + "private_methods", + "stubs", + "test_migrator_double_down", + "fixture_path", + "private_singleton_methods", + "stub_everything", + "test_migrator_one_up_with_exception_and_rollback", + "sequence", + "protected_methods", + "enum_for", + "test_finds_migrations", + "run_before_mocha", + "states", + "protected_singleton_methods", + "to_json", + "instance_values", + "==", + "mocha_setup", + "public_methods", + "test_finds_pending_migrations", + "mocha_verify", + "assert_kind_of", + "===", + "=~", + "test_relative_migrations", + "mocha_teardown", + "gem", + "mocha", + "test_only_loads_pending_migrations", + "test_add_column_with_precision_and_scale", + "require_or_load", + "eql?", + "require_dependency", + "test_native_types", + "test_target_version_zero_should_run_only_once", + "extend", + "to_matcher", + "unloadable", + "require_association", + "hash", + "__id__", + "load_dependency", + "equals", + "test_migrator_db_has_no_schema_migrations_table", + "test_migrator_verbosity", + "kind_of", + "to_yaml", + "to_bool", + "test_migrator_verbosity_off", + "taint", + "test_migrator_going_down_due_to_version_target", + "tainted?", + "mocha_inspect", + "test_migrator_rollback", + "vim", + "untaint", + "taguri=", + "test_migrator_forward", + "test_schema_migrations_table_name", + "test_proper_table_name", + "all_of", + "test_add_drop_table_with_prefix_and_suffix", + "_setup_callbacks", + "setup", + "Not", + "test_create_table_with_binary_column", + "assert_not_equal", + "enable_warnings", + "acts_like?", + "Rational", + "_removed_setup_callbacks", + "Table", + "bind", + "any_of", + "__method__", + "test_migrator_with_duplicates", + "_teardown_callbacks", + "method", + "test_migrator_with_duplicate_names", + "_removed_teardown_callbacks", + "any_parameters", + "test_migrator_with_missing_version_numbers", + "test_add_remove_single_field_using_string_arguments", + "test_create_table_with_custom_sequence_name", + "test_add_remove_single_field_using_symbol_arguments", + "_one_time_conditions_valid_14?", + "_one_time_conditions_valid_16?", + "run_callbacks", + "anything", + "silence_warnings", + "instance_variable_names", + "_fixture_path", + "copy_instance_variables_from", + "fixture_path?", + "has_entry", + "__marshal__", + "_fixture_table_names", + "__kind_of__", + "fixture_table_names?", + "test_add_rename", + "assert_equal", + "_fixture_class_names", + "fixture_class_names?", + "has_entries", + "_use_transactional_fixtures", + "people", + "test_rename_column_using_symbol_arguments", + "use_transactional_fixtures?", + "instance_eval", + "blank?", + "with_warnings", + "__nil__", + "load", + "metaclass", + "_use_instantiated_fixtures", + "has_key", + "class_eval", + "present?", + "test_rename_column", + "teardown", + "use_instantiated_fixtures?", + "method_name", + "silence_stderr", + "presence", + "test_rename_column_preserves_default_value_not_null", + "silence_stream", + "_pre_loaded_fixtures", + "__metaclass__", + "__fixnum__", + "pre_loaded_fixtures?", + "has_value", + "suppress", + "to_yaml_properties", + "test_rename_nonexistent_column", + "test_add_index", + "includes", + "find_correlate_in", + "equality_predicate_sql", + "assert_nothing_raised", + "let", + "not_predicate_sql", + "test_rename_column_with_sql_reserved_word", + "singleton_class", + "test_rename_column_with_an_index", + "display", + "taguri", + "to_yaml_style", + "test_remove_column_with_index", + "size", + "current_adapter?", + "test_remove_column_with_multi_column_index", + "respond_to?", + "test_change_type_of_not_null_column", + "is_a", + "to_a", + "test_rename_table_for_sqlite_should_work_with_reserved_words", + "require_library_or_gem", + "setup_fixtures", + "equal?", + "teardown_fixtures", + "nil?", + "fixture_table_names", + "fixture_class_names", + "test_create_table_without_id", + "use_transactional_fixtures", + "test_add_column_with_primary_key_attribute", + "repair_validations", + "use_instantiated_fixtures", + "instance_of?", + "test_create_table_adds_id", + "test_rename_table", + "pre_loaded_fixtures", + "to_enum", + "test_create_table_with_not_null_column", + "instance_of", + "test_change_column_nullability", + "optionally", + "test_rename_table_with_an_index", + "run", + "test_change_column", + "default_test", + "assert_raise", + "test_create_table_with_defaults", + "assert_nil", + "flunk", + "regexp_matches", + "duplicable?", + "reset_mocha", + "stubba_method", + "filter_backtrace", + "test_create_table_with_limits", + "responds_with", + "stubba_object", + "test_change_column_with_nil_default", + "assert_block", + "__show__", + "assert_date_from_db", + "__respond_to_eh__", + "run_in_transaction?", + "inspect", + "assert_sql", + "test_change_column_with_new_default", + "yaml_equivalent", + "build_message", + "to_s", + "test_change_column_default", + "assert_queries", + "pending", + "as_json", + "assert_no_queries", + "test_change_column_quotes_column_names", + "assert_match", + "test_keeping_default_and_notnull_constraint_on_change", + "methods", + "connection_allow_concurrency_setup", + "connection_allow_concurrency_teardown", + "test_create_table_with_primary_key_prefix_as_table_name_with_underscore", + "__send__", + "make_connection", + "assert_raises", + "tap", + "with_kcode", + "assert_instance_of", + "test_create_table_with_primary_key_prefix_as_table_name", + "assert_respond_to", + "test_change_column_default_to_null", + "assert_same", + "__extend__", + ] + + LargeTestArraySorted = [ + "test_add_column_not_null_with_default", + "test_add_column_with_precision_and_scale", + "test_add_column_with_primary_key_attribute", + "test_add_drop_table_with_prefix_and_suffix", + "test_add_index", + "test_add_remove_single_field_using_string_arguments", + "test_add_remove_single_field_using_symbol_arguments", + "test_add_rename", + "test_add_table", + "test_add_table_with_decimals", + "test_change_column", + "test_change_column_default", + "test_change_column_default_to_null", + "test_change_column_nullability", + "test_change_column_quotes_column_names", + "test_change_column_with_new_default", + "test_change_column_with_nil_default", + "test_change_type_of_not_null_column", + "test_create_table_adds_id", + "test_create_table_with_binary_column", + "test_create_table_with_custom_sequence_name", + "test_create_table_with_defaults", + "test_create_table_with_force_true_does_not_drop_nonexisting_table", + "test_create_table_with_limits", + "test_create_table_with_not_null_column", + "test_create_table_with_primary_key_prefix_as_table_name", + "test_create_table_with_primary_key_prefix_as_table_name_with_underscore", + "test_create_table_with_timestamps_should_create_datetime_columns", + "test_create_table_with_timestamps_should_create_datetime_columns_with_options", + "test_create_table_without_a_block", + "test_create_table_without_id", + "test_finds_migrations", + "test_finds_pending_migrations", + "test_keeping_default_and_notnull_constraint_on_change", + "test_migrator", + "test_migrator_db_has_no_schema_migrations_table", + "test_migrator_double_down", + "test_migrator_double_up", + "test_migrator_forward", + "test_migrator_going_down_due_to_version_target", + "test_migrator_one_down", + "test_migrator_one_up", + "test_migrator_one_up_one_down", + "test_migrator_one_up_with_exception_and_rollback", + "test_migrator_rollback", + "test_migrator_verbosity", + "test_migrator_verbosity_off", + "test_migrator_with_duplicate_names", + "test_migrator_with_duplicates", + "test_migrator_with_missing_version_numbers", + "test_native_decimal_insert_manual_vs_automatic", + "test_native_types", + "test_only_loads_pending_migrations", + "test_proper_table_name", + "test_relative_migrations", + "test_remove_column_with_index", + "test_remove_column_with_multi_column_index", + "test_rename_column", + "test_rename_column_preserves_default_value_not_null", + "test_rename_column_using_symbol_arguments", + "test_rename_column_with_an_index", + "test_rename_column_with_sql_reserved_word", + "test_rename_nonexistent_column", + "test_rename_table", + "test_rename_table_for_sqlite_should_work_with_reserved_words", + "test_rename_table_with_an_index", + "test_schema_migrations_table_name", + "test_target_version_zero_should_run_only_once", + ] + + class PrivateToAry + private + + def to_ary + [1, 2, 3] + end + end +end diff --git a/spec/ruby/core/array/fixtures/encoded_strings.rb b/spec/ruby/core/array/fixtures/encoded_strings.rb new file mode 100644 index 0000000000..b5888d86ae --- /dev/null +++ b/spec/ruby/core/array/fixtures/encoded_strings.rb @@ -0,0 +1,69 @@ +# encoding: utf-8 +module ArraySpecs + def self.array_with_usascii_and_7bit_utf8_strings + [ + 'foo'.dup.force_encoding('US-ASCII'), + 'bar' + ] + end + + def self.array_with_usascii_and_utf8_strings + [ + 'foo'.dup.force_encoding('US-ASCII'), + 'báz' + ] + end + + def self.array_with_7bit_utf8_and_usascii_strings + [ + 'bar', + 'foo'.dup.force_encoding('US-ASCII') + ] + end + + def self.array_with_utf8_and_usascii_strings + [ + 'báz', + 'bar', + 'foo'.dup.force_encoding('US-ASCII') + ] + end + + def self.array_with_usascii_and_utf8_strings + [ + 'foo'.dup.force_encoding('US-ASCII'), + 'bar', + 'báz' + ] + end + + def self.array_with_utf8_and_7bit_binary_strings + [ + 'bar', + 'báz', + 'foo'.dup.force_encoding('BINARY') + ] + end + + def self.array_with_utf8_and_binary_strings + [ + 'bar', + 'báz', + [255].pack('C').force_encoding('BINARY') + ] + end + + def self.array_with_usascii_and_7bit_binary_strings + [ + 'bar'.dup.force_encoding('US-ASCII'), + 'foo'.dup.force_encoding('BINARY') + ] + end + + def self.array_with_usascii_and_binary_strings + [ + 'bar'.dup.force_encoding('US-ASCII'), + [255].pack('C').force_encoding('BINARY') + ] + end +end diff --git a/spec/ruby/core/array/flatten_spec.rb b/spec/ruby/core/array/flatten_spec.rb new file mode 100644 index 0000000000..8c97000c79 --- /dev/null +++ b/spec/ruby/core/array/flatten_spec.rb @@ -0,0 +1,266 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Array#flatten" do + it "returns a one-dimensional flattening recursively" do + [[[1, [2, 3]],[2, 3, [4, [4, [5, 5]], [1, 2, 3]]], [4]], []].flatten.should == [1, 2, 3, 2, 3, 4, 4, 5, 5, 1, 2, 3, 4] + end + + it "takes an optional argument that determines the level of recursion" do + [ 1, 2, [3, [4, 5] ] ].flatten(1).should == [1, 2, 3, [4, 5]] + end + + it "returns dup when the level of recursion is 0" do + a = [ 1, 2, [3, [4, 5] ] ] + a.flatten(0).should == a + a.flatten(0).should_not equal(a) + end + + it "ignores negative levels" do + [ 1, 2, [ 3, 4, [5, 6] ] ].flatten(-1).should == [1, 2, 3, 4, 5, 6] + [ 1, 2, [ 3, 4, [5, 6] ] ].flatten(-10).should == [1, 2, 3, 4, 5, 6] + end + + it "tries to convert passed Objects to Integers using #to_int" do + obj = mock("Converted to Integer") + obj.should_receive(:to_int).and_return(1) + + [ 1, 2, [3, [4, 5] ] ].flatten(obj).should == [1, 2, 3, [4, 5]] + end + + it "raises a TypeError when the passed Object can't be converted to an Integer" do + obj = mock("Not converted") + -> { [ 1, 2, [3, [4, 5] ] ].flatten(obj) }.should raise_error(TypeError) + end + + it "does not call flatten on elements" do + obj = mock('[1,2]') + obj.should_not_receive(:flatten) + [obj, obj].flatten.should == [obj, obj] + + obj = [5, 4] + obj.should_not_receive(:flatten) + [obj, obj].flatten.should == [5, 4, 5, 4] + end + + it "raises an ArgumentError on recursive arrays" do + x = [] + x << x + -> { x.flatten }.should raise_error(ArgumentError) + + x = [] + y = [] + x << y + y << x + -> { x.flatten }.should raise_error(ArgumentError) + end + + it "flattens any element which responds to #to_ary, using the return value of said method" do + x = mock("[3,4]") + x.should_receive(:to_ary).at_least(:once).and_return([3, 4]) + [1, 2, x, 5].flatten.should == [1, 2, 3, 4, 5] + + y = mock("MyArray[]") + y.should_receive(:to_ary).at_least(:once).and_return(ArraySpecs::MyArray[]) + [y].flatten.should == [] + + z = mock("[2,x,y,5]") + z.should_receive(:to_ary).and_return([2, x, y, 5]) + [1, z, 6].flatten.should == [1, 2, 3, 4, 5, 6] + end + + it "does not call #to_ary on elements beyond the given level" do + obj = mock("1") + obj.should_not_receive(:to_ary) + [[obj]].flatten(1) + end + + it "returns Array instance for Array subclasses" do + ArraySpecs::MyArray[].flatten.should be_an_instance_of(Array) + ArraySpecs::MyArray[1, 2, 3].flatten.should be_an_instance_of(Array) + ArraySpecs::MyArray[1, [2], 3].flatten.should be_an_instance_of(Array) + ArraySpecs::MyArray[1, [2, 3], 4].flatten.should == [1, 2, 3, 4] + [ArraySpecs::MyArray[1, 2, 3]].flatten.should be_an_instance_of(Array) + end + + it "is not destructive" do + ary = [1, [2, 3]] + ary.flatten + ary.should == [1, [2, 3]] + end + + describe "with a non-Array object in the Array" do + before :each do + @obj = mock("Array#flatten") + ScratchPad.record [] + end + + it "does not call #to_ary if the method is not defined" do + [@obj].flatten.should == [@obj] + end + + it "does not raise an exception if #to_ary returns nil" do + @obj.should_receive(:to_ary).and_return(nil) + [@obj].flatten.should == [@obj] + end + + it "raises a TypeError if #to_ary does not return an Array" do + @obj.should_receive(:to_ary).and_return(1) + -> { [@obj].flatten }.should raise_error(TypeError) + end + + it "calls respond_to_missing?(:to_ary, true) to try coercing" do + def @obj.respond_to_missing?(*args) ScratchPad << args; false end + [@obj].flatten.should == [@obj] + ScratchPad.recorded.should == [[:to_ary, true]] + end + + it "does not call #to_ary if not defined when #respond_to_missing? returns false" do + def @obj.respond_to_missing?(name, priv) ScratchPad << name; false end + + [@obj].flatten.should == [@obj] + ScratchPad.recorded.should == [:to_ary] + end + + it "calls #to_ary if not defined when #respond_to_missing? returns true" do + def @obj.respond_to_missing?(name, priv) ScratchPad << name; true end + + -> { [@obj].flatten }.should raise_error(NoMethodError) + ScratchPad.recorded.should == [:to_ary] + end + + it "calls #method_missing if defined" do + @obj.should_receive(:method_missing).with(:to_ary).and_return([1, 2, 3]) + [@obj].flatten.should == [1, 2, 3] + end + end + + it "performs respond_to? and method_missing-aware checks when coercing elements to array" do + bo = BasicObject.new + [bo].flatten.should == [bo] + + def bo.method_missing(name, *) + [1,2] + end + + [bo].flatten.should == [1,2] + + def bo.respond_to?(name, *) + false + end + + [bo].flatten.should == [bo] + + def bo.respond_to?(name, *) + true + end + + [bo].flatten.should == [1,2] + end +end + +describe "Array#flatten!" do + it "modifies array to produce a one-dimensional flattening recursively" do + a = [[[1, [2, 3]],[2, 3, [4, [4, [5, 5]], [1, 2, 3]]], [4]], []] + a.flatten! + a.should == [1, 2, 3, 2, 3, 4, 4, 5, 5, 1, 2, 3, 4] + end + + it "returns self if made some modifications" do + a = [[[1, [2, 3]],[2, 3, [4, [4, [5, 5]], [1, 2, 3]]], [4]], []] + a.flatten!.should equal(a) + end + + it "returns nil if no modifications took place" do + a = [1, 2, 3] + a.flatten!.should == nil + a = [1, [2, 3]] + a.flatten!.should_not == nil + end + + it "should not check modification by size" do + a = [1, 2, [3]] + a.flatten!.should_not == nil + a.should == [1, 2, 3] + end + + it "takes an optional argument that determines the level of recursion" do + [ 1, 2, [3, [4, 5] ] ].flatten!(1).should == [1, 2, 3, [4, 5]] + end + + # redmine #1440 + it "returns nil when the level of recursion is 0" do + a = [ 1, 2, [3, [4, 5] ] ] + a.flatten!(0).should == nil + end + + it "treats negative levels as no arguments" do + [ 1, 2, [ 3, 4, [5, 6] ] ].flatten!(-1).should == [1, 2, 3, 4, 5, 6] + [ 1, 2, [ 3, 4, [5, 6] ] ].flatten!(-10).should == [1, 2, 3, 4, 5, 6] + end + + it "tries to convert passed Objects to Integers using #to_int" do + obj = mock("Converted to Integer") + obj.should_receive(:to_int).and_return(1) + + [ 1, 2, [3, [4, 5] ] ].flatten!(obj).should == [1, 2, 3, [4, 5]] + end + + it "raises a TypeError when the passed Object can't be converted to an Integer" do + obj = mock("Not converted") + -> { [ 1, 2, [3, [4, 5] ] ].flatten!(obj) }.should raise_error(TypeError) + end + + it "does not call flatten! on elements" do + obj = mock('[1,2]') + obj.should_not_receive(:flatten!) + [obj, obj].flatten!.should == nil + + obj = [5, 4] + obj.should_not_receive(:flatten!) + [obj, obj].flatten!.should == [5, 4, 5, 4] + end + + it "raises an ArgumentError on recursive arrays" do + x = [] + x << x + -> { x.flatten! }.should raise_error(ArgumentError) + + x = [] + y = [] + x << y + y << x + -> { x.flatten! }.should raise_error(ArgumentError) + end + + it "flattens any elements which responds to #to_ary, using the return value of said method" do + x = mock("[3,4]") + x.should_receive(:to_ary).at_least(:once).and_return([3, 4]) + [1, 2, x, 5].flatten!.should == [1, 2, 3, 4, 5] + + y = mock("MyArray[]") + y.should_receive(:to_ary).at_least(:once).and_return(ArraySpecs::MyArray[]) + [y].flatten!.should == [] + + z = mock("[2,x,y,5]") + z.should_receive(:to_ary).and_return([2, x, y, 5]) + [1, z, 6].flatten!.should == [1, 2, 3, 4, 5, 6] + + ary = [ArraySpecs::MyArray[1, 2, 3]] + ary.flatten! + ary.should be_an_instance_of(Array) + ary.should == [1, 2, 3] + end + + it "raises a FrozenError on frozen arrays when the array is modified" do + nested_ary = [1, 2, []] + nested_ary.freeze + -> { nested_ary.flatten! }.should raise_error(FrozenError) + end + + # see [ruby-core:23663] + it "raises a FrozenError on frozen arrays when the array would not be modified" do + -> { ArraySpecs.frozen_array.flatten! }.should raise_error(FrozenError) + -> { ArraySpecs.empty_frozen_array.flatten! }.should raise_error(FrozenError) + end +end diff --git a/spec/ruby/core/array/frozen_spec.rb b/spec/ruby/core/array/frozen_spec.rb new file mode 100644 index 0000000000..3ba54be46b --- /dev/null +++ b/spec/ruby/core/array/frozen_spec.rb @@ -0,0 +1,16 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Array#frozen?" do + it "returns true if array is frozen" do + a = [1, 2, 3] + a.should_not.frozen? + a.freeze + a.should.frozen? + end + + it "returns false for an array being sorted by #sort" do + a = [1, 2, 3] + a.sort { |x,y| a.should_not.frozen?; x <=> y } + end +end diff --git a/spec/ruby/core/array/hash_spec.rb b/spec/ruby/core/array/hash_spec.rb new file mode 100644 index 0000000000..f3bcc83fce --- /dev/null +++ b/spec/ruby/core/array/hash_spec.rb @@ -0,0 +1,83 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Array#hash" do + it "returns the same fixnum for arrays with the same content" do + [].respond_to?(:hash).should == true + + [[], [1, 2, 3]].each do |ary| + ary.hash.should == ary.dup.hash + ary.hash.should be_an_instance_of(Integer) + end + end + + it "properly handles recursive arrays" do + empty = ArraySpecs.empty_recursive_array + -> { empty.hash }.should_not raise_error + + array = ArraySpecs.recursive_array + -> { array.hash }.should_not raise_error + end + + it "returns the same hash for equal recursive arrays" do + rec = []; rec << rec + rec.hash.should == [rec].hash + rec.hash.should == [[rec]].hash + # This is because rec.eql?([[rec]]) + # Remember that if two objects are eql? + # then the need to have the same hash + # Check the Array#eql? specs! + end + + it "returns the same hash for equal recursive arrays through hashes" do + h = {} ; rec = [h] ; h[:x] = rec + rec.hash.should == [h].hash + rec.hash.should == [{x: rec}].hash + # Like above, this is because rec.eql?([{x: rec}]) + end + + it "calls to_int on result of calling hash on each element" do + ary = Array.new(5) do + obj = mock('0') + obj.should_receive(:hash).and_return(obj) + obj.should_receive(:to_int).and_return(0) + obj + end + + ary.hash + + + hash = mock('1') + hash.should_receive(:to_int).and_return(1.hash) + + obj = mock('@hash') + obj.instance_variable_set(:@hash, hash) + def obj.hash() @hash end + + [obj].hash.should == [1].hash + end + + it "ignores array class differences" do + ArraySpecs::MyArray[].hash.should == [].hash + ArraySpecs::MyArray[1, 2].hash.should == [1, 2].hash + end + + it "returns same hash code for arrays with the same content" do + a = [1, 2, 3, 4] + a.fill 'a', 0..3 + b = %w|a a a a| + a.hash.should == b.hash + end + + it "returns the same value if arrays are #eql?" do + a = [1, 2, 3, 4] + a.fill 'a', 0..3 + b = %w|a a a a| + a.hash.should == b.hash + a.should eql(b) + end + + it "produces different hashes for nested arrays with different values and empty terminator" do + [1, [1, []]].hash.should_not == [2, [2, []]].hash + end +end diff --git a/spec/ruby/core/array/include_spec.rb b/spec/ruby/core/array/include_spec.rb new file mode 100644 index 0000000000..227173218f --- /dev/null +++ b/spec/ruby/core/array/include_spec.rb @@ -0,0 +1,33 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Array#include?" do + it "returns true if object is present, false otherwise" do + [1, 2, "a", "b"].include?("c").should == false + [1, 2, "a", "b"].include?("a").should == true + end + + it "determines presence by using element == obj" do + o = mock('') + + [1, 2, "a", "b"].include?(o).should == false + + def o.==(other); other == 'a'; end + + [1, 2, o, "b"].include?('a').should == true + + [1, 2.0, 3].include?(2).should == true + end + + it "calls == on elements from left to right until success" do + key = "x" + one = mock('one') + two = mock('two') + three = mock('three') + one.should_receive(:==).any_number_of_times.and_return(false) + two.should_receive(:==).any_number_of_times.and_return(true) + three.should_not_receive(:==) + ary = [one, two, three] + ary.include?(key).should == true + end +end diff --git a/spec/ruby/core/array/index_spec.rb b/spec/ruby/core/array/index_spec.rb new file mode 100644 index 0000000000..3acb7d0ef3 --- /dev/null +++ b/spec/ruby/core/array/index_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/index' + +describe "Array#index" do + it_behaves_like :array_index, :index +end diff --git a/spec/ruby/core/array/initialize_spec.rb b/spec/ruby/core/array/initialize_spec.rb new file mode 100644 index 0000000000..b9fa77b16e --- /dev/null +++ b/spec/ruby/core/array/initialize_spec.rb @@ -0,0 +1,158 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Array#initialize" do + before :each do + ScratchPad.clear + end + + it "is private" do + Array.should have_private_instance_method("initialize") + end + + it "is called on subclasses" do + b = ArraySpecs::SubArray.new :size_or_array, :obj + + b.should == [] + ScratchPad.recorded.should == [:size_or_array, :obj] + end + + it "preserves the object's identity even when changing its value" do + a = [1, 2, 3] + a.send(:initialize).should equal(a) + a.should_not == [1, 2, 3] + end + + it "raises an ArgumentError if passed 3 or more arguments" do + -> do + [1, 2].send :initialize, 1, 'x', true + end.should raise_error(ArgumentError) + -> do + [1, 2].send(:initialize, 1, 'x', true) {} + end.should raise_error(ArgumentError) + end + + it "raises a FrozenError on frozen arrays" do + -> do + ArraySpecs.frozen_array.send :initialize + end.should raise_error(FrozenError) + -> do + ArraySpecs.frozen_array.send :initialize, ArraySpecs.frozen_array + end.should raise_error(FrozenError) + end + + it "calls #to_ary to convert the value to an array, even if it's private" do + a = ArraySpecs::PrivateToAry.new + [].send(:initialize, a).should == [1, 2, 3] + end +end + +describe "Array#initialize with no arguments" do + it "makes the array empty" do + [1, 2, 3].send(:initialize).should be_empty + end + + it "does not use the given block" do + -> { + -> { [1, 2, 3].send(:initialize) { raise } }.should_not raise_error + }.should complain(/#{__FILE__}:#{__LINE__-1}: warning: given block not used/, verbose: true) + end +end + +describe "Array#initialize with (array)" do + it "replaces self with the other array" do + b = [4, 5, 6] + [1, 2, 3].send(:initialize, b).should == b + end + + it "does not use the given block" do + ->{ [1, 2, 3].send(:initialize) { raise } }.should_not raise_error + end + + it "calls #to_ary to convert the value to an array" do + a = mock("array") + a.should_receive(:to_ary).and_return([1, 2]) + a.should_not_receive(:to_int) + [].send(:initialize, a).should == [1, 2] + end + + it "does not call #to_ary on instances of Array or subclasses of Array" do + a = [1, 2] + a.should_not_receive(:to_ary) + [].send(:initialize, a).should == a + end + + it "raises a TypeError if an Array type argument and a default object" do + -> { [].send(:initialize, [1, 2], 1) }.should raise_error(TypeError) + end +end + +describe "Array#initialize with (size, object=nil)" do + it "sets the array to size and fills with the object" do + a = [] + obj = [3] + a.send(:initialize, 2, obj).should == [obj, obj] + a[0].should equal(obj) + a[1].should equal(obj) + + b = [] + b.send(:initialize, 3, 14).should == [14, 14, 14] + b.should == [14, 14, 14] + end + + it "sets the array to size and fills with nil when object is omitted" do + [].send(:initialize, 3).should == [nil, nil, nil] + end + + it "raises an ArgumentError if size is negative" do + -> { [].send(:initialize, -1, :a) }.should raise_error(ArgumentError) + -> { [].send(:initialize, -1) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if size is too large" do + -> { [].send(:initialize, fixnum_max+1) }.should raise_error(ArgumentError) + end + + it "calls #to_int to convert the size argument to an Integer when object is given" do + obj = mock('1') + obj.should_receive(:to_int).and_return(1) + [].send(:initialize, obj, :a).should == [:a] + end + + it "calls #to_int to convert the size argument to an Integer when object is not given" do + obj = mock('1') + obj.should_receive(:to_int).and_return(1) + [].send(:initialize, obj).should == [nil] + end + + it "raises a TypeError if the size argument is not an Integer type" do + obj = mock('nonnumeric') + obj.stub!(:to_ary).and_return([1, 2]) + ->{ [].send(:initialize, obj, :a) }.should raise_error(TypeError) + end + + it "yields the index of the element and sets the element to the value of the block" do + [].send(:initialize, 3) { |i| i.to_s }.should == ['0', '1', '2'] + end + + it "uses the block value instead of using the default value" do + -> { + @result = [].send(:initialize, 3, :obj) { |i| i.to_s } + }.should complain(/block supersedes default value argument/) + @result.should == ['0', '1', '2'] + end + + it "returns the value passed to break" do + [].send(:initialize, 3) { break :a }.should == :a + end + + it "sets the array to the values returned by the block before break is executed" do + a = [1, 2, 3] + a.send(:initialize, 3) do |i| + break if i == 2 + i.to_s + end + + a.should == ['0', '1'] + end +end diff --git a/spec/ruby/core/array/insert_spec.rb b/spec/ruby/core/array/insert_spec.rb new file mode 100644 index 0000000000..9e1757f68b --- /dev/null +++ b/spec/ruby/core/array/insert_spec.rb @@ -0,0 +1,78 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Array#insert" do + it "returns self" do + ary = [] + ary.insert(0).should equal(ary) + ary.insert(0, :a).should equal(ary) + end + + it "inserts objects before the element at index for non-negative index" do + ary = [] + ary.insert(0, 3).should == [3] + ary.insert(0, 1, 2).should == [1, 2, 3] + ary.insert(0).should == [1, 2, 3] + + # Let's just assume insert() always modifies the array from now on. + ary.insert(1, 'a').should == [1, 'a', 2, 3] + ary.insert(0, 'b').should == ['b', 1, 'a', 2, 3] + ary.insert(5, 'c').should == ['b', 1, 'a', 2, 3, 'c'] + ary.insert(7, 'd').should == ['b', 1, 'a', 2, 3, 'c', nil, 'd'] + ary.insert(10, 5, 4).should == ['b', 1, 'a', 2, 3, 'c', nil, 'd', nil, nil, 5, 4] + end + + it "appends objects to the end of the array for index == -1" do + [1, 3, 3].insert(-1, 2, 'x', 0.5).should == [1, 3, 3, 2, 'x', 0.5] + end + + it "inserts objects after the element at index with negative index" do + ary = [] + ary.insert(-1, 3).should == [3] + ary.insert(-2, 2).should == [2, 3] + ary.insert(-3, 1).should == [1, 2, 3] + ary.insert(-2, -3).should == [1, 2, -3, 3] + ary.insert(-1, []).should == [1, 2, -3, 3, []] + ary.insert(-2, 'x', 'y').should == [1, 2, -3, 3, 'x', 'y', []] + ary = [1, 2, 3] + end + + it "pads with nils if the index to be inserted to is past the end" do + [].insert(5, 5).should == [nil, nil, nil, nil, nil, 5] + end + + it "can insert before the first element with a negative index" do + [1, 2, 3].insert(-4, -3).should == [-3, 1, 2, 3] + end + + it "raises an IndexError if the negative index is out of bounds" do + -> { [].insert(-2, 1) }.should raise_error(IndexError) + -> { [1].insert(-3, 2) }.should raise_error(IndexError) + end + + it "does nothing of no object is passed" do + [].insert(0).should == [] + [].insert(-1).should == [] + [].insert(10).should == [] + [].insert(-2).should == [] + end + + it "tries to convert the passed position argument to an Integer using #to_int" do + obj = mock('2') + obj.should_receive(:to_int).and_return(2) + [].insert(obj, 'x').should == [nil, nil, 'x'] + end + + it "raises an ArgumentError if no argument passed" do + -> { [].insert() }.should raise_error(ArgumentError) + end + + it "raises a FrozenError on frozen arrays when the array is modified" do + -> { ArraySpecs.frozen_array.insert(0, 'x') }.should raise_error(FrozenError) + end + + # see [ruby-core:23666] + it "raises a FrozenError on frozen arrays when the array would not be modified" do + -> { ArraySpecs.frozen_array.insert(0) }.should raise_error(FrozenError) + end +end diff --git a/spec/ruby/core/array/inspect_spec.rb b/spec/ruby/core/array/inspect_spec.rb new file mode 100644 index 0000000000..0832224f5a --- /dev/null +++ b/spec/ruby/core/array/inspect_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/inspect' + +describe "Array#inspect" do + it_behaves_like :array_inspect, :inspect +end diff --git a/spec/ruby/core/array/intersect_spec.rb b/spec/ruby/core/array/intersect_spec.rb new file mode 100644 index 0000000000..456aa26c6e --- /dev/null +++ b/spec/ruby/core/array/intersect_spec.rb @@ -0,0 +1,64 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe 'Array#intersect?' do + describe 'when at least one element in two Arrays is the same' do + it 'returns true' do + [1, 2].intersect?([2, 3, 4]).should == true + [2, 3, 4].intersect?([1, 2]).should == true + end + end + + describe 'when there are no elements in common between two Arrays' do + it 'returns false' do + [0, 1, 2].intersect?([3, 4]).should == false + [3, 4].intersect?([0, 1, 2]).should == false + [3, 4].intersect?([]).should == false + [].intersect?([0, 1, 2]).should == false + end + end + + it "tries to convert the passed argument to an Array using #to_ary" do + obj = mock('[1,2,3]') + obj.should_receive(:to_ary).and_return([1, 2, 3]) + + [1, 2].intersect?(obj).should == true + end + + it "determines equivalence between elements in the sense of eql?" do + obj1 = mock('1') + obj2 = mock('2') + obj1.stub!(:hash).and_return(0) + obj2.stub!(:hash).and_return(0) + obj1.stub!(:eql?).and_return(true) + obj2.stub!(:eql?).and_return(true) + + [obj1].intersect?([obj2]).should == true + + obj1 = mock('3') + obj2 = mock('4') + obj1.stub!(:hash).and_return(0) + obj2.stub!(:hash).and_return(0) + obj1.stub!(:eql?).and_return(false) + obj2.stub!(:eql?).and_return(false) + + [obj1].intersect?([obj2]).should == false + end + + it "does not call to_ary on array subclasses" do + [5, 6].intersect?(ArraySpecs::ToAryArray[1, 2, 5, 6]).should == true + end + + it "properly handles an identical item even when its #eql? isn't reflexive" do + x = mock('x') + x.stub!(:hash).and_return(42) + x.stub!(:eql?).and_return(false) # Stubbed for clarity and latitude in implementation; not actually sent by MRI. + + [x].intersect?([x]).should == true + end + + it "has semantic of !(a & b).empty?" do + [].intersect?([]).should == false + [nil].intersect?([nil]).should == true + end +end diff --git a/spec/ruby/core/array/intersection_spec.rb b/spec/ruby/core/array/intersection_spec.rb new file mode 100644 index 0000000000..e01a68d389 --- /dev/null +++ b/spec/ruby/core/array/intersection_spec.rb @@ -0,0 +1,19 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/intersection' + +describe "Array#&" do + it_behaves_like :array_intersection, :& +end + +describe "Array#intersection" do + it_behaves_like :array_intersection, :intersection + + it "accepts multiple arguments" do + [1, 2, 3, 4].intersection([1, 2, 3], [2, 3, 4]).should == [2, 3] + end + + it "preserves elements order from original array" do + [1, 2, 3, 4].intersection([3, 2, 1]).should == [1, 2, 3] + end +end diff --git a/spec/ruby/core/array/join_spec.rb b/spec/ruby/core/array/join_spec.rb new file mode 100644 index 0000000000..e78ea6f9e1 --- /dev/null +++ b/spec/ruby/core/array/join_spec.rb @@ -0,0 +1,50 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/join' + +describe "Array#join" do + it_behaves_like :array_join_with_string_separator, :join + it_behaves_like :array_join_with_default_separator, :join + + it "does not separate elements when the passed separator is nil" do + [1, 2, 3].join(nil).should == '123' + end + + it "calls #to_str to convert the separator to a String" do + sep = mock("separator") + sep.should_receive(:to_str).and_return(", ") + [1, 2].join(sep).should == "1, 2" + end + + it "does not call #to_str on the separator if the array is empty" do + sep = mock("separator") + sep.should_not_receive(:to_str) + [].join(sep).should == "" + end + + it "raises a TypeError if the separator cannot be coerced to a String by calling #to_str" do + obj = mock("not a string") + -> { [1, 2].join(obj) }.should raise_error(TypeError) + end + + it "raises a TypeError if passed false as the separator" do + -> { [1, 2].join(false) }.should raise_error(TypeError) + end +end + +describe "Array#join with $," do + before :each do + @before_separator = $, + end + + after :each do + suppress_warning {$, = @before_separator} + end + + it "separates elements with default separator when the passed separator is nil" do + suppress_warning { + $, = "_" + [1, 2, 3].join(nil).should == '1_2_3' + } + end +end diff --git a/spec/ruby/core/array/keep_if_spec.rb b/spec/ruby/core/array/keep_if_spec.rb new file mode 100644 index 0000000000..40f7329b7c --- /dev/null +++ b/spec/ruby/core/array/keep_if_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' +require_relative 'shared/keep_if' + +describe "Array#keep_if" do + it "returns the same array if no changes were made" do + array = [1, 2, 3] + array.keep_if { true }.should equal(array) + end + + it_behaves_like :keep_if, :keep_if +end diff --git a/spec/ruby/core/array/last_spec.rb b/spec/ruby/core/array/last_spec.rb new file mode 100644 index 0000000000..d6fefada09 --- /dev/null +++ b/spec/ruby/core/array/last_spec.rb @@ -0,0 +1,87 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Array#last" do + it "returns the last element" do + [1, 1, 1, 1, 2].last.should == 2 + end + + it "returns nil if self is empty" do + [].last.should == nil + end + + it "returns the last count elements if given a count" do + [1, 2, 3, 4, 5, 9].last(3).should == [4, 5, 9] + end + + it "returns an empty array when passed a count on an empty array" do + [].last(0).should == [] + [].last(1).should == [] + end + + it "returns an empty array when count == 0" do + [1, 2, 3, 4, 5].last(0).should == [] + end + + it "returns an array containing the last element when passed count == 1" do + [1, 2, 3, 4, 5].last(1).should == [5] + end + + it "raises an ArgumentError when count is negative" do + -> { [1, 2].last(-1) }.should raise_error(ArgumentError) + end + + it "returns the entire array when count > length" do + [1, 2, 3, 4, 5, 9].last(10).should == [1, 2, 3, 4, 5, 9] + end + + it "returns an array which is independent to the original when passed count" do + ary = [1, 2, 3, 4, 5] + ary.last(0).replace([1,2]) + ary.should == [1, 2, 3, 4, 5] + ary.last(1).replace([1,2]) + ary.should == [1, 2, 3, 4, 5] + ary.last(6).replace([1,2]) + ary.should == [1, 2, 3, 4, 5] + end + + it "properly handles recursive arrays" do + empty = ArraySpecs.empty_recursive_array + empty.last.should equal(empty) + + array = ArraySpecs.recursive_array + array.last.should equal(array) + end + + it "tries to convert the passed argument to an Integer using #to_int" do + obj = mock('to_int') + obj.should_receive(:to_int).and_return(2) + [1, 2, 3, 4, 5].last(obj).should == [4, 5] + end + + it "raises a TypeError if the passed argument is not numeric" do + -> { [1,2].last(nil) }.should raise_error(TypeError) + -> { [1,2].last("a") }.should raise_error(TypeError) + + obj = mock("nonnumeric") + -> { [1,2].last(obj) }.should raise_error(TypeError) + end + + it "does not return subclass instance on Array subclasses" do + ArraySpecs::MyArray[].last(0).should be_an_instance_of(Array) + ArraySpecs::MyArray[].last(2).should be_an_instance_of(Array) + ArraySpecs::MyArray[1, 2, 3].last(0).should be_an_instance_of(Array) + ArraySpecs::MyArray[1, 2, 3].last(1).should be_an_instance_of(Array) + ArraySpecs::MyArray[1, 2, 3].last(2).should be_an_instance_of(Array) + end + + it "is not destructive" do + a = [1, 2, 3] + a.last + a.should == [1, 2, 3] + a.last(2) + a.should == [1, 2, 3] + a.last(3) + a.should == [1, 2, 3] + end +end diff --git a/spec/ruby/core/array/length_spec.rb b/spec/ruby/core/array/length_spec.rb new file mode 100644 index 0000000000..a90c001300 --- /dev/null +++ b/spec/ruby/core/array/length_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/length' + +describe "Array#length" do + it_behaves_like :array_length, :length +end diff --git a/spec/ruby/core/array/map_spec.rb b/spec/ruby/core/array/map_spec.rb new file mode 100644 index 0000000000..0c7f3afa8c --- /dev/null +++ b/spec/ruby/core/array/map_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/collect' + +describe "Array#map" do + it_behaves_like :array_collect, :map +end + +describe "Array#map!" do + it_behaves_like :array_collect_b, :map! +end diff --git a/spec/ruby/core/array/max_spec.rb b/spec/ruby/core/array/max_spec.rb new file mode 100644 index 0000000000..d1c64519d0 --- /dev/null +++ b/spec/ruby/core/array/max_spec.rb @@ -0,0 +1,116 @@ +require_relative '../../spec_helper' + +describe "Array#max" do + it "is defined on Array" do + [1].method(:max).owner.should equal Array + end + + it "returns nil with no values" do + [].max.should == nil + end + + it "returns only element in one element array" do + [1].max.should == 1 + end + + it "returns largest value with multiple elements" do + [1,2].max.should == 2 + [2,1].max.should == 2 + end + + describe "given a block with one argument" do + it "yields in turn the last length-1 values from the array" do + ary = [] + result = [1,2,3,4,5].max {|x| ary << x; x} + + ary.should == [2,3,4,5] + result.should == 5 + end + end +end + +# From Enumerable#max, copied for better readability +describe "Array#max" do + before :each do + @a = [2, 4, 6, 8, 10] + + @e_strs = ["333", "22", "666666", "1", "55555", "1010101010"] + @e_ints = [333, 22, 666666, 55555, 1010101010] + end + + it "max should return the maximum element" do + [18, 42].max.should == 42 + [2, 5, 3, 6, 1, 4].max.should == 6 + end + + it "returns the maximum element (basics cases)" do + [55].max.should == 55 + + [11,99].max.should == 99 + [99,11].max.should == 99 + [2, 33, 4, 11].max.should == 33 + + [1,2,3,4,5].max.should == 5 + [5,4,3,2,1].max.should == 5 + [1,4,3,5,2].max.should == 5 + [5,5,5,5,5].max.should == 5 + + ["aa","tt"].max.should == "tt" + ["tt","aa"].max.should == "tt" + ["2","33","4","11"].max.should == "4" + + @e_strs.max.should == "666666" + @e_ints.max.should == 1010101010 + end + + it "returns nil for an empty Enumerable" do + [].max.should == nil + end + + it "raises a NoMethodError for elements without #<=>" do + -> do + [BasicObject.new, BasicObject.new].max + end.should raise_error(NoMethodError) + end + + it "raises an ArgumentError for incomparable elements" do + -> do + [11,"22"].max + end.should raise_error(ArgumentError) + -> do + [11,12,22,33].max{|a, b| nil} + end.should raise_error(ArgumentError) + end + + it "returns the maximum element (with block)" do + # with a block + ["2","33","4","11"].max {|a,b| a <=> b }.should == "4" + [ 2 , 33 , 4 , 11 ].max {|a,b| a <=> b }.should == 33 + + ["2","33","4","11"].max {|a,b| b <=> a }.should == "11" + [ 2 , 33 , 4 , 11 ].max {|a,b| b <=> a }.should == 2 + + @e_strs.max {|a,b| a.length <=> b.length }.should == "1010101010" + + @e_strs.max {|a,b| a <=> b }.should == "666666" + @e_strs.max {|a,b| a.to_i <=> b.to_i }.should == "1010101010" + + @e_ints.max {|a,b| a <=> b }.should == 1010101010 + @e_ints.max {|a,b| a.to_s <=> b.to_s }.should == 666666 + end + + it "returns the minimum for enumerables that contain nils" do + arr = [nil, nil, true] + arr.max { |a, b| + x = a.nil? ? 1 : a ? 0 : -1 + y = b.nil? ? 1 : b ? 0 : -1 + x <=> y + }.should == nil + end + + it "gathers whole arrays as elements when each yields multiple" do + multi = [[1,2], [3,4,5], [6,7,8,9]] + multi.max.should == [6, 7, 8, 9] + end + +end diff --git a/spec/ruby/core/array/min_spec.rb b/spec/ruby/core/array/min_spec.rb new file mode 100644 index 0000000000..3bdef0dd00 --- /dev/null +++ b/spec/ruby/core/array/min_spec.rb @@ -0,0 +1,121 @@ +require_relative '../../spec_helper' + +describe "Array#min" do + it "is defined on Array" do + [1].method(:max).owner.should equal Array + end + + it "returns nil with no values" do + [].min.should == nil + end + + it "returns only element in one element array" do + [1].min.should == 1 + end + + it "returns smallest value with multiple elements" do + [1,2].min.should == 1 + [2,1].min.should == 1 + end + + describe "given a block with one argument" do + it "yields in turn the last length-1 values from the array" do + ary = [] + result = [1,2,3,4,5].min {|x| ary << x; x} + + ary.should == [2,3,4,5] + result.should == 1 + end + end +end + +# From Enumerable#min, copied for better readability +describe "Array#min" do + before :each do + @a = [2, 4, 6, 8, 10] + + @e_strs = ["333", "22", "666666", "1", "55555", "1010101010"] + @e_ints = [ 333, 22, 666666, 55555, 1010101010] + end + + it "min should return the minimum element" do + [18, 42].min.should == 18 + [2, 5, 3, 6, 1, 4].min.should == 1 + end + + it "returns the minimum (basic cases)" do + [55].min.should == 55 + + [11,99].min.should == 11 + [99,11].min.should == 11 + [2, 33, 4, 11].min.should == 2 + + [1,2,3,4,5].min.should == 1 + [5,4,3,2,1].min.should == 1 + [4,1,3,5,2].min.should == 1 + [5,5,5,5,5].min.should == 5 + + ["aa","tt"].min.should == "aa" + ["tt","aa"].min.should == "aa" + ["2","33","4","11"].min.should == "11" + + @e_strs.min.should == "1" + @e_ints.min.should == 22 + end + + it "returns nil for an empty Enumerable" do + [].min.should be_nil + end + + it "raises a NoMethodError for elements without #<=>" do + -> do + [BasicObject.new, BasicObject.new].min + end.should raise_error(NoMethodError) + end + + it "raises an ArgumentError for incomparable elements" do + -> do + [11,"22"].min + end.should raise_error(ArgumentError) + -> do + [11,12,22,33].min{|a, b| nil} + end.should raise_error(ArgumentError) + end + + it "returns the minimum when using a block rule" do + ["2","33","4","11"].min {|a,b| a <=> b }.should == "11" + [ 2 , 33 , 4 , 11 ].min {|a,b| a <=> b }.should == 2 + + ["2","33","4","11"].min {|a,b| b <=> a }.should == "4" + [ 2 , 33 , 4 , 11 ].min {|a,b| b <=> a }.should == 33 + + [ 1, 2, 3, 4 ].min {|a,b| 15 }.should == 1 + + [11,12,22,33].min{|a, b| 2 }.should == 11 + @i = -2 + [11,12,22,33].min{|a, b| @i += 1 }.should == 12 + + @e_strs.min {|a,b| a.length <=> b.length }.should == "1" + + @e_strs.min {|a,b| a <=> b }.should == "1" + @e_strs.min {|a,b| a.to_i <=> b.to_i }.should == "1" + + @e_ints.min {|a,b| a <=> b }.should == 22 + @e_ints.min {|a,b| a.to_s <=> b.to_s }.should == 1010101010 + end + + it "returns the minimum for enumerables that contain nils" do + arr = [nil, nil, true] + arr.min { |a, b| + x = a.nil? ? -1 : a ? 0 : 1 + y = b.nil? ? -1 : b ? 0 : 1 + x <=> y + }.should == nil + end + + it "gathers whole arrays as elements when each yields multiple" do + multi = [[1,2], [3,4,5], [6,7,8,9]] + multi.min.should == [1, 2] + end + +end diff --git a/spec/ruby/core/array/minmax_spec.rb b/spec/ruby/core/array/minmax_spec.rb new file mode 100644 index 0000000000..e11fe63347 --- /dev/null +++ b/spec/ruby/core/array/minmax_spec.rb @@ -0,0 +1,14 @@ +require_relative '../../spec_helper' +require_relative '../../shared/enumerable/minmax' + +describe "Array#minmax" do + before :each do + @enum = [6, 4, 5, 10, 8] + @empty_enum = [] + @incomparable_enum = [BasicObject.new, BasicObject.new] + @incompatible_enum = [11, "22"] + @strs = ["333", "2", "60", "55555", "1010", "111"] + end + + it_behaves_like :enumerable_minmax, :minmax +end diff --git a/spec/ruby/core/array/minus_spec.rb b/spec/ruby/core/array/minus_spec.rb new file mode 100644 index 0000000000..cb1bf56d76 --- /dev/null +++ b/spec/ruby/core/array/minus_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/difference' + +describe "Array#-" do + it_behaves_like :array_binary_difference, :- +end diff --git a/spec/ruby/core/array/multiply_spec.rb b/spec/ruby/core/array/multiply_spec.rb new file mode 100644 index 0000000000..eca51142fb --- /dev/null +++ b/spec/ruby/core/array/multiply_spec.rb @@ -0,0 +1,94 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/join' + +describe "Array#*" do + it "tries to convert the passed argument to a String using #to_str" do + obj = mock('separator') + obj.should_receive(:to_str).and_return('::') + ([1, 2, 3, 4] * obj).should == '1::2::3::4' + end + + it "tires to convert the passed argument to an Integer using #to_int" do + obj = mock('count') + obj.should_receive(:to_int).and_return(2) + ([1, 2, 3, 4] * obj).should == [1, 2, 3, 4, 1, 2, 3, 4] + end + + it "raises a TypeError if the argument can neither be converted to a string nor an integer" do + obj = mock('not a string or integer') + ->{ [1,2] * obj }.should raise_error(TypeError) + end + + it "converts the passed argument to a String rather than an Integer" do + obj = mock('2') + def obj.to_int() 2 end + def obj.to_str() "2" end + ([:a, :b, :c] * obj).should == "a2b2c" + end + + it "raises a TypeError is the passed argument is nil" do + ->{ [1,2] * nil }.should raise_error(TypeError) + end + + it "raises an ArgumentError when passed 2 or more arguments" do + ->{ [1,2].send(:*, 1, 2) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when passed no arguments" do + ->{ [1,2].send(:*) }.should raise_error(ArgumentError) + end +end + +describe "Array#* with an integer" do + it "concatenates n copies of the array when passed an integer" do + ([ 1, 2, 3 ] * 0).should == [] + ([ 1, 2, 3 ] * 1).should == [1, 2, 3] + ([ 1, 2, 3 ] * 3).should == [1, 2, 3, 1, 2, 3, 1, 2, 3] + ([] * 10).should == [] + end + + it "does not return self even if the passed integer is 1" do + ary = [1, 2, 3] + (ary * 1).should_not equal(ary) + end + + it "properly handles recursive arrays" do + empty = ArraySpecs.empty_recursive_array + (empty * 0).should == [] + (empty * 1).should == empty + (empty * 3).should == [empty, empty, empty] + + array = ArraySpecs.recursive_array + (array * 0).should == [] + (array * 1).should == array + end + + it "raises an ArgumentError when passed a negative integer" do + -> { [ 1, 2, 3 ] * -1 }.should raise_error(ArgumentError) + -> { [] * -1 }.should raise_error(ArgumentError) + end + + describe "with a subclass of Array" do + before :each do + ScratchPad.clear + + @array = ArraySpecs::MyArray[1, 2, 3, 4, 5] + end + + it "returns an Array instance" do + (@array * 0).should be_an_instance_of(Array) + (@array * 1).should be_an_instance_of(Array) + (@array * 2).should be_an_instance_of(Array) + end + + it "does not call #initialize on the subclass instance" do + (@array * 2).should == [1, 2, 3, 4, 5, 1, 2, 3, 4, 5] + ScratchPad.recorded.should be_nil + end + end +end + +describe "Array#* with a string" do + it_behaves_like :array_join_with_string_separator, :* +end diff --git a/spec/ruby/core/array/new_spec.rb b/spec/ruby/core/array/new_spec.rb new file mode 100644 index 0000000000..b50a4857b0 --- /dev/null +++ b/spec/ruby/core/array/new_spec.rb @@ -0,0 +1,124 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Array.new" do + it "returns an instance of Array" do + Array.new.should be_an_instance_of(Array) + end + + it "returns an instance of a subclass" do + ArraySpecs::MyArray.new(1, 2).should be_an_instance_of(ArraySpecs::MyArray) + end + + it "raises an ArgumentError if passed 3 or more arguments" do + -> do + [1, 2].send :initialize, 1, 'x', true + end.should raise_error(ArgumentError) + -> do + [1, 2].send(:initialize, 1, 'x', true) {} + end.should raise_error(ArgumentError) + end +end + +describe "Array.new with no arguments" do + it "returns an empty array" do + Array.new.should be_empty + end + + it "does not use the given block" do + -> { + -> { Array.new { raise } }.should_not raise_error + }.should complain(/warning: given block not used/, verbose: true) + end +end + +describe "Array.new with (array)" do + it "returns an array initialized to the other array" do + b = [4, 5, 6] + Array.new(b).should == b + end + + it "does not use the given block" do + ->{ Array.new([1, 2]) { raise } }.should_not raise_error + end + + it "calls #to_ary to convert the value to an array" do + a = mock("array") + a.should_receive(:to_ary).and_return([1, 2]) + a.should_not_receive(:to_int) + Array.new(a).should == [1, 2] + end + + it "does not call #to_ary on instances of Array or subclasses of Array" do + a = [1, 2] + a.should_not_receive(:to_ary) + Array.new(a) + end + + it "raises a TypeError if an Array type argument and a default object" do + -> { Array.new([1, 2], 1) }.should raise_error(TypeError) + end +end + +describe "Array.new with (size, object=nil)" do + it "returns an array of size filled with object" do + obj = [3] + a = Array.new(2, obj) + a.should == [obj, obj] + a[0].should equal(obj) + a[1].should equal(obj) + + Array.new(3, 14).should == [14, 14, 14] + end + + it "returns an array of size filled with nil when object is omitted" do + Array.new(3).should == [nil, nil, nil] + end + + it "raises an ArgumentError if size is negative" do + -> { Array.new(-1, :a) }.should raise_error(ArgumentError) + -> { Array.new(-1) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if size is too large" do + -> { Array.new(fixnum_max+1) }.should raise_error(ArgumentError) + end + + it "calls #to_int to convert the size argument to an Integer when object is given" do + obj = mock('1') + obj.should_receive(:to_int).and_return(1) + Array.new(obj, :a).should == [:a] + end + + it "calls #to_int to convert the size argument to an Integer when object is not given" do + obj = mock('1') + obj.should_receive(:to_int).and_return(1) + Array.new(obj).should == [nil] + end + + it "raises a TypeError if the size argument is not an Integer type" do + obj = mock('nonnumeric') + obj.stub!(:to_ary).and_return([1, 2]) + ->{ Array.new(obj, :a) }.should raise_error(TypeError) + end + + it "yields the index of the element and sets the element to the value of the block" do + Array.new(3) { |i| i.to_s }.should == ['0', '1', '2'] + end + + it "uses the block value instead of using the default value" do + -> { + @result = Array.new(3, :obj) { |i| i.to_s } + }.should complain(/block supersedes default value argument/) + @result.should == ['0', '1', '2'] + end + + it "returns the value passed to break" do + a = Array.new(3) do |i| + break if i == 2 + i.to_s + end + + a.should == nil + end +end diff --git a/spec/ruby/core/array/none_spec.rb b/spec/ruby/core/array/none_spec.rb new file mode 100644 index 0000000000..31cd8c46d6 --- /dev/null +++ b/spec/ruby/core/array/none_spec.rb @@ -0,0 +1,13 @@ +require_relative '../../spec_helper' +require_relative 'shared/iterable_and_tolerating_size_increasing' + +describe "Array#none?" do + @value_to_return = -> _ { false } + it_behaves_like :array_iterable_and_tolerating_size_increasing, :none? + + it "ignores the block if there is an argument" do + -> { + ['bar', 'foobar'].none?(/baz/) { true }.should == true + }.should complain(/given block not used/) + end +end diff --git a/spec/ruby/core/array/one_spec.rb b/spec/ruby/core/array/one_spec.rb new file mode 100644 index 0000000000..0c61907881 --- /dev/null +++ b/spec/ruby/core/array/one_spec.rb @@ -0,0 +1,13 @@ +require_relative '../../spec_helper' +require_relative 'shared/iterable_and_tolerating_size_increasing' + +describe "Array#one?" do + @value_to_return = -> _ { false } + it_behaves_like :array_iterable_and_tolerating_size_increasing, :one? + + it "ignores the block if there is an argument" do + -> { + ['bar', 'foobar'].one?(/foo/) { false }.should == true + }.should complain(/given block not used/) + end +end diff --git a/spec/ruby/core/array/pack/a_spec.rb b/spec/ruby/core/array/pack/a_spec.rb new file mode 100644 index 0000000000..8245cd5470 --- /dev/null +++ b/spec/ruby/core/array/pack/a_spec.rb @@ -0,0 +1,73 @@ +# encoding: binary +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' +require_relative 'shared/string' +require_relative 'shared/taint' + +describe "Array#pack with format 'A'" do + it_behaves_like :array_pack_basic, 'A' + it_behaves_like :array_pack_basic_non_float, 'A' + it_behaves_like :array_pack_no_platform, 'A' + it_behaves_like :array_pack_string, 'A' + it_behaves_like :array_pack_taint, 'A' + + it "calls #to_str to convert an Object to a String" do + obj = mock("pack A string") + obj.should_receive(:to_str).and_return("``abcdef") + [obj].pack("A*").should == "``abcdef" + end + + it "will not implicitly convert a number to a string" do + -> { [0].pack('A') }.should raise_error(TypeError) + -> { [0].pack('a') }.should raise_error(TypeError) + end + + it "adds all the bytes to the output when passed the '*' modifier" do + ["abc"].pack("A*").should == "abc" + end + + it "pads the output with spaces when the count exceeds the size of the String" do + ["abc"].pack("A6").should == "abc " + end + + it "adds a space when the value is nil" do + [nil].pack("A").should == " " + end + + it "pads the output with spaces when the value is nil" do + [nil].pack("A3").should == " " + end + + it "does not pad with spaces when passed the '*' modifier and the value is nil" do + [nil].pack("A*").should == "" + end +end + +describe "Array#pack with format 'a'" do + it_behaves_like :array_pack_basic, 'a' + it_behaves_like :array_pack_basic_non_float, 'a' + it_behaves_like :array_pack_no_platform, 'a' + it_behaves_like :array_pack_string, 'a' + it_behaves_like :array_pack_taint, 'a' + + it "adds all the bytes to the output when passed the '*' modifier" do + ["abc"].pack("a*").should == "abc" + end + + it "pads the output with NULL bytes when the count exceeds the size of the String" do + ["abc"].pack("a6").should == "abc\x00\x00\x00" + end + + it "adds a NULL byte when the value is nil" do + [nil].pack("a").should == "\x00" + end + + it "pads the output with NULL bytes when the value is nil" do + [nil].pack("a3").should == "\x00\x00\x00" + end + + it "does not pad with NULL bytes when passed the '*' modifier and the value is nil" do + [nil].pack("a*").should == "" + end +end diff --git a/spec/ruby/core/array/pack/at_spec.rb b/spec/ruby/core/array/pack/at_spec.rb new file mode 100644 index 0000000000..bb9801440a --- /dev/null +++ b/spec/ruby/core/array/pack/at_spec.rb @@ -0,0 +1,30 @@ +# encoding: binary +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' + +describe "Array#pack with format '@'" do + it_behaves_like :array_pack_basic, '@' + it_behaves_like :array_pack_basic_non_float, '@' + it_behaves_like :array_pack_no_platform, '@' + + it "moves the insertion point to the index specified by the count modifier" do + [1, 2, 3, 4, 5].pack("C4@2C").should == "\x01\x02\x05" + end + + it "does not consume any elements" do + [1, 2, 3].pack("C@3C").should == "\x01\x00\x00\x02" + end + + it "extends the string with NULL bytes if the string size is less than the count" do + [1, 2, 3].pack("@3C*").should == "\x00\x00\x00\x01\x02\x03" + end + + it "truncates the string if the string size is greater than the count" do + [1, 2, 3].pack("Cx5@2C").should == "\x01\x00\x02" + end + + it "implicitly has a count of one when no count modifier is passed" do + [1, 2, 3].pack("C*@").should == "\x01" + end +end diff --git a/spec/ruby/core/array/pack/b_spec.rb b/spec/ruby/core/array/pack/b_spec.rb new file mode 100644 index 0000000000..247a9ca023 --- /dev/null +++ b/spec/ruby/core/array/pack/b_spec.rb @@ -0,0 +1,113 @@ +# encoding: binary +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' +require_relative 'shared/encodings' +require_relative 'shared/taint' + +describe "Array#pack with format 'B'" do + it_behaves_like :array_pack_basic, 'B' + it_behaves_like :array_pack_basic_non_float, 'B' + it_behaves_like :array_pack_arguments, 'B' + it_behaves_like :array_pack_hex, 'B' + it_behaves_like :array_pack_taint, 'B' + + it "calls #to_str to convert an Object to a String" do + obj = mock("pack B string") + obj.should_receive(:to_str).and_return("``abcdef") + [obj].pack("B*").should == "\x2a" + end + + it "will not implicitly convert a number to a string" do + -> { [0].pack('B') }.should raise_error(TypeError) + -> { [0].pack('b') }.should raise_error(TypeError) + end + + it "encodes one bit for each character starting with the most significant bit" do + [ [["0"], "\x00"], + [["1"], "\x80"] + ].should be_computed_by(:pack, "B") + end + + it "implicitly has a count of one when not passed a count modifier" do + ["1"].pack("B").should == "\x80" + end + + it "implicitly has count equal to the string length when passed the '*' modifier" do + [ [["00101010"], "\x2a"], + [["00000000"], "\x00"], + [["11111111"], "\xff"], + [["10000000"], "\x80"], + [["00000001"], "\x01"] + ].should be_computed_by(:pack, "B*") + end + + it "encodes the least significant bit of a character other than 0 or 1" do + [ [["bbababab"], "\x2a"], + [["^&#&#^#^"], "\x2a"], + [["(()()()("], "\x2a"], + [["@@%@%@%@"], "\x2a"], + [["ppqrstuv"], "\x2a"], + [["rqtvtrqp"], "\x42"] + ].should be_computed_by(:pack, "B*") + end + + it "returns a binary string" do + ["1"].pack("B").encoding.should == Encoding::BINARY + end + + it "encodes the string as a sequence of bytes" do + ["ã‚ã‚ã‚ã‚ã‚ã‚ã‚ã‚"].pack("B*").should == "\xdbm\xb6" + end +end + +describe "Array#pack with format 'b'" do + it_behaves_like :array_pack_basic, 'b' + it_behaves_like :array_pack_basic_non_float, 'b' + it_behaves_like :array_pack_arguments, 'b' + it_behaves_like :array_pack_hex, 'b' + it_behaves_like :array_pack_taint, 'b' + + it "calls #to_str to convert an Object to a String" do + obj = mock("pack H string") + obj.should_receive(:to_str).and_return("`abcdef`") + [obj].pack("b*").should == "\x2a" + end + + it "encodes one bit for each character starting with the least significant bit" do + [ [["0"], "\x00"], + [["1"], "\x01"] + ].should be_computed_by(:pack, "b") + end + + it "implicitly has a count of one when not passed a count modifier" do + ["1"].pack("b").should == "\x01" + end + + it "implicitly has count equal to the string length when passed the '*' modifier" do + [ [["0101010"], "\x2a"], + [["00000000"], "\x00"], + [["11111111"], "\xff"], + [["10000000"], "\x01"], + [["00000001"], "\x80"] + ].should be_computed_by(:pack, "b*") + end + + it "encodes the least significant bit of a character other than 0 or 1" do + [ [["bababab"], "\x2a"], + [["&#&#^#^"], "\x2a"], + [["()()()("], "\x2a"], + [["@%@%@%@"], "\x2a"], + [["pqrstuv"], "\x2a"], + [["qrtrtvs"], "\x41"] + ].should be_computed_by(:pack, "b*") + end + + it "returns a binary string" do + ["1"].pack("b").encoding.should == Encoding::BINARY + end + + it "encodes the string as a sequence of bytes" do + ["ã‚ã‚ã‚ã‚ã‚ã‚ã‚ã‚"].pack("b*").should == "\xdb\xb6m" + end +end diff --git a/spec/ruby/core/array/pack/buffer_spec.rb b/spec/ruby/core/array/pack/buffer_spec.rb new file mode 100644 index 0000000000..b77b2d1efa --- /dev/null +++ b/spec/ruby/core/array/pack/buffer_spec.rb @@ -0,0 +1,60 @@ +# encoding: binary + +require_relative '../../../spec_helper' + +describe "Array#pack with :buffer option" do + it "returns specified buffer" do + n = [ 65, 66, 67 ] + buffer = " "*3 + result = n.pack("ccc", buffer: buffer) #=> "ABC" + result.should equal(buffer) + end + + it "adds result at the end of buffer content" do + n = [ 65, 66, 67 ] # result without buffer is "ABC" + + buffer = +"" + n.pack("ccc", buffer: buffer).should == "ABC" + + buffer = +"123" + n.pack("ccc", buffer: buffer).should == "123ABC" + + buffer = +"12345" + n.pack("ccc", buffer: buffer).should == "12345ABC" + end + + it "raises TypeError exception if buffer is not String" do + -> { [65].pack("ccc", buffer: []) }.should raise_error( + TypeError, "buffer must be String, not Array") + end + + it "raise FrozenError if buffer is frozen" do + -> { [65].pack("c", buffer: "frozen-string".freeze) }.should raise_error(FrozenError) + end + + it "preserves the encoding of the given buffer" do + buffer = ''.encode(Encoding::ISO_8859_1) + [65, 66, 67].pack("ccc", buffer: buffer) + buffer.encoding.should == Encoding::ISO_8859_1 + end + + context "offset (@) is specified" do + it 'keeps buffer content if it is longer than offset' do + n = [ 65, 66, 67 ] + buffer = +"123456" + n.pack("@3ccc", buffer: buffer).should == "123ABC" + end + + it "fills the gap with \\0 if buffer content is shorter than offset" do + n = [ 65, 66, 67 ] + buffer = +"123" + n.pack("@6ccc", buffer: buffer).should == "123\0\0\0ABC" + end + + it 'does not keep buffer content if it is longer than offset + result' do + n = [ 65, 66, 67 ] + buffer = +"1234567890" + n.pack("@3ccc", buffer: buffer).should == "123ABC" + end + end +end diff --git a/spec/ruby/core/array/pack/c_spec.rb b/spec/ruby/core/array/pack/c_spec.rb new file mode 100644 index 0000000000..47b71b663d --- /dev/null +++ b/spec/ruby/core/array/pack/c_spec.rb @@ -0,0 +1,87 @@ +# encoding: binary + +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' +require_relative 'shared/numeric_basic' + +describe :array_pack_8bit, shared: true do + it "encodes the least significant eight bits of a positive number" do + [ [[49], "1"], + [[0b11111111], "\xFF"], + [[0b100000000], "\x00"], + [[0b100000001], "\x01"] + ].should be_computed_by(:pack, pack_format) + end + + it "encodes the least significant eight bits of a negative number" do + [ [[-1], "\xFF"], + [[-0b10000000], "\x80"], + [[-0b11111111], "\x01"], + [[-0b100000000], "\x00"], + [[-0b100000001], "\xFF"] + ].should be_computed_by(:pack, pack_format) + end + + it "encodes a Float truncated as an Integer" do + [ [[5.2], "\x05"], + [[5.8], "\x05"] + ].should be_computed_by(:pack, pack_format) + end + + it "calls #to_int to convert the pack argument to an Integer" do + obj = mock('to_int') + obj.should_receive(:to_int).and_return(5) + [obj].pack(pack_format).should == "\x05" + end + + it "encodes the number of array elements specified by the count modifier" do + [ [[1, 2, 3], pack_format(3), "\x01\x02\x03"], + [[1, 2, 3], pack_format(2) + pack_format(1), "\x01\x02\x03"] + ].should be_computed_by(:pack) + end + + it "encodes all remaining elements when passed the '*' modifier" do + [1, 2, 3, 4, 5].pack(pack_format('*')).should == "\x01\x02\x03\x04\x05" + end + + ruby_version_is ""..."3.3" do + it "ignores NULL bytes between directives" do + suppress_warning do + [1, 2, 3].pack(pack_format("\000", 2)).should == "\x01\x02" + end + end + end + + ruby_version_is "3.3" do + it "raise ArgumentError for NULL bytes between directives" do + -> { + [1, 2, 3].pack(pack_format("\000", 2)) + }.should raise_error(ArgumentError, /unknown pack directive/) + end + end + + it "ignores spaces between directives" do + [1, 2, 3].pack(pack_format(' ', 2)).should == "\x01\x02" + end +end + +describe "Array#pack with format 'C'" do + it_behaves_like :array_pack_basic, 'C' + it_behaves_like :array_pack_basic_non_float, 'C' + it_behaves_like :array_pack_8bit, 'C' + it_behaves_like :array_pack_arguments, 'C' + it_behaves_like :array_pack_numeric_basic, 'C' + it_behaves_like :array_pack_integer, 'C' + it_behaves_like :array_pack_no_platform, 'C' +end + +describe "Array#pack with format 'c'" do + it_behaves_like :array_pack_basic, 'c' + it_behaves_like :array_pack_basic_non_float, 'c' + it_behaves_like :array_pack_8bit, 'c' + it_behaves_like :array_pack_arguments, 'c' + it_behaves_like :array_pack_numeric_basic, 'c' + it_behaves_like :array_pack_integer, 'c' + it_behaves_like :array_pack_no_platform, 'c' +end diff --git a/spec/ruby/core/array/pack/comment_spec.rb b/spec/ruby/core/array/pack/comment_spec.rb new file mode 100644 index 0000000000..daf1cff06a --- /dev/null +++ b/spec/ruby/core/array/pack/comment_spec.rb @@ -0,0 +1,25 @@ +# encoding: binary +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' + +describe "Array#pack" do + it "ignores directives text from '#' to the first newline" do + [1, 2, 3].pack("c#this is a comment\nc").should == "\x01\x02" + end + + it "ignores directives text from '#' to the end if no newline is present" do + [1, 2, 3].pack("c#this is a comment c").should == "\x01" + end + + it "ignores comments at the start of the directives string" do + [1, 2, 3].pack("#this is a comment\nc").should == "\x01" + end + + it "ignores the entire directive string if it is a comment" do + [1, 2, 3].pack("#this is a comment").should == "" + end + + it "ignores multiple comments" do + [1, 2, 3].pack("c#comment\nc#comment\nc#c").should == "\x01\x02\x03" + end +end diff --git a/spec/ruby/core/array/pack/d_spec.rb b/spec/ruby/core/array/pack/d_spec.rb new file mode 100644 index 0000000000..8bb3654633 --- /dev/null +++ b/spec/ruby/core/array/pack/d_spec.rb @@ -0,0 +1,39 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' +require_relative 'shared/numeric_basic' +require_relative 'shared/float' + +describe "Array#pack with format 'D'" do + it_behaves_like :array_pack_basic, 'D' + it_behaves_like :array_pack_basic_float, 'D' + it_behaves_like :array_pack_arguments, 'D' + it_behaves_like :array_pack_no_platform, 'D' + it_behaves_like :array_pack_numeric_basic, 'D' + it_behaves_like :array_pack_float, 'D' + + little_endian do + it_behaves_like :array_pack_double_le, 'D' + end + + big_endian do + it_behaves_like :array_pack_double_be, 'D' + end +end + +describe "Array#pack with format 'd'" do + it_behaves_like :array_pack_basic, 'd' + it_behaves_like :array_pack_basic_float, 'd' + it_behaves_like :array_pack_arguments, 'd' + it_behaves_like :array_pack_no_platform, 'd' + it_behaves_like :array_pack_numeric_basic, 'd' + it_behaves_like :array_pack_float, 'd' + + little_endian do + it_behaves_like :array_pack_double_le, 'd' + end + + big_endian do + it_behaves_like :array_pack_double_be, 'd' + end +end diff --git a/spec/ruby/core/array/pack/e_spec.rb b/spec/ruby/core/array/pack/e_spec.rb new file mode 100644 index 0000000000..ab61ef578f --- /dev/null +++ b/spec/ruby/core/array/pack/e_spec.rb @@ -0,0 +1,25 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' +require_relative 'shared/numeric_basic' +require_relative 'shared/float' + +describe "Array#pack with format 'E'" do + it_behaves_like :array_pack_basic, 'E' + it_behaves_like :array_pack_basic_float, 'E' + it_behaves_like :array_pack_arguments, 'E' + it_behaves_like :array_pack_no_platform, 'E' + it_behaves_like :array_pack_numeric_basic, 'E' + it_behaves_like :array_pack_float, 'E' + it_behaves_like :array_pack_double_le, 'E' +end + +describe "Array#pack with format 'e'" do + it_behaves_like :array_pack_basic, 'e' + it_behaves_like :array_pack_basic_float, 'e' + it_behaves_like :array_pack_arguments, 'e' + it_behaves_like :array_pack_no_platform, 'e' + it_behaves_like :array_pack_numeric_basic, 'e' + it_behaves_like :array_pack_float, 'e' + it_behaves_like :array_pack_float_le, 'e' +end diff --git a/spec/ruby/core/array/pack/empty_spec.rb b/spec/ruby/core/array/pack/empty_spec.rb new file mode 100644 index 0000000000..d635d6a563 --- /dev/null +++ b/spec/ruby/core/array/pack/empty_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../../spec_helper' + +describe "Array#pack with empty format" do + it "returns an empty String" do + [1, 2, 3].pack("").should == "" + end + + it "returns a String with US-ASCII encoding" do + [1, 2, 3].pack("").encoding.should == Encoding::US_ASCII + end +end diff --git a/spec/ruby/core/array/pack/f_spec.rb b/spec/ruby/core/array/pack/f_spec.rb new file mode 100644 index 0000000000..d436e0787c --- /dev/null +++ b/spec/ruby/core/array/pack/f_spec.rb @@ -0,0 +1,39 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' +require_relative 'shared/numeric_basic' +require_relative 'shared/float' + +describe "Array#pack with format 'F'" do + it_behaves_like :array_pack_basic, 'F' + it_behaves_like :array_pack_basic_float, 'F' + it_behaves_like :array_pack_arguments, 'F' + it_behaves_like :array_pack_no_platform, 'F' + it_behaves_like :array_pack_numeric_basic, 'F' + it_behaves_like :array_pack_float, 'F' + + little_endian do + it_behaves_like :array_pack_float_le, 'F' + end + + big_endian do + it_behaves_like :array_pack_float_be, 'F' + end +end + +describe "Array#pack with format 'f'" do + it_behaves_like :array_pack_basic, 'f' + it_behaves_like :array_pack_basic_float, 'f' + it_behaves_like :array_pack_arguments, 'f' + it_behaves_like :array_pack_no_platform, 'f' + it_behaves_like :array_pack_numeric_basic, 'f' + it_behaves_like :array_pack_float, 'f' + + little_endian do + it_behaves_like :array_pack_float_le, 'f' + end + + big_endian do + it_behaves_like :array_pack_float_be, 'f' + end +end diff --git a/spec/ruby/core/array/pack/g_spec.rb b/spec/ruby/core/array/pack/g_spec.rb new file mode 100644 index 0000000000..83b7f81acc --- /dev/null +++ b/spec/ruby/core/array/pack/g_spec.rb @@ -0,0 +1,25 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' +require_relative 'shared/numeric_basic' +require_relative 'shared/float' + +describe "Array#pack with format 'G'" do + it_behaves_like :array_pack_basic, 'G' + it_behaves_like :array_pack_basic_float, 'G' + it_behaves_like :array_pack_arguments, 'G' + it_behaves_like :array_pack_no_platform, 'G' + it_behaves_like :array_pack_numeric_basic, 'G' + it_behaves_like :array_pack_float, 'G' + it_behaves_like :array_pack_double_be, 'G' +end + +describe "Array#pack with format 'g'" do + it_behaves_like :array_pack_basic, 'g' + it_behaves_like :array_pack_basic_float, 'g' + it_behaves_like :array_pack_arguments, 'g' + it_behaves_like :array_pack_no_platform, 'g' + it_behaves_like :array_pack_numeric_basic, 'g' + it_behaves_like :array_pack_float, 'g' + it_behaves_like :array_pack_float_be, 'g' +end diff --git a/spec/ruby/core/array/pack/h_spec.rb b/spec/ruby/core/array/pack/h_spec.rb new file mode 100644 index 0000000000..ba188874ba --- /dev/null +++ b/spec/ruby/core/array/pack/h_spec.rb @@ -0,0 +1,205 @@ +# encoding: binary +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' +require_relative 'shared/encodings' +require_relative 'shared/taint' + +describe "Array#pack with format 'H'" do + it_behaves_like :array_pack_basic, 'H' + it_behaves_like :array_pack_basic_non_float, 'H' + it_behaves_like :array_pack_arguments, 'H' + it_behaves_like :array_pack_hex, 'H' + it_behaves_like :array_pack_taint, 'H' + + it "calls #to_str to convert an Object to a String" do + obj = mock("pack H string") + obj.should_receive(:to_str).and_return("a") + [obj].pack("H").should == "\xa0" + end + + it "will not implicitly convert a number to a string" do + -> { [0].pack('H') }.should raise_error(TypeError) + -> { [0].pack('h') }.should raise_error(TypeError) + end + + it "encodes the first character as the most significant nibble when passed no count modifier" do + ["ab"].pack("H").should == "\xa0" + end + + it "implicitly has count equal to the string length when passed the '*' modifier" do + ["deadbeef"].pack("H*").should == "\xde\xad\xbe\xef" + end + + it "encodes count nibbles when passed a count modifier exceeding the string length" do + ["ab"].pack('H8').should == "\xab\x00\x00\x00" + end + + it "encodes the first character as the most significant nibble of a hex value" do + [ [["0"], "\x00"], + [["1"], "\x10"], + [["2"], "\x20"], + [["3"], "\x30"], + [["4"], "\x40"], + [["5"], "\x50"], + [["6"], "\x60"], + [["7"], "\x70"], + [["8"], "\x80"], + [["9"], "\x90"], + [["a"], "\xa0"], + [["b"], "\xb0"], + [["c"], "\xc0"], + [["d"], "\xd0"], + [["e"], "\xe0"], + [["f"], "\xf0"], + [["A"], "\xa0"], + [["B"], "\xb0"], + [["C"], "\xc0"], + [["D"], "\xd0"], + [["E"], "\xe0"], + [["F"], "\xf0"] + ].should be_computed_by(:pack, "H") + end + + it "encodes the second character as the least significant nibble of a hex value" do + [ [["00"], "\x00"], + [["01"], "\x01"], + [["02"], "\x02"], + [["03"], "\x03"], + [["04"], "\x04"], + [["05"], "\x05"], + [["06"], "\x06"], + [["07"], "\x07"], + [["08"], "\x08"], + [["09"], "\x09"], + [["0a"], "\x0a"], + [["0b"], "\x0b"], + [["0c"], "\x0c"], + [["0d"], "\x0d"], + [["0e"], "\x0e"], + [["0f"], "\x0f"], + [["0A"], "\x0a"], + [["0B"], "\x0b"], + [["0C"], "\x0c"], + [["0D"], "\x0d"], + [["0E"], "\x0e"], + [["0F"], "\x0f"] + ].should be_computed_by(:pack, "H2") + end + + it "encodes the least significant nibble of a non alphanumeric character as the most significant nibble of the hex value" do + [ [["^"], "\xe0"], + [["*"], "\xa0"], + [["#"], "\x30"], + [["["], "\xb0"], + [["]"], "\xd0"], + [["@"], "\x00"], + [["!"], "\x10"], + [["H"], "\x10"], + [["O"], "\x80"], + [["T"], "\xd0"], + [["Z"], "\x30"], + ].should be_computed_by(:pack, "H") + end + + it "returns a binary string" do + ["41"].pack("H").encoding.should == Encoding::BINARY + end +end + +describe "Array#pack with format 'h'" do + it_behaves_like :array_pack_basic, 'h' + it_behaves_like :array_pack_basic_non_float, 'h' + it_behaves_like :array_pack_arguments, 'h' + it_behaves_like :array_pack_hex, 'h' + it_behaves_like :array_pack_taint, 'h' + + it "calls #to_str to convert an Object to a String" do + obj = mock("pack H string") + obj.should_receive(:to_str).and_return("a") + [obj].pack("h").should == "\x0a" + end + + it "encodes the first character as the least significant nibble when passed no count modifier" do + ["ab"].pack("h").should == "\x0a" + end + + it "implicitly has count equal to the string length when passed the '*' modifier" do + ["deadbeef"].pack("h*").should == "\xed\xda\xeb\xfe" + end + + it "encodes count nibbles when passed a count modifier exceeding the string length" do + ["ab"].pack('h8').should == "\xba\x00\x00\x00" + end + + it "encodes the first character as the least significant nibble of a hex value" do + [ [["0"], "\x00"], + [["1"], "\x01"], + [["2"], "\x02"], + [["3"], "\x03"], + [["4"], "\x04"], + [["5"], "\x05"], + [["6"], "\x06"], + [["7"], "\x07"], + [["8"], "\x08"], + [["9"], "\x09"], + [["a"], "\x0a"], + [["b"], "\x0b"], + [["c"], "\x0c"], + [["d"], "\x0d"], + [["e"], "\x0e"], + [["f"], "\x0f"], + [["A"], "\x0a"], + [["B"], "\x0b"], + [["C"], "\x0c"], + [["D"], "\x0d"], + [["E"], "\x0e"], + [["F"], "\x0f"] + ].should be_computed_by(:pack, "h") + end + + it "encodes the second character as the most significant nibble of a hex value" do + [ [["00"], "\x00"], + [["01"], "\x10"], + [["02"], "\x20"], + [["03"], "\x30"], + [["04"], "\x40"], + [["05"], "\x50"], + [["06"], "\x60"], + [["07"], "\x70"], + [["08"], "\x80"], + [["09"], "\x90"], + [["0a"], "\xa0"], + [["0b"], "\xb0"], + [["0c"], "\xc0"], + [["0d"], "\xd0"], + [["0e"], "\xe0"], + [["0f"], "\xf0"], + [["0A"], "\xa0"], + [["0B"], "\xb0"], + [["0C"], "\xc0"], + [["0D"], "\xd0"], + [["0E"], "\xe0"], + [["0F"], "\xf0"] + ].should be_computed_by(:pack, "h2") + end + + it "encodes the least significant nibble of a non alphanumeric character as the least significant nibble of the hex value" do + [ [["^"], "\x0e"], + [["*"], "\x0a"], + [["#"], "\x03"], + [["["], "\x0b"], + [["]"], "\x0d"], + [["@"], "\x00"], + [["!"], "\x01"], + [["H"], "\x01"], + [["O"], "\x08"], + [["T"], "\x0d"], + [["Z"], "\x03"], + ].should be_computed_by(:pack, "h") + end + + it "returns a binary string" do + ["41"].pack("h").encoding.should == Encoding::BINARY + end +end diff --git a/spec/ruby/core/array/pack/i_spec.rb b/spec/ruby/core/array/pack/i_spec.rb new file mode 100644 index 0000000000..a237071227 --- /dev/null +++ b/spec/ruby/core/array/pack/i_spec.rb @@ -0,0 +1,133 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' +require_relative 'shared/numeric_basic' +require_relative 'shared/integer' + +describe "Array#pack with format 'I'" do + it_behaves_like :array_pack_basic, 'I' + it_behaves_like :array_pack_basic_non_float, 'I' + it_behaves_like :array_pack_arguments, 'I' + it_behaves_like :array_pack_numeric_basic, 'I' + it_behaves_like :array_pack_integer, 'I' +end + +describe "Array#pack with format 'i'" do + it_behaves_like :array_pack_basic, 'i' + it_behaves_like :array_pack_basic_non_float, 'i' + it_behaves_like :array_pack_arguments, 'i' + it_behaves_like :array_pack_numeric_basic, 'i' + it_behaves_like :array_pack_integer, 'i' +end + +describe "Array#pack with format 'I'" do + describe "with modifier '<'" do + it_behaves_like :array_pack_32bit_le, 'I<' + end + + describe "with modifier '<' and '_'" do + it_behaves_like :array_pack_32bit_le, 'I<_' + it_behaves_like :array_pack_32bit_le, 'I_<' + end + + describe "with modifier '<' and '!'" do + it_behaves_like :array_pack_32bit_le, 'I<!' + it_behaves_like :array_pack_32bit_le, 'I!<' + end + + describe "with modifier '>'" do + it_behaves_like :array_pack_32bit_be, 'I>' + end + + describe "with modifier '>' and '_'" do + it_behaves_like :array_pack_32bit_be, 'I>_' + it_behaves_like :array_pack_32bit_be, 'I_>' + end + + describe "with modifier '>' and '!'" do + it_behaves_like :array_pack_32bit_be, 'I>!' + it_behaves_like :array_pack_32bit_be, 'I!>' + end +end + +describe "Array#pack with format 'i'" do + describe "with modifier '<'" do + it_behaves_like :array_pack_32bit_le, 'i<' + end + + describe "with modifier '<' and '_'" do + it_behaves_like :array_pack_32bit_le, 'i<_' + it_behaves_like :array_pack_32bit_le, 'i_<' + end + + describe "with modifier '<' and '!'" do + it_behaves_like :array_pack_32bit_le, 'i<!' + it_behaves_like :array_pack_32bit_le, 'i!<' + end + + describe "with modifier '>'" do + it_behaves_like :array_pack_32bit_be, 'i>' + end + + describe "with modifier '>' and '_'" do + it_behaves_like :array_pack_32bit_be, 'i>_' + it_behaves_like :array_pack_32bit_be, 'i_>' + end + + describe "with modifier '>' and '!'" do + it_behaves_like :array_pack_32bit_be, 'i>!' + it_behaves_like :array_pack_32bit_be, 'i!>' + end +end + +little_endian do + describe "Array#pack with format 'I'" do + it_behaves_like :array_pack_32bit_le, 'I' + end + + describe "Array#pack with format 'I' with modifier '_'" do + it_behaves_like :array_pack_32bit_le_platform, 'I_' + end + + describe "Array#pack with format 'I' with modifier '!'" do + it_behaves_like :array_pack_32bit_le_platform, 'I!' + end + + describe "Array#pack with format 'i'" do + it_behaves_like :array_pack_32bit_le, 'i' + end + + describe "Array#pack with format 'i' with modifier '_'" do + it_behaves_like :array_pack_32bit_le_platform, 'i_' + end + + describe "Array#pack with format 'i' with modifier '!'" do + it_behaves_like :array_pack_32bit_le_platform, 'i!' + end +end + +big_endian do + describe "Array#pack with format 'I'" do + it_behaves_like :array_pack_32bit_be, 'I' + end + + describe "Array#pack with format 'I' with modifier '_'" do + it_behaves_like :array_pack_32bit_be_platform, 'I_' + end + + describe "Array#pack with format 'I' with modifier '!'" do + it_behaves_like :array_pack_32bit_be_platform, 'I!' + end + + describe "Array#pack with format 'i'" do + it_behaves_like :array_pack_32bit_be, 'i' + end + + describe "Array#pack with format 'i' with modifier '_'" do + it_behaves_like :array_pack_32bit_be_platform, 'i_' + end + + describe "Array#pack with format 'i' with modifier '!'" do + it_behaves_like :array_pack_32bit_be_platform, 'i!' + end +end diff --git a/spec/ruby/core/array/pack/j_spec.rb b/spec/ruby/core/array/pack/j_spec.rb new file mode 100644 index 0000000000..7b62d5efdf --- /dev/null +++ b/spec/ruby/core/array/pack/j_spec.rb @@ -0,0 +1,217 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' +require_relative 'shared/numeric_basic' +require_relative 'shared/integer' + +platform_is pointer_size: 64 do + describe "Array#pack with format 'J'" do + it_behaves_like :array_pack_basic, 'J' + it_behaves_like :array_pack_basic_non_float, 'J' + it_behaves_like :array_pack_arguments, 'J' + it_behaves_like :array_pack_numeric_basic, 'J' + it_behaves_like :array_pack_integer, 'J' + end + + describe "Array#pack with format 'j'" do + it_behaves_like :array_pack_basic, 'j' + it_behaves_like :array_pack_basic_non_float, 'j' + it_behaves_like :array_pack_arguments, 'j' + it_behaves_like :array_pack_numeric_basic, 'j' + it_behaves_like :array_pack_integer, 'j' + end + + little_endian do + describe "Array#pack with format 'J'" do + describe "with modifier '_'" do + it_behaves_like :array_pack_64bit_le, 'J_' + end + + describe "with modifier '!'" do + it_behaves_like :array_pack_64bit_le, 'J!' + end + end + + describe "Array#pack with format 'j'" do + describe "with modifier '_'" do + it_behaves_like :array_pack_64bit_le, 'j_' + end + + describe "with modifier '!'" do + it_behaves_like :array_pack_64bit_le, 'j!' + end + end + end + + big_endian do + describe "Array#pack with format 'J'" do + describe "with modifier '_'" do + it_behaves_like :array_pack_64bit_be, 'J_' + end + + describe "with modifier '!'" do + it_behaves_like :array_pack_64bit_be, 'J!' + end + end + + describe "Array#pack with format 'j'" do + describe "with modifier '_'" do + it_behaves_like :array_pack_64bit_be, 'j_' + end + + describe "with modifier '!'" do + it_behaves_like :array_pack_64bit_be, 'j!' + end + end + end + + describe "Array#pack with format 'J'" do + describe "with modifier '<' and '_'" do + it_behaves_like :array_pack_64bit_le, 'J<_' + it_behaves_like :array_pack_64bit_le, 'J_<' + end + + describe "with modifier '<' and '!'" do + it_behaves_like :array_pack_64bit_le, 'J<!' + it_behaves_like :array_pack_64bit_le, 'J!<' + end + + describe "with modifier '>' and '_'" do + it_behaves_like :array_pack_64bit_be, 'J>_' + it_behaves_like :array_pack_64bit_be, 'J_>' + end + + describe "with modifier '>' and '!'" do + it_behaves_like :array_pack_64bit_be, 'J>!' + it_behaves_like :array_pack_64bit_be, 'J!>' + end + end + + describe "Array#pack with format 'j'" do + describe "with modifier '<' and '_'" do + it_behaves_like :array_pack_64bit_le, 'j<_' + it_behaves_like :array_pack_64bit_le, 'j_<' + end + + describe "with modifier '<' and '!'" do + it_behaves_like :array_pack_64bit_le, 'j<!' + it_behaves_like :array_pack_64bit_le, 'j!<' + end + + describe "with modifier '>' and '_'" do + it_behaves_like :array_pack_64bit_be, 'j>_' + it_behaves_like :array_pack_64bit_be, 'j_>' + end + + describe "with modifier '>' and '!'" do + it_behaves_like :array_pack_64bit_be, 'j>!' + it_behaves_like :array_pack_64bit_be, 'j!>' + end + end +end + +platform_is pointer_size: 32 do + describe "Array#pack with format 'J'" do + it_behaves_like :array_pack_basic, 'J' + it_behaves_like :array_pack_basic_non_float, 'J' + it_behaves_like :array_pack_arguments, 'J' + it_behaves_like :array_pack_numeric_basic, 'J' + it_behaves_like :array_pack_integer, 'J' + end + + describe "Array#pack with format 'j'" do + it_behaves_like :array_pack_basic, 'j' + it_behaves_like :array_pack_basic_non_float, 'j' + it_behaves_like :array_pack_arguments, 'j' + it_behaves_like :array_pack_numeric_basic, 'j' + it_behaves_like :array_pack_integer, 'j' + end + + big_endian do + describe "Array#pack with format 'J'" do + describe "with modifier '_'" do + it_behaves_like :array_pack_32bit_be, 'J_' + end + + describe "with modifier '!'" do + it_behaves_like :array_pack_32bit_be, 'J!' + end + end + + describe "Array#pack with format 'j'" do + describe "with modifier '_'" do + it_behaves_like :array_pack_32bit_be, 'j_' + end + + describe "with modifier '!'" do + it_behaves_like :array_pack_32bit_be, 'j!' + end + end + end + + little_endian do + describe "Array#pack with format 'J'" do + describe "with modifier '_'" do + it_behaves_like :array_pack_32bit_le, 'J_' + end + + describe "with modifier '!'" do + it_behaves_like :array_pack_32bit_le, 'J!' + end + end + + describe "Array#pack with format 'j'" do + describe "with modifier '_'" do + it_behaves_like :array_pack_32bit_le, 'j_' + end + + describe "with modifier '!'" do + it_behaves_like :array_pack_32bit_le, 'j!' + end + end + end + + describe "Array#pack with format 'J'" do + describe "with modifier '<' and '_'" do + it_behaves_like :array_pack_32bit_le, 'J<_' + it_behaves_like :array_pack_32bit_le, 'J_<' + end + + describe "with modifier '<' and '!'" do + it_behaves_like :array_pack_32bit_le, 'J<!' + it_behaves_like :array_pack_32bit_le, 'J!<' + end + + describe "with modifier '>' and '_'" do + it_behaves_like :array_pack_32bit_be, 'J>_' + it_behaves_like :array_pack_32bit_be, 'J_>' + end + + describe "with modifier '>' and '!'" do + it_behaves_like :array_pack_32bit_be, 'J>!' + it_behaves_like :array_pack_32bit_be, 'J!>' + end + end + + describe "Array#pack with format 'j'" do + describe "with modifier '<' and '_'" do + it_behaves_like :array_pack_32bit_le, 'j<_' + it_behaves_like :array_pack_32bit_le, 'j_<' + end + + describe "with modifier '<' and '!'" do + it_behaves_like :array_pack_32bit_le, 'j<!' + it_behaves_like :array_pack_32bit_le, 'j!<' + end + + describe "with modifier '>' and '_'" do + it_behaves_like :array_pack_32bit_be, 'j>_' + it_behaves_like :array_pack_32bit_be, 'j_>' + end + + describe "with modifier '>' and '!'" do + it_behaves_like :array_pack_32bit_be, 'j>!' + it_behaves_like :array_pack_32bit_be, 'j!>' + end + end +end diff --git a/spec/ruby/core/array/pack/l_spec.rb b/spec/ruby/core/array/pack/l_spec.rb new file mode 100644 index 0000000000..f6dfb1da83 --- /dev/null +++ b/spec/ruby/core/array/pack/l_spec.rb @@ -0,0 +1,221 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' +require_relative 'shared/numeric_basic' +require_relative 'shared/integer' + +describe "Array#pack with format 'L'" do + it_behaves_like :array_pack_basic, 'L' + it_behaves_like :array_pack_basic_non_float, 'L' + it_behaves_like :array_pack_arguments, 'L' + it_behaves_like :array_pack_numeric_basic, 'L' + it_behaves_like :array_pack_integer, 'L' +end + +describe "Array#pack with format 'l'" do + it_behaves_like :array_pack_basic, 'l' + it_behaves_like :array_pack_basic_non_float, 'l' + it_behaves_like :array_pack_arguments, 'l' + it_behaves_like :array_pack_numeric_basic, 'l' + it_behaves_like :array_pack_integer, 'l' +end + +describe "Array#pack with format 'L'" do + describe "with modifier '<'" do + it_behaves_like :array_pack_32bit_le, 'L<' + end + + describe "with modifier '>'" do + it_behaves_like :array_pack_32bit_be, 'L>' + end + + platform_is c_long_size: 32 do + describe "with modifier '<' and '_'" do + it_behaves_like :array_pack_32bit_le, 'L<_' + it_behaves_like :array_pack_32bit_le, 'L_<' + end + + describe "with modifier '<' and '!'" do + it_behaves_like :array_pack_32bit_le, 'L<!' + it_behaves_like :array_pack_32bit_le, 'L!<' + end + + describe "with modifier '>' and '_'" do + it_behaves_like :array_pack_32bit_be, 'L>_' + it_behaves_like :array_pack_32bit_be, 'L_>' + end + + describe "with modifier '>' and '!'" do + it_behaves_like :array_pack_32bit_be, 'L>!' + it_behaves_like :array_pack_32bit_be, 'L!>' + end + end + + platform_is c_long_size: 64 do + describe "with modifier '<' and '_'" do + it_behaves_like :array_pack_64bit_le, 'L<_' + it_behaves_like :array_pack_64bit_le, 'L_<' + end + + describe "with modifier '<' and '!'" do + it_behaves_like :array_pack_64bit_le, 'L<!' + it_behaves_like :array_pack_64bit_le, 'L!<' + end + + describe "with modifier '>' and '_'" do + it_behaves_like :array_pack_64bit_be, 'L>_' + it_behaves_like :array_pack_64bit_be, 'L_>' + end + + describe "with modifier '>' and '!'" do + it_behaves_like :array_pack_64bit_be, 'L>!' + it_behaves_like :array_pack_64bit_be, 'L!>' + end + end +end + +describe "Array#pack with format 'l'" do + describe "with modifier '<'" do + it_behaves_like :array_pack_32bit_le, 'l<' + end + + describe "with modifier '>'" do + it_behaves_like :array_pack_32bit_be, 'l>' + end + + platform_is c_long_size: 32 do + describe "with modifier '<' and '_'" do + it_behaves_like :array_pack_32bit_le, 'l<_' + it_behaves_like :array_pack_32bit_le, 'l_<' + end + + describe "with modifier '<' and '!'" do + it_behaves_like :array_pack_32bit_le, 'l<!' + it_behaves_like :array_pack_32bit_le, 'l!<' + end + + describe "with modifier '>' and '_'" do + it_behaves_like :array_pack_32bit_be, 'l>_' + it_behaves_like :array_pack_32bit_be, 'l_>' + end + + describe "with modifier '>' and '!'" do + it_behaves_like :array_pack_32bit_be, 'l>!' + it_behaves_like :array_pack_32bit_be, 'l!>' + end + end + + platform_is c_long_size: 64 do + describe "with modifier '<' and '_'" do + it_behaves_like :array_pack_64bit_le, 'l<_' + it_behaves_like :array_pack_64bit_le, 'l_<' + end + + describe "with modifier '<' and '!'" do + it_behaves_like :array_pack_64bit_le, 'l<!' + it_behaves_like :array_pack_64bit_le, 'l!<' + end + + describe "with modifier '>' and '_'" do + it_behaves_like :array_pack_64bit_be, 'l>_' + it_behaves_like :array_pack_64bit_be, 'l_>' + end + + describe "with modifier '>' and '!'" do + it_behaves_like :array_pack_64bit_be, 'l>!' + it_behaves_like :array_pack_64bit_be, 'l!>' + end + end +end + +little_endian do + describe "Array#pack with format 'L'" do + it_behaves_like :array_pack_32bit_le, 'L' + end + + describe "Array#pack with format 'l'" do + it_behaves_like :array_pack_32bit_le, 'l' + end + + platform_is c_long_size: 32 do + describe "Array#pack with format 'L' with modifier '_'" do + it_behaves_like :array_pack_32bit_le, 'L_' + end + + describe "Array#pack with format 'L' with modifier '!'" do + it_behaves_like :array_pack_32bit_le, 'L!' + end + + describe "Array#pack with format 'l' with modifier '_'" do + it_behaves_like :array_pack_32bit_le, 'l_' + end + + describe "Array#pack with format 'l' with modifier '!'" do + it_behaves_like :array_pack_32bit_le, 'l!' + end + end + + platform_is c_long_size: 64 do + describe "Array#pack with format 'L' with modifier '_'" do + it_behaves_like :array_pack_64bit_le, 'L_' + end + + describe "Array#pack with format 'L' with modifier '!'" do + it_behaves_like :array_pack_64bit_le, 'L!' + end + + describe "Array#pack with format 'l' with modifier '_'" do + it_behaves_like :array_pack_64bit_le, 'l_' + end + + describe "Array#pack with format 'l' with modifier '!'" do + it_behaves_like :array_pack_64bit_le, 'l!' + end + end +end + +big_endian do + describe "Array#pack with format 'L'" do + it_behaves_like :array_pack_32bit_be, 'L' + end + + describe "Array#pack with format 'l'" do + it_behaves_like :array_pack_32bit_be, 'l' + end + + platform_is c_long_size: 32 do + describe "Array#pack with format 'L' with modifier '_'" do + it_behaves_like :array_pack_32bit_be, 'L_' + end + + describe "Array#pack with format 'L' with modifier '!'" do + it_behaves_like :array_pack_32bit_be, 'L!' + end + + describe "Array#pack with format 'l' with modifier '_'" do + it_behaves_like :array_pack_32bit_be, 'l_' + end + + describe "Array#pack with format 'l' with modifier '!'" do + it_behaves_like :array_pack_32bit_be, 'l!' + end + end + + platform_is c_long_size: 64 do + describe "Array#pack with format 'L' with modifier '_'" do + it_behaves_like :array_pack_64bit_be, 'L_' + end + + describe "Array#pack with format 'L' with modifier '!'" do + it_behaves_like :array_pack_64bit_be, 'L!' + end + + describe "Array#pack with format 'l' with modifier '_'" do + it_behaves_like :array_pack_64bit_be, 'l_' + end + + describe "Array#pack with format 'l' with modifier '!'" do + it_behaves_like :array_pack_64bit_be, 'l!' + end + end +end diff --git a/spec/ruby/core/array/pack/m_spec.rb b/spec/ruby/core/array/pack/m_spec.rb new file mode 100644 index 0000000000..a80f91275c --- /dev/null +++ b/spec/ruby/core/array/pack/m_spec.rb @@ -0,0 +1,317 @@ +# encoding: binary +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' +require_relative 'shared/taint' + +describe "Array#pack with format 'M'" do + it_behaves_like :array_pack_basic, 'M' + it_behaves_like :array_pack_basic_non_float, 'M' + it_behaves_like :array_pack_arguments, 'M' + it_behaves_like :array_pack_taint, 'M' + + it "encodes an empty string as an empty string" do + [""].pack("M").should == "" + end + + it "encodes nil as an empty string" do + [nil].pack("M").should == "" + end + + it "appends a soft line break at the end of an encoded string" do + ["a"].pack("M").should == "a=\n" + end + + it "does not append a soft break if the string ends with a newline" do + ["a\n"].pack("M").should == "a\n" + end + + it "encodes one element for each directive" do + ["a", "b", "c"].pack("MM").should == "a=\nb=\n" + end + + it "encodes byte values 33..60 directly" do + [ [["!\"\#$%&'()*+,-./"], "!\"\#$%&'()*+,-./=\n"], + [["0123456789"], "0123456789=\n"], + [[":;<"], ":;<=\n"] + ].should be_computed_by(:pack, "M") + end + + it "encodes byte values 62..126 directly" do + [ [[">?@"], ">?@=\n"], + [["ABCDEFGHIJKLMNOPQRSTUVWXYZ"], "ABCDEFGHIJKLMNOPQRSTUVWXYZ=\n"], + [["[\\]^_`"], "[\\]^_`=\n"], + [["abcdefghijklmnopqrstuvwxyz"], "abcdefghijklmnopqrstuvwxyz=\n"], + [["{|}~"], "{|}~=\n"] + ].should be_computed_by(:pack, "M") + end + + it "encodes an '=' character in hex format" do + ["="].pack("M").should == "=3D=\n" + end + + it "encodes an embedded space directly" do + ["a b"].pack("M").should == "a b=\n" + end + + it "encodes a space at the end of the string directly" do + ["a "].pack("M").should == "a =\n" + end + + it "encodes an embedded tab directly" do + ["a\tb"].pack("M").should == "a\tb=\n" + end + + it "encodes a tab at the end of the string directly" do + ["a\t"].pack("M").should == "a\t=\n" + end + + it "encodes an embedded newline directly" do + ["a\nb"].pack("M").should == "a\nb=\n" + end + + it "encodes 0..31 except tab and newline in hex format" do + [ [["\x00\x01\x02\x03\x04\x05\x06"], "=00=01=02=03=04=05=06=\n"], + [["\a\b\v\f\r"], "=07=08=0B=0C=0D=\n"], + [["\x0e\x0f\x10\x11\x12\x13\x14"], "=0E=0F=10=11=12=13=14=\n"], + [["\x15\x16\x17\x18\x19\x1a"], "=15=16=17=18=19=1A=\n"], + [["\e"], "=1B=\n"], + [["\x1c\x1d\x1e\x1f"], "=1C=1D=1E=1F=\n"] + ].should be_computed_by(:pack, "M") + end + + it "encodes a tab at the end of a line with an encoded newline" do + ["\t"].pack("M").should == "\t=\n" + ["\t\n"].pack("M").should == "\t=\n\n" + ["abc\t\nxyz"].pack("M").should == "abc\t=\n\nxyz=\n" + end + + it "encodes a space at the end of a line with an encoded newline" do + [" "].pack("M").should == " =\n" + [" \n"].pack("M").should == " =\n\n" + ["abc \nxyz"].pack("M").should == "abc =\n\nxyz=\n" + end + + it "encodes 127..255 in hex format" do + [ [["\x7f\x80\x81\x82\x83\x84\x85\x86"], "=7F=80=81=82=83=84=85=86=\n"], + [["\x87\x88\x89\x8a\x8b\x8c\x8d\x8e"], "=87=88=89=8A=8B=8C=8D=8E=\n"], + [["\x8f\x90\x91\x92\x93\x94\x95\x96"], "=8F=90=91=92=93=94=95=96=\n"], + [["\x97\x98\x99\x9a\x9b\x9c\x9d\x9e"], "=97=98=99=9A=9B=9C=9D=9E=\n"], + [["\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6"], "=9F=A0=A1=A2=A3=A4=A5=A6=\n"], + [["\xa7\xa8\xa9\xaa\xab\xac\xad\xae"], "=A7=A8=A9=AA=AB=AC=AD=AE=\n"], + [["\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6"], "=AF=B0=B1=B2=B3=B4=B5=B6=\n"], + [["\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe"], "=B7=B8=B9=BA=BB=BC=BD=BE=\n"], + [["\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6"], "=BF=C0=C1=C2=C3=C4=C5=C6=\n"], + [["\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce"], "=C7=C8=C9=CA=CB=CC=CD=CE=\n"], + [["\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6"], "=CF=D0=D1=D2=D3=D4=D5=D6=\n"], + [["\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde"], "=D7=D8=D9=DA=DB=DC=DD=DE=\n"], + [["\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6"], "=DF=E0=E1=E2=E3=E4=E5=E6=\n"], + [["\xe7\xe8\xe9\xea\xeb\xec\xed\xee"], "=E7=E8=E9=EA=EB=EC=ED=EE=\n"], + [["\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6"], "=EF=F0=F1=F2=F3=F4=F5=F6=\n"], + [["\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe"], "=F7=F8=F9=FA=FB=FC=FD=FE=\n"], + [["\xff"], "=FF=\n"] + ].should be_computed_by(:pack, "M") + end + + it "emits a soft line break when the output exceeds 72 characters when passed '*', 0, 1, or no count modifier" do + s1 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + r1 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=\na=\n" + s2 = "\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19" + r2 = "=19=19=19=19=19=19=19=19=19=19=19=19=19=19=19=19=19=19=19=19=19=19=19=19=19=\n=19=\n" + s3 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\x15a" + r3 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=15=\na=\n" + s4 = "\x15aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\x15a" + r4 = "=15aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=\na=15a=\n" + + [ [[s1], "M", r1], + [[s1], "M0", r1], + [[s1], "M1", r1], + [[s2], "M", r2], + [[s2], "M0", r2], + [[s2], "M1", r2], + [[s3], "M", r3], + [[s3], "M0", r3], + [[s3], "M1", r3], + [[s4], "M", r4], + [[s4], "M0", r4], + [[s4], "M1", r4] + ].should be_computed_by(:pack) + end + + it "emits a soft line break when the output exceeds count characters" do + [ [["abcdefghi"], "M2", "abc=\ndef=\nghi=\n"], + [["abcdefghi"], "M3", "abcd=\nefgh=\ni=\n"], + [["abcdefghi"], "M4", "abcde=\nfghi=\n"], + [["abcdefghi"], "M5", "abcdef=\nghi=\n"], + [["abcdefghi"], "M6", "abcdefg=\nhi=\n"], + [["\x19\x19\x19\x19"], "M2", "=19=\n=19=\n=19=\n=19=\n"], + [["\x19\x19\x19\x19"], "M3", "=19=19=\n=19=19=\n"], + [["\x19\x19\x19\x19"], "M4", "=19=19=\n=19=19=\n"], + [["\x19\x19\x19\x19"], "M5", "=19=19=\n=19=19=\n"], + [["\x19\x19\x19\x19"], "M6", "=19=19=19=\n=19=\n"], + [["\x19\x19\x19\x19"], "M7", "=19=19=19=\n=19=\n"] + ].should be_computed_by(:pack) + end + + it "encodes a recursive array" do + empty = ArraySpecs.empty_recursive_array + empty.pack('M').should be_an_instance_of(String) + + array = ArraySpecs.recursive_array + array.pack('M').should == "1=\n" + end + + it "calls #to_s to convert an object to a String" do + obj = mock("pack M string") + obj.should_receive(:to_s).and_return("packing") + + [obj].pack("M").should == "packing=\n" + end + + it "converts the object to a String representation if #to_s does not return a String" do + obj = mock("pack M non-string") + obj.should_receive(:to_s).and_return(2) + + [obj].pack("M").should be_an_instance_of(String) + end + + it "encodes a Symbol as a String" do + [:symbol].pack("M").should == "symbol=\n" + end + + it "encodes an Integer as a String" do + [ [[1], "1=\n"], + [[bignum_value], "#{bignum_value}=\n"] + ].should be_computed_by(:pack, "M") + end + + it "encodes a Float as a String" do + [1.0].pack("M").should == "1.0=\n" + end + + it "converts Floats to the minimum unique representation" do + [1.0 / 3.0].pack("M").should == "0.3333333333333333=\n" + end + + it "sets the output string to US-ASCII encoding" do + ["abcd"].pack("M").encoding.should == Encoding::US_ASCII + end +end + +describe "Array#pack with format 'm'" do + it_behaves_like :array_pack_basic, 'm' + it_behaves_like :array_pack_basic_non_float, 'm' + it_behaves_like :array_pack_arguments, 'm' + it_behaves_like :array_pack_taint, 'm' + + it "encodes an empty string as an empty string" do + [""].pack("m").should == "" + end + + it "appends a newline to the end of the encoded string" do + ["a"].pack("m").should == "YQ==\n" + end + + it "encodes one element per directive" do + ["abc", "DEF"].pack("mm").should == "YWJj\nREVG\n" + end + + it "encodes 1, 2, or 3 characters in 4 output characters (Base64 encoding)" do + [ [["a"], "YQ==\n"], + [["ab"], "YWI=\n"], + [["abc"], "YWJj\n"], + [["abcd"], "YWJjZA==\n"], + [["abcde"], "YWJjZGU=\n"], + [["abcdef"], "YWJjZGVm\n"], + [["abcdefg"], "YWJjZGVmZw==\n"], + ].should be_computed_by(:pack, "m") + end + + it "emits a newline after complete groups of count / 3 input characters when passed a count modifier" do + ["abcdefg"].pack("m3").should == "YWJj\nZGVm\nZw==\n" + end + + it "implicitly has a count of 45 when passed '*', 1, 2 or no count modifier" do + s = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + r = "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh\nYWFhYWE=\n" + [ [[s], "m", r], + [[s], "m*", r], + [[s], "m1", r], + [[s], "m2", r], + ].should be_computed_by(:pack) + end + + it "encodes all ascii characters" do + [ [["\x00\x01\x02\x03\x04\x05\x06"], "AAECAwQFBg==\n"], + [["\a\b\t\n\v\f\r"], "BwgJCgsMDQ==\n"], + [["\x0E\x0F\x10\x11\x12\x13\x14\x15\x16"], "Dg8QERITFBUW\n"], + [["\x17\x18\x19\x1a\e\x1c\x1d\x1e\x1f"], "FxgZGhscHR4f\n"], + [["!\"\#$%&'()*+,-./"], "ISIjJCUmJygpKissLS4v\n"], + [["0123456789"], "MDEyMzQ1Njc4OQ==\n"], + [[":;<=>?@"], "Ojs8PT4/QA==\n"], + [["ABCDEFGHIJKLMNOPQRSTUVWXYZ"], "QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVo=\n"], + [["[\\]^_`"], "W1xdXl9g\n"], + [["abcdefghijklmnopqrstuvwxyz"], "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo=\n"], + [["{|}~"], "e3x9fg==\n"], + [["\x7f\xc2\x80\xc2\x81\xc2\x82\xc2\x83"], "f8KAwoHCgsKD\n"], + [["\xc2\x84\xc2\x85\xc2\x86\xc2\x87\xc2"], "woTChcKGwofC\n"], + [["\x88\xc2\x89\xc2\x8a\xc2\x8b\xc2\x8c"], "iMKJworCi8KM\n"], + [["\xc2\x8d\xc2\x8e\xc2\x8f\xc2\x90\xc2"], "wo3CjsKPwpDC\n"], + [["\x91\xc2\x92\xc2\x93\xc2\x94\xc2\x95"], "kcKSwpPClMKV\n"], + [["\xc2\x96\xc2\x97\xc2\x98\xc2\x99\xc2"], "wpbCl8KYwpnC\n"], + [["\x9a\xc2\x9b\xc2\x9c\xc2\x9d\xc2\x9e"], "msKbwpzCncKe\n"], + [["\xc2\x9f\xc2\xa0\xc2\xa1\xc2\xa2\xc2"], "wp/CoMKhwqLC\n"], + [["\xa3\xc2\xa4\xc2\xa5\xc2\xa6\xc2\xa7"], "o8KkwqXCpsKn\n"], + [["\xc2\xa8\xc2\xa9\xc2\xaa\xc2\xab\xc2"], "wqjCqcKqwqvC\n"], + [["\xac\xc2\xad\xc2\xae\xc2\xaf\xc2\xb0"], "rMKtwq7Cr8Kw\n"], + [["\xc2\xb1\xc2\xb2\xc2\xb3\xc2\xb4\xc2"], "wrHCssKzwrTC\n"], + [["\xb5\xc2\xb6\xc2\xb7\xc2\xb8\xc2\xb9"], "tcK2wrfCuMK5\n"], + [["\xc2\xba\xc2\xbb\xc2\xbc\xc2\xbd\xc2"], "wrrCu8K8wr3C\n"], + [["\xbe\xc2\xbf\xc3\x80\xc3\x81\xc3\x82"], "vsK/w4DDgcOC\n"], + [["\xc3\x83\xc3\x84\xc3\x85\xc3\x86\xc3"], "w4PDhMOFw4bD\n"], + [["\x87\xc3\x88\xc3\x89\xc3\x8a\xc3\x8b"], "h8OIw4nDisOL\n"], + [["\xc3\x8c\xc3\x8d\xc3\x8e\xc3\x8f\xc3"], "w4zDjcOOw4/D\n"], + [["\x90\xc3\x91\xc3\x92\xc3\x93\xc3\x94"], "kMORw5LDk8OU\n"], + [["\xc3\x95\xc3\x96\xc3\x97\xc3\x98\xc3"], "w5XDlsOXw5jD\n"], + [["\x99\xc3\x9a\xc3\x9b\xc3\x9c\xc3\x9d"], "mcOaw5vDnMOd\n"], + [["\xc3\x9e\xc3\x9f\xc3\xa0\xc3\xa1\xc3"], "w57Dn8Ogw6HD\n"], + [["\xa2\xc3\xa3\xc3\xa4\xc3\xa5\xc3\xa6"], "osOjw6TDpcOm\n"], + [["\xc3\xa7\xc3\xa8\xc3\xa9\xc3\xaa\xc3"], "w6fDqMOpw6rD\n"], + [["\xab\xc3\xac\xc3\xad\xc3\xae\xc3\xaf"], "q8Osw63DrsOv\n"], + [["\xc3\xb0\xc3\xb1\xc3\xb2\xc3\xb3\xc3"], "w7DDscOyw7PD\n"], + [["\xb4\xc3\xb5\xc3\xb6\xc3\xb7\xc3\xb8"], "tMO1w7bDt8O4\n"], + [["\xc3\xb9\xc3\xba\xc3\xbb\xc3\xbc\xc3"], "w7nDusO7w7zD\n"], + [["\xbd\xc3\xbe\xc3\xbf"], "vcO+w78=\n"] + ].should be_computed_by(:pack, "m") + end + + it "calls #to_str to convert an object to a String" do + obj = mock("pack m string") + obj.should_receive(:to_str).and_return("abc") + [obj].pack("m").should == "YWJj\n" + end + + it "raises a TypeError if #to_str does not return a String" do + obj = mock("pack m non-string") + -> { [obj].pack("m") }.should raise_error(TypeError) + end + + it "raises a TypeError if passed nil" do + -> { [nil].pack("m") }.should raise_error(TypeError) + end + + it "raises a TypeError if passed an Integer" do + -> { [0].pack("m") }.should raise_error(TypeError) + -> { [bignum_value].pack("m") }.should raise_error(TypeError) + end + + it "does not emit a newline if passed zero as the count modifier" do + s = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + r = "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=" + [s].pack("m0").should == r + end + + it "sets the output string to US-ASCII encoding" do + ["abcd"].pack("m").encoding.should == Encoding::US_ASCII + end +end diff --git a/spec/ruby/core/array/pack/n_spec.rb b/spec/ruby/core/array/pack/n_spec.rb new file mode 100644 index 0000000000..ab9409fc1e --- /dev/null +++ b/spec/ruby/core/array/pack/n_spec.rb @@ -0,0 +1,25 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' +require_relative 'shared/numeric_basic' +require_relative 'shared/integer' + +describe "Array#pack with format 'N'" do + it_behaves_like :array_pack_basic, 'N' + it_behaves_like :array_pack_basic_non_float, 'N' + it_behaves_like :array_pack_arguments, 'N' + it_behaves_like :array_pack_numeric_basic, 'N' + it_behaves_like :array_pack_integer, 'N' + it_behaves_like :array_pack_no_platform, 'N' + it_behaves_like :array_pack_32bit_be, 'N' +end + +describe "Array#pack with format 'n'" do + it_behaves_like :array_pack_basic, 'n' + it_behaves_like :array_pack_basic_non_float, 'n' + it_behaves_like :array_pack_arguments, 'n' + it_behaves_like :array_pack_numeric_basic, 'n' + it_behaves_like :array_pack_integer, 'n' + it_behaves_like :array_pack_no_platform, 'n' + it_behaves_like :array_pack_16bit_be, 'n' +end diff --git a/spec/ruby/core/array/pack/p_spec.rb b/spec/ruby/core/array/pack/p_spec.rb new file mode 100644 index 0000000000..b023bf9110 --- /dev/null +++ b/spec/ruby/core/array/pack/p_spec.rb @@ -0,0 +1,38 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' +require_relative 'shared/taint' + +describe "Array#pack with format 'P'" do + it_behaves_like :array_pack_basic_non_float, 'P' + it_behaves_like :array_pack_taint, 'P' + + it "produces as many bytes as there are in a pointer" do + ["hello"].pack("P").size.should == [0].pack("J").size + end + + it "round-trips a string through pack and unpack" do + ["hello"].pack("P").unpack("P5").should == ["hello"] + end + + it "with nil gives a null pointer" do + [nil].pack("P").unpack("J").should == [0] + end +end + +describe "Array#pack with format 'p'" do + it_behaves_like :array_pack_basic_non_float, 'p' + it_behaves_like :array_pack_taint, 'p' + + it "produces as many bytes as there are in a pointer" do + ["hello"].pack("p").size.should == [0].pack("J").size + end + + it "round-trips a string through pack and unpack" do + ["hello"].pack("p").unpack("p").should == ["hello"] + end + + it "with nil gives a null pointer" do + [nil].pack("p").unpack("J").should == [0] + end +end diff --git a/spec/ruby/core/array/pack/percent_spec.rb b/spec/ruby/core/array/pack/percent_spec.rb new file mode 100644 index 0000000000..5d56dea5fe --- /dev/null +++ b/spec/ruby/core/array/pack/percent_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../../spec_helper' + +describe "Array#pack with format '%'" do + it "raises an Argument Error" do + -> { [1].pack("%") }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/array/pack/q_spec.rb b/spec/ruby/core/array/pack/q_spec.rb new file mode 100644 index 0000000000..bd6b2a4b71 --- /dev/null +++ b/spec/ruby/core/array/pack/q_spec.rb @@ -0,0 +1,61 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' +require_relative 'shared/numeric_basic' +require_relative 'shared/integer' + +describe "Array#pack with format 'Q'" do + it_behaves_like :array_pack_basic, 'Q' + it_behaves_like :array_pack_basic_non_float, 'Q' + it_behaves_like :array_pack_arguments, 'Q' + it_behaves_like :array_pack_numeric_basic, 'Q' + it_behaves_like :array_pack_integer, 'Q' +end + +describe "Array#pack with format 'q'" do + it_behaves_like :array_pack_basic, 'q' + it_behaves_like :array_pack_basic_non_float, 'q' + it_behaves_like :array_pack_arguments, 'q' + it_behaves_like :array_pack_numeric_basic, 'q' + it_behaves_like :array_pack_integer, 'q' +end + +describe "Array#pack with format 'Q'" do + describe "with modifier '<'" do + it_behaves_like :array_pack_64bit_le, 'Q<' + end + + describe "with modifier '>'" do + it_behaves_like :array_pack_64bit_be, 'Q>' + end +end + +describe "Array#pack with format 'q'" do + describe "with modifier '<'" do + it_behaves_like :array_pack_64bit_le, 'q<' + end + + describe "with modifier '>'" do + it_behaves_like :array_pack_64bit_be, 'q>' + end +end + +little_endian do + describe "Array#pack with format 'Q'" do + it_behaves_like :array_pack_64bit_le, 'Q' + end + + describe "Array#pack with format 'q'" do + it_behaves_like :array_pack_64bit_le, 'q' + end +end + +big_endian do + describe "Array#pack with format 'Q'" do + it_behaves_like :array_pack_64bit_be, 'Q' + end + + describe "Array#pack with format 'q'" do + it_behaves_like :array_pack_64bit_be, 'q' + end +end diff --git a/spec/ruby/core/array/pack/s_spec.rb b/spec/ruby/core/array/pack/s_spec.rb new file mode 100644 index 0000000000..4212d6a0b1 --- /dev/null +++ b/spec/ruby/core/array/pack/s_spec.rb @@ -0,0 +1,133 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' +require_relative 'shared/numeric_basic' +require_relative 'shared/integer' + +describe "Array#pack with format 'S'" do + it_behaves_like :array_pack_basic, 'S' + it_behaves_like :array_pack_basic_non_float, 'S' + it_behaves_like :array_pack_arguments, 'S' + it_behaves_like :array_pack_numeric_basic, 'S' + it_behaves_like :array_pack_integer, 'S' +end + +describe "Array#pack with format 's'" do + it_behaves_like :array_pack_basic, 's' + it_behaves_like :array_pack_basic_non_float, 's' + it_behaves_like :array_pack_arguments, 's' + it_behaves_like :array_pack_numeric_basic, 's' + it_behaves_like :array_pack_integer, 's' +end + +describe "Array#pack with format 'S'" do + describe "with modifier '<'" do + it_behaves_like :array_pack_16bit_le, 'S<' + end + + describe "with modifier '<' and '_'" do + it_behaves_like :array_pack_16bit_le, 'S<_' + it_behaves_like :array_pack_16bit_le, 'S_<' + end + + describe "with modifier '<' and '!'" do + it_behaves_like :array_pack_16bit_le, 'S<!' + it_behaves_like :array_pack_16bit_le, 'S!<' + end + + describe "with modifier '>'" do + it_behaves_like :array_pack_16bit_be, 'S>' + end + + describe "with modifier '>' and '_'" do + it_behaves_like :array_pack_16bit_be, 'S>_' + it_behaves_like :array_pack_16bit_be, 'S_>' + end + + describe "with modifier '>' and '!'" do + it_behaves_like :array_pack_16bit_be, 'S>!' + it_behaves_like :array_pack_16bit_be, 'S!>' + end +end + +describe "Array#pack with format 's'" do + describe "with modifier '<'" do + it_behaves_like :array_pack_16bit_le, 's<' + end + + describe "with modifier '<' and '_'" do + it_behaves_like :array_pack_16bit_le, 's<_' + it_behaves_like :array_pack_16bit_le, 's_<' + end + + describe "with modifier '<' and '!'" do + it_behaves_like :array_pack_16bit_le, 's<!' + it_behaves_like :array_pack_16bit_le, 's!<' + end + + describe "with modifier '>'" do + it_behaves_like :array_pack_16bit_be, 's>' + end + + describe "with modifier '>' and '_'" do + it_behaves_like :array_pack_16bit_be, 's>_' + it_behaves_like :array_pack_16bit_be, 's_>' + end + + describe "with modifier '>' and '!'" do + it_behaves_like :array_pack_16bit_be, 's>!' + it_behaves_like :array_pack_16bit_be, 's!>' + end +end + +little_endian do + describe "Array#pack with format 'S'" do + it_behaves_like :array_pack_16bit_le, 'S' + end + + describe "Array#pack with format 'S' with modifier '_'" do + it_behaves_like :array_pack_16bit_le, 'S_' + end + + describe "Array#pack with format 'S' with modifier '!'" do + it_behaves_like :array_pack_16bit_le, 'S!' + end + + describe "Array#pack with format 's'" do + it_behaves_like :array_pack_16bit_le, 's' + end + + describe "Array#pack with format 's' with modifier '_'" do + it_behaves_like :array_pack_16bit_le, 's_' + end + + describe "Array#pack with format 's' with modifier '!'" do + it_behaves_like :array_pack_16bit_le, 's!' + end +end + +big_endian do + describe "Array#pack with format 'S'" do + it_behaves_like :array_pack_16bit_be, 'S' + end + + describe "Array#pack with format 'S' with modifier '_'" do + it_behaves_like :array_pack_16bit_be, 'S_' + end + + describe "Array#pack with format 'S' with modifier '!'" do + it_behaves_like :array_pack_16bit_be, 'S!' + end + + describe "Array#pack with format 's'" do + it_behaves_like :array_pack_16bit_be, 's' + end + + describe "Array#pack with format 's' with modifier '_'" do + it_behaves_like :array_pack_16bit_be, 's_' + end + + describe "Array#pack with format 's' with modifier '!'" do + it_behaves_like :array_pack_16bit_be, 's!' + end +end diff --git a/spec/ruby/core/array/pack/shared/basic.rb b/spec/ruby/core/array/pack/shared/basic.rb new file mode 100644 index 0000000000..a63f64d296 --- /dev/null +++ b/spec/ruby/core/array/pack/shared/basic.rb @@ -0,0 +1,84 @@ +describe :array_pack_arguments, shared: true do + it "raises an ArgumentError if there are fewer elements than the format requires" do + -> { [].pack(pack_format(1)) }.should raise_error(ArgumentError) + end +end + +describe :array_pack_basic, shared: true do + before :each do + @obj = ArraySpecs.universal_pack_object + end + + it "raises a TypeError when passed nil" do + -> { [@obj].pack(nil) }.should raise_error(TypeError) + end + + it "raises a TypeError when passed an Integer" do + -> { [@obj].pack(1) }.should raise_error(TypeError) + end +end + +describe :array_pack_basic_non_float, shared: true do + before :each do + @obj = ArraySpecs.universal_pack_object + end + + it "ignores whitespace in the format string" do + [@obj, @obj].pack("a \t\n\v\f\r"+pack_format).should be_an_instance_of(String) + end + + it "ignores comments in the format string" do + # 2 additional directives ('a') are required for the X directive + [@obj, @obj, @obj, @obj].pack("aa #{pack_format} # some comment \n#{pack_format}").should be_an_instance_of(String) + end + + ruby_version_is ""..."3.3" do + it "warns that a directive is unknown" do + # additional directive ('a') is required for the X directive + -> { [@obj, @obj].pack("a K" + pack_format) }.should complain(/unknown pack directive 'K' in 'a K#{pack_format}'/) + -> { [@obj, @obj].pack("a 0" + pack_format) }.should complain(/unknown pack directive '0' in 'a 0#{pack_format}'/) + -> { [@obj, @obj].pack("a :" + pack_format) }.should complain(/unknown pack directive ':' in 'a :#{pack_format}'/) + end + end + + ruby_version_is "3.3" do + it "raise ArgumentError when a directive is unknown" do + # additional directive ('a') is required for the X directive + -> { [@obj, @obj].pack("a R" + pack_format) }.should raise_error(ArgumentError, /unknown pack directive 'R'/) + -> { [@obj, @obj].pack("a 0" + pack_format) }.should raise_error(ArgumentError, /unknown pack directive '0'/) + -> { [@obj, @obj].pack("a :" + pack_format) }.should raise_error(ArgumentError, /unknown pack directive ':'/) + end + end + + it "calls #to_str to coerce the directives string" do + d = mock("pack directive") + d.should_receive(:to_str).and_return("x"+pack_format) + [@obj, @obj].pack(d).should be_an_instance_of(String) + end +end + +describe :array_pack_basic_float, shared: true do + it "ignores whitespace in the format string" do + [9.3, 4.7].pack(" \t\n\v\f\r"+pack_format).should be_an_instance_of(String) + end + + it "ignores comments in the format string" do + [9.3, 4.7].pack(pack_format + "# some comment \n" + pack_format).should be_an_instance_of(String) + end + + it "calls #to_str to coerce the directives string" do + d = mock("pack directive") + d.should_receive(:to_str).and_return("x"+pack_format) + [1.2, 4.7].pack(d).should be_an_instance_of(String) + end +end + +describe :array_pack_no_platform, shared: true do + it "raises ArgumentError when the format modifier is '_'" do + ->{ [1].pack(pack_format("_")) }.should raise_error(ArgumentError) + end + + it "raises ArgumentError when the format modifier is '!'" do + ->{ [1].pack(pack_format("!")) }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/array/pack/shared/encodings.rb b/spec/ruby/core/array/pack/shared/encodings.rb new file mode 100644 index 0000000000..6b7ffac764 --- /dev/null +++ b/spec/ruby/core/array/pack/shared/encodings.rb @@ -0,0 +1,16 @@ +describe :array_pack_hex, shared: true do + it "encodes no bytes when passed zero as the count modifier" do + ["abc"].pack(pack_format(0)).should == "" + end + + it "raises a TypeError if the object does not respond to #to_str" do + obj = mock("pack hex non-string") + -> { [obj].pack(pack_format) }.should raise_error(TypeError) + end + + it "raises a TypeError if #to_str does not return a String" do + obj = mock("pack hex non-string") + obj.should_receive(:to_str).and_return(1) + -> { [obj].pack(pack_format) }.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/array/pack/shared/float.rb b/spec/ruby/core/array/pack/shared/float.rb new file mode 100644 index 0000000000..76c800b74d --- /dev/null +++ b/spec/ruby/core/array/pack/shared/float.rb @@ -0,0 +1,295 @@ +# encoding: binary + +describe :array_pack_float_le, shared: true do + it "encodes a positive Float" do + [1.42].pack(pack_format).should == "\x8f\xc2\xb5?" + end + + it "encodes a negative Float" do + [-34.2].pack(pack_format).should == "\xcd\xcc\x08\xc2" + end + + it "converts an Integer to a Float" do + [8].pack(pack_format).should == "\x00\x00\x00A" + end + + it "raises a TypeError if passed a String representation of a floating point number" do + -> { ["13"].pack(pack_format) }.should raise_error(TypeError) + end + + it "encodes the number of array elements specified by the count modifier" do + [2.9, 1.4, 8.2].pack(pack_format(nil, 2)).should == "\x9a\x999@33\xb3?" + end + + it "encodes all remaining elements when passed the '*' modifier" do + [2.9, 1.4, 8.2].pack(pack_format("*")).should == "\x9a\x999@33\xb3?33\x03A" + end + + ruby_version_is ""..."3.3" do + it "ignores NULL bytes between directives" do + suppress_warning do + [5.3, 9.2].pack(pack_format("\000", 2)).should == "\x9a\x99\xa9@33\x13A" + end + end + end + + ruby_version_is "3.3" do + it "raise ArgumentError for NULL bytes between directives" do + -> { + [5.3, 9.2].pack(pack_format("\000", 2)) + }.should raise_error(ArgumentError, /unknown pack directive/) + end + end + + it "ignores spaces between directives" do + [5.3, 9.2].pack(pack_format(" ", 2)).should == "\x9a\x99\xa9@33\x13A" + end + + it "encodes positive Infinity" do + [infinity_value].pack(pack_format).should == "\x00\x00\x80\x7f" + end + + it "encodes negative Infinity" do + [-infinity_value].pack(pack_format).should == "\x00\x00\x80\xff" + end + + it "encodes NaN" do + nans = ["\x00\x00\xc0\xff", "\x00\x00\xc0\x7f", "\xFF\xFF\xFF\x7F"] + nans.should include([nan_value].pack(pack_format)) + end + + it "encodes a positive Float outside the range of a single precision float" do + [1e150].pack(pack_format).should == "\x00\x00\x80\x7f" + end + + it "encodes a negative Float outside the range of a single precision float" do + [-1e150].pack(pack_format).should == "\x00\x00\x80\xff" + end + + it "encodes a bignum as a float" do + [2 ** 65].pack(pack_format).should == [(2 ** 65).to_f].pack(pack_format) + end + + it "encodes a rational as a float" do + [Rational(3, 4)].pack(pack_format).should == [Rational(3, 4).to_f].pack(pack_format) + end +end + +describe :array_pack_float_be, shared: true do + it "encodes a positive Float" do + [1.42].pack(pack_format).should == "?\xb5\xc2\x8f" + end + + it "encodes a negative Float" do + [-34.2].pack(pack_format).should == "\xc2\x08\xcc\xcd" + end + + it "converts an Integer to a Float" do + [8].pack(pack_format).should == "A\x00\x00\x00" + [bignum_value].pack(pack_format).should == "_\x80\x00\x00" + end + + it "converts a Rational to a Float" do + [Rational(8)].pack(pack_format).should == "A\x00\x00\x00" + end + + it "raises a TypeError if passed a String representation of a floating point number" do + -> { ["13"].pack(pack_format) }.should raise_error(TypeError) + end + + it "encodes the number of array elements specified by the count modifier" do + [2.9, 1.4, 8.2].pack(pack_format(nil, 2)).should == "@9\x99\x9a?\xb333" + end + + it "encodes all remaining elements when passed the '*' modifier" do + [2.9, 1.4, 8.2].pack(pack_format("*")).should == "@9\x99\x9a?\xb333A\x0333" + end + + ruby_version_is ""..."3.3" do + it "ignores NULL bytes between directives" do + suppress_warning do + [5.3, 9.2].pack(pack_format("\000", 2)).should == "@\xa9\x99\x9aA\x1333" + end + end + end + + ruby_version_is "3.3" do + it "raise ArgumentError for NULL bytes between directives" do + -> { + [5.3, 9.2].pack(pack_format("\000", 2)) + }.should raise_error(ArgumentError, /unknown pack directive/) + end + end + + it "ignores spaces between directives" do + [5.3, 9.2].pack(pack_format(" ", 2)).should == "@\xa9\x99\x9aA\x1333" + end + + it "encodes positive Infinity" do + [infinity_value].pack(pack_format).should == "\x7f\x80\x00\x00" + end + + it "encodes negative Infinity" do + [-infinity_value].pack(pack_format).should == "\xff\x80\x00\x00" + end + + it "encodes NaN" do + nans = ["\xff\xc0\x00\x00", "\x7f\xc0\x00\x00", "\x7F\xFF\xFF\xFF"] + nans.should include([nan_value].pack(pack_format)) + end + + it "encodes a positive Float outside the range of a single precision float" do + [1e150].pack(pack_format).should == "\x7f\x80\x00\x00" + end + + it "encodes a negative Float outside the range of a single precision float" do + [-1e150].pack(pack_format).should == "\xff\x80\x00\x00" + end +end + +describe :array_pack_double_le, shared: true do + it "encodes a positive Float" do + [1.42].pack(pack_format).should == "\xb8\x1e\x85\xebQ\xb8\xf6?" + end + + it "encodes a negative Float" do + [-34.2].pack(pack_format).should == "\x9a\x99\x99\x99\x99\x19A\xc0" + end + + it "converts an Integer to a Float" do + [8].pack(pack_format).should == "\x00\x00\x00\x00\x00\x00\x20@" + [bignum_value].pack(pack_format).should == "\x00\x00\x00\x00\x00\x00\xF0C" + end + + it "converts a Rational to a Float" do + [Rational(8)].pack(pack_format).should == "\x00\x00\x00\x00\x00\x00 @" + end + + it "raises a TypeError if passed a String representation of a floating point number" do + -> { ["13"].pack(pack_format) }.should raise_error(TypeError) + end + + it "encodes the number of array elements specified by the count modifier" do + [2.9, 1.4, 8.2].pack(pack_format(nil, 2)).should == "333333\x07@ffffff\xf6?" + end + + it "encodes all remaining elements when passed the '*' modifier" do + [2.9, 1.4, 8.2].pack(pack_format("*")).should == "333333\x07@ffffff\xf6?ffffff\x20@" + end + + ruby_version_is ""..."3.3" do + it "ignores NULL bytes between directives" do + suppress_warning do + [5.3, 9.2].pack(pack_format("\000", 2)).should == "333333\x15@ffffff\x22@" + end + end + end + + ruby_version_is "3.3" do + it "raise ArgumentError for NULL bytes between directives" do + -> { + [5.3, 9.2].pack(pack_format("\000", 2)) + }.should raise_error(ArgumentError, /unknown pack directive/) + end + end + + it "ignores spaces between directives" do + [5.3, 9.2].pack(pack_format(" ", 2)).should == "333333\x15@ffffff\x22@" + end + + it "encodes positive Infinity" do + [infinity_value].pack(pack_format).should == "\x00\x00\x00\x00\x00\x00\xf0\x7f" + end + + it "encodes negative Infinity" do + [-infinity_value].pack(pack_format).should == "\x00\x00\x00\x00\x00\x00\xf0\xff" + end + + it "encodes NaN" do + nans = [ + "\x00\x00\x00\x00\x00\x00\xf8\xff", + "\x00\x00\x00\x00\x00\x00\xf8\x7f", + "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x7F" + ] + nans.should include([nan_value].pack(pack_format)) + end + + it "encodes a positive Float outside the range of a single precision float" do + [1e150].pack(pack_format).should == "\xaf\x96P\x2e5\x8d\x13_" + end + + it "encodes a negative Float outside the range of a single precision float" do + [-1e150].pack(pack_format).should == "\xaf\x96P\x2e5\x8d\x13\xdf" + end +end + +describe :array_pack_double_be, shared: true do + it "encodes a positive Float" do + [1.42].pack(pack_format).should == "?\xf6\xb8Q\xeb\x85\x1e\xb8" + end + + it "encodes a negative Float" do + [-34.2].pack(pack_format).should == "\xc0A\x19\x99\x99\x99\x99\x9a" + end + + it "converts an Integer to a Float" do + [8].pack(pack_format).should == "@\x20\x00\x00\x00\x00\x00\x00" + end + + it "raises a TypeError if passed a String representation of a floating point number" do + -> { ["13"].pack(pack_format) }.should raise_error(TypeError) + end + + it "encodes the number of array elements specified by the count modifier" do + [2.9, 1.4, 8.2].pack(pack_format(nil, 2)).should == "@\x07333333?\xf6ffffff" + end + + it "encodes all remaining elements when passed the '*' modifier" do + [2.9, 1.4, 8.2].pack(pack_format("*")).should == "@\x07333333?\xf6ffffff@\x20ffffff" + end + + ruby_version_is ""..."3.3" do + it "ignores NULL bytes between directives" do + suppress_warning do + [5.3, 9.2].pack(pack_format("\000", 2)).should == "@\x15333333@\x22ffffff" + end + end + end + + ruby_version_is "3.3" do + it "raise ArgumentError for NULL bytes between directives" do + -> { + [5.3, 9.2].pack(pack_format("\000", 2)) + }.should raise_error(ArgumentError, /unknown pack directive/) + end + end + + it "ignores spaces between directives" do + [5.3, 9.2].pack(pack_format(" ", 2)).should == "@\x15333333@\x22ffffff" + end + + it "encodes positive Infinity" do + [infinity_value].pack(pack_format).should == "\x7f\xf0\x00\x00\x00\x00\x00\x00" + end + + it "encodes negative Infinity" do + [-infinity_value].pack(pack_format).should == "\xff\xf0\x00\x00\x00\x00\x00\x00" + end + + it "encodes NaN" do + nans = [ + "\xff\xf8\x00\x00\x00\x00\x00\x00", + "\x7f\xf8\x00\x00\x00\x00\x00\x00", + "\x7F\xFF\xFF\xFF\xFF\xFF\xFF\xFF" + ] + nans.should include([nan_value].pack(pack_format)) + end + + it "encodes a positive Float outside the range of a single precision float" do + [1e150].pack(pack_format).should == "_\x13\x8d5\x2eP\x96\xaf" + end + + it "encodes a negative Float outside the range of a single precision float" do + [-1e150].pack(pack_format).should == "\xdf\x13\x8d5\x2eP\x96\xaf" + end +end diff --git a/spec/ruby/core/array/pack/shared/integer.rb b/spec/ruby/core/array/pack/shared/integer.rb new file mode 100644 index 0000000000..61f7cca184 --- /dev/null +++ b/spec/ruby/core/array/pack/shared/integer.rb @@ -0,0 +1,453 @@ +# encoding: binary + +describe :array_pack_16bit_le, shared: true do + it "encodes the least significant 16 bits of a positive number" do + [ [[0x0000_0021], "\x21\x00"], + [[0x0000_4321], "\x21\x43"], + [[0x0065_4321], "\x21\x43"], + [[0x7865_4321], "\x21\x43"] + ].should be_computed_by(:pack, pack_format()) + end + + it "encodes the least significant 16 bits of a negative number" do + [ [[-0x0000_0021], "\xdf\xff"], + [[-0x0000_4321], "\xdf\xbc"], + [[-0x0065_4321], "\xdf\xbc"], + [[-0x7865_4321], "\xdf\xbc"] + ].should be_computed_by(:pack, pack_format()) + end + + it "encodes a Float truncated as an Integer" do + [ [[2019902241.2], "\x21\x43"], + [[2019902241.8], "\x21\x43"], + [[-2019902241.2], "\xdf\xbc"], + [[-2019902241.8], "\xdf\xbc"] + ].should be_computed_by(:pack, pack_format()) + end + + it "calls #to_int to convert the pack argument to an Integer" do + obj = mock('to_int') + obj.should_receive(:to_int).and_return(0x1234_5678) + [obj].pack(pack_format()).should == "\x78\x56" + end + + it "encodes the number of array elements specified by the count modifier" do + str = [0x1243_6578, 0xdef0_abcd, 0x7865_4321].pack(pack_format(2)) + str.should == "\x78\x65\xcd\xab" + end + + it "encodes all remaining elements when passed the '*' modifier" do + str = [0x1243_6578, 0xdef0_abcd, 0x7865_4321].pack(pack_format('*')) + str.should == "\x78\x65\xcd\xab\x21\x43" + end + + ruby_version_is ""..."3.3" do + it "ignores NULL bytes between directives" do + suppress_warning do + str = [0x1243_6578, 0xdef0_abcd].pack(pack_format("\000", 2)) + str.should == "\x78\x65\xcd\xab" + end + end + end + + ruby_version_is "3.3" do + it "raise ArgumentError for NULL bytes between directives" do + -> { + [0x1243_6578, 0xdef0_abcd].pack(pack_format("\000", 2)) + }.should raise_error(ArgumentError, /unknown pack directive/) + end + end + + it "ignores spaces between directives" do + str = [0x1243_6578, 0xdef0_abcd].pack(pack_format(' ', 2)) + str.should == "\x78\x65\xcd\xab" + end +end + +describe :array_pack_16bit_be, shared: true do + it "encodes the least significant 16 bits of a positive number" do + [ [[0x0000_0021], "\x00\x21"], + [[0x0000_4321], "\x43\x21"], + [[0x0065_4321], "\x43\x21"], + [[0x7865_4321], "\x43\x21"] + ].should be_computed_by(:pack, pack_format()) + end + + it "encodes the least significant 16 bits of a negative number" do + [ [[-0x0000_0021], "\xff\xdf"], + [[-0x0000_4321], "\xbc\xdf"], + [[-0x0065_4321], "\xbc\xdf"], + [[-0x7865_4321], "\xbc\xdf"] + ].should be_computed_by(:pack, pack_format()) + end + + it "encodes a Float truncated as an Integer" do + [ [[2019902241.2], "\x43\x21"], + [[2019902241.8], "\x43\x21"], + [[-2019902241.2], "\xbc\xdf"], + [[-2019902241.8], "\xbc\xdf"] + ].should be_computed_by(:pack, pack_format()) + end + + it "calls #to_int to convert the pack argument to an Integer" do + obj = mock('to_int') + obj.should_receive(:to_int).and_return(0x1234_5678) + [obj].pack(pack_format()).should == "\x56\x78" + end + + it "encodes the number of array elements specified by the count modifier" do + str = [0x1243_6578, 0xdef0_abcd, 0x7865_4321].pack(pack_format(2)) + str.should == "\x65\x78\xab\xcd" + end + + it "encodes all remaining elements when passed the '*' modifier" do + str = [0x1243_6578, 0xdef0_abcd, 0x7865_4321].pack(pack_format('*')) + str.should == "\x65\x78\xab\xcd\x43\x21" + end + + ruby_version_is ""..."3.3" do + it "ignores NULL bytes between directives" do + suppress_warning do + str = [0x1243_6578, 0xdef0_abcd].pack(pack_format("\000", 2)) + str.should == "\x65\x78\xab\xcd" + end + end + end + + ruby_version_is "3.3" do + it "raise ArgumentError for NULL bytes between directives" do + -> { + [0x1243_6578, 0xdef0_abcd].pack(pack_format("\000", 2)) + }.should raise_error(ArgumentError, /unknown pack directive/) + end + end + + it "ignores spaces between directives" do + str = [0x1243_6578, 0xdef0_abcd].pack(pack_format(' ', 2)) + str.should == "\x65\x78\xab\xcd" + end +end + +describe :array_pack_32bit_le, shared: true do + it "encodes the least significant 32 bits of a positive number" do + [ [[0x0000_0021], "\x21\x00\x00\x00"], + [[0x0000_4321], "\x21\x43\x00\x00"], + [[0x0065_4321], "\x21\x43\x65\x00"], + [[0x7865_4321], "\x21\x43\x65\x78"] + ].should be_computed_by(:pack, pack_format()) + end + + it "encodes the least significant 32 bits of a negative number" do + [ [[-0x0000_0021], "\xdf\xff\xff\xff"], + [[-0x0000_4321], "\xdf\xbc\xff\xff"], + [[-0x0065_4321], "\xdf\xbc\x9a\xff"], + [[-0x7865_4321], "\xdf\xbc\x9a\x87"] + ].should be_computed_by(:pack, pack_format()) + end + + it "encodes a Float truncated as an Integer" do + [ [[2019902241.2], "\x21\x43\x65\x78"], + [[2019902241.8], "\x21\x43\x65\x78"], + [[-2019902241.2], "\xdf\xbc\x9a\x87"], + [[-2019902241.8], "\xdf\xbc\x9a\x87"] + ].should be_computed_by(:pack, pack_format()) + end + + it "calls #to_int to convert the pack argument to an Integer" do + obj = mock('to_int') + obj.should_receive(:to_int).and_return(0x1234_5678) + [obj].pack(pack_format()).should == "\x78\x56\x34\x12" + end + + it "encodes the number of array elements specified by the count modifier" do + str = [0x1243_6578, 0xdef0_abcd, 0x7865_4321].pack(pack_format(2)) + str.should == "\x78\x65\x43\x12\xcd\xab\xf0\xde" + end + + it "encodes all remaining elements when passed the '*' modifier" do + str = [0x1243_6578, 0xdef0_abcd, 0x7865_4321].pack(pack_format('*')) + str.should == "\x78\x65\x43\x12\xcd\xab\xf0\xde\x21\x43\x65\x78" + end + + ruby_version_is ""..."3.3" do + it "ignores NULL bytes between directives" do + suppress_warning do + str = [0x1243_6578, 0xdef0_abcd].pack(pack_format("\000", 2)) + str.should == "\x78\x65\x43\x12\xcd\xab\xf0\xde" + end + end + end + + ruby_version_is "3.3" do + it "raise ArgumentError for NULL bytes between directives" do + -> { + [0x1243_6578, 0xdef0_abcd].pack(pack_format("\000", 2)) + }.should raise_error(ArgumentError, /unknown pack directive/) + end + end + + it "ignores spaces between directives" do + str = [0x1243_6578, 0xdef0_abcd].pack(pack_format(' ', 2)) + str.should == "\x78\x65\x43\x12\xcd\xab\xf0\xde" + end +end + +describe :array_pack_32bit_be, shared: true do + it "encodes the least significant 32 bits of a positive number" do + [ [[0x0000_0021], "\x00\x00\x00\x21"], + [[0x0000_4321], "\x00\x00\x43\x21"], + [[0x0065_4321], "\x00\x65\x43\x21"], + [[0x7865_4321], "\x78\x65\x43\x21"] + ].should be_computed_by(:pack, pack_format()) + end + + it "encodes the least significant 32 bits of a negative number" do + [ [[-0x0000_0021], "\xff\xff\xff\xdf"], + [[-0x0000_4321], "\xff\xff\xbc\xdf"], + [[-0x0065_4321], "\xff\x9a\xbc\xdf"], + [[-0x7865_4321], "\x87\x9a\xbc\xdf"] + ].should be_computed_by(:pack, pack_format()) + end + + it "encodes a Float truncated as an Integer" do + [ [[2019902241.2], "\x78\x65\x43\x21"], + [[2019902241.8], "\x78\x65\x43\x21"], + [[-2019902241.2], "\x87\x9a\xbc\xdf"], + [[-2019902241.8], "\x87\x9a\xbc\xdf"] + ].should be_computed_by(:pack, pack_format()) + end + + it "calls #to_int to convert the pack argument to an Integer" do + obj = mock('to_int') + obj.should_receive(:to_int).and_return(0x1234_5678) + [obj].pack(pack_format()).should == "\x12\x34\x56\x78" + end + + it "encodes the number of array elements specified by the count modifier" do + str = [0x1243_6578, 0xdef0_abcd, 0x7865_4321].pack(pack_format(2)) + str.should == "\x12\x43\x65\x78\xde\xf0\xab\xcd" + end + + it "encodes all remaining elements when passed the '*' modifier" do + str = [0x1243_6578, 0xdef0_abcd, 0x7865_4321].pack(pack_format('*')) + str.should == "\x12\x43\x65\x78\xde\xf0\xab\xcd\x78\x65\x43\x21" + end + + ruby_version_is ""..."3.3" do + it "ignores NULL bytes between directives" do + suppress_warning do + str = [0x1243_6578, 0xdef0_abcd].pack(pack_format("\000", 2)) + str.should == "\x12\x43\x65\x78\xde\xf0\xab\xcd" + end + end + end + + ruby_version_is "3.3" do + it "raise ArgumentError for NULL bytes between directives" do + -> { + [0x1243_6578, 0xdef0_abcd].pack(pack_format("\000", 2)) + }.should raise_error(ArgumentError, /unknown pack directive/) + end + end + + it "ignores spaces between directives" do + str = [0x1243_6578, 0xdef0_abcd].pack(pack_format(' ', 2)) + str.should == "\x12\x43\x65\x78\xde\xf0\xab\xcd" + end +end + +describe :array_pack_32bit_le_platform, shared: true do + it "encodes the least significant 32 bits of a number" do + [ [[0x7865_4321], "\x21\x43\x65\x78"], + [[-0x7865_4321], "\xdf\xbc\x9a\x87"] + ].should be_computed_by(:pack, pack_format()) + end + + it "encodes the number of array elements specified by the count modifier" do + str = [0x1243_6578, 0xdef0_abcd, 0x7865_4321].pack(pack_format(2)) + str.should == "\x78\x65\x43\x12\xcd\xab\xf0\xde" + end + + it "encodes all remaining elements when passed the '*' modifier" do + str = [0x1243_6578, 0xdef0_abcd, 0x7865_4321].pack(pack_format('*')) + str.should == "\x78\x65\x43\x12\xcd\xab\xf0\xde\x21\x43\x65\x78" + end + + platform_is c_long_size: 64 do + it "encodes the least significant 32 bits of a number that is greater than 32 bits" do + [ [[0xff_7865_4321], "\x21\x43\x65\x78"], + [[-0xff_7865_4321], "\xdf\xbc\x9a\x87"] + ].should be_computed_by(:pack, pack_format()) + end + end +end + +describe :array_pack_32bit_be_platform, shared: true do + it "encodes the least significant 32 bits of a number" do + [ [[0x7865_4321], "\x78\x65\x43\x21"], + [[-0x7865_4321], "\x87\x9a\xbc\xdf"] + ].should be_computed_by(:pack, pack_format()) + end + + it "encodes the number of array elements specified by the count modifier" do + str = [0x1243_6578, 0xdef0_abcd, 0x7865_4321].pack(pack_format(2)) + str.should == "\x12\x43\x65\x78\xde\xf0\xab\xcd" + end + + it "encodes all remaining elements when passed the '*' modifier" do + str = [0x1243_6578, 0xdef0_abcd, 0x7865_4321].pack(pack_format('*')) + str.should == "\x12\x43\x65\x78\xde\xf0\xab\xcd\x78\x65\x43\x21" + end + + platform_is c_long_size: 64 do + it "encodes the least significant 32 bits of a number that is greater than 32 bits" do + [ [[0xff_7865_4321], "\x78\x65\x43\x21"], + [[-0xff_7865_4321], "\x87\x9a\xbc\xdf"] + ].should be_computed_by(:pack, pack_format()) + end + end +end + +describe :array_pack_64bit_le, shared: true do + it "encodes the least significant 64 bits of a positive number" do + [ [[0x0000_0000_0000_0021], "\x21\x00\x00\x00\x00\x00\x00\x00"], + [[0x0000_0000_0000_4321], "\x21\x43\x00\x00\x00\x00\x00\x00"], + [[0x0000_0000_0065_4321], "\x21\x43\x65\x00\x00\x00\x00\x00"], + [[0x0000_0000_7865_4321], "\x21\x43\x65\x78\x00\x00\x00\x00"], + [[0x0000_0090_7865_4321], "\x21\x43\x65\x78\x90\x00\x00\x00"], + [[0x0000_ba90_7865_4321], "\x21\x43\x65\x78\x90\xba\x00\x00"], + [[0x00dc_ba90_7865_4321], "\x21\x43\x65\x78\x90\xba\xdc\x00"], + [[0x7edc_ba90_7865_4321], "\x21\x43\x65\x78\x90\xba\xdc\x7e"] + ].should be_computed_by(:pack, pack_format()) + end + + it "encodes the least significant 64 bits of a negative number" do + [ [[-0x0000_0000_0000_0021], "\xdf\xff\xff\xff\xff\xff\xff\xff"], + [[-0x0000_0000_0000_4321], "\xdf\xbc\xff\xff\xff\xff\xff\xff"], + [[-0x0000_0000_0065_4321], "\xdf\xbc\x9a\xff\xff\xff\xff\xff"], + [[-0x0000_0000_7865_4321], "\xdf\xbc\x9a\x87\xff\xff\xff\xff"], + [[-0x0000_0090_7865_4321], "\xdf\xbc\x9a\x87\x6f\xff\xff\xff"], + [[-0x0000_ba90_7865_4321], "\xdf\xbc\x9a\x87\x6f\x45\xff\xff"], + [[-0x00dc_ba90_7865_4321], "\xdf\xbc\x9a\x87\x6f\x45\x23\xff"], + [[-0x7edc_ba90_7865_4321], "\xdf\xbc\x9a\x87\x6f\x45\x23\x81"] + ].should be_computed_by(:pack, pack_format()) + end + + it "encodes a Float truncated as an Integer" do + [ [[9.14138647331322368e+18], "\x00\x44\x65\x78\x90\xba\xdc\x7e"], + [[-9.14138647331322368e+18], "\x00\xbc\x9a\x87\x6f\x45\x23\x81"] + ].should be_computed_by(:pack, pack_format()) + end + + it "calls #to_int to convert the pack argument to an Integer" do + obj = mock('to_int') + obj.should_receive(:to_int).and_return(0x1234_5678_90ab_cdef) + [obj].pack(pack_format()).should == "\xef\xcd\xab\x90\x78\x56\x34\x12" + end + + it "encodes the number of array elements specified by the count modifier" do + str = [0x1234_5678_90ab_cdef, + 0xdef0_abcd_3412_7856, + 0x7865_4321_dcba_def0].pack(pack_format(2)) + str.should == "\xef\xcd\xab\x90\x78\x56\x34\x12\x56\x78\x12\x34\xcd\xab\xf0\xde" + end + + it "encodes all remaining elements when passed the '*' modifier" do + str = [0xdef0_abcd_3412_7856, 0x7865_4321_dcba_def0].pack(pack_format('*')) + str.should == "\x56\x78\x12\x34\xcd\xab\xf0\xde\xf0\xde\xba\xdc\x21\x43\x65\x78" + end + + ruby_version_is ""..."3.3" do + it "ignores NULL bytes between directives" do + suppress_warning do + str = [0xdef0_abcd_3412_7856, 0x7865_4321_dcba_def0].pack(pack_format("\000", 2)) + str.should == "\x56\x78\x12\x34\xcd\xab\xf0\xde\xf0\xde\xba\xdc\x21\x43\x65\x78" + end + end + end + + ruby_version_is "3.3" do + it "raise ArgumentError for NULL bytes between directives" do + -> { + [0xdef0_abcd_3412_7856, 0x7865_4321_dcba_def0].pack(pack_format("\000", 2)) + }.should raise_error(ArgumentError, /unknown pack directive/) + end + end + + it "ignores spaces between directives" do + str = [0xdef0_abcd_3412_7856, 0x7865_4321_dcba_def0].pack(pack_format(' ', 2)) + str.should == "\x56\x78\x12\x34\xcd\xab\xf0\xde\xf0\xde\xba\xdc\x21\x43\x65\x78" + end +end + +describe :array_pack_64bit_be, shared: true do + it "encodes the least significant 64 bits of a positive number" do + [ [[0x0000_0000_0000_0021], "\x00\x00\x00\x00\x00\x00\x00\x21"], + [[0x0000_0000_0000_4321], "\x00\x00\x00\x00\x00\x00\x43\x21"], + [[0x0000_0000_0065_4321], "\x00\x00\x00\x00\x00\x65\x43\x21"], + [[0x0000_0000_7865_4321], "\x00\x00\x00\x00\x78\x65\x43\x21"], + [[0x0000_0090_7865_4321], "\x00\x00\x00\x90\x78\x65\x43\x21"], + [[0x0000_ba90_7865_4321], "\x00\x00\xba\x90\x78\x65\x43\x21"], + [[0x00dc_ba90_7865_4321], "\x00\xdc\xba\x90\x78\x65\x43\x21"], + [[0x7edc_ba90_7865_4321], "\x7e\xdc\xba\x90\x78\x65\x43\x21"] + ].should be_computed_by(:pack, pack_format()) + end + + it "encodes the least significant 64 bits of a negative number" do + [ [[-0x0000_0000_0000_0021], "\xff\xff\xff\xff\xff\xff\xff\xdf"], + [[-0x0000_0000_0000_4321], "\xff\xff\xff\xff\xff\xff\xbc\xdf"], + [[-0x0000_0000_0065_4321], "\xff\xff\xff\xff\xff\x9a\xbc\xdf"], + [[-0x0000_0000_7865_4321], "\xff\xff\xff\xff\x87\x9a\xbc\xdf"], + [[-0x0000_0090_7865_4321], "\xff\xff\xff\x6f\x87\x9a\xbc\xdf"], + [[-0x0000_ba90_7865_4321], "\xff\xff\x45\x6f\x87\x9a\xbc\xdf"], + [[-0x00dc_ba90_7865_4321], "\xff\x23\x45\x6f\x87\x9a\xbc\xdf"], + [[-0x7edc_ba90_7865_4321], "\x81\x23\x45\x6f\x87\x9a\xbc\xdf"] + ].should be_computed_by(:pack, pack_format()) + end + + it "encodes a Float truncated as an Integer" do + [ [[9.14138647331322368e+18], "\x7e\xdc\xba\x90\x78\x65\x44\x00"], + [[-9.14138647331322368e+18], "\x81\x23\x45\x6f\x87\x9a\xbc\x00"] + ].should be_computed_by(:pack, pack_format()) + end + + it "calls #to_int to convert the pack argument to an Integer" do + obj = mock('to_int') + obj.should_receive(:to_int).and_return(0x1234_5678_90ab_cdef) + [obj].pack(pack_format()).should == "\x12\x34\x56\x78\x90\xab\xcd\xef" + end + + it "encodes the number of array elements specified by the count modifier" do + str = [0x1234_5678_90ab_cdef, + 0xdef0_abcd_3412_7856, + 0x7865_4321_dcba_def0].pack(pack_format(2)) + str.should == "\x12\x34\x56\x78\x90\xab\xcd\xef\xde\xf0\xab\xcd\x34\x12\x78\x56" + end + + it "encodes all remaining elements when passed the '*' modifier" do + str = [0xdef0_abcd_3412_7856, 0x7865_4321_dcba_def0].pack(pack_format('*')) + str.should == "\xde\xf0\xab\xcd\x34\x12\x78\x56\x78\x65\x43\x21\xdc\xba\xde\xf0" + end + + ruby_version_is ""..."3.3" do + it "ignores NULL bytes between directives" do + suppress_warning do + str = [0xdef0_abcd_3412_7856, 0x7865_4321_dcba_def0].pack(pack_format("\000", 2)) + str.should == "\xde\xf0\xab\xcd\x34\x12\x78\x56\x78\x65\x43\x21\xdc\xba\xde\xf0" + end + end + end + + ruby_version_is "3.3" do + it "raise ArgumentError for NULL bytes between directives" do + -> { + [0xdef0_abcd_3412_7856, 0x7865_4321_dcba_def0].pack(pack_format("\000", 2)) + }.should raise_error(ArgumentError, /unknown pack directive/) + end + end + + it "ignores spaces between directives" do + str = [0xdef0_abcd_3412_7856, 0x7865_4321_dcba_def0].pack(pack_format(' ', 2)) + str.should == "\xde\xf0\xab\xcd\x34\x12\x78\x56\x78\x65\x43\x21\xdc\xba\xde\xf0" + end +end diff --git a/spec/ruby/core/array/pack/shared/numeric_basic.rb b/spec/ruby/core/array/pack/shared/numeric_basic.rb new file mode 100644 index 0000000000..545e215e64 --- /dev/null +++ b/spec/ruby/core/array/pack/shared/numeric_basic.rb @@ -0,0 +1,50 @@ +describe :array_pack_numeric_basic, shared: true do + it "returns an empty String if count is zero" do + [1].pack(pack_format(0)).should == "" + end + + it "raises a TypeError when passed nil" do + -> { [nil].pack(pack_format) }.should raise_error(TypeError) + end + + it "raises a TypeError when passed true" do + -> { [true].pack(pack_format) }.should raise_error(TypeError) + end + + it "raises a TypeError when passed false" do + -> { [false].pack(pack_format) }.should raise_error(TypeError) + end + + it "returns a binary string" do + [0xFF].pack(pack_format).encoding.should == Encoding::BINARY + [0xE3, 0x81, 0x82].pack(pack_format(3)).encoding.should == Encoding::BINARY + end +end + +describe :array_pack_integer, shared: true do + it "raises a TypeError when the object does not respond to #to_int" do + obj = mock('not an integer') + -> { [obj].pack(pack_format) }.should raise_error(TypeError) + end + + it "raises a TypeError when passed a String" do + -> { ["5"].pack(pack_format) }.should raise_error(TypeError) + end +end + +describe :array_pack_float, shared: true do + it "raises a TypeError if a String does not represent a floating point number" do + -> { ["a"].pack(pack_format) }.should raise_error(TypeError) + end + + it "raises a TypeError when the object is not Numeric" do + obj = Object.new + -> { [obj].pack(pack_format) }.should raise_error(TypeError, /can't convert Object into Float/) + end + + it "raises a TypeError when the Numeric object does not respond to #to_f" do + klass = Class.new(Numeric) + obj = klass.new + -> { [obj].pack(pack_format) }.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/array/pack/shared/string.rb b/spec/ruby/core/array/pack/shared/string.rb new file mode 100644 index 0000000000..805f78b53b --- /dev/null +++ b/spec/ruby/core/array/pack/shared/string.rb @@ -0,0 +1,48 @@ +# encoding: binary +describe :array_pack_string, shared: true do + it "adds count bytes of a String to the output" do + ["abc"].pack(pack_format(2)).should == "ab" + end + + it "implicitly has a count of one when no count is specified" do + ["abc"].pack(pack_format).should == "a" + end + + it "does not add any bytes when the count is zero" do + ["abc"].pack(pack_format(0)).should == "" + end + + it "is not affected by a previous count modifier" do + ["abcde", "defg"].pack(pack_format(3)+pack_format).should == "abcd" + end + + it "raises an ArgumentError when the Array is empty" do + -> { [].pack(pack_format) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when the Array has too few elements" do + -> { ["a"].pack(pack_format(nil, 2)) }.should raise_error(ArgumentError) + end + + it "calls #to_str to convert the element to a String" do + obj = mock('pack string') + obj.should_receive(:to_str).and_return("abc") + + [obj].pack(pack_format).should == "a" + end + + it "raises a TypeError when the object does not respond to #to_str" do + obj = mock("not a string") + -> { [obj].pack(pack_format) }.should raise_error(TypeError) + end + + it "returns a string in encoding of common to the concatenated results" do + f = pack_format("*") + [ [["\u{3042 3044 3046 3048}", 0x2000B].pack(f+"U"), Encoding::BINARY], + [["abcde\xd1", "\xFF\xFe\x81\x82"].pack(f+"u"), Encoding::BINARY], + [["a".dup.force_encoding("ascii"), "\xFF\xFe\x81\x82"].pack(f+"u"), Encoding::BINARY], + # under discussion [ruby-dev:37294] + [["\u{3042 3044 3046 3048}", 1].pack(f+"N"), Encoding::BINARY] + ].should be_computed_by(:encoding) + end +end diff --git a/spec/ruby/core/array/pack/shared/taint.rb b/spec/ruby/core/array/pack/shared/taint.rb new file mode 100644 index 0000000000..2c2b011c34 --- /dev/null +++ b/spec/ruby/core/array/pack/shared/taint.rb @@ -0,0 +1,2 @@ +describe :array_pack_taint, shared: true do +end diff --git a/spec/ruby/core/array/pack/shared/unicode.rb b/spec/ruby/core/array/pack/shared/unicode.rb new file mode 100644 index 0000000000..4d8eaef323 --- /dev/null +++ b/spec/ruby/core/array/pack/shared/unicode.rb @@ -0,0 +1,106 @@ +# -*- encoding: utf-8 -*- + +describe :array_pack_unicode, shared: true do + it "encodes ASCII values as a Unicode codepoint" do + [ [[0], "\x00"], + [[1], "\x01"], + [[8], "\x08"], + [[15], "\x0f"], + [[24], "\x18"], + [[31], "\x1f"], + [[127], "\x7f"], + [[128], "\xc2\x80"], + [[129], "\xc2\x81"], + [[255], "\xc3\xbf"] + ].should be_computed_by(:pack, "U") + end + + it "encodes UTF-8 BMP codepoints" do + [ [[0x80], "\xc2\x80"], + [[0x7ff], "\xdf\xbf"], + [[0x800], "\xe0\xa0\x80"], + [[0xffff], "\xef\xbf\xbf"] + ].should be_computed_by(:pack, "U") + end + + it "constructs strings with valid encodings" do + str = [0x85].pack("U*") + str.should == "\xc2\x85" + str.valid_encoding?.should be_true + end + + it "encodes values larger than UTF-8 max codepoints" do + [ + [[0x00110000], [244, 144, 128, 128].pack('C*').force_encoding('utf-8')], + [[0x04000000], [252, 132, 128, 128, 128, 128].pack('C*').force_encoding('utf-8')], + [[0x7FFFFFFF], [253, 191, 191, 191, 191, 191].pack('C*').force_encoding('utf-8')] + ].should be_computed_by(:pack, "U") + end + + it "encodes UTF-8 max codepoints" do + [ [[0x10000], "\xf0\x90\x80\x80"], + [[0xfffff], "\xf3\xbf\xbf\xbf"], + [[0x100000], "\xf4\x80\x80\x80"], + [[0x10ffff], "\xf4\x8f\xbf\xbf"] + ].should be_computed_by(:pack, "U") + end + + it "encodes the number of array elements specified by the count modifier" do + [ [[0x41, 0x42, 0x43, 0x44], "U2", "\x41\x42"], + [[0x41, 0x42, 0x43, 0x44], "U2U", "\x41\x42\x43"] + ].should be_computed_by(:pack) + end + + it "encodes all remaining elements when passed the '*' modifier" do + [0x41, 0x42, 0x43, 0x44].pack("U*").should == "\x41\x42\x43\x44" + end + + it "calls #to_int to convert the pack argument to an Integer" do + obj = mock('to_int') + obj.should_receive(:to_int).and_return(5) + [obj].pack("U").should == "\x05" + end + + it "raises a TypeError if #to_int does not return an Integer" do + obj = mock('to_int') + obj.should_receive(:to_int).and_return("5") + -> { [obj].pack("U") }.should raise_error(TypeError) + end + + ruby_version_is ""..."3.3" do + it "ignores NULL bytes between directives" do + suppress_warning do + [1, 2, 3].pack("U\x00U").should == "\x01\x02" + end + end + end + + ruby_version_is "3.3" do + it "raise ArgumentError for NULL bytes between directives" do + -> { + [1, 2, 3].pack("U\x00U") + }.should raise_error(ArgumentError, /unknown pack directive/) + end + end + + it "ignores spaces between directives" do + [1, 2, 3].pack("U U").should == "\x01\x02" + end + + it "raises a RangeError if passed a negative number" do + -> { [-1].pack("U") }.should raise_error(RangeError) + end + + it "raises a RangeError if passed a number larger than an unsigned 32-bit integer" do + -> { [2**32].pack("U") }.should raise_error(RangeError) + end + + it "sets the output string to UTF-8 encoding" do + [ [[0x00].pack("U"), Encoding::UTF_8], + [[0x41].pack("U"), Encoding::UTF_8], + [[0x7F].pack("U"), Encoding::UTF_8], + [[0x80].pack("U"), Encoding::UTF_8], + [[0x10FFFF].pack("U"), Encoding::UTF_8] + ].should be_computed_by(:encoding) + end +end diff --git a/spec/ruby/core/array/pack/u_spec.rb b/spec/ruby/core/array/pack/u_spec.rb new file mode 100644 index 0000000000..1f84095ac4 --- /dev/null +++ b/spec/ruby/core/array/pack/u_spec.rb @@ -0,0 +1,140 @@ +# encoding: binary +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' +require_relative 'shared/unicode' +require_relative 'shared/taint' + +describe "Array#pack with format 'U'" do + it_behaves_like :array_pack_basic, 'U' + it_behaves_like :array_pack_basic_non_float, 'U' + it_behaves_like :array_pack_arguments, 'U' + it_behaves_like :array_pack_unicode, 'U' +end + +describe "Array#pack with format 'u'" do + it_behaves_like :array_pack_basic, 'u' + it_behaves_like :array_pack_basic_non_float, 'u' + it_behaves_like :array_pack_arguments, 'u' + it_behaves_like :array_pack_taint, 'u' + + it "calls #to_str to convert an Object to a String" do + obj = mock("pack u string") + obj.should_receive(:to_str).and_return("``abcdef") + [obj].pack("u*").should == "(8&!A8F-D968`\n" + end + + it "will not implicitly convert a number to a string" do + -> { [0].pack('u') }.should raise_error(TypeError) + end + + it "encodes an empty string as an empty string" do + [""].pack("u").should == "" + end + + it "appends a newline to the end of the encoded string" do + ["a"].pack("u").should == "!80``\n" + end + + it "encodes one element per directive" do + ["abc", "DEF"].pack("uu").should == "#86)C\n#1$5&\n" + end + + it "prepends the length of each segment of the input string as the first character (+32) in each line of the output" do + ["abcdefghijklm"].pack("u7").should == "&86)C9&5F\n&9VAI:FML\n!;0``\n" + end + + it "encodes 1, 2, or 3 characters in 4 output characters (uuencoding)" do + [ [["a"], "!80``\n"], + [["ab"], "\"86(`\n"], + [["abc"], "#86)C\n"], + [["abcd"], "$86)C9```\n"], + [["abcde"], "%86)C9&4`\n"], + [["abcdef"], "&86)C9&5F\n"], + [["abcdefg"], "'86)C9&5F9P``\n"], + ].should be_computed_by(:pack, "u") + end + + it "emits a newline after complete groups of count / 3 input characters when passed a count modifier" do + ["abcdefg"].pack("u3").should == "#86)C\n#9&5F\n!9P``\n" + end + + it "implicitly has a count of 45 when passed '*', 0, 1, 2 or no count modifier" do + s = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + r = "M86%A86%A86%A86%A86%A86%A86%A86%A86%A86%A86%A86%A86%A86%A86%A\n%86%A86$`\n" + [ [[s], "u", r], + [[s], "u*", r], + [[s], "u0", r], + [[s], "u1", r], + [[s], "u2", r], + ].should be_computed_by(:pack) + end + + it "encodes all ascii characters" do + [ [["\x00\x01\x02\x03\x04\x05\x06"], "'``$\"`P0%!@``\n"], + [["\a\b\t\n\v\f\r"], "'!P@)\"@L,#0``\n"], + [["\x0E\x0F\x10\x11\x12\x13\x14\x15\x16"], ")\#@\\0$1(3%!46\n"], + [["\x17\x18\x19\x1a\e\x1c\x1d\x1e\x1f"], ")%Q@9&AL<'1X?\n"], + [["!\"\#$%&'()*+,-./"], "/(2(C)\"4F)R@I*BLL+2XO\n"], + [["0123456789"], "*,\#$R,S0U-C<X.0``\n"], + [[":;<=>?@"], "'.CL\\/3X_0```\n"], + [["ABCDEFGHIJKLMNOPQRSTUVWXYZ"], ":04)#1$5&1TA)2DM,34Y/4%%24U155E=865H`\n"], + [["[\\]^_`"], "&6UQ=7E]@\n"], + [["abcdefghijklmnopqrstuvwxyz"], ":86)C9&5F9VAI:FML;6YO<'%R<W1U=G=X>7H`\n"], + [["{|}~"], "$>WQ]?@``\n"], + [["\x7f\xc2\x80\xc2\x81\xc2\x82\xc2\x83"], ")?\\*`PH'\"@L*#\n"], + [["\xc2\x84\xc2\x85\xc2\x86\xc2\x87\xc2"], ")PH3\"A<*&PH?\"\n"], + [["\x88\xc2\x89\xc2\x8a\xc2\x8b\xc2\x8c"], ")B,*)PHK\"B\\*,\n"], + [["\xc2\x8d\xc2\x8e\xc2\x8f\xc2\x90\xc2"], ")PHW\"CL*/PI#\"\n"], + [["\x91\xc2\x92\xc2\x93\xc2\x94\xc2\x95"], ")D<*2PI/\"E,*5\n"], + [["\xc2\x96\xc2\x97\xc2\x98\xc2\x99\xc2"], ")PI;\"E\\*8PIG\"\n"], + [["\x9a\xc2\x9b\xc2\x9c\xc2\x9d\xc2\x9e"], ")FL*;PIS\"G<*>\n"], + [["\xc2\x9f\xc2\xa0\xc2\xa1\xc2\xa2\xc2"], ")PI_\"H,*APJ+\"\n"], + [["\xa3\xc2\xa4\xc2\xa5\xc2\xa6\xc2\xa7"], ")H\\*DPJ7\"IL*G\n"], + [["\xc2\xa8\xc2\xa9\xc2\xaa\xc2\xab\xc2"], ")PJC\"J<*JPJO\"\n"], + [["\xac\xc2\xad\xc2\xae\xc2\xaf\xc2\xb0"], ")K,*MPJ[\"K\\*P\n"], + [["\xc2\xb1\xc2\xb2\xc2\xb3\xc2\xb4\xc2"], ")PK'\"LL*SPK3\"\n"], + [["\xb5\xc2\xb6\xc2\xb7\xc2\xb8\xc2\xb9"], ")M<*VPK?\"N,*Y\n"], + [["\xc2\xba\xc2\xbb\xc2\xbc\xc2\xbd\xc2"], ")PKK\"N\\*\\PKW\"\n"], + [["\xbe\xc2\xbf\xc3\x80\xc3\x81\xc3\x82"], ")OL*_PX#\#@<.\"\n"], + [["\xc3\x83\xc3\x84\xc3\x85\xc3\x86\xc3"], ")PX/#A,.%PX;#\n"], + [["\x87\xc3\x88\xc3\x89\xc3\x8a\xc3\x8b"], ")A\\.(PXG#BL.+\n"], + [["\xc3\x8c\xc3\x8d\xc3\x8e\xc3\x8f\xc3"], ")PXS#C<..PX_#\n"], + [["\x90\xc3\x91\xc3\x92\xc3\x93\xc3\x94"], ")D,.1PY+#D\\.4\n"], + [["\xc3\x95\xc3\x96\xc3\x97\xc3\x98\xc3"], ")PY7#EL.7PYC#\n"], + [["\x99\xc3\x9a\xc3\x9b\xc3\x9c\xc3\x9d"], ")F<.:PYO#G,.=\n"], + [["\xc3\x9e\xc3\x9f\xc3\xa0\xc3\xa1\xc3"], ")PY[#G\\.@PZ'#\n"], + [["\xa2\xc3\xa3\xc3\xa4\xc3\xa5\xc3\xa6"], ")HL.CPZ3#I<.F\n"], + [["\xc3\xa7\xc3\xa8\xc3\xa9\xc3\xaa\xc3"], ")PZ?#J,.IPZK#\n"], + [["\xab\xc3\xac\xc3\xad\xc3\xae\xc3\xaf"], ")J\\.LPZW#KL.O\n"], + [["\xc3\xb0\xc3\xb1\xc3\xb2\xc3\xb3\xc3"], ")P[##L<.RP[/#\n"], + [["\xb4\xc3\xb5\xc3\xb6\xc3\xb7\xc3\xb8"], ")M,.UP[;#M\\.X\n"], + [["\xc3\xb9\xc3\xba\xc3\xbb\xc3\xbc\xc3"], ")P[G#NL.[P[S#\n"], + [["\xbd\xc3\xbe\xc3\xbf"], "%O<.^P[\\`\n"] + ].should be_computed_by(:pack, "u") + end + + it "calls #to_str to convert an object to a String" do + obj = mock("pack m string") + obj.should_receive(:to_str).and_return("abc") + [obj].pack("u").should == "#86)C\n" + end + + it "raises a TypeError if #to_str does not return a String" do + obj = mock("pack m non-string") + -> { [obj].pack("u") }.should raise_error(TypeError) + end + + it "raises a TypeError if passed nil" do + -> { [nil].pack("u") }.should raise_error(TypeError) + end + + it "raises a TypeError if passed an Integer" do + -> { [0].pack("u") }.should raise_error(TypeError) + -> { [bignum_value].pack("u") }.should raise_error(TypeError) + end + + it "sets the output string to US-ASCII encoding" do + ["abcd"].pack("u").encoding.should == Encoding::US_ASCII + end +end diff --git a/spec/ruby/core/array/pack/v_spec.rb b/spec/ruby/core/array/pack/v_spec.rb new file mode 100644 index 0000000000..d3932c84af --- /dev/null +++ b/spec/ruby/core/array/pack/v_spec.rb @@ -0,0 +1,25 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' +require_relative 'shared/numeric_basic' +require_relative 'shared/integer' + +describe "Array#pack with format 'V'" do + it_behaves_like :array_pack_basic, 'V' + it_behaves_like :array_pack_basic_non_float, 'V' + it_behaves_like :array_pack_arguments, 'V' + it_behaves_like :array_pack_numeric_basic, 'V' + it_behaves_like :array_pack_integer, 'V' + it_behaves_like :array_pack_no_platform, 'V' + it_behaves_like :array_pack_32bit_le, 'V' +end + +describe "Array#pack with format 'v'" do + it_behaves_like :array_pack_basic, 'v' + it_behaves_like :array_pack_basic_non_float, 'v' + it_behaves_like :array_pack_arguments, 'v' + it_behaves_like :array_pack_numeric_basic, 'v' + it_behaves_like :array_pack_integer, 'v' + it_behaves_like :array_pack_no_platform, 'v' + it_behaves_like :array_pack_16bit_le, 'v' +end diff --git a/spec/ruby/core/array/pack/w_spec.rb b/spec/ruby/core/array/pack/w_spec.rb new file mode 100644 index 0000000000..e770288d67 --- /dev/null +++ b/spec/ruby/core/array/pack/w_spec.rb @@ -0,0 +1,54 @@ +# encoding: binary +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' +require_relative 'shared/numeric_basic' + +describe "Array#pack with format 'w'" do + it_behaves_like :array_pack_basic, 'w' + it_behaves_like :array_pack_basic_non_float, 'w' + it_behaves_like :array_pack_arguments, 'w' + it_behaves_like :array_pack_numeric_basic, 'w' + + it "encodes a BER-compressed integer" do + [ [[0], "\x00"], + [[1], "\x01"], + [[9999], "\xce\x0f"], + [[2**65], "\x84\x80\x80\x80\x80\x80\x80\x80\x80\x00"] + ].should be_computed_by(:pack, "w") + end + + it "calls #to_int to convert the pack argument to an Integer" do + obj = mock('to_int') + obj.should_receive(:to_int).and_return(5) + [obj].pack("w").should == "\x05" + end + + ruby_version_is ""..."3.3" do + it "ignores NULL bytes between directives" do + suppress_warning do + [1, 2, 3].pack("w\x00w").should == "\x01\x02" + end + end + end + + ruby_version_is "3.3" do + it "raise ArgumentError for NULL bytes between directives" do + -> { + [1, 2, 3].pack("w\x00w") + }.should raise_error(ArgumentError, /unknown pack directive/) + end + end + + it "ignores spaces between directives" do + [1, 2, 3].pack("w w").should == "\x01\x02" + end + + it "raises an ArgumentError when passed a negative value" do + -> { [-1].pack("w") }.should raise_error(ArgumentError) + end + + it "returns a binary string" do + [1].pack('w').encoding.should == Encoding::BINARY + end +end diff --git a/spec/ruby/core/array/pack/x_spec.rb b/spec/ruby/core/array/pack/x_spec.rb new file mode 100644 index 0000000000..012fe4567f --- /dev/null +++ b/spec/ruby/core/array/pack/x_spec.rb @@ -0,0 +1,65 @@ +# encoding: binary +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' + +describe "Array#pack with format 'x'" do + it_behaves_like :array_pack_basic, 'x' + it_behaves_like :array_pack_basic_non_float, 'x' + it_behaves_like :array_pack_no_platform, 'x' + + it "adds a NULL byte with an empty array" do + [].pack("x").should == "\x00" + end + + it "adds a NULL byte without consuming an element" do + [1, 2].pack("CxC").should == "\x01\x00\x02" + end + + it "is not affected by a previous count modifier" do + [].pack("x3x").should == "\x00\x00\x00\x00" + end + + it "adds multiple NULL bytes when passed a count modifier" do + [].pack("x3").should == "\x00\x00\x00" + end + + it "does not add a NULL byte if the count modifier is zero" do + [].pack("x0").should == "" + end + + it "does not add a NULL byte when passed the '*' modifier" do + [].pack("x*").should == "" + [1, 2].pack("Cx*C").should == "\x01\x02" + end +end + +describe "Array#pack with format 'X'" do + it_behaves_like :array_pack_basic, 'X' + it_behaves_like :array_pack_basic_non_float, 'X' + it_behaves_like :array_pack_no_platform, 'X' + + it "reduces the output string by one byte at the point it is encountered" do + [1, 2, 3].pack("C2XC").should == "\x01\x03" + end + + it "does not consume any elements" do + [1, 2, 3].pack("CXC").should == "\x02" + end + + it "reduces the output string by multiple bytes when passed a count modifier" do + [1, 2, 3, 4, 5].pack("C2X2C").should == "\x03" + end + + it "has no affect when passed the '*' modifier" do + [1, 2, 3].pack("C2X*C").should == "\x01\x02\x03" + end + + it "raises an ArgumentError if the output string is empty" do + -> { [1, 2, 3].pack("XC") }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if the count modifier is greater than the bytes in the string" do + -> { [1, 2, 3].pack("C2X3") }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/array/pack/z_spec.rb b/spec/ruby/core/array/pack/z_spec.rb new file mode 100644 index 0000000000..60f8f7bf10 --- /dev/null +++ b/spec/ruby/core/array/pack/z_spec.rb @@ -0,0 +1,44 @@ +# encoding: binary +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' +require_relative 'shared/string' +require_relative 'shared/taint' + +describe "Array#pack with format 'Z'" do + it_behaves_like :array_pack_basic, 'Z' + it_behaves_like :array_pack_basic_non_float, 'Z' + it_behaves_like :array_pack_no_platform, 'Z' + it_behaves_like :array_pack_string, 'Z' + it_behaves_like :array_pack_taint, 'Z' + + it "calls #to_str to convert an Object to a String" do + obj = mock("pack Z string") + obj.should_receive(:to_str).and_return("``abcdef") + [obj].pack("Z*").should == "``abcdef\x00" + end + + it "will not implicitly convert a number to a string" do + -> { [0].pack('Z') }.should raise_error(TypeError) + end + + it "adds all the bytes and appends a NULL byte when passed the '*' modifier" do + ["abc"].pack("Z*").should == "abc\x00" + end + + it "pads the output with NULL bytes when the count exceeds the size of the String" do + ["abc"].pack("Z6").should == "abc\x00\x00\x00" + end + + it "adds a NULL byte when the value is nil" do + [nil].pack("Z").should == "\x00" + end + + it "pads the output with NULL bytes when the value is nil" do + [nil].pack("Z3").should == "\x00\x00\x00" + end + + it "does not append a NULL byte when passed the '*' modifier and the value is nil" do + [nil].pack("Z*").should == "\x00" + end +end diff --git a/spec/ruby/core/array/partition_spec.rb b/spec/ruby/core/array/partition_spec.rb new file mode 100644 index 0000000000..be36fffcab --- /dev/null +++ b/spec/ruby/core/array/partition_spec.rb @@ -0,0 +1,43 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Array#partition" do + it "returns two arrays" do + [].partition {}.should == [[], []] + end + + it "returns in the left array values for which the block evaluates to true" do + ary = [0, 1, 2, 3, 4, 5] + + ary.partition { |i| true }.should == [ary, []] + ary.partition { |i| 5 }.should == [ary, []] + ary.partition { |i| false }.should == [[], ary] + ary.partition { |i| nil }.should == [[], ary] + ary.partition { |i| i % 2 == 0 }.should == [[0, 2, 4], [1, 3, 5]] + ary.partition { |i| i / 3 == 0 }.should == [[0, 1, 2], [3, 4, 5]] + end + + it "properly handles recursive arrays" do + empty = ArraySpecs.empty_recursive_array + empty.partition { true }.should == [[empty], []] + empty.partition { false }.should == [[], [empty]] + + array = ArraySpecs.recursive_array + array.partition { true }.should == [ + [1, 'two', 3.0, array, array, array, array, array], + [] + ] + condition = true + array.partition { condition = !condition }.should == [ + ['two', array, array, array], + [1, 3.0, array, array] + ] + end + + it "does not return subclass instances on Array subclasses" do + result = ArraySpecs::MyArray[1, 2, 3].partition { |x| x % 2 == 0 } + result.should be_an_instance_of(Array) + result[0].should be_an_instance_of(Array) + result[1].should be_an_instance_of(Array) + end +end diff --git a/spec/ruby/core/array/permutation_spec.rb b/spec/ruby/core/array/permutation_spec.rb new file mode 100644 index 0000000000..f15bd76639 --- /dev/null +++ b/spec/ruby/core/array/permutation_spec.rb @@ -0,0 +1,138 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + + +describe "Array#permutation" do + + before :each do + @numbers = (1..3).to_a + @yielded = [] + end + + it "returns an Enumerator of all permutations when called without a block or arguments" do + enum = @numbers.permutation + enum.should be_an_instance_of(Enumerator) + enum.to_a.sort.should == [ + [1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1] + ].sort + end + + it "returns an Enumerator of permutations of given length when called with an argument but no block" do + enum = @numbers.permutation(1) + enum.should be_an_instance_of(Enumerator) + enum.to_a.sort.should == [[1],[2],[3]] + end + + it "yields all permutations to the block then returns self when called with block but no arguments" do + array = @numbers.permutation {|n| @yielded << n} + array.should be_an_instance_of(Array) + array.sort.should == @numbers.sort + @yielded.sort.should == [ + [1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1] + ].sort + end + + it "yields all permutations of given length to the block then returns self when called with block and argument" do + array = @numbers.permutation(2) {|n| @yielded << n} + array.should be_an_instance_of(Array) + array.sort.should == @numbers.sort + @yielded.sort.should == [[1,2],[1,3],[2,1],[2,3],[3,1],[3,2]].sort + end + + it "returns the empty permutation ([[]]) when the given length is 0" do + @numbers.permutation(0).to_a.should == [[]] + @numbers.permutation(0) { |n| @yielded << n } + @yielded.should == [[]] + end + + it "returns the empty permutation([]) when called on an empty Array" do + [].permutation.to_a.should == [[]] + [].permutation { |n| @yielded << n } + @yielded.should == [[]] + end + + it "returns no permutations when the given length has no permutations" do + @numbers.permutation(9).entries.size.should == 0 + @numbers.permutation(9) { |n| @yielded << n } + @yielded.should == [] + end + + it "handles duplicate elements correctly" do + @numbers << 1 + @numbers.permutation(2).sort.should == [ + [1,1],[1,1],[1,2],[1,2],[1,3],[1,3], + [2,1],[2,1],[2,3], + [3,1],[3,1],[3,2] + ].sort + end + + it "handles nested Arrays correctly" do + # The ugliness is due to the order of permutations returned by + # permutation being undefined combined with #sort croaking on Arrays of + # Arrays. + @numbers << [4,5] + got = @numbers.permutation(2).to_a + expected = [ + [1, 2], [1, 3], [1, [4, 5]], + [2, 1], [2, 3], [2, [4, 5]], + [3, 1], [3, 2], [3, [4, 5]], + [[4, 5], 1], [[4, 5], 2], [[4, 5], 3] + ] + expected.each {|e| got.include?(e).should be_true} + got.size.should == expected.size + end + + it "truncates Float arguments" do + @numbers.permutation(3.7).to_a.sort.should == + @numbers.permutation(3).to_a.sort + end + + it "returns an Enumerator which works as expected even when the array was modified" do + @numbers = [1, 2] + enum = @numbers.permutation + @numbers << 3 + enum.to_a.sort.should == [ + [1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1] + ].sort + end + + it "generates from a defensive copy, ignoring mutations" do + accum = [] + ary = [1,2,3] + ary.permutation(3) do |x| + accum << x + ary[0] = 5 + end + + accum.should == [[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]] + end + + describe "when no block is given" do + describe "returned Enumerator" do + describe "size" do + describe "with an array size greater than 0" do + it "returns the descending factorial of array size and given length" do + @numbers.permutation(4).size.should == 0 + @numbers.permutation(3).size.should == 6 + @numbers.permutation(2).size.should == 6 + @numbers.permutation(1).size.should == 3 + @numbers.permutation(0).size.should == 1 + end + it "returns the descending factorial of array size with array size when there's no param" do + @numbers.permutation.size.should == 6 + [1,2,3,4].permutation.size.should == 24 + [1].permutation.size.should == 1 + end + end + describe "with an empty array" do + it "returns 1 when the given length is 0" do + [].permutation(0).size.should == 1 + end + it "returns 1 when there's param" do + [].permutation.size.should == 1 + end + end + end + end + end +end diff --git a/spec/ruby/core/array/plus_spec.rb b/spec/ruby/core/array/plus_spec.rb new file mode 100644 index 0000000000..b7153fd3ef --- /dev/null +++ b/spec/ruby/core/array/plus_spec.rb @@ -0,0 +1,56 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Array#+" do + it "concatenates two arrays" do + ([ 1, 2, 3 ] + [ 3, 4, 5 ]).should == [1, 2, 3, 3, 4, 5] + ([ 1, 2, 3 ] + []).should == [1, 2, 3] + ([] + [ 1, 2, 3 ]).should == [1, 2, 3] + ([] + []).should == [] + end + + it "can concatenate an array with itself" do + ary = [1, 2, 3] + (ary + ary).should == [1, 2, 3, 1, 2, 3] + end + + describe "converts the passed argument to an Array using #to_ary" do + it "successfully concatenates the resulting array from the #to_ary call" do + obj = mock('["x", "y"]') + obj.should_receive(:to_ary).and_return(["x", "y"]) + ([1, 2, 3] + obj).should == [1, 2, 3, "x", "y"] + end + + it "raises a TypeError if the given argument can't be converted to an array" do + -> { [1, 2, 3] + nil }.should raise_error(TypeError) + -> { [1, 2, 3] + "abc" }.should raise_error(TypeError) + end + + it "raises a NoMethodError if the given argument raises a NoMethodError during type coercion to an Array" do + obj = mock("hello") + obj.should_receive(:to_ary).and_raise(NoMethodError) + -> { [1, 2, 3] + obj }.should raise_error(NoMethodError) + end + end + + it "properly handles recursive arrays" do + empty = ArraySpecs.empty_recursive_array + (empty + empty).should == [empty, empty] + + array = ArraySpecs.recursive_array + (empty + array).should == [empty, 1, 'two', 3.0, array, array, array, array, array] + (array + array).should == [ + 1, 'two', 3.0, array, array, array, array, array, + 1, 'two', 3.0, array, array, array, array, array] + end + + it "does return subclass instances with Array subclasses" do + (ArraySpecs::MyArray[1, 2, 3] + []).should be_an_instance_of(Array) + (ArraySpecs::MyArray[1, 2, 3] + ArraySpecs::MyArray[]).should be_an_instance_of(Array) + ([1, 2, 3] + ArraySpecs::MyArray[]).should be_an_instance_of(Array) + end + + it "does not call to_ary on array subclasses" do + ([5, 6] + ArraySpecs::ToAryArray[1, 2]).should == [5, 6, 1, 2] + end +end diff --git a/spec/ruby/core/array/pop_spec.rb b/spec/ruby/core/array/pop_spec.rb new file mode 100644 index 0000000000..2a19408660 --- /dev/null +++ b/spec/ruby/core/array/pop_spec.rb @@ -0,0 +1,124 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Array#pop" do + it "removes and returns the last element of the array" do + a = ["a", 1, nil, true] + + a.pop.should == true + a.should == ["a", 1, nil] + + a.pop.should == nil + a.should == ["a", 1] + + a.pop.should == 1 + a.should == ["a"] + + a.pop.should == "a" + a.should == [] + end + + it "returns nil if there are no more elements" do + [].pop.should == nil + end + + it "properly handles recursive arrays" do + empty = ArraySpecs.empty_recursive_array + empty.pop.should == [] + + array = ArraySpecs.recursive_array + array.pop.should == [1, 'two', 3.0, array, array, array, array] + end + + it "raises a FrozenError on a frozen array" do + -> { ArraySpecs.frozen_array.pop }.should raise_error(FrozenError) + end + + it "raises a FrozenError on an empty frozen array" do + -> { ArraySpecs.empty_frozen_array.pop }.should raise_error(FrozenError) + end + + describe "passed a number n as an argument" do + it "removes and returns an array with the last n elements of the array" do + a = [1, 2, 3, 4, 5, 6] + + a.pop(0).should == [] + a.should == [1, 2, 3, 4, 5, 6] + + a.pop(1).should == [6] + a.should == [1, 2, 3, 4, 5] + + a.pop(2).should == [4, 5] + a.should == [1, 2, 3] + + a.pop(3).should == [1, 2, 3] + a.should == [] + end + + it "returns an array with the last n elements even if shift was invoked" do + a = [1, 2, 3, 4] + a.shift + a.pop(3).should == [2, 3, 4] + end + + it "returns a new empty array if there are no more elements" do + a = [] + popped1 = a.pop(1) + popped1.should == [] + a.should == [] + + popped2 = a.pop(2) + popped2.should == [] + a.should == [] + + popped1.should_not equal(popped2) + end + + it "returns whole elements if n exceeds size of the array" do + a = [1, 2, 3, 4, 5] + a.pop(6).should == [1, 2, 3, 4, 5] + a.should == [] + end + + it "does not return self even when it returns whole elements" do + a = [1, 2, 3, 4, 5] + a.pop(5).should_not equal(a) + + a = [1, 2, 3, 4, 5] + a.pop(6).should_not equal(a) + end + + it "raises an ArgumentError if n is negative" do + ->{ [1, 2, 3].pop(-1) }.should raise_error(ArgumentError) + end + + it "tries to convert n to an Integer using #to_int" do + a = [1, 2, 3, 4] + a.pop(2.3).should == [3, 4] + + obj = mock('to_int') + obj.should_receive(:to_int).and_return(2) + a.should == [1, 2] + a.pop(obj).should == [1, 2] + a.should == [] + end + + it "raises a TypeError when the passed n cannot be coerced to Integer" do + ->{ [1, 2].pop("cat") }.should raise_error(TypeError) + ->{ [1, 2].pop(nil) }.should raise_error(TypeError) + end + + it "raises an ArgumentError if more arguments are passed" do + ->{ [1, 2].pop(1, 2) }.should raise_error(ArgumentError) + end + + it "does not return subclass instances with Array subclass" do + ArraySpecs::MyArray[1, 2, 3].pop(2).should be_an_instance_of(Array) + end + + it "raises a FrozenError on a frozen array" do + -> { ArraySpecs.frozen_array.pop(2) }.should raise_error(FrozenError) + -> { ArraySpecs.frozen_array.pop(0) }.should raise_error(FrozenError) + end + end +end diff --git a/spec/ruby/core/array/prepend_spec.rb b/spec/ruby/core/array/prepend_spec.rb new file mode 100644 index 0000000000..368b8dcfcd --- /dev/null +++ b/spec/ruby/core/array/prepend_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/unshift' + +describe "Array#prepend" do + it_behaves_like :array_unshift, :prepend +end diff --git a/spec/ruby/core/array/product_spec.rb b/spec/ruby/core/array/product_spec.rb new file mode 100644 index 0000000000..6fb3818508 --- /dev/null +++ b/spec/ruby/core/array/product_spec.rb @@ -0,0 +1,73 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Array#product" do + it "returns converted arguments using :to_ary" do + ->{ [1].product(2..3) }.should raise_error(TypeError) + ar = ArraySpecs::ArrayConvertible.new(2,3) + [1].product(ar).should == [[1,2],[1,3]] + ar.called.should == :to_ary + end + + it "returns converted arguments using :method_missing" do + ar = ArraySpecs::ArrayMethodMissing.new(2,3) + [1].product(ar).should == [[1,2],[1,3]] + end + + it "returns the expected result" do + [1,2].product([3,4,5],[6,8]).should == [[1, 3, 6], [1, 3, 8], [1, 4, 6], [1, 4, 8], [1, 5, 6], [1, 5, 8], + [2, 3, 6], [2, 3, 8], [2, 4, 6], [2, 4, 8], [2, 5, 6], [2, 5, 8]] + end + + it "has no required argument" do + [1,2].product.should == [[1],[2]] + end + + it "returns an empty array when the argument is an empty array" do + [1, 2].product([]).should == [] + end + + it "does not attempt to produce an unreasonable number of products" do + a = (0..100).to_a + -> do + a.product(a, a, a, a, a, a, a, a, a, a) + end.should raise_error(RangeError) + end + + describe "when given a block" do + it "yields all combinations in turn" do + acc = [] + [1,2].product([3,4,5],[6,8]){|array| acc << array} + acc.should == [[1, 3, 6], [1, 3, 8], [1, 4, 6], [1, 4, 8], [1, 5, 6], [1, 5, 8], + [2, 3, 6], [2, 3, 8], [2, 4, 6], [2, 4, 8], [2, 5, 6], [2, 5, 8]] + + acc = [] + [1,2].product([3,4,5],[],[6,8]){|array| acc << array} + acc.should be_empty + end + + it "returns self" do + a = [1, 2, 3].freeze + + a.product([1, 2]) { |p| p.first }.should == a + end + + it "will ignore unreasonable numbers of products and yield anyway" do + a = (0..100).to_a + -> do + a.product(a, a, a, a, a, a, a, a, a, a) + end.should raise_error(RangeError) + end + end + + describe "when given an empty block" do + it "returns self" do + arr = [1,2] + arr.product([3,4,5],[6,8]){}.should equal(arr) + arr = [] + arr.product([3,4,5],[6,8]){}.should equal(arr) + arr = [1,2] + arr.product([]){}.should equal(arr) + end + end +end diff --git a/spec/ruby/core/array/push_spec.rb b/spec/ruby/core/array/push_spec.rb new file mode 100644 index 0000000000..607cbc7b4d --- /dev/null +++ b/spec/ruby/core/array/push_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/push' + +describe "Array#push" do + it_behaves_like :array_push, :push +end diff --git a/spec/ruby/core/array/rassoc_spec.rb b/spec/ruby/core/array/rassoc_spec.rb new file mode 100644 index 0000000000..632a05e8b3 --- /dev/null +++ b/spec/ruby/core/array/rassoc_spec.rb @@ -0,0 +1,52 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Array#rassoc" do + it "returns the first contained array whose second element is == object" do + ary = [[1, "a", 0.5], [2, "b"], [3, "b"], [4, "c"], [], [5], [6, "d"]] + ary.rassoc("a").should == [1, "a", 0.5] + ary.rassoc("b").should == [2, "b"] + ary.rassoc("d").should == [6, "d"] + ary.rassoc("z").should == nil + end + + it "properly handles recursive arrays" do + empty = ArraySpecs.empty_recursive_array + empty.rassoc([]).should be_nil + [[empty, empty]].rassoc(empty).should == [empty, empty] + + array = ArraySpecs.recursive_array + array.rassoc(array).should be_nil + [[empty, array]].rassoc(array).should == [empty, array] + end + + it "calls elem == obj on the second element of each contained array" do + key = 'foobar' + o = mock('foobar') + def o.==(other); other == 'foobar'; end + + [[1, :foobar], [2, o], [3, mock('foo')]].rassoc(key).should == [2, o] + end + + it "does not check the last element in each contained but specifically the second" do + key = 'foobar' + o = mock('foobar') + def o.==(other); other == 'foobar'; end + + [[1, :foobar, o], [2, o, 1], [3, mock('foo')]].rassoc(key).should == [2, o, 1] + end + + ruby_version_is "3.3" do + it "calls to_ary on non-array elements" do + s1 = [1, 2] + s2 = ArraySpecs::ArrayConvertible.new(2, 3) + a = [s1, s2] + + s1.should_not_receive(:to_ary) + a.rassoc(2).should equal(s1) + + a.rassoc(3).should == [2, 3] + s2.called.should equal(:to_ary) + end + end +end diff --git a/spec/ruby/core/array/reject_spec.rb b/spec/ruby/core/array/reject_spec.rb new file mode 100644 index 0000000000..81a467e364 --- /dev/null +++ b/spec/ruby/core/array/reject_spec.rb @@ -0,0 +1,158 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/enumeratorize' +require_relative 'shared/delete_if' +require_relative 'shared/iterable_and_tolerating_size_increasing' +require_relative '../enumerable/shared/enumeratorized' + +describe "Array#reject" do + it "returns a new array without elements for which block is true" do + ary = [1, 2, 3, 4, 5] + ary.reject { true }.should == [] + ary.reject { false }.should == ary + ary.reject { false }.should_not equal ary + ary.reject { nil }.should == ary + ary.reject { nil }.should_not equal ary + ary.reject { 5 }.should == [] + ary.reject { |i| i < 3 }.should == [3, 4, 5] + ary.reject { |i| i % 2 == 0 }.should == [1, 3, 5] + end + + it "returns self when called on an Array emptied with #shift" do + array = [1] + array.shift + array.reject { |x| true }.should == [] + end + + it "properly handles recursive arrays" do + empty = ArraySpecs.empty_recursive_array + empty.reject { false }.should == [empty] + empty.reject { true }.should == [] + + array = ArraySpecs.recursive_array + array.reject { false }.should == [1, 'two', 3.0, array, array, array, array, array] + array.reject { true }.should == [] + end + + it "does not return subclass instance on Array subclasses" do + ArraySpecs::MyArray[1, 2, 3].reject { |x| x % 2 == 0 }.should be_an_instance_of(Array) + end + + it "does not retain instance variables" do + array = [] + array.instance_variable_set("@variable", "value") + array.reject { false }.instance_variable_get("@variable").should == nil + end + + it_behaves_like :enumeratorize, :reject + it_behaves_like :enumeratorized_with_origin_size, :reject, [1,2,3] +end + +describe "Array#reject" do + it_behaves_like :array_iterable_and_tolerating_size_increasing, :reject +end + +describe "Array#reject!" do + it "removes elements for which block is true" do + a = [3, 4, 5, 6, 7, 8, 9, 10, 11] + a.reject! { |i| i % 2 == 0 }.should equal(a) + a.should == [3, 5, 7, 9, 11] + a.reject! { |i| i > 8 } + a.should == [3, 5, 7] + a.reject! { |i| i < 4 } + a.should == [5, 7] + a.reject! { |i| i == 5 } + a.should == [7] + a.reject! { true } + a.should == [] + a.reject! { true } + a.should == [] + end + + it "properly handles recursive arrays" do + empty = ArraySpecs.empty_recursive_array + empty_dup = empty.dup + empty.reject! { false }.should == nil + empty.should == empty_dup + + empty = ArraySpecs.empty_recursive_array + empty.reject! { true }.should == [] + empty.should == [] + + array = ArraySpecs.recursive_array + array_dup = array.dup + array.reject! { false }.should == nil + array.should == array_dup + + array = ArraySpecs.recursive_array + array.reject! { true }.should == [] + array.should == [] + end + + it "returns nil when called on an Array emptied with #shift" do + array = [1] + array.shift + array.reject! { |x| true }.should == nil + end + + it "returns nil if no changes are made" do + a = [1, 2, 3] + + a.reject! { |i| i < 0 }.should == nil + + a.reject! { true } + a.reject! { true }.should == nil + end + + it "returns an Enumerator if no block given, and the array is frozen" do + ArraySpecs.frozen_array.reject!.should be_an_instance_of(Enumerator) + end + + it "raises a FrozenError on a frozen array" do + -> { ArraySpecs.frozen_array.reject! {} }.should raise_error(FrozenError) + end + + it "raises a FrozenError on an empty frozen array" do + -> { ArraySpecs.empty_frozen_array.reject! {} }.should raise_error(FrozenError) + end + + it "raises a FrozenError on a frozen array only during iteration if called without a block" do + enum = ArraySpecs.frozen_array.reject! + -> { enum.each {} }.should raise_error(FrozenError) + end + + it "does not truncate the array is the block raises an exception" do + a = [1, 2, 3] + begin + a.reject! { raise StandardError, 'Oops' } + rescue + end + + a.should == [1, 2, 3] + end + + it "only removes elements for which the block returns true, keeping the element which raised an error." do + a = [1, 2, 3, 4] + begin + a.reject! do |x| + case x + when 2 then true + when 3 then raise StandardError, 'Oops' + else false + end + end + rescue StandardError + end + + a.should == [1, 3, 4] + end + + it_behaves_like :enumeratorize, :reject! + it_behaves_like :enumeratorized_with_origin_size, :reject!, [1,2,3] + it_behaves_like :delete_if, :reject! +end + +describe "Array#reject!" do + @value_to_return = -> _ { false } + it_behaves_like :array_iterable_and_tolerating_size_increasing, :reject! +end diff --git a/spec/ruby/core/array/repeated_combination_spec.rb b/spec/ruby/core/array/repeated_combination_spec.rb new file mode 100644 index 0000000000..b62382024a --- /dev/null +++ b/spec/ruby/core/array/repeated_combination_spec.rb @@ -0,0 +1,84 @@ +require_relative '../../spec_helper' + +describe "Array#repeated_combination" do + before :each do + @array = [10, 11, 12] + end + + it "returns an enumerator when no block is provided" do + @array.repeated_combination(2).should be_an_instance_of(Enumerator) + end + + it "returns self when a block is given" do + @array.repeated_combination(2){}.should equal(@array) + end + + it "yields nothing for negative length and return self" do + @array.repeated_combination(-1){ fail }.should equal(@array) + @array.repeated_combination(-10){ fail }.should equal(@array) + end + + it "yields the expected repeated_combinations" do + @array.repeated_combination(2).to_a.sort.should == [[10, 10], [10, 11], [10, 12], [11, 11], [11, 12], [12, 12]] + @array.repeated_combination(3).to_a.sort.should == [[10, 10, 10], [10, 10, 11], [10, 10, 12], [10, 11, 11], [10, 11, 12], + [10, 12, 12], [11, 11, 11], [11, 11, 12], [11, 12, 12], [12, 12, 12]] + end + + it "yields [] when length is 0" do + @array.repeated_combination(0).to_a.should == [[]] # one repeated_combination of length 0 + [].repeated_combination(0).to_a.should == [[]] # one repeated_combination of length 0 + end + + it "yields nothing when the array is empty and num is non zero" do + [].repeated_combination(5).to_a.should == [] # one repeated_combination of length 0 + end + + it "yields a partition consisting of only singletons" do + @array.repeated_combination(1).sort.to_a.should == [[10],[11],[12]] + end + + it "accepts sizes larger than the original array" do + @array.repeated_combination(4).to_a.sort.should == + [[10, 10, 10, 10], [10, 10, 10, 11], [10, 10, 10, 12], + [10, 10, 11, 11], [10, 10, 11, 12], [10, 10, 12, 12], + [10, 11, 11, 11], [10, 11, 11, 12], [10, 11, 12, 12], + [10, 12, 12, 12], [11, 11, 11, 11], [11, 11, 11, 12], + [11, 11, 12, 12], [11, 12, 12, 12], [12, 12, 12, 12]] + end + + it "generates from a defensive copy, ignoring mutations" do + accum = [] + @array.repeated_combination(2) do |x| + accum << x + @array[0] = 1 + end + accum.sort.should == [[10, 10], [10, 11], [10, 12], [11, 11], [11, 12], [12, 12]] + end + + describe "when no block is given" do + describe "returned Enumerator" do + describe "size" do + it "returns 0 when the combination_size is < 0" do + @array.repeated_combination(-1).size.should == 0 + [].repeated_combination(-2).size.should == 0 + end + + it "returns 1 when the combination_size is 0" do + @array.repeated_combination(0).size.should == 1 + [].repeated_combination(0).size.should == 1 + end + + it "returns the binomial coefficient between combination_size and array size + combination_size -1" do + @array.repeated_combination(5).size.should == 21 + @array.repeated_combination(4).size.should == 15 + @array.repeated_combination(3).size.should == 10 + @array.repeated_combination(2).size.should == 6 + @array.repeated_combination(1).size.should == 3 + @array.repeated_combination(0).size.should == 1 + [].repeated_combination(0).size.should == 1 + [].repeated_combination(1).size.should == 0 + end + end + end + end +end diff --git a/spec/ruby/core/array/repeated_permutation_spec.rb b/spec/ruby/core/array/repeated_permutation_spec.rb new file mode 100644 index 0000000000..a165fda09e --- /dev/null +++ b/spec/ruby/core/array/repeated_permutation_spec.rb @@ -0,0 +1,94 @@ +require_relative '../../spec_helper' + + +describe "Array#repeated_permutation" do + + before :each do + @numbers = [10, 11, 12] + @permutations = [[10, 10], [10, 11], [10, 12], [11, 10], [11, 11], [11, 12], [12, 10], [12, 11], [12, 12]] + end + + it "returns an Enumerator of all repeated permutations of given length when called without a block" do + enum = @numbers.repeated_permutation(2) + enum.should be_an_instance_of(Enumerator) + enum.to_a.sort.should == @permutations + end + + it "yields all repeated_permutations to the block then returns self when called with block but no arguments" do + yielded = [] + @numbers.repeated_permutation(2) {|n| yielded << n}.should equal(@numbers) + yielded.sort.should == @permutations + end + + it "yields the empty repeated_permutation ([[]]) when the given length is 0" do + @numbers.repeated_permutation(0).to_a.should == [[]] + [].repeated_permutation(0).to_a.should == [[]] + end + + it "does not yield when called on an empty Array with a nonzero argument" do + [].repeated_permutation(10).to_a.should == [] + end + + it "handles duplicate elements correctly" do + @numbers[-1] = 10 + @numbers.repeated_permutation(2).sort.should == + [[10, 10], [10, 10], [10, 10], [10, 10], [10, 11], [10, 11], [11, 10], [11, 10], [11, 11]] + end + + it "truncates Float arguments" do + @numbers.repeated_permutation(3.7).to_a.sort.should == + @numbers.repeated_permutation(3).to_a.sort + end + + it "returns an Enumerator which works as expected even when the array was modified" do + @numbers.shift + enum = @numbers.repeated_permutation(2) + @numbers.unshift 10 + enum.to_a.sort.should == @permutations + end + + it "allows permutations larger than the number of elements" do + [1,2].repeated_permutation(3).sort.should == + [[1, 1, 1], [1, 1, 2], [1, 2, 1], + [1, 2, 2], [2, 1, 1], [2, 1, 2], + [2, 2, 1], [2, 2, 2]] + end + + it "generates from a defensive copy, ignoring mutations" do + accum = [] + ary = [1,2] + ary.repeated_permutation(3) do |x| + accum << x + ary[0] = 5 + end + + accum.sort.should == + [[1, 1, 1], [1, 1, 2], [1, 2, 1], + [1, 2, 2], [2, 1, 1], [2, 1, 2], + [2, 2, 1], [2, 2, 2]] + end + + describe "when no block is given" do + describe "returned Enumerator" do + describe "size" do + it "returns 0 when combination_size is < 0" do + @numbers.repeated_permutation(-1).size.should == 0 + [].repeated_permutation(-1).size.should == 0 + end + + it "returns array size ** combination_size" do + @numbers.repeated_permutation(4).size.should == 81 + @numbers.repeated_permutation(3).size.should == 27 + @numbers.repeated_permutation(2).size.should == 9 + @numbers.repeated_permutation(1).size.should == 3 + @numbers.repeated_permutation(0).size.should == 1 + [].repeated_permutation(4).size.should == 0 + [].repeated_permutation(3).size.should == 0 + [].repeated_permutation(2).size.should == 0 + [].repeated_permutation(1).size.should == 0 + [].repeated_permutation(0).size.should == 1 + end + end + end + end +end diff --git a/spec/ruby/core/array/replace_spec.rb b/spec/ruby/core/array/replace_spec.rb new file mode 100644 index 0000000000..2f53338f5e --- /dev/null +++ b/spec/ruby/core/array/replace_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/replace' + +describe "Array#replace" do + it_behaves_like :array_replace, :replace +end diff --git a/spec/ruby/core/array/reverse_each_spec.rb b/spec/ruby/core/array/reverse_each_spec.rb new file mode 100644 index 0000000000..59dabcd33d --- /dev/null +++ b/spec/ruby/core/array/reverse_each_spec.rb @@ -0,0 +1,57 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/enumeratorize' +require_relative '../enumerable/shared/enumeratorized' + +# Modifying a collection while the contents are being iterated +# gives undefined behavior. See +# https://blade.ruby-lang.org/ruby-core/23633 + +describe "Array#reverse_each" do + before :each do + ScratchPad.record [] + end + + it "traverses array in reverse order and pass each element to block" do + [1, 3, 4, 6].reverse_each { |i| ScratchPad << i } + ScratchPad.recorded.should == [6, 4, 3, 1] + end + + it "returns self" do + a = [:a, :b, :c] + a.reverse_each { |x| }.should equal(a) + end + + it "yields only the top level element of an empty recursive arrays" do + empty = ArraySpecs.empty_recursive_array + empty.reverse_each { |i| ScratchPad << i } + ScratchPad.recorded.should == [empty] + end + + it "yields only the top level element of a recursive array" do + array = ArraySpecs.recursive_array + array.reverse_each { |i| ScratchPad << i } + ScratchPad.recorded.should == [array, array, array, array, array, 3.0, 'two', 1] + end + + it "returns the correct size when no block is given" do + [1, 2, 3].reverse_each.size.should == 3 + end + + it "tolerates increasing an array size during iteration" do + array = [:a, :b, :c] + ScratchPad.record [] + i = 0 + + array.reverse_each do |e| + ScratchPad << e + array.prepend i if i < 100 + i += 1 + end + + ScratchPad.recorded.should == [:c, :a, 1] + end + + it_behaves_like :enumeratorize, :reverse_each + it_behaves_like :enumeratorized_with_origin_size, :reverse_each, [1,2,3] +end diff --git a/spec/ruby/core/array/reverse_spec.rb b/spec/ruby/core/array/reverse_spec.rb new file mode 100644 index 0000000000..05dbd2efcf --- /dev/null +++ b/spec/ruby/core/array/reverse_spec.rb @@ -0,0 +1,42 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Array#reverse" do + it "returns a new array with the elements in reverse order" do + [].reverse.should == [] + [1, 3, 5, 2].reverse.should == [2, 5, 3, 1] + end + + it "properly handles recursive arrays" do + empty = ArraySpecs.empty_recursive_array + empty.reverse.should == empty + + array = ArraySpecs.recursive_array + array.reverse.should == [array, array, array, array, array, 3.0, 'two', 1] + end + + it "does not return subclass instance on Array subclasses" do + ArraySpecs::MyArray[1, 2, 3].reverse.should be_an_instance_of(Array) + end +end + +describe "Array#reverse!" do + it "reverses the elements in place" do + a = [6, 3, 4, 2, 1] + a.reverse!.should equal(a) + a.should == [1, 2, 4, 3, 6] + [].reverse!.should == [] + end + + it "properly handles recursive arrays" do + empty = ArraySpecs.empty_recursive_array + empty.reverse!.should == [empty] + + array = ArraySpecs.recursive_array + array.reverse!.should == [array, array, array, array, array, 3.0, 'two', 1] + end + + it "raises a FrozenError on a frozen array" do + -> { ArraySpecs.frozen_array.reverse! }.should raise_error(FrozenError) + end +end diff --git a/spec/ruby/core/array/rindex_spec.rb b/spec/ruby/core/array/rindex_spec.rb new file mode 100644 index 0000000000..13de88818c --- /dev/null +++ b/spec/ruby/core/array/rindex_spec.rb @@ -0,0 +1,95 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative '../enumerable/shared/enumeratorized' + +# Modifying a collection while the contents are being iterated +# gives undefined behavior. See +# https://blade.ruby-lang.org/ruby-core/23633 + +describe "Array#rindex" do + it "returns the first index backwards from the end where element == to object" do + key = 3 + uno = mock('one') + dos = mock('two') + tres = mock('three') + tres.should_receive(:==).any_number_of_times.and_return(false) + dos.should_receive(:==).any_number_of_times.and_return(true) + uno.should_not_receive(:==) + ary = [uno, dos, tres] + + ary.rindex(key).should == 1 + end + + it "returns size-1 if last element == to object" do + [2, 1, 3, 2, 5].rindex(5).should == 4 + end + + it "returns 0 if only first element == to object" do + [2, 1, 3, 1, 5].rindex(2).should == 0 + end + + it "returns nil if no element == to object" do + [1, 1, 3, 2, 1, 3].rindex(4).should == nil + end + + it "returns correct index even after delete_at" do + array = ["fish", "bird", "lion", "cat"] + array.delete_at(0) + array.rindex("lion").should == 1 + end + + it "properly handles empty recursive arrays" do + empty = ArraySpecs.empty_recursive_array + empty.rindex(empty).should == 0 + empty.rindex(1).should be_nil + end + + it "properly handles recursive arrays" do + array = ArraySpecs.recursive_array + array.rindex(1).should == 0 + array.rindex(array).should == 7 + end + + it "accepts a block instead of an argument" do + [4, 2, 1, 5, 1, 3].rindex { |x| x < 2 }.should == 4 + end + + it "ignores the block if there is an argument" do + -> { + [4, 2, 1, 5, 1, 3].rindex(5) { |x| x < 2 }.should == 3 + }.should complain(/given block not used/) + end + + it "rechecks the array size during iteration" do + ary = [4, 2, 1, 5, 1, 3] + seen = [] + ary.rindex { |x| seen << x; ary.clear; false } + + seen.should == [3] + end + + it "tolerates increasing an array size during iteration" do + array = [:a, :b, :c] + ScratchPad.record [] + i = 0 + + array.rindex do |e| + ScratchPad << e + array.prepend i if i < 100 + i += 1 + false + end + + ScratchPad.recorded.should == [:c, :a, 1] + end + + describe "given no argument and no block" do + it "produces an Enumerator" do + enum = [4, 2, 1, 5, 1, 3].rindex + enum.should be_an_instance_of(Enumerator) + enum.each { |x| x < 2 }.should == 4 + end + end + + it_behaves_like :enumeratorized_with_unknown_size, :bsearch, [1,2,3] +end diff --git a/spec/ruby/core/array/rotate_spec.rb b/spec/ruby/core/array/rotate_spec.rb new file mode 100644 index 0000000000..60dcc8b113 --- /dev/null +++ b/spec/ruby/core/array/rotate_spec.rb @@ -0,0 +1,129 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Array#rotate" do + describe "when passed no argument" do + it "returns a copy of the array with the first element moved at the end" do + [1, 2, 3, 4, 5].rotate.should == [2, 3, 4, 5, 1] + end + end + + describe "with an argument n" do + it "returns a copy of the array with the first (n % size) elements moved at the end" do + a = [1, 2, 3, 4, 5] + a.rotate( 2).should == [3, 4, 5, 1, 2] + a.rotate( -1).should == [5, 1, 2, 3, 4] + a.rotate(-21).should == [5, 1, 2, 3, 4] + a.rotate( 13).should == [4, 5, 1, 2, 3] + a.rotate( 0).should == a + end + + it "coerces the argument using to_int" do + [1, 2, 3].rotate(2.6).should == [3, 1, 2] + + obj = mock('integer_like') + obj.should_receive(:to_int).and_return(2) + [1, 2, 3].rotate(obj).should == [3, 1, 2] + end + + it "raises a TypeError if not passed an integer-like argument" do + -> { + [1, 2].rotate(nil) + }.should raise_error(TypeError) + -> { + [1, 2].rotate("4") + }.should raise_error(TypeError) + end + end + + it "returns a copy of the array when its length is one or zero" do + [1].rotate.should == [1] + [1].rotate(2).should == [1] + [1].rotate(-42).should == [1] + [ ].rotate.should == [] + [ ].rotate(2).should == [] + [ ].rotate(-42).should == [] + end + + it "does not mutate the receiver" do + -> { + [].freeze.rotate + [2].freeze.rotate(2) + [1,2,3].freeze.rotate(-3) + }.should_not raise_error + end + + it "does not return self" do + a = [1, 2, 3] + a.rotate.should_not equal(a) + a = [] + a.rotate(0).should_not equal(a) + end + + it "does not return subclass instance for Array subclasses" do + ArraySpecs::MyArray[1, 2, 3].rotate.should be_an_instance_of(Array) + end +end + +describe "Array#rotate!" do + describe "when passed no argument" do + it "moves the first element to the end and returns self" do + a = [1, 2, 3, 4, 5] + a.rotate!.should equal(a) + a.should == [2, 3, 4, 5, 1] + end + end + + describe "with an argument n" do + it "moves the first (n % size) elements at the end and returns self" do + a = [1, 2, 3, 4, 5] + a.rotate!(2).should equal(a) + a.should == [3, 4, 5, 1, 2] + a.rotate!(-12).should equal(a) + a.should == [1, 2, 3, 4, 5] + a.rotate!(13).should equal(a) + a.should == [4, 5, 1, 2, 3] + end + + it "coerces the argument using to_int" do + [1, 2, 3].rotate!(2.6).should == [3, 1, 2] + + obj = mock('integer_like') + obj.should_receive(:to_int).and_return(2) + [1, 2, 3].rotate!(obj).should == [3, 1, 2] + end + + it "raises a TypeError if not passed an integer-like argument" do + -> { + [1, 2].rotate!(nil) + }.should raise_error(TypeError) + -> { + [1, 2].rotate!("4") + }.should raise_error(TypeError) + end + end + + it "does nothing and returns self when the length is zero or one" do + a = [1] + a.rotate!.should equal(a) + a.should == [1] + a.rotate!(2).should equal(a) + a.should == [1] + a.rotate!(-21).should equal(a) + a.should == [1] + + a = [] + a.rotate!.should equal(a) + a.should == [] + a.rotate!(2).should equal(a) + a.should == [] + a.rotate!(-21).should equal(a) + a.should == [] + end + + it "raises a FrozenError on a frozen array" do + -> { [1, 2, 3].freeze.rotate!(0) }.should raise_error(FrozenError) + -> { [1].freeze.rotate!(42) }.should raise_error(FrozenError) + -> { [].freeze.rotate! }.should raise_error(FrozenError) + end +end diff --git a/spec/ruby/core/array/sample_spec.rb b/spec/ruby/core/array/sample_spec.rb new file mode 100644 index 0000000000..d4e945152d --- /dev/null +++ b/spec/ruby/core/array/sample_spec.rb @@ -0,0 +1,155 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Array#sample" do + it "samples evenly" do + ArraySpecs.measure_sample_fairness(4, 1, 400) + ArraySpecs.measure_sample_fairness(4, 2, 400) + ArraySpecs.measure_sample_fairness(4, 3, 400) + ArraySpecs.measure_sample_fairness(40, 3, 400) + ArraySpecs.measure_sample_fairness(40, 4, 400) + ArraySpecs.measure_sample_fairness(40, 8, 400) + ArraySpecs.measure_sample_fairness(40, 16, 400) + ArraySpecs.measure_sample_fairness_large_sample_size(100, 80, 4000) + end + + it "returns nil for an empty Array" do + [].sample.should be_nil + end + + it "returns nil for an empty array when called without n and a Random is given" do + [].sample(random: Random.new(42)).should be_nil + end + + it "returns a single value when not passed a count" do + [4].sample.should equal(4) + end + + it "returns a single value when not passed a count and a Random is given" do + [4].sample(random: Random.new(42)).should equal(4) + end + + it "returns a single value when not passed a count and a Random class is given" do + [4].sample(random: Random).should equal(4) + end + + it "returns an empty Array when passed zero" do + [4].sample(0).should == [] + end + + it "returns an Array of elements when passed a count" do + [1, 2, 3, 4].sample(3).should be_an_instance_of(Array) + end + + it "returns elements from the Array" do + array = [1, 2, 3, 4] + array.sample(3).all? { |x| array.should include(x) } + end + + it "returns at most the number of elements in the Array" do + array = [1, 2, 3, 4] + result = array.sample(20) + result.size.should == 4 + end + + it "does not return the same value if the Array has unique values" do + array = [1, 2, 3, 4] + result = array.sample(20) + result.sort.should == array + end + + it "may return the same value if the array is not unique" do + [4, 4].sample(2).should == [4,4] + end + + it "calls #to_int to convert the count when passed an Object" do + [1, 2, 3, 4].sample(mock_int(2)).size.should == 2 + end + + it "raises ArgumentError when passed a negative count" do + -> { [1, 2].sample(-1) }.should raise_error(ArgumentError) + end + + it "does not return subclass instances with Array subclass" do + ArraySpecs::MyArray[1, 2, 3].sample(2).should be_an_instance_of(Array) + end + + describe "with options" do + it "calls #rand on the Object passed by the :random key in the arguments Hash" do + obj = mock("array_sample_random") + obj.should_receive(:rand).and_return(0.5) + + [1, 2].sample(random: obj).should be_an_instance_of(Integer) + end + + it "raises a NoMethodError if an object passed for the RNG does not define #rand" do + obj = BasicObject.new + + -> { [1, 2].sample(random: obj) }.should raise_error(NoMethodError) + end + + describe "when the object returned by #rand is an Integer" do + it "uses the integer as index" do + random = mock("array_sample_random_ret") + random.should_receive(:rand).and_return(0) + + [1, 2].sample(random: random).should == 1 + + random = mock("array_sample_random_ret") + random.should_receive(:rand).and_return(1) + + [1, 2].sample(random: random).should == 2 + end + + it "raises a RangeError if the value is less than zero" do + random = mock("array_sample_random") + random.should_receive(:rand).and_return(-1) + + -> { [1, 2].sample(random: random) }.should raise_error(RangeError) + end + + it "raises a RangeError if the value is equal to the Array size" do + random = mock("array_sample_random") + random.should_receive(:rand).and_return(2) + + -> { [1, 2].sample(random: random) }.should raise_error(RangeError) + end + + it "raises a RangeError if the value is greater than the Array size" do + random = mock("array_sample_random") + random.should_receive(:rand).and_return(3) + + -> { [1, 2].sample(random: random) }.should raise_error(RangeError) + end + end + end + + describe "when the object returned by #rand is not an Integer but responds to #to_int" do + it "calls #to_int on the Object" do + value = mock("array_sample_random_value") + value.should_receive(:to_int).and_return(1) + random = mock("array_sample_random") + random.should_receive(:rand).and_return(value) + + [1, 2].sample(random: random).should == 2 + end + + it "raises a RangeError if the value is less than zero" do + value = mock("array_sample_random_value") + value.should_receive(:to_int).and_return(-1) + random = mock("array_sample_random") + random.should_receive(:rand).and_return(value) + + -> { [1, 2].sample(random: random) }.should raise_error(RangeError) + end + + it "raises a RangeError if the value is equal to the Array size" do + value = mock("array_sample_random_value") + value.should_receive(:to_int).and_return(2) + random = mock("array_sample_random") + random.should_receive(:rand).and_return(value) + + -> { [1, 2].sample(random: random) }.should raise_error(RangeError) + end + end +end diff --git a/spec/ruby/core/array/select_spec.rb b/spec/ruby/core/array/select_spec.rb new file mode 100644 index 0000000000..298b591744 --- /dev/null +++ b/spec/ruby/core/array/select_spec.rb @@ -0,0 +1,14 @@ +require_relative '../../spec_helper' +require_relative 'shared/select' + +describe "Array#select" do + it_behaves_like :array_select, :select +end + +describe "Array#select!" do + it "returns nil if no changes were made in the array" do + [1, 2, 3].select! { true }.should be_nil + end + + it_behaves_like :keep_if, :select! +end diff --git a/spec/ruby/core/array/shared/clone.rb b/spec/ruby/core/array/shared/clone.rb new file mode 100644 index 0000000000..035b45ec99 --- /dev/null +++ b/spec/ruby/core/array/shared/clone.rb @@ -0,0 +1,20 @@ +describe :array_clone, shared: true do + it "returns an Array or a subclass instance" do + [].send(@method).should be_an_instance_of(Array) + ArraySpecs::MyArray[1, 2].send(@method).should be_an_instance_of(ArraySpecs::MyArray) + end + + it "produces a shallow copy where the references are directly copied" do + a = [mock('1'), mock('2')] + b = a.send @method + b.first.should equal a.first + b.last.should equal a.last + end + + it "creates a new array containing all elements or the original" do + a = [1, 2, 3, 4] + b = a.send @method + b.should == a + b.__id__.should_not == a.__id__ + end +end diff --git a/spec/ruby/core/array/shared/collect.rb b/spec/ruby/core/array/shared/collect.rb new file mode 100644 index 0000000000..030302ced6 --- /dev/null +++ b/spec/ruby/core/array/shared/collect.rb @@ -0,0 +1,141 @@ +require_relative '../../enumerable/shared/enumeratorized' +require_relative '../shared/iterable_and_tolerating_size_increasing' + +describe :array_collect, shared: true do + it "returns a copy of array with each element replaced by the value returned by block" do + a = ['a', 'b', 'c', 'd'] + b = a.send(@method) { |i| i + '!' } + b.should == ["a!", "b!", "c!", "d!"] + b.should_not equal a + end + + it "does not return subclass instance" do + ArraySpecs::MyArray[1, 2, 3].send(@method) { |x| x + 1 }.should be_an_instance_of(Array) + end + + it "does not change self" do + a = ['a', 'b', 'c', 'd'] + a.send(@method) { |i| i + '!' } + a.should == ['a', 'b', 'c', 'd'] + end + + it "returns the evaluated value of block if it broke in the block" do + a = ['a', 'b', 'c', 'd'] + b = a.send(@method) {|i| + if i == 'c' + break 0 + else + i + '!' + end + } + b.should == 0 + end + + it "returns an Enumerator when no block given" do + a = [1, 2, 3] + a.send(@method).should be_an_instance_of(Enumerator) + end + + it "raises an ArgumentError when no block and with arguments" do + a = [1, 2, 3] + -> { + a.send(@method, :foo) + }.should raise_error(ArgumentError) + end + + before :all do + @object = [1, 2, 3, 4] + end + it_should_behave_like :enumeratorized_with_origin_size + + it_should_behave_like :array_iterable_and_tolerating_size_increasing +end + +describe :array_collect_b, shared: true do + it "replaces each element with the value returned by block" do + a = [7, 9, 3, 5] + a.send(@method) { |i| i - 1 }.should equal(a) + a.should == [6, 8, 2, 4] + end + + it "returns self" do + a = [1, 2, 3, 4, 5] + b = a.send(@method) {|i| i+1 } + a.should equal b + end + + it "returns the evaluated value of block but its contents is partially modified, if it broke in the block" do + a = ['a', 'b', 'c', 'd'] + b = a.send(@method) {|i| + if i == 'c' + break 0 + else + i + '!' + end + } + b.should == 0 + a.should == ['a!', 'b!', 'c', 'd'] + end + + it "returns an Enumerator when no block given, and the enumerator can modify the original array" do + a = [1, 2, 3] + enum = a.send(@method) + enum.should be_an_instance_of(Enumerator) + enum.each{|i| "#{i}!" } + a.should == ["1!", "2!", "3!"] + end + + describe "when frozen" do + it "raises a FrozenError" do + -> { ArraySpecs.frozen_array.send(@method) {} }.should raise_error(FrozenError) + end + + it "raises a FrozenError when empty" do + -> { ArraySpecs.empty_frozen_array.send(@method) {} }.should raise_error(FrozenError) + end + + it "raises a FrozenError when calling #each on the returned Enumerator" do + enumerator = ArraySpecs.frozen_array.send(@method) + -> { enumerator.each {|x| x } }.should raise_error(FrozenError) + end + + it "raises a FrozenError when calling #each on the returned Enumerator when empty" do + enumerator = ArraySpecs.empty_frozen_array.send(@method) + -> { enumerator.each {|x| x } }.should raise_error(FrozenError) + end + end + + it "does not truncate the array is the block raises an exception" do + a = [1, 2, 3] + begin + a.send(@method) { raise StandardError, 'Oops' } + rescue + end + + a.should == [1, 2, 3] + end + + it "only changes elements before error is raised, keeping the element which raised an error." do + a = [1, 2, 3, 4] + begin + a.send(@method) do |e| + case e + when 1 then -1 + when 2 then -2 + when 3 then raise StandardError, 'Oops' + else 0 + end + end + rescue StandardError + end + + a.should == [-1, -2, 3, 4] + end + + before :all do + @object = [1, 2, 3, 4] + end + it_should_behave_like :enumeratorized_with_origin_size + + it_should_behave_like :array_iterable_and_tolerating_size_increasing +end diff --git a/spec/ruby/core/array/shared/delete_if.rb b/spec/ruby/core/array/shared/delete_if.rb new file mode 100644 index 0000000000..a3fdcf4fac --- /dev/null +++ b/spec/ruby/core/array/shared/delete_if.rb @@ -0,0 +1,13 @@ +describe :delete_if, shared: true do + before :each do + @object = [1,2,3] + end + + it "updates the receiver after all blocks" do + @object.send(@method) do |e| + @object.length.should == 3 + true + end + @object.length.should == 0 + end +end diff --git a/spec/ruby/core/array/shared/difference.rb b/spec/ruby/core/array/shared/difference.rb new file mode 100644 index 0000000000..3e69050d82 --- /dev/null +++ b/spec/ruby/core/array/shared/difference.rb @@ -0,0 +1,78 @@ +describe :array_binary_difference, shared: true do + it "creates an array minus any items from other array" do + [].send(@method, [ 1, 2, 4 ]).should == [] + [1, 2, 4].send(@method, []).should == [1, 2, 4] + [ 1, 2, 3, 4, 5 ].send(@method, [ 1, 2, 4 ]).should == [3, 5] + end + + it "removes multiple items on the lhs equal to one on the rhs" do + [1, 1, 2, 2, 3, 3, 4, 5].send(@method, [1, 2, 4]).should == [3, 3, 5] + end + + it "properly handles recursive arrays" do + empty = ArraySpecs.empty_recursive_array + empty.send(@method, empty).should == [] + + [].send(@method, ArraySpecs.recursive_array).should == [] + + array = ArraySpecs.recursive_array + array.send(@method, array).should == [] + end + + it "tries to convert the passed arguments to Arrays using #to_ary" do + obj = mock('[2,3,3,4]') + obj.should_receive(:to_ary).and_return([2, 3, 3, 4]) + [1, 1, 2, 2, 3, 4].send(@method, obj).should == [1, 1] + end + + it "raises a TypeError if the argument cannot be coerced to an Array by calling #to_ary" do + obj = mock('not an array') + -> { [1, 2, 3].send(@method, obj) }.should raise_error(TypeError) + end + + it "does not return subclass instance for Array subclasses" do + ArraySpecs::MyArray[1, 2, 3].send(@method, []).should be_an_instance_of(Array) + ArraySpecs::MyArray[1, 2, 3].send(@method, ArraySpecs::MyArray[]).should be_an_instance_of(Array) + [1, 2, 3].send(@method, ArraySpecs::MyArray[]).should be_an_instance_of(Array) + end + + it "does not call to_ary on array subclasses" do + [5, 6, 7].send(@method, ArraySpecs::ToAryArray[7]).should == [5, 6] + end + + it "removes an item identified as equivalent via #hash and #eql?" do + obj1 = mock('1') + obj2 = mock('2') + obj1.stub!(:hash).and_return(0) + obj2.stub!(:hash).and_return(0) + obj1.should_receive(:eql?).at_least(1).and_return(true) + + [obj1].send(@method, [obj2]).should == [] + [obj1, obj1, obj2, obj2].send(@method, [obj2]).should == [] + end + + it "doesn't remove an item with the same hash but not #eql?" do + obj1 = mock('1') + obj2 = mock('2') + obj1.stub!(:hash).and_return(0) + obj2.stub!(:hash).and_return(0) + obj1.should_receive(:eql?).at_least(1).and_return(false) + + [obj1].send(@method, [obj2]).should == [obj1] + [obj1, obj1, obj2, obj2].send(@method, [obj2]).should == [obj1, obj1] + end + + it "removes an identical item even when its #eql? isn't reflexive" do + x = mock('x') + x.stub!(:hash).and_return(42) + x.stub!(:eql?).and_return(false) # Stubbed for clarity and latitude in implementation; not actually sent by MRI. + + [x].send(@method, [x]).should == [] + end + + it "is not destructive" do + a = [1, 2, 3] + a.send(@method, [1]) + a.should == [1, 2, 3] + end +end diff --git a/spec/ruby/core/array/shared/enumeratorize.rb b/spec/ruby/core/array/shared/enumeratorize.rb new file mode 100644 index 0000000000..a19a5d3b9b --- /dev/null +++ b/spec/ruby/core/array/shared/enumeratorize.rb @@ -0,0 +1,5 @@ +describe :enumeratorize, shared: true do + it "returns an Enumerator if no block given" do + [1,2].send(@method).should be_an_instance_of(Enumerator) + end +end diff --git a/spec/ruby/core/array/shared/eql.rb b/spec/ruby/core/array/shared/eql.rb new file mode 100644 index 0000000000..b5d9128434 --- /dev/null +++ b/spec/ruby/core/array/shared/eql.rb @@ -0,0 +1,92 @@ +describe :array_eql, shared: true do + it "returns true if other is the same array" do + a = [1] + a.send(@method, a).should be_true + end + + it "returns true if corresponding elements are #eql?" do + [].send(@method, []).should be_true + [1, 2, 3, 4].send(@method, [1, 2, 3, 4]).should be_true + end + + it "returns false if other is shorter than self" do + [1, 2, 3, 4].send(@method, [1, 2, 3]).should be_false + end + + it "returns false if other is longer than self" do + [1, 2, 3, 4].send(@method, [1, 2, 3, 4, 5]).should be_false + end + + it "returns false immediately when sizes of the arrays differ" do + obj = mock('1') + obj.should_not_receive(@method) + + [] .send(@method, [obj] ).should be_false + [obj] .send(@method, [] ).should be_false + end + + it "handles well recursive arrays" do + a = ArraySpecs.empty_recursive_array + a .send(@method, [a] ).should be_true + a .send(@method, [[a]] ).should be_true + [a] .send(@method, a ).should be_true + [[a]] .send(@method, a ).should be_true + # These may be surprising, but no difference can be + # found between these arrays, so they are ==. + # There is no "path" that will lead to a difference + # (contrary to other examples below) + + a2 = ArraySpecs.empty_recursive_array + a .send(@method, a2 ).should be_true + a .send(@method, [a2] ).should be_true + a .send(@method, [[a2]] ).should be_true + [a] .send(@method, a2 ).should be_true + [[a]] .send(@method, a2 ).should be_true + + back = [] + forth = [back]; back << forth; + back .send(@method, a ).should be_true + + x = []; x << x << x + x .send(@method, a ).should be_false # since x.size != a.size + x .send(@method, [a, a] ).should be_false # since x[0].size != [a, a][0].size + x .send(@method, [x, a] ).should be_false # since x[1].size != [x, a][1].size + [x, a] .send(@method, [a, x] ).should be_false # etc... + x .send(@method, [x, x] ).should be_true + x .send(@method, [[x, x], [x, x]] ).should be_true + + tree = []; + branch = []; branch << tree << tree; tree << branch + tree2 = []; + branch2 = []; branch2 << tree2 << tree2; tree2 << branch2 + forest = [tree, branch, :bird, a]; forest << forest + forest2 = [tree2, branch2, :bird, a2]; forest2 << forest2 + + forest .send(@method, forest2 ).should be_true + forest .send(@method, [tree2, branch, :bird, a, forest2]).should be_true + + diffforest = [branch2, tree2, :bird, a2]; diffforest << forest2 + forest .send(@method, diffforest ).should be_false # since forest[0].size == 1 != 3 == diffforest[0] + forest .send(@method, [nil] ).should be_false + forest .send(@method, [forest] ).should be_false + end + + it "does not call #to_ary on its argument" do + obj = mock('to_ary') + obj.should_not_receive(:to_ary) + + [1, 2, 3].send(@method, obj).should be_false + end + + it "does not call #to_ary on Array subclasses" do + ary = ArraySpecs::ToAryArray[5, 6, 7] + ary.should_not_receive(:to_ary) + [5, 6, 7].send(@method, ary).should be_true + end + + it "ignores array class differences" do + ArraySpecs::MyArray[1, 2, 3].send(@method, [1, 2, 3]).should be_true + ArraySpecs::MyArray[1, 2, 3].send(@method, ArraySpecs::MyArray[1, 2, 3]).should be_true + [1, 2, 3].send(@method, ArraySpecs::MyArray[1, 2, 3]).should be_true + end +end diff --git a/spec/ruby/core/array/shared/index.rb b/spec/ruby/core/array/shared/index.rb new file mode 100644 index 0000000000..a4a0adbab6 --- /dev/null +++ b/spec/ruby/core/array/shared/index.rb @@ -0,0 +1,41 @@ +require_relative '../shared/iterable_and_tolerating_size_increasing' + +describe :array_index, shared: true do + it "returns the index of the first element == to object" do + x = mock('3') + def x.==(obj) 3 == obj; end + + [2, x, 3, 1, 3, 1].send(@method, 3).should == 1 + [2, 3.0, 3, x, 1, 3, 1].send(@method, x).should == 1 + end + + it "returns 0 if first element == to object" do + [2, 1, 3, 2, 5].send(@method, 2).should == 0 + end + + it "returns size-1 if only last element == to object" do + [2, 1, 3, 1, 5].send(@method, 5).should == 4 + end + + it "returns nil if no element == to object" do + [2, 1, 1, 1, 1].send(@method, 3).should == nil + end + + it "accepts a block instead of an argument" do + [4, 2, 1, 5, 1, 3].send(@method) {|x| x < 2}.should == 2 + end + + it "ignores the block if there is an argument" do + -> { + [4, 2, 1, 5, 1, 3].send(@method, 5) {|x| x < 2}.should == 3 + }.should complain(/given block not used/) + end + + describe "given no argument and no block" do + it "produces an Enumerator" do + [].send(@method).should be_an_instance_of(Enumerator) + end + end + + it_should_behave_like :array_iterable_and_tolerating_size_increasing +end diff --git a/spec/ruby/core/array/shared/inspect.rb b/spec/ruby/core/array/shared/inspect.rb new file mode 100644 index 0000000000..af5128c645 --- /dev/null +++ b/spec/ruby/core/array/shared/inspect.rb @@ -0,0 +1,107 @@ +require_relative '../fixtures/encoded_strings' + +describe :array_inspect, shared: true do + it "returns a string" do + [1, 2, 3].send(@method).should be_an_instance_of(String) + end + + it "returns '[]' for an empty Array" do + [].send(@method).should == "[]" + end + + it "calls inspect on its elements and joins the results with commas" do + items = Array.new(3) do |i| + obj = mock(i.to_s) + obj.should_receive(:inspect).and_return(i.to_s) + obj + end + items.send(@method).should == "[0, 1, 2]" + end + + it "does not call #to_s on a String returned from #inspect" do + str = +"abc" + str.should_not_receive(:to_s) + + [str].send(@method).should == '["abc"]' + end + + it "calls #to_s on the object returned from #inspect if the Object isn't a String" do + obj = mock("Array#inspect/to_s calls #to_s") + obj.should_receive(:inspect).and_return(obj) + obj.should_receive(:to_s).and_return("abc") + + [obj].send(@method).should == "[abc]" + end + + it "does not call #to_str on the object returned from #inspect when it is not a String" do + obj = mock("Array#inspect/to_s does not call #to_str") + obj.should_receive(:inspect).and_return(obj) + obj.should_not_receive(:to_str) + + [obj].send(@method).should =~ /^\[#<MockObject:0x[0-9a-f]+>\]$/ + end + + it "does not call #to_str on the object returned from #to_s when it is not a String" do + obj = mock("Array#inspect/to_s does not call #to_str on #to_s result") + obj.should_receive(:inspect).and_return(obj) + obj.should_receive(:to_s).and_return(obj) + obj.should_not_receive(:to_str) + + [obj].send(@method).should =~ /^\[#<MockObject:0x[0-9a-f]+>\]$/ + end + + it "does not swallow exceptions raised by #to_s" do + obj = mock("Array#inspect/to_s does not swallow #to_s exceptions") + obj.should_receive(:inspect).and_return(obj) + obj.should_receive(:to_s).and_raise(Exception) + + -> { [obj].send(@method) }.should raise_error(Exception) + end + + it "represents a recursive element with '[...]'" do + ArraySpecs.recursive_array.send(@method).should == "[1, \"two\", 3.0, [...], [...], [...], [...], [...]]" + ArraySpecs.head_recursive_array.send(@method).should == "[[...], [...], [...], [...], [...], 1, \"two\", 3.0]" + ArraySpecs.empty_recursive_array.send(@method).should == "[[...]]" + end + + describe "with encoding" do + before :each do + @default_external_encoding = Encoding.default_external + end + + after :each do + Encoding.default_external = @default_external_encoding + end + + it "returns a US-ASCII string for an empty Array" do + [].send(@method).encoding.should == Encoding::US_ASCII + end + + it "use the default external encoding if it is ascii compatible" do + Encoding.default_external = Encoding.find('UTF-8') + + utf8 = "utf8".encode("UTF-8") + jp = "jp".encode("EUC-JP") + array = [jp, utf8] + + array.send(@method).encoding.name.should == "UTF-8" + end + + it "use US-ASCII encoding if the default external encoding is not ascii compatible" do + Encoding.default_external = Encoding.find('UTF-32') + + utf8 = "utf8".encode("UTF-8") + jp = "jp".encode("EUC-JP") + array = [jp, utf8] + + array.send(@method).encoding.name.should == "US-ASCII" + end + + it "does not raise if inspected result is not default external encoding" do + utf_16be = mock(+"utf_16be") + utf_16be.should_receive(:inspect).and_return(%<"utf_16be \u3042">.encode(Encoding::UTF_16BE)) + + [utf_16be].send(@method).should == '["utf_16be \u3042"]' + end + end +end diff --git a/spec/ruby/core/array/shared/intersection.rb b/spec/ruby/core/array/shared/intersection.rb new file mode 100644 index 0000000000..0b4166ab63 --- /dev/null +++ b/spec/ruby/core/array/shared/intersection.rb @@ -0,0 +1,85 @@ +describe :array_intersection, shared: true do + it "creates an array with elements common to both arrays (intersection)" do + [].send(@method, []).should == [] + [1, 2].send(@method, []).should == [] + [].send(@method, [1, 2]).should == [] + [ 1, 3, 5 ].send(@method, [ 1, 2, 3 ]).should == [1, 3] + end + + it "creates an array with no duplicates" do + [ 1, 1, 3, 5 ].send(@method, [ 1, 2, 3 ]).uniq!.should == nil + end + + it "creates an array with elements in order they are first encountered" do + [ 1, 2, 3, 2, 5, 6, 7, 8 ].send(@method, [ 5, 2, 3, 4 ]).should == [2, 3, 5] # array > other + [ 5, 2, 3, 4 ].send(@method, [ 1, 2, 3, 2, 5, 6, 7, 8 ]).should == [5, 2, 3] # array < other + end + + it "does not modify the original Array" do + a = [1, 1, 3, 5] + a.send(@method, [1, 2, 3]).should == [1, 3] + a.should == [1, 1, 3, 5] + end + + it "properly handles recursive arrays" do + empty = ArraySpecs.empty_recursive_array + empty.send(@method, empty).should == empty + + ArraySpecs.recursive_array.send(@method, []).should == [] + [].send(@method, ArraySpecs.recursive_array).should == [] + + ArraySpecs.recursive_array.send(@method, ArraySpecs.recursive_array).should == [1, 'two', 3.0, ArraySpecs.recursive_array] + end + + it "tries to convert the passed argument to an Array using #to_ary" do + obj = mock('[1,2,3]') + obj.should_receive(:to_ary).and_return([1, 2, 3]) + [1, 2].send(@method, obj).should == ([1, 2]) + end + + it "determines equivalence between elements in the sense of eql?" do + not_supported_on :opal do + [5.0, 4.0].send(@method, [5, 4]).should == [] + end + + str = "x" + [str].send(@method, [str.dup]).should == [str] + + obj1 = mock('1') + obj2 = mock('2') + obj1.stub!(:hash).and_return(0) + obj2.stub!(:hash).and_return(0) + obj1.should_receive(:eql?).at_least(1).and_return(true) + obj2.stub!(:eql?).and_return(true) + + [obj1].send(@method, [obj2]).should == [obj1] + [obj1, obj1, obj2, obj2].send(@method, [obj2]).should == [obj1] + + obj1 = mock('3') + obj2 = mock('4') + obj1.stub!(:hash).and_return(0) + obj2.stub!(:hash).and_return(0) + obj1.should_receive(:eql?).at_least(1).and_return(false) + + [obj1].send(@method, [obj2]).should == [] + [obj1, obj1, obj2, obj2].send(@method, [obj2]).should == [obj2] + end + + it "does return subclass instances for Array subclasses" do + ArraySpecs::MyArray[1, 2, 3].send(@method, []).should be_an_instance_of(Array) + ArraySpecs::MyArray[1, 2, 3].send(@method, ArraySpecs::MyArray[1, 2, 3]).should be_an_instance_of(Array) + [].send(@method, ArraySpecs::MyArray[1, 2, 3]).should be_an_instance_of(Array) + end + + it "does not call to_ary on array subclasses" do + [5, 6].send(@method, ArraySpecs::ToAryArray[1, 2, 5, 6]).should == [5, 6] + end + + it "properly handles an identical item even when its #eql? isn't reflexive" do + x = mock('x') + x.stub!(:hash).and_return(42) + x.stub!(:eql?).and_return(false) # Stubbed for clarity and latitude in implementation; not actually sent by MRI. + + [x].send(@method, [x]).should == [x] + end +end diff --git a/spec/ruby/core/array/shared/iterable_and_tolerating_size_increasing.rb b/spec/ruby/core/array/shared/iterable_and_tolerating_size_increasing.rb new file mode 100644 index 0000000000..3e73bad44b --- /dev/null +++ b/spec/ruby/core/array/shared/iterable_and_tolerating_size_increasing.rb @@ -0,0 +1,25 @@ +describe :array_iterable_and_tolerating_size_increasing, shared: true do + before do + @value_to_return ||= -> _ { nil } + end + + it "tolerates increasing an array size during iteration" do + # The goal is to trigger potential reallocation of internal array storage, so we: + # - use elements of different types, starting with the less generic (Integer) + # - add reasonably big number of new elements (~ 100) + array = [1, 2, 3] # to test some methods we need several uniq elements + array_to_join = [:a, :b, :c] + (4..100).to_a + + ScratchPad.record [] + i = 0 + + array.send(@method) do |e| + ScratchPad << e + array << array_to_join[i] if i < array_to_join.size + i += 1 + @value_to_return.call(e) + end + + ScratchPad.recorded.should == [1, 2, 3] + array_to_join + end +end diff --git a/spec/ruby/core/array/shared/join.rb b/spec/ruby/core/array/shared/join.rb new file mode 100644 index 0000000000..507b13e3c8 --- /dev/null +++ b/spec/ruby/core/array/shared/join.rb @@ -0,0 +1,112 @@ +require_relative '../fixtures/classes' +require_relative '../fixtures/encoded_strings' + +describe :array_join_with_default_separator, shared: true do + before :each do + @separator = $, + end + + after :each do + $, = @separator + end + + it "returns an empty string if the Array is empty" do + [].send(@method).should == '' + end + + it "returns a US-ASCII string for an empty Array" do + [].send(@method).encoding.should == Encoding::US_ASCII + end + + it "returns a string formed by concatenating each String element separated by $," do + suppress_warning { + $, = " | " + ["1", "2", "3"].send(@method).should == "1 | 2 | 3" + } + end + + it "attempts coercion via #to_str first" do + obj = mock('foo') + obj.should_receive(:to_str).any_number_of_times.and_return("foo") + [obj].send(@method).should == "foo" + end + + it "attempts coercion via #to_ary second" do + obj = mock('foo') + obj.should_receive(:to_str).any_number_of_times.and_return(nil) + obj.should_receive(:to_ary).any_number_of_times.and_return(["foo"]) + [obj].send(@method).should == "foo" + end + + it "attempts coercion via #to_s third" do + obj = mock('foo') + obj.should_receive(:to_str).any_number_of_times.and_return(nil) + obj.should_receive(:to_ary).any_number_of_times.and_return(nil) + obj.should_receive(:to_s).any_number_of_times.and_return("foo") + [obj].send(@method).should == "foo" + end + + it "raises a NoMethodError if an element does not respond to #to_str, #to_ary, or #to_s" do + obj = mock('o') + class << obj; undef :to_s; end + -> { [1, obj].send(@method) }.should raise_error(NoMethodError) + end + + it "raises an ArgumentError when the Array is recursive" do + -> { ArraySpecs.recursive_array.send(@method) }.should raise_error(ArgumentError) + -> { ArraySpecs.head_recursive_array.send(@method) }.should raise_error(ArgumentError) + -> { ArraySpecs.empty_recursive_array.send(@method) }.should raise_error(ArgumentError) + end + + it "uses the first encoding when other strings are compatible" do + ary1 = ArraySpecs.array_with_7bit_utf8_and_usascii_strings + ary2 = ArraySpecs.array_with_usascii_and_7bit_utf8_strings + ary3 = ArraySpecs.array_with_utf8_and_7bit_binary_strings + ary4 = ArraySpecs.array_with_usascii_and_7bit_binary_strings + + ary1.send(@method).encoding.should == Encoding::UTF_8 + ary2.send(@method).encoding.should == Encoding::US_ASCII + ary3.send(@method).encoding.should == Encoding::UTF_8 + ary4.send(@method).encoding.should == Encoding::US_ASCII + end + + it "uses the widest common encoding when other strings are incompatible" do + ary1 = ArraySpecs.array_with_utf8_and_usascii_strings + ary2 = ArraySpecs.array_with_usascii_and_utf8_strings + + ary1.send(@method).encoding.should == Encoding::UTF_8 + ary2.send(@method).encoding.should == Encoding::UTF_8 + end + + it "fails for arrays with incompatibly-encoded strings" do + ary_utf8_bad_binary = ArraySpecs.array_with_utf8_and_binary_strings + + -> { ary_utf8_bad_binary.send(@method) }.should raise_error(EncodingError) + end + + context "when $, is not nil" do + before do + suppress_warning do + $, = '*' + end + end + + it "warns" do + -> { [].join }.should complain(/warning: \$, is set to non-nil value/) + -> { [].join(nil) }.should complain(/warning: \$, is set to non-nil value/) + end + end +end + +describe :array_join_with_string_separator, shared: true do + it "returns a string formed by concatenating each element.to_str separated by separator" do + obj = mock('foo') + obj.should_receive(:to_str).and_return("foo") + [1, 2, 3, 4, obj].send(@method, ' | ').should == '1 | 2 | 3 | 4 | foo' + end + + it "uses the same separator with nested arrays" do + [1, [2, [3, 4], 5], 6].send(@method, ":").should == "1:2:3:4:5:6" + [1, [2, ArraySpecs::MyArray[3, 4], 5], 6].send(@method, ":").should == "1:2:3:4:5:6" + end +end diff --git a/spec/ruby/core/array/shared/keep_if.rb b/spec/ruby/core/array/shared/keep_if.rb new file mode 100644 index 0000000000..43a047c0a7 --- /dev/null +++ b/spec/ruby/core/array/shared/keep_if.rb @@ -0,0 +1,95 @@ +require_relative '../../enumerable/shared/enumeratorized' +require_relative '../shared/iterable_and_tolerating_size_increasing' + +describe :keep_if, shared: true do + it "deletes elements for which the block returns a false value" do + array = [1, 2, 3, 4, 5] + array.send(@method) {|item| item > 3 }.should equal(array) + array.should == [4, 5] + end + + it "returns an enumerator if no block is given" do + [1, 2, 3].send(@method).should be_an_instance_of(Enumerator) + end + + it "updates the receiver after all blocks" do + a = [1, 2, 3] + a.send(@method) do |e| + a.length.should == 3 + false + end + a.length.should == 0 + end + + before :all do + @object = [1,2,3] + end + it_should_behave_like :enumeratorized_with_origin_size + + describe "on frozen objects" do + before :each do + @origin = [true, false] + @frozen = @origin.dup.freeze + end + + it "returns an Enumerator if no block is given" do + @frozen.send(@method).should be_an_instance_of(Enumerator) + end + + describe "with truthy block" do + it "keeps elements after any exception" do + -> { @frozen.send(@method) { true } }.should raise_error(Exception) + @frozen.should == @origin + end + + it "raises a FrozenError" do + -> { @frozen.send(@method) { true } }.should raise_error(FrozenError) + end + end + + describe "with falsy block" do + it "keeps elements after any exception" do + -> { @frozen.send(@method) { false } }.should raise_error(Exception) + @frozen.should == @origin + end + + it "raises a FrozenError" do + -> { @frozen.send(@method) { false } }.should raise_error(FrozenError) + end + end + + it "raises a FrozenError on a frozen array only during iteration if called without a block" do + enum = @frozen.send(@method) + -> { enum.each {} }.should raise_error(FrozenError) + end + end + + it "does not truncate the array is the block raises an exception" do + a = [1, 2, 3] + begin + a.send(@method) { raise StandardError, 'Oops' } + rescue + end + + a.should == [1, 2, 3] + end + + it "only changes elements before error is raised, keeping the element which raised an error." do + a = [1, 2, 3, 4] + begin + a.send(@method) do |e| + case e + when 2 then false + when 3 then raise StandardError, 'Oops' + else true + end + end + rescue StandardError + end + + a.should == [1, 3, 4] + end + + @value_to_return = -> _ { true } + it_should_behave_like :array_iterable_and_tolerating_size_increasing +end diff --git a/spec/ruby/core/array/shared/length.rb b/spec/ruby/core/array/shared/length.rb new file mode 100644 index 0000000000..f84966d0ba --- /dev/null +++ b/spec/ruby/core/array/shared/length.rb @@ -0,0 +1,11 @@ +describe :array_length, shared: true do + it "returns the number of elements" do + [].send(@method).should == 0 + [1, 2, 3].send(@method).should == 3 + end + + it "properly handles recursive arrays" do + ArraySpecs.empty_recursive_array.send(@method).should == 1 + ArraySpecs.recursive_array.send(@method).should == 8 + end +end diff --git a/spec/ruby/core/array/shared/push.rb b/spec/ruby/core/array/shared/push.rb new file mode 100644 index 0000000000..ac790fb6a4 --- /dev/null +++ b/spec/ruby/core/array/shared/push.rb @@ -0,0 +1,33 @@ +describe :array_push, shared: true do + it "appends the arguments to the array" do + a = [ "a", "b", "c" ] + a.send(@method, "d", "e", "f").should equal(a) + a.send(@method).should == ["a", "b", "c", "d", "e", "f"] + a.send(@method, 5) + a.should == ["a", "b", "c", "d", "e", "f", 5] + + a = [0, 1] + a.send(@method, 2) + a.should == [0, 1, 2] + end + + it "isn't confused by previous shift" do + a = [ "a", "b", "c" ] + a.shift + a.send(@method, "foo") + a.should == ["b", "c", "foo"] + end + + it "properly handles recursive arrays" do + empty = ArraySpecs.empty_recursive_array + empty.send(@method, :last).should == [empty, :last] + + array = ArraySpecs.recursive_array + array.send(@method, :last).should == [1, 'two', 3.0, array, array, array, array, array, :last] + end + + it "raises a FrozenError on a frozen array" do + -> { ArraySpecs.frozen_array.send(@method, 1) }.should raise_error(FrozenError) + -> { ArraySpecs.frozen_array.send(@method) }.should raise_error(FrozenError) + end +end diff --git a/spec/ruby/core/array/shared/replace.rb b/spec/ruby/core/array/shared/replace.rb new file mode 100644 index 0000000000..9a6e60c1b0 --- /dev/null +++ b/spec/ruby/core/array/shared/replace.rb @@ -0,0 +1,60 @@ +describe :array_replace, shared: true do + it "replaces the elements with elements from other array" do + a = [1, 2, 3, 4, 5] + b = ['a', 'b', 'c'] + a.send(@method, b).should equal(a) + a.should == b + a.should_not equal(b) + + a.send(@method, [4] * 10) + a.should == [4] * 10 + + a.send(@method, []) + a.should == [] + end + + it "properly handles recursive arrays" do + orig = [1, 2, 3] + empty = ArraySpecs.empty_recursive_array + orig.send(@method, empty) + orig.should == empty + + array = ArraySpecs.recursive_array + orig.send(@method, array) + orig.should == array + end + + it "returns self" do + ary = [1, 2, 3] + other = [:a, :b, :c] + ary.send(@method, other).should equal(ary) + end + + it "does not make self dependent to the original array" do + ary = [1, 2, 3] + other = [:a, :b, :c] + ary.send(@method, other) + ary.should == [:a, :b, :c] + ary << :d + ary.should == [:a, :b, :c, :d] + other.should == [:a, :b, :c] + end + + it "tries to convert the passed argument to an Array using #to_ary" do + obj = mock('to_ary') + obj.stub!(:to_ary).and_return([1, 2, 3]) + [].send(@method, obj).should == [1, 2, 3] + end + + it "does not call #to_ary on Array subclasses" do + obj = ArraySpecs::ToAryArray[5, 6, 7] + obj.should_not_receive(:to_ary) + [].send(@method, ArraySpecs::ToAryArray[5, 6, 7]).should == [5, 6, 7] + end + + it "raises a FrozenError on a frozen array" do + -> { + ArraySpecs.frozen_array.send(@method, ArraySpecs.frozen_array) + }.should raise_error(FrozenError) + end +end diff --git a/spec/ruby/core/array/shared/select.rb b/spec/ruby/core/array/shared/select.rb new file mode 100644 index 0000000000..9c2cbf76c4 --- /dev/null +++ b/spec/ruby/core/array/shared/select.rb @@ -0,0 +1,35 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative '../shared/enumeratorize' +require_relative '../shared/keep_if' +require_relative '../shared/iterable_and_tolerating_size_increasing' +require_relative '../../enumerable/shared/enumeratorized' + +describe :array_select, shared: true do + it_should_behave_like :enumeratorize + + it_should_behave_like :array_iterable_and_tolerating_size_increasing + + before :each do + @object = [1,2,3] + end + it_should_behave_like :enumeratorized_with_origin_size + + it "returns a new array of elements for which block is true" do + [1, 3, 4, 5, 6, 9].send(@method) { |i| i % ((i + 1) / 2) == 0}.should == [1, 4, 6] + end + + it "does not return subclass instance on Array subclasses" do + ArraySpecs::MyArray[1, 2, 3].send(@method) { true }.should be_an_instance_of(Array) + end + + it "properly handles recursive arrays" do + empty = ArraySpecs.empty_recursive_array + empty.send(@method) { true }.should == empty + empty.send(@method) { false }.should == [] + + array = ArraySpecs.recursive_array + array.send(@method) { true }.should == [1, 'two', 3.0, array, array, array, array, array] + array.send(@method) { false }.should == [] + end +end diff --git a/spec/ruby/core/array/shared/slice.rb b/spec/ruby/core/array/shared/slice.rb new file mode 100644 index 0000000000..b80261d32f --- /dev/null +++ b/spec/ruby/core/array/shared/slice.rb @@ -0,0 +1,857 @@ +describe :array_slice, shared: true do + it "returns the element at index with [index]" do + [ "a", "b", "c", "d", "e" ].send(@method, 1).should == "b" + + a = [1, 2, 3, 4] + + a.send(@method, 0).should == 1 + a.send(@method, 1).should == 2 + a.send(@method, 2).should == 3 + a.send(@method, 3).should == 4 + a.send(@method, 4).should == nil + a.send(@method, 10).should == nil + + a.should == [1, 2, 3, 4] + end + + it "returns the element at index from the end of the array with [-index]" do + [ "a", "b", "c", "d", "e" ].send(@method, -2).should == "d" + + a = [1, 2, 3, 4] + + a.send(@method, -1).should == 4 + a.send(@method, -2).should == 3 + a.send(@method, -3).should == 2 + a.send(@method, -4).should == 1 + a.send(@method, -5).should == nil + a.send(@method, -10).should == nil + + a.should == [1, 2, 3, 4] + end + + it "returns count elements starting from index with [index, count]" do + [ "a", "b", "c", "d", "e" ].send(@method, 2, 3).should == ["c", "d", "e"] + + a = [1, 2, 3, 4] + + a.send(@method, 0, 0).should == [] + a.send(@method, 0, 1).should == [1] + a.send(@method, 0, 2).should == [1, 2] + a.send(@method, 0, 4).should == [1, 2, 3, 4] + a.send(@method, 0, 6).should == [1, 2, 3, 4] + a.send(@method, 0, -1).should == nil + a.send(@method, 0, -2).should == nil + a.send(@method, 0, -4).should == nil + + a.send(@method, 2, 0).should == [] + a.send(@method, 2, 1).should == [3] + a.send(@method, 2, 2).should == [3, 4] + a.send(@method, 2, 4).should == [3, 4] + a.send(@method, 2, -1).should == nil + + a.send(@method, 4, 0).should == [] + a.send(@method, 4, 2).should == [] + a.send(@method, 4, -1).should == nil + + a.send(@method, 5, 0).should == nil + a.send(@method, 5, 2).should == nil + a.send(@method, 5, -1).should == nil + + a.send(@method, 6, 0).should == nil + a.send(@method, 6, 2).should == nil + a.send(@method, 6, -1).should == nil + + a.should == [1, 2, 3, 4] + end + + it "returns count elements starting at index from the end of array with [-index, count]" do + [ "a", "b", "c", "d", "e" ].send(@method, -2, 2).should == ["d", "e"] + + a = [1, 2, 3, 4] + + a.send(@method, -1, 0).should == [] + a.send(@method, -1, 1).should == [4] + a.send(@method, -1, 2).should == [4] + a.send(@method, -1, -1).should == nil + + a.send(@method, -2, 0).should == [] + a.send(@method, -2, 1).should == [3] + a.send(@method, -2, 2).should == [3, 4] + a.send(@method, -2, 4).should == [3, 4] + a.send(@method, -2, -1).should == nil + + a.send(@method, -4, 0).should == [] + a.send(@method, -4, 1).should == [1] + a.send(@method, -4, 2).should == [1, 2] + a.send(@method, -4, 4).should == [1, 2, 3, 4] + a.send(@method, -4, 6).should == [1, 2, 3, 4] + a.send(@method, -4, -1).should == nil + + a.send(@method, -5, 0).should == nil + a.send(@method, -5, 1).should == nil + a.send(@method, -5, 10).should == nil + a.send(@method, -5, -1).should == nil + + a.should == [1, 2, 3, 4] + end + + it "returns the first count elements with [0, count]" do + [ "a", "b", "c", "d", "e" ].send(@method, 0, 3).should == ["a", "b", "c"] + end + + it "returns the subarray which is independent to self with [index,count]" do + a = [1, 2, 3] + sub = a.send(@method, 1,2) + sub.replace([:a, :b]) + a.should == [1, 2, 3] + end + + it "tries to convert the passed argument to an Integer using #to_int" do + obj = mock('to_int') + obj.stub!(:to_int).and_return(2) + + a = [1, 2, 3, 4] + a.send(@method, obj).should == 3 + a.send(@method, obj, 1).should == [3] + a.send(@method, obj, obj).should == [3, 4] + a.send(@method, 0, obj).should == [1, 2] + end + + it "raises TypeError if to_int returns non-integer" do + from = mock('from') + to = mock('to') + + # So we can construct a range out of them... + def from.<=>(o) 0 end + def to.<=>(o) 0 end + + a = [1, 2, 3, 4, 5] + + def from.to_int() 'cat' end + def to.to_int() -2 end + + -> { a.send(@method, from..to) }.should raise_error(TypeError) + + def from.to_int() 1 end + def to.to_int() 'cat' end + + -> { a.send(@method, from..to) }.should raise_error(TypeError) + end + + it "returns the elements specified by Range indexes with [m..n]" do + [ "a", "b", "c", "d", "e" ].send(@method, 1..3).should == ["b", "c", "d"] + [ "a", "b", "c", "d", "e" ].send(@method, 4..-1).should == ['e'] + [ "a", "b", "c", "d", "e" ].send(@method, 3..3).should == ['d'] + [ "a", "b", "c", "d", "e" ].send(@method, 3..-2).should == ['d'] + ['a'].send(@method, 0..-1).should == ['a'] + + a = [1, 2, 3, 4] + + a.send(@method, 0..-10).should == [] + a.send(@method, 0..0).should == [1] + a.send(@method, 0..1).should == [1, 2] + a.send(@method, 0..2).should == [1, 2, 3] + a.send(@method, 0..3).should == [1, 2, 3, 4] + a.send(@method, 0..4).should == [1, 2, 3, 4] + a.send(@method, 0..10).should == [1, 2, 3, 4] + + a.send(@method, 2..-10).should == [] + a.send(@method, 2..0).should == [] + a.send(@method, 2..2).should == [3] + a.send(@method, 2..3).should == [3, 4] + a.send(@method, 2..4).should == [3, 4] + + a.send(@method, 3..0).should == [] + a.send(@method, 3..3).should == [4] + a.send(@method, 3..4).should == [4] + + a.send(@method, 4..0).should == [] + a.send(@method, 4..4).should == [] + a.send(@method, 4..5).should == [] + + a.send(@method, 5..0).should == nil + a.send(@method, 5..5).should == nil + a.send(@method, 5..6).should == nil + + a.should == [1, 2, 3, 4] + end + + it "returns elements specified by Range indexes except the element at index n with [m...n]" do + [ "a", "b", "c", "d", "e" ].send(@method, 1...3).should == ["b", "c"] + + a = [1, 2, 3, 4] + + a.send(@method, 0...-10).should == [] + a.send(@method, 0...0).should == [] + a.send(@method, 0...1).should == [1] + a.send(@method, 0...2).should == [1, 2] + a.send(@method, 0...3).should == [1, 2, 3] + a.send(@method, 0...4).should == [1, 2, 3, 4] + a.send(@method, 0...10).should == [1, 2, 3, 4] + + a.send(@method, 2...-10).should == [] + a.send(@method, 2...0).should == [] + a.send(@method, 2...2).should == [] + a.send(@method, 2...3).should == [3] + a.send(@method, 2...4).should == [3, 4] + + a.send(@method, 3...0).should == [] + a.send(@method, 3...3).should == [] + a.send(@method, 3...4).should == [4] + + a.send(@method, 4...0).should == [] + a.send(@method, 4...4).should == [] + a.send(@method, 4...5).should == [] + + a.send(@method, 5...0).should == nil + a.send(@method, 5...5).should == nil + a.send(@method, 5...6).should == nil + + a.should == [1, 2, 3, 4] + end + + it "returns elements that exist if range start is in the array but range end is not with [m..n]" do + [ "a", "b", "c", "d", "e" ].send(@method, 4..7).should == ["e"] + end + + it "accepts Range instances having a negative m and both signs for n with [m..n] and [m...n]" do + a = [1, 2, 3, 4] + + a.send(@method, -1..-1).should == [4] + a.send(@method, -1...-1).should == [] + a.send(@method, -1..3).should == [4] + a.send(@method, -1...3).should == [] + a.send(@method, -1..4).should == [4] + a.send(@method, -1...4).should == [4] + a.send(@method, -1..10).should == [4] + a.send(@method, -1...10).should == [4] + a.send(@method, -1..0).should == [] + a.send(@method, -1..-4).should == [] + a.send(@method, -1...-4).should == [] + a.send(@method, -1..-6).should == [] + a.send(@method, -1...-6).should == [] + + a.send(@method, -2..-2).should == [3] + a.send(@method, -2...-2).should == [] + a.send(@method, -2..-1).should == [3, 4] + a.send(@method, -2...-1).should == [3] + a.send(@method, -2..10).should == [3, 4] + a.send(@method, -2...10).should == [3, 4] + + a.send(@method, -4..-4).should == [1] + a.send(@method, -4..-2).should == [1, 2, 3] + a.send(@method, -4...-2).should == [1, 2] + a.send(@method, -4..-1).should == [1, 2, 3, 4] + a.send(@method, -4...-1).should == [1, 2, 3] + a.send(@method, -4..3).should == [1, 2, 3, 4] + a.send(@method, -4...3).should == [1, 2, 3] + a.send(@method, -4..4).should == [1, 2, 3, 4] + a.send(@method, -4...4).should == [1, 2, 3, 4] + a.send(@method, -4...4).should == [1, 2, 3, 4] + a.send(@method, -4..0).should == [1] + a.send(@method, -4...0).should == [] + a.send(@method, -4..1).should == [1, 2] + a.send(@method, -4...1).should == [1] + + a.send(@method, -5..-5).should == nil + a.send(@method, -5...-5).should == nil + a.send(@method, -5..-4).should == nil + a.send(@method, -5..-1).should == nil + a.send(@method, -5..10).should == nil + + a.should == [1, 2, 3, 4] + end + + it "returns the subarray which is independent to self with [m..n]" do + a = [1, 2, 3] + sub = a.send(@method, 1..2) + sub.replace([:a, :b]) + a.should == [1, 2, 3] + end + + it "tries to convert Range elements to Integers using #to_int with [m..n] and [m...n]" do + from = mock('from') + to = mock('to') + + # So we can construct a range out of them... + def from.<=>(o) 0 end + def to.<=>(o) 0 end + + def from.to_int() 1 end + def to.to_int() -2 end + + a = [1, 2, 3, 4] + + a.send(@method, from..to).should == [2, 3] + a.send(@method, from...to).should == [2] + a.send(@method, 1..0).should == [] + a.send(@method, 1...0).should == [] + + -> { a.send(@method, "a" .. "b") }.should raise_error(TypeError) + -> { a.send(@method, "a" ... "b") }.should raise_error(TypeError) + -> { a.send(@method, from .. "b") }.should raise_error(TypeError) + -> { a.send(@method, from ... "b") }.should raise_error(TypeError) + end + + it "returns the same elements as [m..n] and [m...n] with Range subclasses" do + a = [1, 2, 3, 4] + range_incl = ArraySpecs::MyRange.new(1, 2) + range_excl = ArraySpecs::MyRange.new(-3, -1, true) + + a.send(@method, range_incl).should == [2, 3] + a.send(@method, range_excl).should == [2, 3] + end + + it "returns nil for a requested index not in the array with [index]" do + [ "a", "b", "c", "d", "e" ].send(@method, 5).should == nil + end + + it "returns [] if the index is valid but length is zero with [index, length]" do + [ "a", "b", "c", "d", "e" ].send(@method, 0, 0).should == [] + [ "a", "b", "c", "d", "e" ].send(@method, 2, 0).should == [] + end + + it "returns nil if length is zero but index is invalid with [index, length]" do + [ "a", "b", "c", "d", "e" ].send(@method, 100, 0).should == nil + [ "a", "b", "c", "d", "e" ].send(@method, -50, 0).should == nil + end + + # This is by design. It is in the official documentation. + it "returns [] if index == array.size with [index, length]" do + %w|a b c d e|.send(@method, 5, 2).should == [] + end + + it "returns nil if index > array.size with [index, length]" do + %w|a b c d e|.send(@method, 6, 2).should == nil + end + + it "returns nil if length is negative with [index, length]" do + %w|a b c d e|.send(@method, 3, -1).should == nil + %w|a b c d e|.send(@method, 2, -2).should == nil + %w|a b c d e|.send(@method, 1, -100).should == nil + end + + it "returns nil if no requested index is in the array with [m..n]" do + [ "a", "b", "c", "d", "e" ].send(@method, 6..10).should == nil + end + + it "returns nil if range start is not in the array with [m..n]" do + [ "a", "b", "c", "d", "e" ].send(@method, -10..2).should == nil + [ "a", "b", "c", "d", "e" ].send(@method, 10..12).should == nil + end + + it "returns an empty array when m == n with [m...n]" do + [1, 2, 3, 4, 5].send(@method, 1...1).should == [] + end + + it "returns an empty array with [0...0]" do + [1, 2, 3, 4, 5].send(@method, 0...0).should == [] + end + + it "returns a subarray where m, n negatives and m < n with [m..n]" do + [ "a", "b", "c", "d", "e" ].send(@method, -3..-2).should == ["c", "d"] + end + + it "returns an array containing the first element with [0..0]" do + [1, 2, 3, 4, 5].send(@method, 0..0).should == [1] + end + + it "returns the entire array with [0..-1]" do + [1, 2, 3, 4, 5].send(@method, 0..-1).should == [1, 2, 3, 4, 5] + end + + it "returns all but the last element with [0...-1]" do + [1, 2, 3, 4, 5].send(@method, 0...-1).should == [1, 2, 3, 4] + end + + it "returns [3] for [2..-1] out of [1, 2, 3]" do + [1,2,3].send(@method, 2..-1).should == [3] + end + + it "returns an empty array when m > n and m, n are positive with [m..n]" do + [1, 2, 3, 4, 5].send(@method, 3..2).should == [] + end + + it "returns an empty array when m > n and m, n are negative with [m..n]" do + [1, 2, 3, 4, 5].send(@method, -2..-3).should == [] + end + + it "does not expand array when the indices are outside of the array bounds" do + a = [1, 2] + a.send(@method, 4).should == nil + a.should == [1, 2] + a.send(@method, 4, 0).should == nil + a.should == [1, 2] + a.send(@method, 6, 1).should == nil + a.should == [1, 2] + a.send(@method, 8...8).should == nil + a.should == [1, 2] + a.send(@method, 10..10).should == nil + a.should == [1, 2] + end + + describe "with a subclass of Array" do + before :each do + ScratchPad.clear + + @array = ArraySpecs::MyArray[1, 2, 3, 4, 5] + end + + it "returns a Array instance with [n, m]" do + @array.send(@method, 0, 2).should be_an_instance_of(Array) + end + + it "returns a Array instance with [-n, m]" do + @array.send(@method, -3, 2).should be_an_instance_of(Array) + end + + it "returns a Array instance with [n..m]" do + @array.send(@method, 1..3).should be_an_instance_of(Array) + end + + it "returns a Array instance with [n...m]" do + @array.send(@method, 1...3).should be_an_instance_of(Array) + end + + it "returns a Array instance with [-n..-m]" do + @array.send(@method, -3..-1).should be_an_instance_of(Array) + end + + it "returns a Array instance with [-n...-m]" do + @array.send(@method, -3...-1).should be_an_instance_of(Array) + end + + it "returns an empty array when m == n with [m...n]" do + @array.send(@method, 1...1).should == [] + ScratchPad.recorded.should be_nil + end + + it "returns an empty array with [0...0]" do + @array.send(@method, 0...0).should == [] + ScratchPad.recorded.should be_nil + end + + it "returns an empty array when m > n and m, n are positive with [m..n]" do + @array.send(@method, 3..2).should == [] + ScratchPad.recorded.should be_nil + end + + it "returns an empty array when m > n and m, n are negative with [m..n]" do + @array.send(@method, -2..-3).should == [] + ScratchPad.recorded.should be_nil + end + + it "returns [] if index == array.size with [index, length]" do + @array.send(@method, 5, 2).should == [] + ScratchPad.recorded.should be_nil + end + + it "returns [] if the index is valid but length is zero with [index, length]" do + @array.send(@method, 0, 0).should == [] + @array.send(@method, 2, 0).should == [] + ScratchPad.recorded.should be_nil + end + + it "does not call #initialize on the subclass instance" do + @array.send(@method, 0, 3).should == [1, 2, 3] + ScratchPad.recorded.should be_nil + end + end + + it "raises a RangeError when the start index is out of range of Fixnum" do + array = [1, 2, 3, 4, 5, 6] + obj = mock('large value') + obj.should_receive(:to_int).and_return(bignum_value) + -> { array.send(@method, obj) }.should raise_error(RangeError) + + obj = 8e19 + -> { array.send(@method, obj) }.should raise_error(RangeError) + + # boundary value when longs are 64 bits + -> { array.send(@method, 2.0**63) }.should raise_error(RangeError) + + # just under the boundary value when longs are 64 bits + array.send(@method, max_long.to_f.prev_float).should == nil + end + + it "raises a RangeError when the length is out of range of Fixnum" do + array = [1, 2, 3, 4, 5, 6] + obj = mock('large value') + obj.should_receive(:to_int).and_return(bignum_value) + -> { array.send(@method, 1, obj) }.should raise_error(RangeError) + + obj = 8e19 + -> { array.send(@method, 1, obj) }.should raise_error(RangeError) + end + + it "raises a type error if a range is passed with a length" do + ->{ [1, 2, 3].send(@method, 1..2, 1) }.should raise_error(TypeError) + end + + it "raises a RangeError if passed a range with a bound that is too large" do + array = [1, 2, 3, 4, 5, 6] + -> { array.send(@method, bignum_value..(bignum_value + 1)) }.should raise_error(RangeError) + -> { array.send(@method, 0..bignum_value) }.should raise_error(RangeError) + end + + it "can accept endless ranges" do + a = [0, 1, 2, 3, 4, 5] + a.send(@method, eval("(2..)")).should == [2, 3, 4, 5] + a.send(@method, eval("(2...)")).should == [2, 3, 4, 5] + a.send(@method, eval("(-2..)")).should == [4, 5] + a.send(@method, eval("(-2...)")).should == [4, 5] + a.send(@method, eval("(9..)")).should == nil + a.send(@method, eval("(9...)")).should == nil + a.send(@method, eval("(-9..)")).should == nil + a.send(@method, eval("(-9...)")).should == nil + end + + describe "can be sliced with Enumerator::ArithmeticSequence" do + before :each do + @array = [0, 1, 2, 3, 4, 5] + end + + it "has endless range and positive steps" do + @array.send(@method, eval("(0..).step(1)")).should == [0, 1, 2, 3, 4, 5] + @array.send(@method, eval("(0..).step(2)")).should == [0, 2, 4] + @array.send(@method, eval("(0..).step(10)")).should == [0] + + @array.send(@method, eval("(2..).step(1)")).should == [2, 3, 4, 5] + @array.send(@method, eval("(2..).step(2)")).should == [2, 4] + @array.send(@method, eval("(2..).step(10)")).should == [2] + + @array.send(@method, eval("(-3..).step(1)")).should == [3, 4, 5] + @array.send(@method, eval("(-3..).step(2)")).should == [3, 5] + @array.send(@method, eval("(-3..).step(10)")).should == [3] + end + + it "has beginless range and positive steps" do + # end with zero index + @array.send(@method, (..0).step(1)).should == [0] + @array.send(@method, (...0).step(1)).should == [] + + @array.send(@method, (..0).step(2)).should == [0] + @array.send(@method, (...0).step(2)).should == [] + + @array.send(@method, (..0).step(10)).should == [0] + @array.send(@method, (...0).step(10)).should == [] + + # end with positive index + @array.send(@method, (..3).step(1)).should == [0, 1, 2, 3] + @array.send(@method, (...3).step(1)).should == [0, 1, 2] + + @array.send(@method, (..3).step(2)).should == [0, 2] + @array.send(@method, (...3).step(2)).should == [0, 2] + + @array.send(@method, (..3).step(10)).should == [0] + @array.send(@method, (...3).step(10)).should == [0] + + # end with negative index + @array.send(@method, (..-2).step(1)).should == [0, 1, 2, 3, 4,] + @array.send(@method, (...-2).step(1)).should == [0, 1, 2, 3] + + @array.send(@method, (..-2).step(2)).should == [0, 2, 4] + @array.send(@method, (...-2).step(2)).should == [0, 2] + + @array.send(@method, (..-2).step(10)).should == [0] + @array.send(@method, (...-2).step(10)).should == [0] + end + + it "has endless range and negative steps" do + @array.send(@method, eval("(0..).step(-1)")).should == [0] + @array.send(@method, eval("(0..).step(-2)")).should == [0] + @array.send(@method, eval("(0..).step(-10)")).should == [0] + + @array.send(@method, eval("(2..).step(-1)")).should == [2, 1, 0] + @array.send(@method, eval("(2..).step(-2)")).should == [2, 0] + + @array.send(@method, eval("(-3..).step(-1)")).should == [3, 2, 1, 0] + @array.send(@method, eval("(-3..).step(-2)")).should == [3, 1] + end + + it "has closed range and positive steps" do + # start and end with 0 + @array.send(@method, eval("(0..0).step(1)")).should == [0] + @array.send(@method, eval("(0...0).step(1)")).should == [] + + @array.send(@method, eval("(0..0).step(2)")).should == [0] + @array.send(@method, eval("(0...0).step(2)")).should == [] + + @array.send(@method, eval("(0..0).step(10)")).should == [0] + @array.send(@method, eval("(0...0).step(10)")).should == [] + + # start and end with positive index + @array.send(@method, eval("(1..3).step(1)")).should == [1, 2, 3] + @array.send(@method, eval("(1...3).step(1)")).should == [1, 2] + + @array.send(@method, eval("(1..3).step(2)")).should == [1, 3] + @array.send(@method, eval("(1...3).step(2)")).should == [1] + + @array.send(@method, eval("(1..3).step(10)")).should == [1] + @array.send(@method, eval("(1...3).step(10)")).should == [1] + + # start with positive index, end with negative index + @array.send(@method, eval("(1..-2).step(1)")).should == [1, 2, 3, 4] + @array.send(@method, eval("(1...-2).step(1)")).should == [1, 2, 3] + + @array.send(@method, eval("(1..-2).step(2)")).should == [1, 3] + @array.send(@method, eval("(1...-2).step(2)")).should == [1, 3] + + @array.send(@method, eval("(1..-2).step(10)")).should == [1] + @array.send(@method, eval("(1...-2).step(10)")).should == [1] + + # start with negative index, end with positive index + @array.send(@method, eval("(-4..4).step(1)")).should == [2, 3, 4] + @array.send(@method, eval("(-4...4).step(1)")).should == [2, 3] + + @array.send(@method, eval("(-4..4).step(2)")).should == [2, 4] + @array.send(@method, eval("(-4...4).step(2)")).should == [2] + + @array.send(@method, eval("(-4..4).step(10)")).should == [2] + @array.send(@method, eval("(-4...4).step(10)")).should == [2] + + # start with negative index, end with negative index + @array.send(@method, eval("(-4..-2).step(1)")).should == [2, 3, 4] + @array.send(@method, eval("(-4...-2).step(1)")).should == [2, 3] + + @array.send(@method, eval("(-4..-2).step(2)")).should == [2, 4] + @array.send(@method, eval("(-4...-2).step(2)")).should == [2] + + @array.send(@method, eval("(-4..-2).step(10)")).should == [2] + @array.send(@method, eval("(-4...-2).step(10)")).should == [2] + end + + it "has closed range and negative steps" do + # start and end with 0 + @array.send(@method, eval("(0..0).step(-1)")).should == [0] + @array.send(@method, eval("(0...0).step(-1)")).should == [] + + @array.send(@method, eval("(0..0).step(-2)")).should == [0] + @array.send(@method, eval("(0...0).step(-2)")).should == [] + + @array.send(@method, eval("(0..0).step(-10)")).should == [0] + @array.send(@method, eval("(0...0).step(-10)")).should == [] + + # start and end with positive index + @array.send(@method, eval("(1..3).step(-1)")).should == [] + @array.send(@method, eval("(1...3).step(-1)")).should == [] + + @array.send(@method, eval("(1..3).step(-2)")).should == [] + @array.send(@method, eval("(1...3).step(-2)")).should == [] + + @array.send(@method, eval("(1..3).step(-10)")).should == [] + @array.send(@method, eval("(1...3).step(-10)")).should == [] + + # start with positive index, end with negative index + @array.send(@method, eval("(1..-2).step(-1)")).should == [] + @array.send(@method, eval("(1...-2).step(-1)")).should == [] + + @array.send(@method, eval("(1..-2).step(-2)")).should == [] + @array.send(@method, eval("(1...-2).step(-2)")).should == [] + + @array.send(@method, eval("(1..-2).step(-10)")).should == [] + @array.send(@method, eval("(1...-2).step(-10)")).should == [] + + # start with negative index, end with positive index + @array.send(@method, eval("(-4..4).step(-1)")).should == [] + @array.send(@method, eval("(-4...4).step(-1)")).should == [] + + @array.send(@method, eval("(-4..4).step(-2)")).should == [] + @array.send(@method, eval("(-4...4).step(-2)")).should == [] + + @array.send(@method, eval("(-4..4).step(-10)")).should == [] + @array.send(@method, eval("(-4...4).step(-10)")).should == [] + + # start with negative index, end with negative index + @array.send(@method, eval("(-4..-2).step(-1)")).should == [] + @array.send(@method, eval("(-4...-2).step(-1)")).should == [] + + @array.send(@method, eval("(-4..-2).step(-2)")).should == [] + @array.send(@method, eval("(-4...-2).step(-2)")).should == [] + + @array.send(@method, eval("(-4..-2).step(-10)")).should == [] + @array.send(@method, eval("(-4...-2).step(-10)")).should == [] + end + + it "has inverted closed range and positive steps" do + # start and end with positive index + @array.send(@method, eval("(3..1).step(1)")).should == [] + @array.send(@method, eval("(3...1).step(1)")).should == [] + + @array.send(@method, eval("(3..1).step(2)")).should == [] + @array.send(@method, eval("(3...1).step(2)")).should == [] + + @array.send(@method, eval("(3..1).step(10)")).should == [] + @array.send(@method, eval("(3...1).step(10)")).should == [] + + # start with negative index, end with positive index + @array.send(@method, eval("(-2..1).step(1)")).should == [] + @array.send(@method, eval("(-2...1).step(1)")).should == [] + + @array.send(@method, eval("(-2..1).step(2)")).should == [] + @array.send(@method, eval("(-2...1).step(2)")).should == [] + + @array.send(@method, eval("(-2..1).step(10)")).should == [] + @array.send(@method, eval("(-2...1).step(10)")).should == [] + + # start with positive index, end with negative index + @array.send(@method, eval("(4..-4).step(1)")).should == [] + @array.send(@method, eval("(4...-4).step(1)")).should == [] + + @array.send(@method, eval("(4..-4).step(2)")).should == [] + @array.send(@method, eval("(4...-4).step(2)")).should == [] + + @array.send(@method, eval("(4..-4).step(10)")).should == [] + @array.send(@method, eval("(4...-4).step(10)")).should == [] + + # start with negative index, end with negative index + @array.send(@method, eval("(-2..-4).step(1)")).should == [] + @array.send(@method, eval("(-2...-4).step(1)")).should == [] + + @array.send(@method, eval("(-2..-4).step(2)")).should == [] + @array.send(@method, eval("(-2...-4).step(2)")).should == [] + + @array.send(@method, eval("(-2..-4).step(10)")).should == [] + @array.send(@method, eval("(-2...-4).step(10)")).should == [] + end + + it "has range with bounds outside of array" do + # end is equal to array's length + @array.send(@method, (0..6).step(1)).should == [0, 1, 2, 3, 4, 5] + -> { @array.send(@method, (0..6).step(2)) }.should raise_error(RangeError) + + # end is greater than length with positive steps + @array.send(@method, (1..6).step(2)).should == [1, 3, 5] + @array.send(@method, (2..7).step(2)).should == [2, 4] + -> { @array.send(@method, (2..8).step(2)) }.should raise_error(RangeError) + + # begin is greater than length with negative steps + @array.send(@method, (6..1).step(-2)).should == [5, 3, 1] + @array.send(@method, (7..2).step(-2)).should == [5, 3] + -> { @array.send(@method, (8..2).step(-2)) }.should raise_error(RangeError) + end + + it "has endless range with start outside of array's bounds" do + @array.send(@method, eval("(6..).step(1)")).should == [] + @array.send(@method, eval("(7..).step(1)")).should == nil + + @array.send(@method, eval("(6..).step(2)")).should == [] + -> { @array.send(@method, eval("(7..).step(2)")) }.should raise_error(RangeError) + end + end + + it "can accept beginless ranges" do + a = [0, 1, 2, 3, 4, 5] + a.send(@method, (..3)).should == [0, 1, 2, 3] + a.send(@method, (...3)).should == [0, 1, 2] + a.send(@method, (..-3)).should == [0, 1, 2, 3] + a.send(@method, (...-3)).should == [0, 1, 2] + a.send(@method, (..0)).should == [0] + a.send(@method, (...0)).should == [] + a.send(@method, (..9)).should == [0, 1, 2, 3, 4, 5] + a.send(@method, (...9)).should == [0, 1, 2, 3, 4, 5] + a.send(@method, (..-9)).should == [] + a.send(@method, (...-9)).should == [] + end + + describe "can be sliced with Enumerator::ArithmeticSequence" do + it "with infinite/inverted ranges and negative steps" do + @array = [0, 1, 2, 3, 4, 5] + @array.send(@method, (2..).step(-1)).should == [2, 1, 0] + @array.send(@method, (2..).step(-2)).should == [2, 0] + @array.send(@method, (2..).step(-3)).should == [2] + @array.send(@method, (2..).step(-4)).should == [2] + + @array.send(@method, (-3..).step(-1)).should == [3, 2, 1, 0] + @array.send(@method, (-3..).step(-2)).should == [3, 1] + @array.send(@method, (-3..).step(-3)).should == [3, 0] + @array.send(@method, (-3..).step(-4)).should == [3] + @array.send(@method, (-3..).step(-5)).should == [3] + + @array.send(@method, (..0).step(-1)).should == [5, 4, 3, 2, 1, 0] + @array.send(@method, (..0).step(-2)).should == [5, 3, 1] + @array.send(@method, (..0).step(-3)).should == [5, 2] + @array.send(@method, (..0).step(-4)).should == [5, 1] + @array.send(@method, (..0).step(-5)).should == [5, 0] + @array.send(@method, (..0).step(-6)).should == [5] + @array.send(@method, (..0).step(-7)).should == [5] + + @array.send(@method, (...0).step(-1)).should == [5, 4, 3, 2, 1] + @array.send(@method, (...0).step(-2)).should == [5, 3, 1] + @array.send(@method, (...0).step(-3)).should == [5, 2] + @array.send(@method, (...0).step(-4)).should == [5, 1] + @array.send(@method, (...0).step(-5)).should == [5] + @array.send(@method, (...0).step(-6)).should == [5] + + @array.send(@method, (...1).step(-1)).should == [5, 4, 3, 2] + @array.send(@method, (...1).step(-2)).should == [5, 3] + @array.send(@method, (...1).step(-3)).should == [5, 2] + @array.send(@method, (...1).step(-4)).should == [5] + @array.send(@method, (...1).step(-5)).should == [5] + + @array.send(@method, (..-5).step(-1)).should == [5, 4, 3, 2, 1] + @array.send(@method, (..-5).step(-2)).should == [5, 3, 1] + @array.send(@method, (..-5).step(-3)).should == [5, 2] + @array.send(@method, (..-5).step(-4)).should == [5, 1] + @array.send(@method, (..-5).step(-5)).should == [5] + @array.send(@method, (..-5).step(-6)).should == [5] + + @array.send(@method, (...-5).step(-1)).should == [5, 4, 3, 2] + @array.send(@method, (...-5).step(-2)).should == [5, 3] + @array.send(@method, (...-5).step(-3)).should == [5, 2] + @array.send(@method, (...-5).step(-4)).should == [5] + @array.send(@method, (...-5).step(-5)).should == [5] + + @array.send(@method, (4..1).step(-1)).should == [4, 3, 2, 1] + @array.send(@method, (4..1).step(-2)).should == [4, 2] + @array.send(@method, (4..1).step(-3)).should == [4, 1] + @array.send(@method, (4..1).step(-4)).should == [4] + @array.send(@method, (4..1).step(-5)).should == [4] + + @array.send(@method, (4...1).step(-1)).should == [4, 3, 2] + @array.send(@method, (4...1).step(-2)).should == [4, 2] + @array.send(@method, (4...1).step(-3)).should == [4] + @array.send(@method, (4...1).step(-4)).should == [4] + + @array.send(@method, (-2..1).step(-1)).should == [4, 3, 2, 1] + @array.send(@method, (-2..1).step(-2)).should == [4, 2] + @array.send(@method, (-2..1).step(-3)).should == [4, 1] + @array.send(@method, (-2..1).step(-4)).should == [4] + @array.send(@method, (-2..1).step(-5)).should == [4] + + @array.send(@method, (-2...1).step(-1)).should == [4, 3, 2] + @array.send(@method, (-2...1).step(-2)).should == [4, 2] + @array.send(@method, (-2...1).step(-3)).should == [4] + @array.send(@method, (-2...1).step(-4)).should == [4] + + @array.send(@method, (4..-5).step(-1)).should == [4, 3, 2, 1] + @array.send(@method, (4..-5).step(-2)).should == [4, 2] + @array.send(@method, (4..-5).step(-3)).should == [4, 1] + @array.send(@method, (4..-5).step(-4)).should == [4] + @array.send(@method, (4..-5).step(-5)).should == [4] + + @array.send(@method, (4...-5).step(-1)).should == [4, 3, 2] + @array.send(@method, (4...-5).step(-2)).should == [4, 2] + @array.send(@method, (4...-5).step(-3)).should == [4] + @array.send(@method, (4...-5).step(-4)).should == [4] + + @array.send(@method, (-2..-5).step(-1)).should == [4, 3, 2, 1] + @array.send(@method, (-2..-5).step(-2)).should == [4, 2] + @array.send(@method, (-2..-5).step(-3)).should == [4, 1] + @array.send(@method, (-2..-5).step(-4)).should == [4] + @array.send(@method, (-2..-5).step(-5)).should == [4] + + @array.send(@method, (-2...-5).step(-1)).should == [4, 3, 2] + @array.send(@method, (-2...-5).step(-2)).should == [4, 2] + @array.send(@method, (-2...-5).step(-3)).should == [4] + @array.send(@method, (-2...-5).step(-4)).should == [4] + end + end + + it "can accept nil...nil ranges" do + a = [0, 1, 2, 3, 4, 5] + a.send(@method, eval("(nil...nil)")).should == a + a.send(@method, (...nil)).should == a + a.send(@method, eval("(nil..)")).should == a + end +end diff --git a/spec/ruby/core/array/shared/union.rb b/spec/ruby/core/array/shared/union.rb new file mode 100644 index 0000000000..0b60df9ca4 --- /dev/null +++ b/spec/ruby/core/array/shared/union.rb @@ -0,0 +1,79 @@ +describe :array_binary_union, shared: true do + it "returns an array of elements that appear in either array (union)" do + [].send(@method, []).should == [] + [1, 2].send(@method, []).should == [1, 2] + [].send(@method, [1, 2]).should == [1, 2] + [ 1, 2, 3, 4 ].send(@method, [ 3, 4, 5 ]).should == [1, 2, 3, 4, 5] + end + + it "creates an array with no duplicates" do + [ 1, 2, 3, 1, 4, 5 ].send(@method, [ 1, 3, 4, 5, 3, 6 ]).should == [1, 2, 3, 4, 5, 6] + end + + it "creates an array with elements in order they are first encountered" do + [ 1, 2, 3, 1 ].send(@method, [ 1, 3, 4, 5 ]).should == [1, 2, 3, 4, 5] + end + + it "properly handles recursive arrays" do + empty = ArraySpecs.empty_recursive_array + empty.send(@method, empty).should == empty + + array = ArraySpecs.recursive_array + array.send(@method, []).should == [1, 'two', 3.0, array] + [].send(@method, array).should == [1, 'two', 3.0, array] + array.send(@method, array).should == [1, 'two', 3.0, array] + array.send(@method, empty).should == [1, 'two', 3.0, array, empty] + end + + it "tries to convert the passed argument to an Array using #to_ary" do + obj = mock('[1,2,3]') + obj.should_receive(:to_ary).and_return([1, 2, 3]) + [0].send(@method, obj).should == ([0] | [1, 2, 3]) + end + + # MRI follows hashing semantics here, so doesn't actually call eql?/hash for Integer/Symbol + it "acts as if using an intermediate hash to collect values" do + not_supported_on :opal do + [5.0, 4.0].send(@method, [5, 4]).should == [5.0, 4.0, 5, 4] + end + + str = "x" + [str].send(@method, [str.dup]).should == [str] + + obj1 = mock('1') + obj2 = mock('2') + obj1.stub!(:hash).and_return(0) + obj2.stub!(:hash).and_return(0) + obj2.should_receive(:eql?).at_least(1).and_return(true) + + [obj1].send(@method, [obj2]).should == [obj1] + [obj1, obj1, obj2, obj2].send(@method, [obj2]).should == [obj1] + + obj1 = mock('3') + obj2 = mock('4') + obj1.stub!(:hash).and_return(0) + obj2.stub!(:hash).and_return(0) + obj2.should_receive(:eql?).at_least(1).and_return(false) + + [obj1].send(@method, [obj2]).should == [obj1, obj2] + [obj1, obj1, obj2, obj2].send(@method, [obj2]).should == [obj1, obj2] + end + + it "does not return subclass instances for Array subclasses" do + ArraySpecs::MyArray[1, 2, 3].send(@method, []).should be_an_instance_of(Array) + ArraySpecs::MyArray[1, 2, 3].send(@method, ArraySpecs::MyArray[1, 2, 3]).should be_an_instance_of(Array) + [].send(@method, ArraySpecs::MyArray[1, 2, 3]).should be_an_instance_of(Array) + end + + it "does not call to_ary on array subclasses" do + [1, 2].send(@method, ArraySpecs::ToAryArray[5, 6]).should == [1, 2, 5, 6] + end + + it "properly handles an identical item even when its #eql? isn't reflexive" do + x = mock('x') + x.stub!(:hash).and_return(42) + x.stub!(:eql?).and_return(false) # Stubbed for clarity and latitude in implementation; not actually sent by MRI. + + [x].send(@method, [x]).should == [x] + end +end diff --git a/spec/ruby/core/array/shared/unshift.rb b/spec/ruby/core/array/shared/unshift.rb new file mode 100644 index 0000000000..9e0fe7556a --- /dev/null +++ b/spec/ruby/core/array/shared/unshift.rb @@ -0,0 +1,64 @@ +describe :array_unshift, shared: true do + it "prepends object to the original array" do + a = [1, 2, 3] + a.send(@method, "a").should equal(a) + a.should == ['a', 1, 2, 3] + a.send(@method).should equal(a) + a.should == ['a', 1, 2, 3] + a.send(@method, 5, 4, 3) + a.should == [5, 4, 3, 'a', 1, 2, 3] + + # shift all but one element + a = [1, 2] + a.shift + a.send(@method, 3, 4) + a.should == [3, 4, 2] + + # now shift all elements + a.shift + a.shift + a.shift + a.send(@method, 3, 4) + a.should == [3, 4] + end + + it "returns self" do + a = [1, 2, 3] + a.send(@method, "a").should.equal?(a) + end + + it "quietly ignores unshifting nothing" do + [].send(@method).should == [] + end + + it "properly handles recursive arrays" do + empty = ArraySpecs.empty_recursive_array + empty.send(@method, :new).should == [:new, empty] + + array = ArraySpecs.recursive_array + array.send(@method, :new) + array[0..5].should == [:new, 1, 'two', 3.0, array, array] + end + + it "raises a FrozenError on a frozen array when the array is modified" do + -> { ArraySpecs.frozen_array.send(@method, 1) }.should raise_error(FrozenError) + end + + # see [ruby-core:23666] + it "raises a FrozenError on a frozen array when the array would not be modified" do + -> { ArraySpecs.frozen_array.send(@method) }.should raise_error(FrozenError) + end + + # https://github.com/truffleruby/truffleruby/issues/2772 + it "doesn't rely on Array#[]= so it can be overridden" do + subclass = Class.new(Array) do + def []=(*) + raise "[]= is called" + end + end + + array = subclass.new + array.send(@method, 1) + array.should == [1] + end +end diff --git a/spec/ruby/core/array/shift_spec.rb b/spec/ruby/core/array/shift_spec.rb new file mode 100644 index 0000000000..6b4ef39f77 --- /dev/null +++ b/spec/ruby/core/array/shift_spec.rb @@ -0,0 +1,120 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Array#shift" do + it "removes and returns the first element" do + a = [5, 1, 1, 5, 4] + a.shift.should == 5 + a.should == [1, 1, 5, 4] + a.shift.should == 1 + a.should == [1, 5, 4] + a.shift.should == 1 + a.should == [5, 4] + a.shift.should == 5 + a.should == [4] + a.shift.should == 4 + a.should == [] + end + + it "returns nil when the array is empty" do + [].shift.should == nil + end + + it "properly handles recursive arrays" do + empty = ArraySpecs.empty_recursive_array + empty.shift.should == [] + empty.should == [] + + array = ArraySpecs.recursive_array + array.shift.should == 1 + array[0..2].should == ['two', 3.0, array] + end + + it "raises a FrozenError on a frozen array" do + -> { ArraySpecs.frozen_array.shift }.should raise_error(FrozenError) + end + it "raises a FrozenError on an empty frozen array" do + -> { ArraySpecs.empty_frozen_array.shift }.should raise_error(FrozenError) + end + + describe "passed a number n as an argument" do + it "removes and returns an array with the first n element of the array" do + a = [1, 2, 3, 4, 5, 6] + + a.shift(0).should == [] + a.should == [1, 2, 3, 4, 5, 6] + + a.shift(1).should == [1] + a.should == [2, 3, 4, 5, 6] + + a.shift(2).should == [2, 3] + a.should == [4, 5, 6] + + a.shift(3).should == [4, 5, 6] + a.should == [] + end + + it "does not corrupt the array when shift without arguments is followed by shift with an argument" do + a = [1, 2, 3, 4, 5] + + a.shift.should == 1 + a.shift(3).should == [2, 3, 4] + a.should == [5] + end + + it "returns a new empty array if there are no more elements" do + a = [] + popped1 = a.shift(1) + popped1.should == [] + a.should == [] + + popped2 = a.shift(2) + popped2.should == [] + a.should == [] + + popped1.should_not equal(popped2) + end + + it "returns whole elements if n exceeds size of the array" do + a = [1, 2, 3, 4, 5] + a.shift(6).should == [1, 2, 3, 4, 5] + a.should == [] + end + + it "does not return self even when it returns whole elements" do + a = [1, 2, 3, 4, 5] + a.shift(5).should_not equal(a) + + a = [1, 2, 3, 4, 5] + a.shift(6).should_not equal(a) + end + + it "raises an ArgumentError if n is negative" do + ->{ [1, 2, 3].shift(-1) }.should raise_error(ArgumentError) + end + + it "tries to convert n to an Integer using #to_int" do + a = [1, 2, 3, 4] + a.shift(2.3).should == [1, 2] + + obj = mock('to_int') + obj.should_receive(:to_int).and_return(2) + a.should == [3, 4] + a.shift(obj).should == [3, 4] + a.should == [] + end + + it "raises a TypeError when the passed n cannot be coerced to Integer" do + ->{ [1, 2].shift("cat") }.should raise_error(TypeError) + ->{ [1, 2].shift(nil) }.should raise_error(TypeError) + end + + it "raises an ArgumentError if more arguments are passed" do + ->{ [1, 2].shift(1, 2) }.should raise_error(ArgumentError) + end + + it "does not return subclass instances with Array subclass" do + ArraySpecs::MyArray[1, 2, 3].shift(2).should be_an_instance_of(Array) + end + end +end diff --git a/spec/ruby/core/array/shuffle_spec.rb b/spec/ruby/core/array/shuffle_spec.rb new file mode 100644 index 0000000000..b84394bcb5 --- /dev/null +++ b/spec/ruby/core/array/shuffle_spec.rb @@ -0,0 +1,119 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Array#shuffle" do + it "returns the same values, in a usually different order" do + a = [1, 2, 3, 4] + different = false + 10.times do + s = a.shuffle + s.sort.should == a + different ||= (a != s) + end + different.should be_true # Will fail once in a blue moon (4!^10) + end + + it "is not destructive" do + a = [1, 2, 3] + 10.times do + a.shuffle + a.should == [1, 2, 3] + end + end + + it "does not return subclass instances with Array subclass" do + ArraySpecs::MyArray[1, 2, 3].shuffle.should be_an_instance_of(Array) + end + + it "calls #rand on the Object passed by the :random key in the arguments Hash" do + obj = mock("array_shuffle_random") + obj.should_receive(:rand).at_least(1).times.and_return(0.5) + + result = [1, 2].shuffle(random: obj) + result.size.should == 2 + result.should include(1, 2) + end + + it "raises a NoMethodError if an object passed for the RNG does not define #rand" do + obj = BasicObject.new + + -> { [1, 2].shuffle(random: obj) }.should raise_error(NoMethodError) + end + + it "accepts a Float for the value returned by #rand" do + random = mock("array_shuffle_random") + random.should_receive(:rand).at_least(1).times.and_return(0.3) + + [1, 2].shuffle(random: random).should be_an_instance_of(Array) + end + + it "accepts a Random class for the value for random: argument" do + [1, 2].shuffle(random: Random).should be_an_instance_of(Array) + end + + it "calls #to_int on the Object returned by #rand" do + value = mock("array_shuffle_random_value") + value.should_receive(:to_int).at_least(1).times.and_return(0) + random = mock("array_shuffle_random") + random.should_receive(:rand).at_least(1).times.and_return(value) + + [1, 2].shuffle(random: random).should be_an_instance_of(Array) + end + + it "raises a RangeError if the value is less than zero" do + value = mock("array_shuffle_random_value") + value.should_receive(:to_int).and_return(-1) + random = mock("array_shuffle_random") + random.should_receive(:rand).and_return(value) + + -> { [1, 2].shuffle(random: random) }.should raise_error(RangeError) + end + + it "raises a RangeError if the value is equal to the Array size" do + value = mock("array_shuffle_random_value") + value.should_receive(:to_int).at_least(1).times.and_return(2) + random = mock("array_shuffle_random") + random.should_receive(:rand).at_least(1).times.and_return(value) + + -> { [1, 2].shuffle(random: random) }.should raise_error(RangeError) + end + + it "raises a RangeError if the value is greater than the Array size" do + value = mock("array_shuffle_random_value") + value.should_receive(:to_int).at_least(1).times.and_return(3) + random = mock("array_shuffle_random") + random.should_receive(:rand).at_least(1).times.and_return(value) + + -> { [1, 2].shuffle(random: random) }.should raise_error(RangeError) + end +end + +describe "Array#shuffle!" do + it "returns the same values, in a usually different order" do + a = [1, 2, 3, 4] + original = a + different = false + 10.times do + a = a.shuffle! + a.sort.should == [1, 2, 3, 4] + different ||= (a != [1, 2, 3, 4]) + end + different.should be_true # Will fail once in a blue moon (4!^10) + a.should equal(original) + end + + it "raises a FrozenError on a frozen array" do + -> { ArraySpecs.frozen_array.shuffle! }.should raise_error(FrozenError) + -> { ArraySpecs.empty_frozen_array.shuffle! }.should raise_error(FrozenError) + end + + it "matches CRuby with random:" do + %w[a b c].shuffle(random: Random.new(1)).should == %w[a c b] + (0..10).to_a.shuffle(random: Random.new(10)).should == [2, 6, 8, 5, 7, 10, 3, 1, 0, 4, 9] + end + + it "matches CRuby with srand" do + srand(123) + %w[a b c d e f g h i j k].shuffle.should == %w[a e f h i j d b g k c] + end +end diff --git a/spec/ruby/core/array/size_spec.rb b/spec/ruby/core/array/size_spec.rb new file mode 100644 index 0000000000..d68f956a83 --- /dev/null +++ b/spec/ruby/core/array/size_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/length' + +describe "Array#size" do + it_behaves_like :array_length, :size +end diff --git a/spec/ruby/core/array/slice_spec.rb b/spec/ruby/core/array/slice_spec.rb new file mode 100644 index 0000000000..731c129251 --- /dev/null +++ b/spec/ruby/core/array/slice_spec.rb @@ -0,0 +1,218 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/slice' + +describe "Array#slice!" do + it "removes and return the element at index" do + a = [1, 2, 3, 4] + a.slice!(10).should == nil + a.should == [1, 2, 3, 4] + a.slice!(-10).should == nil + a.should == [1, 2, 3, 4] + a.slice!(2).should == 3 + a.should == [1, 2, 4] + a.slice!(-1).should == 4 + a.should == [1, 2] + a.slice!(1).should == 2 + a.should == [1] + a.slice!(-1).should == 1 + a.should == [] + a.slice!(-1).should == nil + a.should == [] + a.slice!(0).should == nil + a.should == [] + end + + it "removes and returns length elements beginning at start" do + a = [1, 2, 3, 4, 5, 6] + a.slice!(2, 3).should == [3, 4, 5] + a.should == [1, 2, 6] + a.slice!(1, 1).should == [2] + a.should == [1, 6] + a.slice!(1, 0).should == [] + a.should == [1, 6] + a.slice!(2, 0).should == [] + a.should == [1, 6] + a.slice!(0, 4).should == [1, 6] + a.should == [] + a.slice!(0, 4).should == [] + a.should == [] + + a = [1] + a.slice!(0, 1).should == [1] + a.should == [] + a[-1].should == nil + + a = [1, 2, 3] + a.slice!(0,1).should == [1] + a.should == [2, 3] + end + + it "returns nil if length is negative" do + a = [1, 2, 3] + a.slice!(2, -1).should == nil + a.should == [1, 2, 3] + end + + it "properly handles recursive arrays" do + empty = ArraySpecs.empty_recursive_array + empty.slice(0).should == empty + + array = ArraySpecs.recursive_array + array.slice(4).should == array + array.slice(0..3).should == [1, 'two', 3.0, array] + end + + it "calls to_int on start and length arguments" do + obj = mock('2') + def obj.to_int() 2 end + + a = [1, 2, 3, 4, 5] + a.slice!(obj).should == 3 + a.should == [1, 2, 4, 5] + a.slice!(obj, obj).should == [4, 5] + a.should == [1, 2] + a.slice!(0, obj).should == [1, 2] + a.should == [] + end + + it "removes and return elements in range" do + a = [1, 2, 3, 4, 5, 6, 7, 8] + a.slice!(1..4).should == [2, 3, 4, 5] + a.should == [1, 6, 7, 8] + a.slice!(1...3).should == [6, 7] + a.should == [1, 8] + a.slice!(-1..-1).should == [8] + a.should == [1] + a.slice!(0...0).should == [] + a.should == [1] + a.slice!(0..0).should == [1] + a.should == [] + + a = [1,2,3] + a.slice!(0..3).should == [1,2,3] + a.should == [] + end + + it "removes and returns elements in end-exclusive ranges" do + a = [1, 2, 3, 4, 5, 6, 7, 8] + a.slice!(4...a.length).should == [5, 6, 7, 8] + a.should == [1, 2, 3, 4] + end + + it "calls to_int on range arguments" do + from = mock('from') + to = mock('to') + + # So we can construct a range out of them... + def from.<=>(o) 0 end + def to.<=>(o) 0 end + + def from.to_int() 1 end + def to.to_int() -2 end + + a = [1, 2, 3, 4, 5] + + a.slice!(from .. to).should == [2, 3, 4] + a.should == [1, 5] + + -> { a.slice!("a" .. "b") }.should raise_error(TypeError) + -> { a.slice!(from .. "b") }.should raise_error(TypeError) + end + + it "returns last element for consecutive calls at zero index" do + a = [ 1, 2, 3 ] + a.slice!(0).should == 1 + a.slice!(0).should == 2 + a.slice!(0).should == 3 + a.should == [] + end + + it "does not expand array with indices out of bounds" do + a = [1, 2] + a.slice!(4).should == nil + a.should == [1, 2] + a.slice!(4, 0).should == nil + a.should == [1, 2] + a.slice!(6, 1).should == nil + a.should == [1, 2] + a.slice!(8...8).should == nil + a.should == [1, 2] + a.slice!(10..10).should == nil + a.should == [1, 2] + end + + it "does not expand array with negative indices out of bounds" do + a = [1, 2] + a.slice!(-3, 1).should == nil + a.should == [1, 2] + a.slice!(-3..2).should == nil + a.should == [1, 2] + end + + it "raises a FrozenError on a frozen array" do + -> { ArraySpecs.frozen_array.slice!(0, 0) }.should raise_error(FrozenError) + end + + it "works with endless ranges" do + a = [1, 2, 3] + a.slice!(eval("(1..)")).should == [2, 3] + a.should == [1] + + a = [1, 2, 3] + a.slice!(eval("(2...)")).should == [3] + a.should == [1, 2] + + a = [1, 2, 3] + a.slice!(eval("(-2..)")).should == [2, 3] + a.should == [1] + + a = [1, 2, 3] + a.slice!(eval("(-1...)")).should == [3] + a.should == [1, 2] + end + + it "works with beginless ranges" do + a = [0,1,2,3,4] + a.slice!((..3)).should == [0, 1, 2, 3] + a.should == [4] + + a = [0,1,2,3,4] + a.slice!((...-2)).should == [0, 1, 2] + a.should == [3, 4] + end + + describe "with a subclass of Array" do + before :each do + @array = ArraySpecs::MyArray[1, 2, 3, 4, 5] + end + + it "returns a Array instance with [n, m]" do + @array.slice!(0, 2).should be_an_instance_of(Array) + end + + it "returns a Array instance with [-n, m]" do + @array.slice!(-3, 2).should be_an_instance_of(Array) + end + + it "returns a Array instance with [n..m]" do + @array.slice!(1..3).should be_an_instance_of(Array) + end + + it "returns a Array instance with [n...m]" do + @array.slice!(1...3).should be_an_instance_of(Array) + end + + it "returns a Array instance with [-n..-m]" do + @array.slice!(-3..-1).should be_an_instance_of(Array) + end + + it "returns a Array instance with [-n...-m]" do + @array.slice!(-3...-1).should be_an_instance_of(Array) + end + end +end + +describe "Array#slice" do + it_behaves_like :array_slice, :slice +end diff --git a/spec/ruby/core/array/sort_by_spec.rb b/spec/ruby/core/array/sort_by_spec.rb new file mode 100644 index 0000000000..0334f953f6 --- /dev/null +++ b/spec/ruby/core/array/sort_by_spec.rb @@ -0,0 +1,85 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/iterable_and_tolerating_size_increasing' +require_relative '../enumerable/shared/enumeratorized' + +describe "Array#sort_by!" do + it "sorts array in place by passing each element to the given block" do + a = [-100, -2, 1, 200, 30000] + a.sort_by!{ |e| e.to_s.size } + a.should == [1, -2, 200, -100, 30000] + end + + it "returns an Enumerator if not given a block" do + (1..10).to_a.sort_by!.should be_an_instance_of(Enumerator) + end + + it "completes when supplied a block that always returns the same result" do + a = [2, 3, 5, 1, 4] + a.sort_by!{ 1 } + a.should be_an_instance_of(Array) + a.sort_by!{ 0 } + a.should be_an_instance_of(Array) + a.sort_by!{ -1 } + a.should be_an_instance_of(Array) + end + + it "raises a FrozenError on a frozen array" do + -> { ArraySpecs.frozen_array.sort_by! {}}.should raise_error(FrozenError) + end + + it "raises a FrozenError on an empty frozen array" do + -> { ArraySpecs.empty_frozen_array.sort_by! {}}.should raise_error(FrozenError) + end + + it "raises a FrozenError on a frozen array only during iteration if called without a block" do + enum = ArraySpecs.frozen_array.sort_by! + -> { enum.each {} }.should raise_error(FrozenError) + end + + it "returns the specified value when it would break in the given block" do + [1, 2, 3].sort_by!{ break :a }.should == :a + end + + it "makes some modification even if finished sorting when it would break in the given block" do + partially_sorted = (1..5).map{|i| + ary = [5, 4, 3, 2, 1] + ary.sort_by!{|x,y| break if x==i; x<=>y} + ary + } + partially_sorted.any?{|ary| ary != [1, 2, 3, 4, 5]}.should be_true + end + + it "changes nothing when called on a single element array" do + [1].sort_by!(&:to_s).should == [1] + end + + it "does not truncate the array is the block raises an exception" do + a = [1, 2, 3] + begin + a.sort_by! { raise StandardError, 'Oops' } + rescue + end + + a.should == [1, 2, 3] + end + + it "doesn't change array if error is raised" do + a = [4, 3, 2, 1] + begin + a.sort_by! do |e| + raise StandardError, 'Oops' if e == 1 + e + end + rescue StandardError + end + + a.should == [4, 3, 2, 1] + end + + it_behaves_like :enumeratorized_with_origin_size, :sort_by!, [1,2,3] +end + +describe "Array#sort_by!" do + it_behaves_like :array_iterable_and_tolerating_size_increasing, :sort_by! +end diff --git a/spec/ruby/core/array/sort_spec.rb b/spec/ruby/core/array/sort_spec.rb new file mode 100644 index 0000000000..e20b650516 --- /dev/null +++ b/spec/ruby/core/array/sort_spec.rb @@ -0,0 +1,252 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Array#sort" do + it "returns a new array sorted based on comparing elements with <=>" do + a = [1, -2, 3, 9, 1, 5, -5, 1000, -5, 2, -10, 14, 6, 23, 0] + a.sort.should == [-10, -5, -5, -2, 0, 1, 1, 2, 3, 5, 6, 9, 14, 23, 1000] + end + + it "does not affect the original Array" do + a = [3, 1, 2] + a.sort.should == [1, 2, 3] + a.should == [3, 1, 2] + + a = [0, 15, 2, 3, 4, 6, 14, 5, 7, 12, 8, 9, 1, 10, 11, 13] + b = a.sort + a.should == [0, 15, 2, 3, 4, 6, 14, 5, 7, 12, 8, 9, 1, 10, 11, 13] + b.should == (0..15).to_a + end + + it "sorts already-sorted Arrays" do + (0..15).to_a.sort.should == (0..15).to_a + end + + it "sorts reverse-sorted Arrays" do + (0..15).to_a.reverse.sort.should == (0..15).to_a + end + + it "sorts Arrays that consist entirely of equal elements" do + a = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + a.sort.should == a + b = Array.new(15).map { ArraySpecs::SortSame.new } + b.sort.should == b + end + + it "sorts Arrays that consist mostly of equal elements" do + a = [1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1] + a.sort.should == [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + end + + it "does not return self even if the array would be already sorted" do + a = [1, 2, 3] + sorted = a.sort + sorted.should == a + sorted.should_not equal(a) + end + + it "properly handles recursive arrays" do + empty = ArraySpecs.empty_recursive_array + empty.sort.should == empty + + array = [[]]; array << array + array.sort.should == [[], array] + end + + it "uses #<=> of elements in order to sort" do + a = ArraySpecs::MockForCompared.new + b = ArraySpecs::MockForCompared.new + c = ArraySpecs::MockForCompared.new + + ArraySpecs::MockForCompared.should_not.compared? + [a, b, c].sort.should == [c, b, a] + ArraySpecs::MockForCompared.should.compared? + end + + it "does not deal with exceptions raised by unimplemented or incorrect #<=>" do + o = Object.new + + -> { + [o, 1].sort + }.should raise_error(ArgumentError) + end + + it "may take a block which is used to determine the order of objects a and b described as -1, 0 or +1" do + a = [5, 1, 4, 3, 2] + a.sort.should == [1, 2, 3, 4, 5] + a.sort {|x, y| y <=> x}.should == [5, 4, 3, 2, 1] + end + + it "raises an error when a given block returns nil" do + -> { [1, 2].sort {} }.should raise_error(ArgumentError) + end + + it "does not call #<=> on contained objects when invoked with a block" do + a = Array.new(25) + (0...25).each {|i| a[i] = ArraySpecs::UFOSceptic.new } + + a.sort { -1 }.should be_an_instance_of(Array) + end + + it "does not call #<=> on elements when invoked with a block even if Array is large (Rubinius #412)" do + a = Array.new(1500) + (0...1500).each {|i| a[i] = ArraySpecs::UFOSceptic.new } + + a.sort { -1 }.should be_an_instance_of(Array) + end + + it "completes when supplied a block that always returns the same result" do + a = [2, 3, 5, 1, 4] + a.sort { 1 }.should be_an_instance_of(Array) + a.sort { 0 }.should be_an_instance_of(Array) + a.sort { -1 }.should be_an_instance_of(Array) + end + + it "does not freezes self during being sorted" do + a = [1, 2, 3] + a.sort { |x,y| a.should_not.frozen?; x <=> y } + end + + it "returns the specified value when it would break in the given block" do + [1, 2, 3].sort{ break :a }.should == :a + end + + it "uses the sign of Integer block results as the sort result" do + a = [1, 2, 5, 10, 7, -4, 12] + begin + class Integer + alias old_spaceship <=> + def <=>(other) + raise + end + end + a.sort {|n, m| (n - m) * (2 ** 200)}.should == [-4, 1, 2, 5, 7, 10, 12] + ensure + class Integer + alias <=> old_spaceship + end + end + end + + it "compares values returned by block with 0" do + a = [1, 2, 5, 10, 7, -4, 12] + a.sort { |n, m| n - m }.should == [-4, 1, 2, 5, 7, 10, 12] + a.sort { |n, m| + ArraySpecs::ComparableWithInteger.new(n-m) + }.should == [-4, 1, 2, 5, 7, 10, 12] + -> { + a.sort { |n, m| (n - m).to_s } + }.should raise_error(ArgumentError) + end + + it "sorts an array that has a value shifted off without a block" do + a = Array.new(20, 1) + a.shift + a[0] = 2 + a.sort.last.should == 2 + end + + it "sorts an array that has a value shifted off with a block" do + a = Array.new(20, 1) + a.shift + a[0] = 2 + a.sort {|x, y| x <=> y }.last.should == 2 + end + + it "raises an error if objects can't be compared" do + a=[ArraySpecs::Uncomparable.new, ArraySpecs::Uncomparable.new] + -> {a.sort}.should raise_error(ArgumentError) + end + + # From a strange Rubinius bug + it "handles a large array that has been pruned" do + pruned = ArraySpecs::LargeArray.dup.delete_if { |n| n !~ /^test./ } + pruned.sort.should == ArraySpecs::LargeTestArraySorted + end + + it "does not return subclass instance on Array subclasses" do + ary = ArraySpecs::MyArray[1, 2, 3] + ary.sort.should be_an_instance_of(Array) + end +end + +describe "Array#sort!" do + it "sorts array in place using <=>" do + a = [1, -2, 3, 9, 1, 5, -5, 1000, -5, 2, -10, 14, 6, 23, 0] + a.sort! + a.should == [-10, -5, -5, -2, 0, 1, 1, 2, 3, 5, 6, 9, 14, 23, 1000] + end + + it "sorts array in place using block value if a block given" do + a = [0, 15, 2, 3, 4, 6, 14, 5, 7, 12, 8, 9, 1, 10, 11, 13] + a.sort! { |x, y| y <=> x }.should == (0..15).to_a.reverse + end + + it "returns self if the order of elements changed" do + a = [6, 7, 2, 3, 7] + a.sort!.should equal(a) + a.should == [2, 3, 6, 7, 7] + end + + it "returns self even if makes no modification" do + a = [1, 2, 3, 4, 5] + a.sort!.should equal(a) + a.should == [1, 2, 3, 4, 5] + end + + it "properly handles recursive arrays" do + empty = ArraySpecs.empty_recursive_array + empty.sort!.should == empty + + array = [[]]; array << array + array.sort!.should == array + end + + it "uses #<=> of elements in order to sort" do + a = ArraySpecs::MockForCompared.new + b = ArraySpecs::MockForCompared.new + c = ArraySpecs::MockForCompared.new + + ArraySpecs::MockForCompared.should_not.compared? + [a, b, c].sort!.should == [c, b, a] + ArraySpecs::MockForCompared.should.compared? + end + + it "does not call #<=> on contained objects when invoked with a block" do + a = Array.new(25) + (0...25).each {|i| a[i] = ArraySpecs::UFOSceptic.new } + + a.sort! { -1 }.should be_an_instance_of(Array) + end + + it "does not call #<=> on elements when invoked with a block even if Array is large (Rubinius #412)" do + a = Array.new(1500) + (0...1500).each {|i| a[i] = ArraySpecs::UFOSceptic.new } + + a.sort! { -1 }.should be_an_instance_of(Array) + end + + it "completes when supplied a block that always returns the same result" do + a = [2, 3, 5, 1, 4] + a.sort!{ 1 }.should be_an_instance_of(Array) + a.sort!{ 0 }.should be_an_instance_of(Array) + a.sort!{ -1 }.should be_an_instance_of(Array) + end + + it "raises a FrozenError on a frozen array" do + -> { ArraySpecs.frozen_array.sort! }.should raise_error(FrozenError) + end + + it "returns the specified value when it would break in the given block" do + [1, 2, 3].sort{ break :a }.should == :a + end + + it "makes some modification even if finished sorting when it would break in the given block" do + partially_sorted = (1..5).map{|i| + ary = [5, 4, 3, 2, 1] + ary.sort!{|x,y| break if x==i; x<=>y} + ary + } + partially_sorted.any?{|ary| ary != [1, 2, 3, 4, 5]}.should be_true + end +end diff --git a/spec/ruby/core/array/sum_spec.rb b/spec/ruby/core/array/sum_spec.rb new file mode 100644 index 0000000000..06abe06135 --- /dev/null +++ b/spec/ruby/core/array/sum_spec.rb @@ -0,0 +1,90 @@ +require_relative '../../spec_helper' +require_relative 'shared/iterable_and_tolerating_size_increasing' + +describe "Array#sum" do + it "returns the sum of elements" do + [1, 2, 3].sum.should == 6 + end + + it "applies a block to each element before adding if it's given" do + [1, 2, 3].sum { |i| i * 10 }.should == 60 + end + + it "doesn't apply the block init" do + [1, 2, 3].sum(1) { |i| i * 10 }.should == 61 + end + + # https://bugs.ruby-lang.org/issues/12217 + # https://github.com/ruby/ruby/blob/master/doc/ChangeLog/ChangeLog-2.4.0#L6208-L6214 + it "uses Kahan's compensated summation algorithm for precise sum of float numbers" do + floats = [2.7800000000000002, 5.0, 2.5, 4.44, 3.89, 3.89, 4.44, 7.78, 5.0, 2.7800000000000002, 5.0, 2.5] + naive_sum = floats.reduce { |sum, e| sum + e } + naive_sum.should == 50.00000000000001 + floats.sum.should == 50.0 + end + + it "handles infinite values and NaN" do + [1.0, Float::INFINITY].sum.should == Float::INFINITY + [1.0, -Float::INFINITY].sum.should == -Float::INFINITY + [1.0, Float::NAN].sum.should.nan? + + [Float::INFINITY, 1.0].sum.should == Float::INFINITY + [-Float::INFINITY, 1.0].sum.should == -Float::INFINITY + [Float::NAN, 1.0].sum.should.nan? + + [Float::NAN, Float::INFINITY].sum.should.nan? + [Float::INFINITY, Float::NAN].sum.should.nan? + + [Float::INFINITY, -Float::INFINITY].sum.should.nan? + [-Float::INFINITY, Float::INFINITY].sum.should.nan? + + [Float::INFINITY, Float::INFINITY].sum.should == Float::INFINITY + [-Float::INFINITY, -Float::INFINITY].sum.should == -Float::INFINITY + [Float::NAN, Float::NAN].sum.should.nan? + end + + it "returns init value if array is empty" do + [].sum(-1).should == -1 + end + + it "returns 0 if array is empty and init is omitted" do + [].sum.should == 0 + end + + it "adds init value to the sum of elements" do + [1, 2, 3].sum(10).should == 16 + end + + it "can be used for non-numeric objects by providing init value" do + ["a", "b", "c"].sum("").should == "abc" + end + + it 'raises TypeError if any element are not numeric' do + -> { ["a"].sum }.should raise_error(TypeError) + end + + it 'raises TypeError if any element cannot be added to init value' do + -> { [1].sum([]) }.should raise_error(TypeError) + end + + it "calls + to sum the elements" do + a = mock("a") + b = mock("b") + a.should_receive(:+).with(b).and_return(42) + [b].sum(a).should == 42 + end + + ruby_bug '#19530', ''...'3.3' do + it "calls + on the init value" do + a = mock("a") + b = mock("b") + a.should_receive(:+).with(42).and_return(b) + [42].sum(a).should == b + end + end +end + +describe "Array#sum" do + @value_to_return = -> _ { 1 } + it_behaves_like :array_iterable_and_tolerating_size_increasing, :sum +end diff --git a/spec/ruby/core/array/take_spec.rb b/spec/ruby/core/array/take_spec.rb new file mode 100644 index 0000000000..c4f0ac9aa4 --- /dev/null +++ b/spec/ruby/core/array/take_spec.rb @@ -0,0 +1,32 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Array#take" do + it "returns the first specified number of elements" do + [1, 2, 3].take(2).should == [1, 2] + end + + it "returns all elements when the argument is greater than the Array size" do + [1, 2].take(99).should == [1, 2] + end + + it "returns all elements when the argument is less than the Array size" do + [1, 2].take(4).should == [1, 2] + end + + it "returns an empty Array when passed zero" do + [1].take(0).should == [] + end + + it "returns an empty Array when called on an empty Array" do + [].take(3).should == [] + end + + it "raises an ArgumentError when the argument is negative" do + ->{ [1].take(-3) }.should raise_error(ArgumentError) + end + + it 'returns a Array instance for Array subclasses' do + ArraySpecs::MyArray[1, 2, 3, 4, 5].take(1).should be_an_instance_of(Array) + end +end diff --git a/spec/ruby/core/array/take_while_spec.rb b/spec/ruby/core/array/take_while_spec.rb new file mode 100644 index 0000000000..8f50260b42 --- /dev/null +++ b/spec/ruby/core/array/take_while_spec.rb @@ -0,0 +1,26 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/iterable_and_tolerating_size_increasing' + +describe "Array#take_while" do + it "returns all elements until the block returns false" do + [1, 2, 3].take_while{ |element| element < 3 }.should == [1, 2] + end + + it "returns all elements until the block returns nil" do + [1, 2, nil, 4].take_while{ |element| element }.should == [1, 2] + end + + it "returns all elements until the block returns false" do + [1, 2, false, 4].take_while{ |element| element }.should == [1, 2] + end + + it 'returns a Array instance for Array subclasses' do + ArraySpecs::MyArray[1, 2, 3, 4, 5].take_while { |n| n < 4 }.should be_an_instance_of(Array) + end +end + +describe "Array#take_while" do + @value_to_return = -> _ { true } + it_behaves_like :array_iterable_and_tolerating_size_increasing, :take_while +end diff --git a/spec/ruby/core/array/to_a_spec.rb b/spec/ruby/core/array/to_a_spec.rb new file mode 100644 index 0000000000..49d0a4782e --- /dev/null +++ b/spec/ruby/core/array/to_a_spec.rb @@ -0,0 +1,24 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Array#to_a" do + it "returns self" do + a = [1, 2, 3] + a.to_a.should == [1, 2, 3] + a.should equal(a.to_a) + end + + it "does not return subclass instance on Array subclasses" do + e = ArraySpecs::MyArray.new(1, 2) + e.to_a.should be_an_instance_of(Array) + e.to_a.should == [1, 2] + end + + it "properly handles recursive arrays" do + empty = ArraySpecs.empty_recursive_array + empty.to_a.should == empty + + array = ArraySpecs.recursive_array + array.to_a.should == array + end +end diff --git a/spec/ruby/core/array/to_ary_spec.rb b/spec/ruby/core/array/to_ary_spec.rb new file mode 100644 index 0000000000..314699b709 --- /dev/null +++ b/spec/ruby/core/array/to_ary_spec.rb @@ -0,0 +1,20 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Array#to_ary" do + it "returns self" do + a = [1, 2, 3] + a.should equal(a.to_ary) + a = ArraySpecs::MyArray[1, 2, 3] + a.should equal(a.to_ary) + end + + it "properly handles recursive arrays" do + empty = ArraySpecs.empty_recursive_array + empty.to_ary.should == empty + + array = ArraySpecs.recursive_array + array.to_ary.should == array + end + +end diff --git a/spec/ruby/core/array/to_h_spec.rb b/spec/ruby/core/array/to_h_spec.rb new file mode 100644 index 0000000000..1c814f3d01 --- /dev/null +++ b/spec/ruby/core/array/to_h_spec.rb @@ -0,0 +1,91 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/iterable_and_tolerating_size_increasing' + +describe "Array#to_h" do + it "converts empty array to empty hash" do + [].to_h.should == {} + end + + it "converts [key, value] pairs to a hash" do + hash = [[:a, 1], [:b, 2]].to_h + hash.should == { a: 1, b: 2 } + end + + it "uses the last value of a duplicated key" do + hash = [[:a, 1], [:b, 2], [:a, 3]].to_h + hash.should == { a: 3, b: 2 } + end + + it "calls #to_ary on contents" do + pair = mock('to_ary') + pair.should_receive(:to_ary).and_return([:b, 2]) + hash = [[:a, 1], pair].to_h + hash.should == { a: 1, b: 2 } + end + + it "raises TypeError if an element is not an array" do + -> { [:x].to_h }.should raise_error(TypeError) + end + + it "raises ArgumentError if an element is not a [key, value] pair" do + -> { [[:x]].to_h }.should raise_error(ArgumentError) + end + + it "does not accept arguments" do + -> { [].to_h(:a, :b) }.should raise_error(ArgumentError) + end + + it "produces a hash that returns nil for a missing element" do + [[:a, 1], [:b, 2]].to_h[:c].should be_nil + end + + context "with block" do + it "converts [key, value] pairs returned by the block to a Hash" do + [:a, :b].to_h { |k| [k, k.to_s] }.should == { a: 'a', b: 'b' } + end + + it "passes to a block each element as a single argument" do + ScratchPad.record [] + [[:a, 1], [:b, 2]].to_h { |*args| ScratchPad << args; [args[0], args[1]] } + ScratchPad.recorded.sort.should == [[[:a, 1]], [[:b, 2]]] + end + + it "raises ArgumentError if block returns longer or shorter array" do + -> do + [:a, :b].to_h { |k| [k, k.to_s, 1] } + end.should raise_error(ArgumentError, /wrong array length at 0/) + + -> do + [:a, :b].to_h { |k| [k] } + end.should raise_error(ArgumentError, /wrong array length at 0/) + end + + it "raises TypeError if block returns something other than Array" do + -> do + [:a, :b].to_h { |k| "not-array" } + end.should raise_error(TypeError, /wrong element type String at 0/) + end + + it "coerces returned pair to Array with #to_ary" do + x = mock('x') + x.stub!(:to_ary).and_return([:b, 'b']) + + [:a].to_h { |k| x }.should == { :b => 'b' } + end + + it "does not coerce returned pair to Array with #to_a" do + x = mock('x') + x.stub!(:to_a).and_return([:b, 'b']) + + -> do + [:a].to_h { |k| x } + end.should raise_error(TypeError, /wrong element type MockObject at 0/) + end + end +end + +describe "Array#to_h" do + @value_to_return = -> e { [e, e.to_s] } + it_behaves_like :array_iterable_and_tolerating_size_increasing, :to_h +end diff --git a/spec/ruby/core/array/to_s_spec.rb b/spec/ruby/core/array/to_s_spec.rb new file mode 100644 index 0000000000..e8476702ec --- /dev/null +++ b/spec/ruby/core/array/to_s_spec.rb @@ -0,0 +1,8 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/join' +require_relative 'shared/inspect' + +describe "Array#to_s" do + it_behaves_like :array_inspect, :to_s +end diff --git a/spec/ruby/core/array/transpose_spec.rb b/spec/ruby/core/array/transpose_spec.rb new file mode 100644 index 0000000000..b39077f4c9 --- /dev/null +++ b/spec/ruby/core/array/transpose_spec.rb @@ -0,0 +1,53 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Array#transpose" do + it "assumes an array of arrays and returns the result of transposing rows and columns" do + [[1, 'a'], [2, 'b'], [3, 'c']].transpose.should == [[1, 2, 3], ["a", "b", "c"]] + [[1, 2, 3], ["a", "b", "c"]].transpose.should == [[1, 'a'], [2, 'b'], [3, 'c']] + [].transpose.should == [] + [[]].transpose.should == [] + [[], []].transpose.should == [] + [[0]].transpose.should == [[0]] + [[0], [1]].transpose.should == [[0, 1]] + end + + it "tries to convert the passed argument to an Array using #to_ary" do + obj = mock('[1,2]') + obj.should_receive(:to_ary).and_return([1, 2]) + [obj, [:a, :b]].transpose.should == [[1, :a], [2, :b]] + end + + it "properly handles recursive arrays" do + empty = ArraySpecs.empty_recursive_array + empty.transpose.should == empty + + a = []; a << a + b = []; b << b + [a, b].transpose.should == [[a, b]] + + a = [1]; a << a + b = [2]; b << b + [a, b].transpose.should == [ [1, 2], [a, b] ] + end + + it "raises a TypeError if the passed Argument does not respond to #to_ary" do + -> { [Object.new, [:a, :b]].transpose }.should raise_error(TypeError) + end + + it "does not call to_ary on array subclass elements" do + ary = [ArraySpecs::ToAryArray[1, 2], ArraySpecs::ToAryArray[4, 6]] + ary.transpose.should == [[1, 4], [2, 6]] + end + + it "raises an IndexError if the arrays are not of the same length" do + -> { [[1, 2], [:a]].transpose }.should raise_error(IndexError) + end + + it "does not return subclass instance on Array subclasses" do + result = ArraySpecs::MyArray[ArraySpecs::MyArray[1, 2, 3], ArraySpecs::MyArray[4, 5, 6]].transpose + result.should be_an_instance_of(Array) + result[0].should be_an_instance_of(Array) + result[1].should be_an_instance_of(Array) + end +end diff --git a/spec/ruby/core/array/try_convert_spec.rb b/spec/ruby/core/array/try_convert_spec.rb new file mode 100644 index 0000000000..bea8815006 --- /dev/null +++ b/spec/ruby/core/array/try_convert_spec.rb @@ -0,0 +1,50 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Array.try_convert" do + it "returns the argument if it's an Array" do + x = Array.new + Array.try_convert(x).should equal(x) + end + + it "returns the argument if it's a kind of Array" do + x = ArraySpecs::MyArray[] + Array.try_convert(x).should equal(x) + end + + it "returns nil when the argument does not respond to #to_ary" do + Array.try_convert(Object.new).should be_nil + end + + it "sends #to_ary to the argument and returns the result if it's nil" do + obj = mock("to_ary") + obj.should_receive(:to_ary).and_return(nil) + Array.try_convert(obj).should be_nil + end + + it "sends #to_ary to the argument and returns the result if it's an Array" do + x = Array.new + obj = mock("to_ary") + obj.should_receive(:to_ary).and_return(x) + Array.try_convert(obj).should equal(x) + end + + it "sends #to_ary to the argument and returns the result if it's a kind of Array" do + x = ArraySpecs::MyArray[] + obj = mock("to_ary") + obj.should_receive(:to_ary).and_return(x) + Array.try_convert(obj).should equal(x) + end + + it "sends #to_ary to the argument and raises TypeError if it's not a kind of Array" do + obj = mock("to_ary") + obj.should_receive(:to_ary).and_return(Object.new) + -> { Array.try_convert obj }.should raise_error(TypeError, "can't convert MockObject to Array (MockObject#to_ary gives Object)") + end + + it "does not rescue exceptions raised by #to_ary" do + obj = mock("to_ary") + obj.should_receive(:to_ary).and_raise(RuntimeError) + -> { Array.try_convert obj }.should raise_error(RuntimeError) + end +end diff --git a/spec/ruby/core/array/union_spec.rb b/spec/ruby/core/array/union_spec.rb new file mode 100644 index 0000000000..ba2cc0d6b7 --- /dev/null +++ b/spec/ruby/core/array/union_spec.rb @@ -0,0 +1,25 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/union' + +describe "Array#|" do + it_behaves_like :array_binary_union, :| +end + +describe "Array#union" do + it_behaves_like :array_binary_union, :union + + it "returns unique elements when given no argument" do + x = [1, 2, 3, 2] + x.union.should == [1, 2, 3] + end + + it "does not return subclass instances for Array subclasses" do + ArraySpecs::MyArray[1, 2, 3].union.should be_an_instance_of(Array) + end + + it "accepts multiple arguments" do + x = [1, 2, 3] + x.union(x, x, x, x, [3, 4], x).should == [1, 2, 3, 4] + end +end diff --git a/spec/ruby/core/array/uniq_spec.rb b/spec/ruby/core/array/uniq_spec.rb new file mode 100644 index 0000000000..d5d826db15 --- /dev/null +++ b/spec/ruby/core/array/uniq_spec.rb @@ -0,0 +1,243 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/iterable_and_tolerating_size_increasing' + +describe "Array#uniq" do + it "returns an array with no duplicates" do + ["a", "a", "b", "b", "c"].uniq.should == ["a", "b", "c"] + end + + it "properly handles recursive arrays" do + empty = ArraySpecs.empty_recursive_array + empty.uniq.should == [empty] + + array = ArraySpecs.recursive_array + array.uniq.should == [1, 'two', 3.0, array] + end + + it "uses eql? semantics" do + [1.0, 1].uniq.should == [1.0, 1] + end + + it "compares elements first with hash" do + x = mock('0') + x.should_receive(:hash).at_least(1).and_return(0) + y = mock('0') + y.should_receive(:hash).at_least(1).and_return(0) + + [x, y].uniq.should == [x, y] + end + + it "does not compare elements with different hash codes via eql?" do + x = mock('0') + x.should_not_receive(:eql?) + y = mock('1') + y.should_not_receive(:eql?) + + x.should_receive(:hash).at_least(1).and_return(0) + y.should_receive(:hash).at_least(1).and_return(1) + + [x, y].uniq.should == [x, y] + end + + it "compares elements with matching hash codes with #eql?" do + a = Array.new(2) do + obj = mock('0') + obj.should_receive(:hash).at_least(1).and_return(0) + + def obj.eql?(o) + false + end + + obj + end + + a.uniq.should == a + + a = Array.new(2) do + obj = mock('0') + obj.should_receive(:hash).at_least(1).and_return(0) + + def obj.eql?(o) + true + end + + obj + end + + a.uniq.size.should == 1 + end + + it "compares elements based on the value returned from the block" do + a = [1, 2, 3, 4] + a.uniq { |x| x >= 2 ? 1 : 0 }.should == [1, 2] + end + + it "yields items in order" do + a = [1, 2, 3] + yielded = [] + a.uniq { |v| yielded << v } + yielded.should == a + end + + it "handles nil and false like any other values" do + [nil, false, 42].uniq { :foo }.should == [nil] + [false, nil, 42].uniq { :bar }.should == [false] + end + + it "returns Array instance on Array subclasses" do + ArraySpecs::MyArray[1, 2, 3].uniq.should be_an_instance_of(Array) + end + + it "properly handles an identical item even when its #eql? isn't reflexive" do + x = mock('x') + x.should_receive(:hash).at_least(1).and_return(42) + x.stub!(:eql?).and_return(false) # Stubbed for clarity and latitude in implementation; not actually sent by MRI. + + [x, x].uniq.should == [x] + end + + describe "given an array of BasicObject subclasses that define ==, eql?, and hash" do + # jruby/jruby#3227 + it "filters equivalent elements using those definitions" do + + basic = Class.new(BasicObject) do + attr_reader :x + + def initialize(x) + @x = x + end + + def ==(rhs) + @x == rhs.x + end + alias_method :eql?, :== + + def hash + @x.hash + end + end + + a = [basic.new(3), basic.new(2), basic.new(1), basic.new(4), basic.new(1), basic.new(2), basic.new(3)] + a.uniq.should == [basic.new(3), basic.new(2), basic.new(1), basic.new(4)] + end + end +end + +describe "Array#uniq" do + @value_to_return = -> e { e } + it_behaves_like :array_iterable_and_tolerating_size_increasing, :uniq +end + +describe "Array#uniq!" do + it "modifies the array in place" do + a = [ "a", "a", "b", "b", "c" ] + a.uniq! + a.should == ["a", "b", "c"] + end + + it "returns self" do + a = [ "a", "a", "b", "b", "c" ] + a.should equal(a.uniq!) + end + + it "properly handles recursive arrays" do + empty = ArraySpecs.empty_recursive_array + empty_dup = empty.dup + empty.uniq! + empty.should == empty_dup + + array = ArraySpecs.recursive_array + expected = array[0..3] + array.uniq! + array.should == expected + end + + it "compares elements first with hash" do + x = mock('0') + x.should_receive(:hash).at_least(1).and_return(0) + y = mock('0') + y.should_receive(:hash).at_least(1).and_return(0) + + a = [x, y] + a.uniq! + a.should == [x, y] + end + + it "does not compare elements with different hash codes via eql?" do + x = mock('0') + x.should_not_receive(:eql?) + y = mock('1') + y.should_not_receive(:eql?) + + x.should_receive(:hash).at_least(1).and_return(0) + y.should_receive(:hash).at_least(1).and_return(1) + + a = [x, y] + a.uniq! + a.should == [x, y] + end + + it "returns nil if no changes are made to the array" do + [ "a", "b", "c" ].uniq!.should == nil + end + + it "raises a FrozenError on a frozen array when the array is modified" do + dup_ary = [1, 1, 2] + dup_ary.freeze + -> { dup_ary.uniq! }.should raise_error(FrozenError) + end + + # see [ruby-core:23666] + it "raises a FrozenError on a frozen array when the array would not be modified" do + -> { ArraySpecs.frozen_array.uniq!}.should raise_error(FrozenError) + -> { ArraySpecs.empty_frozen_array.uniq!}.should raise_error(FrozenError) + end + + it "doesn't yield to the block on a frozen array" do + -> { ArraySpecs.frozen_array.uniq!{ raise RangeError, "shouldn't yield"}}.should raise_error(FrozenError) + end + + it "compares elements based on the value returned from the block" do + a = [1, 2, 3, 4] + a.uniq! { |x| x >= 2 ? 1 : 0 }.should == [1, 2] + end + + it "properly handles an identical item even when its #eql? isn't reflexive" do + x = mock('x') + x.should_receive(:hash).at_least(1).and_return(42) + x.stub!(:eql?).and_return(false) # Stubbed for clarity and latitude in implementation; not actually sent by MRI. + + a = [x, x] + a.uniq! + a.should == [x] + end + + it "does not truncate the array is the block raises an exception" do + a = [1, 2, 3] + begin + a.send(@method) { raise StandardError, 'Oops' } + rescue + end + + a.should == [1, 2, 3] + end + + it "doesn't change array if error is raised" do + a = [1, 1, 2, 2, 3, 3, 4, 4] + begin + a.send(@method) do |e| + raise StandardError, 'Oops' if e == 3 + e + end + rescue StandardError + end + + a.should == [1, 1, 2, 2, 3, 3, 4, 4] + end +end + +describe "Array#uniq!" do + @value_to_return = -> e { e } + it_behaves_like :array_iterable_and_tolerating_size_increasing, :uniq! +end diff --git a/spec/ruby/core/array/unshift_spec.rb b/spec/ruby/core/array/unshift_spec.rb new file mode 100644 index 0000000000..b8b675e5f8 --- /dev/null +++ b/spec/ruby/core/array/unshift_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/unshift' + +describe "Array#unshift" do + it_behaves_like :array_unshift, :unshift +end diff --git a/spec/ruby/core/array/values_at_spec.rb b/spec/ruby/core/array/values_at_spec.rb new file mode 100644 index 0000000000..e85bbee400 --- /dev/null +++ b/spec/ruby/core/array/values_at_spec.rb @@ -0,0 +1,74 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +# Should be synchronized with core/struct/values_at_spec.rb +describe "Array#values_at" do + it "returns an array of elements at the indexes when passed indexes" do + [1, 2, 3, 4, 5].values_at().should == [] + [1, 2, 3, 4, 5].values_at(1, 0, 5, -1, -8, 10).should == [2, 1, nil, 5, nil, nil] + end + + it "calls to_int on its indices" do + obj = mock('1') + def obj.to_int() 1 end + [1, 2].values_at(obj, obj, obj).should == [2, 2, 2] + end + + it "properly handles recursive arrays" do + empty = ArraySpecs.empty_recursive_array + empty.values_at(0, 1, 2).should == [empty, nil, nil] + + array = ArraySpecs.recursive_array + array.values_at(0, 1, 2, 3).should == [1, 'two', 3.0, array] + end + + describe "when passed ranges" do + it "returns an array of elements in the ranges" do + [1, 2, 3, 4, 5].values_at(0..2, 1...3, 2..-2).should == [1, 2, 3, 2, 3, 3, 4] + [1, 2, 3, 4, 5].values_at(6..4).should == [] + end + + it "calls to_int on arguments of ranges" do + from = mock('from') + to = mock('to') + + # So we can construct a range out of them... + def from.<=>(o) 0 end + def to.<=>(o) 0 end + + def from.to_int() 1 end + def to.to_int() -2 end + + ary = [1, 2, 3, 4, 5] + ary.values_at(from .. to, from ... to, to .. from).should == [2, 3, 4, 2, 3] + end + end + + describe "when passed a range" do + it "fills with nil if the index is out of the range" do + [0, 1].values_at(0..3).should == [0, 1, nil, nil] + [0, 1].values_at(2..4).should == [nil, nil, nil] + end + + describe "on an empty array" do + it "fills with nils if the index is out of the range" do + [].values_at(0..2).should == [nil, nil, nil] + [].values_at(1..3).should == [nil, nil, nil] + end + end + end + + it "does not return subclass instance on Array subclasses" do + ArraySpecs::MyArray[1, 2, 3].values_at(0, 1..2, 1).should be_an_instance_of(Array) + end + + it "works when given endless ranges" do + [1, 2, 3, 4].values_at(eval("(1..)")).should == [2, 3, 4] + [1, 2, 3, 4].values_at(eval("(3...)")).should == [4] + end + + it "works when given beginless ranges" do + [1, 2, 3, 4].values_at((..2)).should == [1, 2, 3] + [1, 2, 3, 4].values_at((...2)).should == [1, 2] + end +end diff --git a/spec/ruby/core/array/zip_spec.rb b/spec/ruby/core/array/zip_spec.rb new file mode 100644 index 0000000000..2a0f64cb49 --- /dev/null +++ b/spec/ruby/core/array/zip_spec.rb @@ -0,0 +1,71 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Array#zip" do + it "returns an array of arrays containing corresponding elements of each array" do + [1, 2, 3, 4].zip(["a", "b", "c", "d", "e"]).should == + [[1, "a"], [2, "b"], [3, "c"], [4, "d"]] + end + + it "fills in missing values with nil" do + [1, 2, 3, 4, 5].zip(["a", "b", "c", "d"]).should == + [[1, "a"], [2, "b"], [3, "c"], [4, "d"], [5, nil]] + end + + it "properly handles recursive arrays" do + a = []; a << a + b = [1]; b << b + + a.zip(a).should == [ [a[0], a[0]] ] + a.zip(b).should == [ [a[0], b[0]] ] + b.zip(a).should == [ [b[0], a[0]], [b[1], a[1]] ] + b.zip(b).should == [ [b[0], b[0]], [b[1], b[1]] ] + end + + it "calls #to_ary to convert the argument to an Array" do + obj = mock('[3,4]') + obj.should_receive(:to_ary).and_return([3, 4]) + [1, 2].zip(obj).should == [[1, 3], [2, 4]] + end + + it "uses #each to extract arguments' elements when #to_ary fails" do + obj = Class.new do + def each(&b) + [3,4].each(&b) + end + end.new + + [1, 2].zip(obj).should == [[1, 3], [2, 4]] + end + + it "stops at own size when given an infinite enumerator" do + [1, 2].zip(10.upto(Float::INFINITY)).should == [[1, 10], [2, 11]] + end + + it "fills nil when the given enumerator is shorter than self" do + obj = Object.new + def obj.each + yield 10 + end + [1, 2].zip(obj).should == [[1, 10], [2, nil]] + end + + it "calls block if supplied" do + values = [] + [1, 2, 3, 4].zip(["a", "b", "c", "d", "e"]) { |value| + values << value + }.should == nil + + values.should == [[1, "a"], [2, "b"], [3, "c"], [4, "d"]] + end + + it "does not return subclass instance on Array subclasses" do + ArraySpecs::MyArray[1, 2, 3].zip(["a", "b"]).should be_an_instance_of(Array) + end + + it "raises TypeError when some argument isn't Array and doesn't respond to #to_ary and #to_enum" do + -> { [1, 2, 3].zip(Object.new) }.should raise_error(TypeError, "wrong argument type Object (must respond to :each)") + -> { [1, 2, 3].zip(1) }.should raise_error(TypeError, "wrong argument type Integer (must respond to :each)") + -> { [1, 2, 3].zip(true) }.should raise_error(TypeError, "wrong argument type TrueClass (must respond to :each)") + end +end diff --git a/spec/ruby/core/basicobject/__id__spec.rb b/spec/ruby/core/basicobject/__id__spec.rb new file mode 100644 index 0000000000..6766db4e82 --- /dev/null +++ b/spec/ruby/core/basicobject/__id__spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/kernel/object_id' + +describe "BasicObject#__id__" do + it_behaves_like :object_id, :__id__, BasicObject +end diff --git a/spec/ruby/core/basicobject/__send___spec.rb b/spec/ruby/core/basicobject/__send___spec.rb new file mode 100644 index 0000000000..005b1d0d90 --- /dev/null +++ b/spec/ruby/core/basicobject/__send___spec.rb @@ -0,0 +1,10 @@ +require_relative '../../spec_helper' +require_relative '../../shared/basicobject/send' + +describe "BasicObject#__send__" do + it "is a public instance method" do + BasicObject.should have_public_instance_method(:__send__) + end + + it_behaves_like :basicobject_send, :__send__ +end diff --git a/spec/ruby/core/basicobject/basicobject_spec.rb b/spec/ruby/core/basicobject/basicobject_spec.rb new file mode 100644 index 0000000000..27a322e72c --- /dev/null +++ b/spec/ruby/core/basicobject/basicobject_spec.rb @@ -0,0 +1,91 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' + +describe "BasicObject" do + it "raises NoMethodError for nonexistent methods after #method_missing is removed" do + script = fixture __FILE__, "remove_method_missing.rb" + ruby_exe(script).chomp.should == "NoMethodError" + end + + it "raises NameError when referencing built-in constants" do + -> { class BasicObjectSpecs::BOSubclass; Kernel; end }.should raise_error(NameError) + end + + it "does not define built-in constants (according to const_defined?)" do + BasicObject.const_defined?(:Kernel).should be_false + end + + it "does not define built-in constants (according to defined?)" do + BasicObjectSpecs::BOSubclass.kernel_defined?.should be_nil + end + + it "is included in Object's list of constants" do + Object.constants(false).should include(:BasicObject) + end + + it "includes itself in its list of constants" do + BasicObject.constants(false).should include(:BasicObject) + end +end + +describe "BasicObject metaclass" do + before :each do + @meta = class << BasicObject; self; end + end + + it "is an instance of Class" do + @meta.should be_an_instance_of(Class) + end + + it "has Class as superclass" do + @meta.superclass.should equal(Class) + end + + it "contains methods for the BasicObject class" do + @meta.class_eval do + def rubyspec_test_method() :test end + end + + BasicObject.rubyspec_test_method.should == :test + end +end + +describe "BasicObject instance metaclass" do + before :each do + @object = BasicObject.new + @meta = class << @object; self; end + end + + it "is an instance of Class" do + @meta.should be_an_instance_of(Class) + end + + it "has BasicObject as superclass" do + @meta.superclass.should equal(BasicObject) + end + + it "contains methods defined for the BasicObject instance" do + @meta.class_eval do + def test_method() :test end + end + + @object.test_method.should == :test + end +end + +describe "BasicObject subclass" do + it "contains Kernel methods when including Kernel" do + obj = BasicObjectSpecs::BOSubclass.new + + obj.instance_variable_set(:@test, :value) + obj.instance_variable_get(:@test).should == :value + + obj.respond_to?(:hash).should == true + end + + describe "BasicObject references" do + it "can refer to BasicObject from within itself" do + -> { BasicObject::BasicObject }.should_not raise_error + end + end +end diff --git a/spec/ruby/core/basicobject/equal_spec.rb b/spec/ruby/core/basicobject/equal_spec.rb new file mode 100644 index 0000000000..ce28eaed95 --- /dev/null +++ b/spec/ruby/core/basicobject/equal_spec.rb @@ -0,0 +1,54 @@ +require_relative '../../spec_helper' +require_relative '../../shared/kernel/equal' + +describe "BasicObject#equal?" do + it "is a public instance method" do + BasicObject.should have_public_instance_method(:equal?) + end + + it_behaves_like :object_equal, :equal? + + it "is unaffected by overriding __id__" do + o1 = mock("object") + o2 = mock("object") + suppress_warning { + def o1.__id__; 10; end + def o2.__id__; 10; end + } + o1.equal?(o2).should be_false + end + + it "is unaffected by overriding object_id" do + o1 = mock("object") + o1.stub!(:object_id).and_return(10) + o2 = mock("object") + o2.stub!(:object_id).and_return(10) + o1.equal?(o2).should be_false + end + + it "is unaffected by overriding ==" do + # different objects, overriding == to return true + o1 = mock("object") + o1.stub!(:==).and_return(true) + o2 = mock("object") + o1.equal?(o2).should be_false + + # same objects, overriding == to return false + o3 = mock("object") + o3.stub!(:==).and_return(false) + o3.equal?(o3).should be_true + end + + it "is unaffected by overriding eql?" do + # different objects, overriding eql? to return true + o1 = mock("object") + o1.stub!(:eql?).and_return(true) + o2 = mock("object") + o1.equal?(o2).should be_false + + # same objects, overriding eql? to return false + o3 = mock("object") + o3.stub!(:eql?).and_return(false) + o3.equal?(o3).should be_true + end +end diff --git a/spec/ruby/core/basicobject/equal_value_spec.rb b/spec/ruby/core/basicobject/equal_value_spec.rb new file mode 100644 index 0000000000..6c825513c1 --- /dev/null +++ b/spec/ruby/core/basicobject/equal_value_spec.rb @@ -0,0 +1,10 @@ +require_relative '../../spec_helper' +require_relative '../../shared/kernel/equal' + +describe "BasicObject#==" do + it "is a public instance method" do + BasicObject.should have_public_instance_method(:==) + end + + it_behaves_like :object_equal, :== +end diff --git a/spec/ruby/core/basicobject/fixtures/classes.rb b/spec/ruby/core/basicobject/fixtures/classes.rb new file mode 100644 index 0000000000..ed5a2dda17 --- /dev/null +++ b/spec/ruby/core/basicobject/fixtures/classes.rb @@ -0,0 +1,255 @@ +module BasicObjectSpecs + class IVars + def initialize + @secret = 99 + end + end + + module InstExec + def self.included(base) + base.instance_exec { @@count = 2 } + end + end + + module InstExecIncluded + include InstExec + end + + module InstEval + module CVar + module Get + class ReceiverScope + @@cvar = :value_defined_in_receiver_scope + end + + class BlockDefinitionScope + @@cvar = :value_defined_in_block_definition_scope + + def block + -> * { @@cvar } + end + end + + class CallerScope + @@cvar = :value_defined_in_caller_scope + + def get_class_variable_with_string(obj) + obj.instance_eval("@@cvar") + end + + def get_class_variable_with_block(obj, block) + obj.instance_eval(&block) + end + end + + class CallerWithoutCVarScope + def get_class_variable_with_string(obj) + obj.instance_eval("@@cvar") + end + end + + ReceiverWithCVarDefinedInSingletonClass = Class.new.new.tap do |obj| + obj.singleton_class.class_variable_set(:@@cvar, :value_defined_in_receiver_singleton_class) + end + end + + module Set + class ReceiverScope + end + + class BlockDefinitionScope + def self.get_class_variable + @@cvar + end + + def block_to_assign(value) + -> * { @@cvar = value } + end + end + + class CallerScope + def self.get_class_variable + @@cvar + end + + def set_class_variable_with_string(obj, value) + obj.instance_eval("@@cvar=#{value.inspect}") + end + + def set_class_variable_with_block(obj, block) + obj.instance_eval(&block) + end + end + end + end + end + + module InstEval + module Constants + module ConstantInReceiverSingletonClass + module ReceiverScope + FOO = :ReceiverScope + + class ReceiverParent + FOO = :ReceiverParent + end + + class Receiver < ReceiverParent + FOO = :Receiver + + def initialize + self.singleton_class.const_set(:FOO, :singleton_class) + end + end + end + + module CallerScope + FOO = :CallerScope + + class CallerParent + FOO = :CallerParent + end + + class Caller < CallerParent + FOO = :Caller + + def get_constant_with_string(receiver) + receiver.instance_eval("FOO") + end + end + end + end + + module ConstantInReceiverClass + module ReceiverScope + FOO = :ReceiverScope + + class ReceiverParent + FOO = :ReceiverParent + end + + class Receiver < ReceiverParent + FOO = :Receiver + end + end + + module CallerScope + FOO = :CallerScope + + class CallerParent + FOO = :CallerParent + end + + class Caller < CallerParent + FOO = :Caller + + def get_constant_with_string(receiver) + receiver.instance_eval("FOO") + end + end + end + end + + module ConstantInCallerClass + module ReceiverScope + FOO = :ReceiverScope + + class ReceiverParent + FOO = :ReceiverParent + end + + class Receiver < ReceiverParent + # FOO is not declared in a receiver class + end + end + + module CallerScope + FOO = :CallerScope + + class CallerParent + FOO = :CallerParent + end + + class Caller < CallerParent + FOO = :Caller + + def get_constant_with_string(receiver) + receiver.instance_eval("FOO") + end + end + end + end + + module ConstantInCallerOuterScopes + module ReceiverScope + FOO = :ReceiverScope + + class ReceiverParent + FOO = :ReceiverParent + end + + class Receiver < ReceiverParent + # FOO is not declared in a receiver class + end + end + + module CallerScope + FOO = :CallerScope + + class CallerParent + FOO = :CallerParent + end + + class Caller < CallerParent + # FOO is not declared in a caller class + + def get_constant_with_string(receiver) + receiver.instance_eval("FOO") + end + end + end + end + + module ConstantInReceiverParentClass + module ReceiverScope + FOO = :ReceiverScope + + class ReceiverParent + FOO = :ReceiverParent + end + + class Receiver < ReceiverParent + # FOO is not declared in a receiver class + end + end + + module CallerScope + # FOO is not declared in a caller outer scopes + + class CallerParent + FOO = :CallerParent + end + + class Caller < CallerParent + # FOO is not declared in a caller class + + def get_constant_with_string(receiver) + receiver.instance_eval("FOO") + end + end + end + end + end + end + + class InstEvalConst + INST_EVAL_CONST_X = 2 + end + + module InstEvalOuter + module Inner + obj = InstEvalConst.new + X_BY_BLOCK = obj.instance_eval { INST_EVAL_CONST_X } rescue nil + end + end +end diff --git a/spec/ruby/core/basicobject/fixtures/common.rb b/spec/ruby/core/basicobject/fixtures/common.rb new file mode 100644 index 0000000000..3447a3a5e7 --- /dev/null +++ b/spec/ruby/core/basicobject/fixtures/common.rb @@ -0,0 +1,9 @@ +module BasicObjectSpecs + class BOSubclass < BasicObject + def self.kernel_defined? + defined?(Kernel) + end + + include ::Kernel + end +end diff --git a/spec/ruby/core/basicobject/fixtures/remove_method_missing.rb b/spec/ruby/core/basicobject/fixtures/remove_method_missing.rb new file mode 100644 index 0000000000..095b982d3a --- /dev/null +++ b/spec/ruby/core/basicobject/fixtures/remove_method_missing.rb @@ -0,0 +1,9 @@ +class BasicObject + remove_method :method_missing +end + +begin + Object.new.test_method +rescue NoMethodError => e + puts e.class.name +end diff --git a/spec/ruby/core/basicobject/fixtures/singleton_method.rb b/spec/ruby/core/basicobject/fixtures/singleton_method.rb new file mode 100644 index 0000000000..0e00e035fa --- /dev/null +++ b/spec/ruby/core/basicobject/fixtures/singleton_method.rb @@ -0,0 +1,10 @@ +module BasicObjectSpecs + class SingletonMethod + def self.singleton_method_added name + ScratchPad.record [:singleton_method_added, name] + end + + def self.singleton_method_to_alias + end + end +end diff --git a/spec/ruby/core/basicobject/initialize_spec.rb b/spec/ruby/core/basicobject/initialize_spec.rb new file mode 100644 index 0000000000..b7ce73ffd5 --- /dev/null +++ b/spec/ruby/core/basicobject/initialize_spec.rb @@ -0,0 +1,13 @@ +require_relative '../../spec_helper' + +describe "BasicObject#initialize" do + it "is a private instance method" do + BasicObject.should have_private_instance_method(:initialize) + end + + it "does not accept arguments" do + -> { + BasicObject.new("This", "makes it easier", "to call super", "from other constructors") + }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/basicobject/instance_eval_spec.rb b/spec/ruby/core/basicobject/instance_eval_spec.rb new file mode 100644 index 0000000000..633b5c2cb1 --- /dev/null +++ b/spec/ruby/core/basicobject/instance_eval_spec.rb @@ -0,0 +1,329 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "BasicObject#instance_eval" do + before :each do + ScratchPad.clear + end + + it "is a public instance method" do + BasicObject.should have_public_instance_method(:instance_eval) + end + + it "sets self to the receiver in the context of the passed block" do + a = BasicObject.new + a.instance_eval { self }.equal?(a).should be_true + end + + it "evaluates strings" do + a = BasicObject.new + a.instance_eval('self').equal?(a).should be_true + end + + it "raises an ArgumentError when no arguments and no block are given" do + -> { "hola".instance_eval }.should raise_error(ArgumentError, "wrong number of arguments (given 0, expected 1..3)") + end + + it "raises an ArgumentError when a block and normal arguments are given" do + -> { "hola".instance_eval(4, 5) {|a,b| a + b } }.should raise_error(ArgumentError, "wrong number of arguments (given 2, expected 0)") + end + + it "raises an ArgumentError when more than 3 arguments are given" do + -> { + "hola".instance_eval("1 + 1", "some file", 0, "bogus") + }.should raise_error(ArgumentError, "wrong number of arguments (given 4, expected 1..3)") + end + + it "yields the object to the block" do + "hola".instance_eval {|o| ScratchPad.record o } + ScratchPad.recorded.should == "hola" + end + + it "returns the result of the block" do + "hola".instance_eval { :result }.should == :result + end + + it "only binds the eval to the receiver" do + f = Object.new + f.instance_eval do + def foo + 1 + end + end + f.foo.should == 1 + -> { Object.new.foo }.should raise_error(NoMethodError) + end + + it "preserves self in the original block when passed a block argument" do + prc = proc { self } + + old_self = prc.call + + new_self = Object.new + new_self.instance_eval(&prc).should == new_self + + prc.call.should == old_self + end + + # TODO: This should probably be replaced with a "should behave like" that uses + # the many scoping/binding specs from kernel/eval_spec, since most of those + # behaviors are the same for instance_eval. See also module_eval/class_eval. + + it "binds self to the receiver" do + s = "hola" + (s == s.instance_eval { self }).should be_true + o = mock('o') + (o == o.instance_eval("self")).should be_true + end + + it "executes in the context of the receiver" do + "Ruby-fu".instance_eval { size }.should == 7 + "hola".instance_eval("size").should == 4 + Object.class_eval { "hola".instance_eval("to_s") }.should == "hola" + Object.class_eval { "Ruby-fu".instance_eval{ to_s } }.should == "Ruby-fu" + + end + + ruby_version_is "3.3" do + it "uses the caller location as default location" do + f = Object.new + f.instance_eval("[__FILE__, __LINE__]").should == ["(eval at #{__FILE__}:#{__LINE__})", 1] + end + end + + it "has access to receiver's instance variables" do + BasicObjectSpecs::IVars.new.instance_eval { @secret }.should == 99 + BasicObjectSpecs::IVars.new.instance_eval("@secret").should == 99 + end + + it "raises TypeError for frozen objects when tries to set receiver's instance variables" do + -> { nil.instance_eval { @foo = 42 } }.should raise_error(FrozenError, "can't modify frozen NilClass: nil") + -> { true.instance_eval { @foo = 42 } }.should raise_error(FrozenError, "can't modify frozen TrueClass: true") + -> { false.instance_eval { @foo = 42 } }.should raise_error(FrozenError, "can't modify frozen FalseClass: false") + -> { 1.instance_eval { @foo = 42 } }.should raise_error(FrozenError, "can't modify frozen Integer: 1") + -> { :symbol.instance_eval { @foo = 42 } }.should raise_error(FrozenError, "can't modify frozen Symbol: :symbol") + + obj = Object.new + obj.freeze + -> { obj.instance_eval { @foo = 42 } }.should raise_error(FrozenError) + end + + it "treats block-local variables as local to the block" do + prc = instance_eval <<-CODE + proc do |x, prc| + if x + n = 2 + else + n = 1 + prc.call(true, prc) + n + end + end + CODE + + prc.call(false, prc).should == 1 + end + + it "makes the receiver metaclass the scoped class when used with a string" do + obj = Object.new + obj.instance_eval %{ + class B; end + B + } + obj.singleton_class.const_get(:B).should be_an_instance_of(Class) + end + + describe "constants lookup when a String given" do + it "looks in the receiver singleton class first" do + receiver = BasicObjectSpecs::InstEval::Constants::ConstantInReceiverSingletonClass::ReceiverScope::Receiver.new + caller = BasicObjectSpecs::InstEval::Constants::ConstantInReceiverSingletonClass::CallerScope::Caller.new + + caller.get_constant_with_string(receiver).should == :singleton_class + end + + it "looks in the receiver class next" do + receiver = BasicObjectSpecs::InstEval::Constants::ConstantInReceiverClass::ReceiverScope::Receiver.new + caller = BasicObjectSpecs::InstEval::Constants::ConstantInReceiverClass::CallerScope::Caller.new + + caller.get_constant_with_string(receiver).should == :Receiver + end + + it "looks in the caller class next" do + receiver = BasicObjectSpecs::InstEval::Constants::ConstantInCallerClass::ReceiverScope::Receiver.new + caller = BasicObjectSpecs::InstEval::Constants::ConstantInCallerClass::CallerScope::Caller.new + + caller.get_constant_with_string(receiver).should == :Caller + end + + it "looks in the caller outer scopes next" do + receiver = BasicObjectSpecs::InstEval::Constants::ConstantInCallerOuterScopes::ReceiverScope::Receiver.new + caller = BasicObjectSpecs::InstEval::Constants::ConstantInCallerOuterScopes::CallerScope::Caller.new + + caller.get_constant_with_string(receiver).should == :CallerScope + end + + it "looks in the receiver class hierarchy next" do + receiver = BasicObjectSpecs::InstEval::Constants::ConstantInReceiverParentClass::ReceiverScope::Receiver.new + caller = BasicObjectSpecs::InstEval::Constants::ConstantInReceiverParentClass::CallerScope::Caller.new + + caller.get_constant_with_string(receiver).should == :ReceiverParent + end + end + + it "doesn't get constants in the receiver if a block given" do + BasicObjectSpecs::InstEvalOuter::Inner::X_BY_BLOCK.should be_nil + end + + it "raises a TypeError when defining methods on an immediate" do + -> do + 1.instance_eval { def foo; end } + end.should raise_error(TypeError) + -> do + :foo.instance_eval { def foo; end } + end.should raise_error(TypeError) + end + + describe "class variables lookup" do + it "gets class variables in the caller class when called with a String" do + receiver = BasicObjectSpecs::InstEval::CVar::Get::ReceiverScope.new + caller = BasicObjectSpecs::InstEval::CVar::Get::CallerScope.new + + caller.get_class_variable_with_string(receiver).should == :value_defined_in_caller_scope + end + + it "gets class variables in the block definition scope when called with a block" do + receiver = BasicObjectSpecs::InstEval::CVar::Get::ReceiverScope.new + caller = BasicObjectSpecs::InstEval::CVar::Get::CallerScope.new + block = BasicObjectSpecs::InstEval::CVar::Get::BlockDefinitionScope.new.block + + caller.get_class_variable_with_block(receiver, block).should == :value_defined_in_block_definition_scope + end + + it "sets class variables in the caller class when called with a String" do + receiver = BasicObjectSpecs::InstEval::CVar::Set::ReceiverScope.new + caller = BasicObjectSpecs::InstEval::CVar::Set::CallerScope.new + + caller.set_class_variable_with_string(receiver, 1) + BasicObjectSpecs::InstEval::CVar::Set::CallerScope.get_class_variable.should == 1 + end + + it "sets class variables in the block definition scope when called with a block" do + receiver = BasicObjectSpecs::InstEval::CVar::Set::ReceiverScope.new + caller = BasicObjectSpecs::InstEval::CVar::Set::CallerScope.new + block = BasicObjectSpecs::InstEval::CVar::Set::BlockDefinitionScope.new.block_to_assign(1) + + caller.set_class_variable_with_block(receiver, block) + BasicObjectSpecs::InstEval::CVar::Set::BlockDefinitionScope.get_class_variable.should == 1 + end + + it "does not have access to class variables in the receiver class when called with a String" do + receiver = BasicObjectSpecs::InstEval::CVar::Get::ReceiverScope.new + caller = BasicObjectSpecs::InstEval::CVar::Get::CallerWithoutCVarScope.new + -> { caller.get_class_variable_with_string(receiver) }.should raise_error(NameError, /uninitialized class variable @@cvar/) + end + + it "does not have access to class variables in the receiver's singleton class when called with a String" do + receiver = BasicObjectSpecs::InstEval::CVar::Get::ReceiverWithCVarDefinedInSingletonClass + caller = BasicObjectSpecs::InstEval::CVar::Get::CallerWithoutCVarScope.new + -> { caller.get_class_variable_with_string(receiver) }.should raise_error(NameError, /uninitialized class variable @@cvar/) + end + end + + it "raises a TypeError when defining methods on numerics" do + -> do + (1.0).instance_eval { def foo; end } + end.should raise_error(TypeError) + -> do + (1 << 64).instance_eval { def foo; end } + end.should raise_error(TypeError) + end + + it "evaluates procs originating from methods" do + def meth(arg); arg; end + + m = method(:meth) + obj = Object.new + + obj.instance_eval(&m).should == obj + end + + it "evaluates string with given filename and linenumber" do + err = begin + Object.new.instance_eval("raise", "a_file", 10) + rescue => e + e + end + err.backtrace.first.split(":")[0..1].should == ["a_file", "10"] + end + + it "evaluates string with given filename and negative linenumber" do + err = begin + Object.new.instance_eval("\n\nraise\n", "b_file", -100) + rescue => e + e + end + err.backtrace.first.split(":")[0..1].should == ["b_file", "-98"] + end + + it "has access to the caller's local variables" do + x = nil + + instance_eval "x = :value" + + x.should == :value + end + + it "converts string argument with #to_str method" do + source_code = Object.new + def source_code.to_str() "1" end + + a = BasicObject.new + a.instance_eval(source_code).should == 1 + end + + it "raises ArgumentError if returned value is not String" do + source_code = Object.new + def source_code.to_str() :symbol end + + a = BasicObject.new + -> { a.instance_eval(source_code) }.should raise_error(TypeError, /can't convert Object to String/) + end + + it "converts filename argument with #to_str method" do + filename = Object.new + def filename.to_str() "file.rb" end + + err = begin + Object.new.instance_eval("raise", filename) + rescue => e + e + end + err.backtrace.first.split(":")[0].should == "file.rb" + end + + it "raises ArgumentError if returned value is not String" do + filename = Object.new + def filename.to_str() :symbol end + + -> { Object.new.instance_eval("raise", filename) }.should raise_error(TypeError, /can't convert Object to String/) + end + + it "converts lineno argument with #to_int method" do + lineno = Object.new + def lineno.to_int() 15 end + + err = begin + Object.new.instance_eval("raise", "file.rb", lineno) + rescue => e + e + end + err.backtrace.first.split(":")[1].should == "15" + end + + it "raises ArgumentError if returned value is not Integer" do + lineno = Object.new + def lineno.to_int() :symbol end + + -> { Object.new.instance_eval("raise", "file.rb", lineno) }.should raise_error(TypeError, /can't convert Object to Integer/) + end +end diff --git a/spec/ruby/core/basicobject/instance_exec_spec.rb b/spec/ruby/core/basicobject/instance_exec_spec.rb new file mode 100644 index 0000000000..370f03d33c --- /dev/null +++ b/spec/ruby/core/basicobject/instance_exec_spec.rb @@ -0,0 +1,107 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "BasicObject#instance_exec" do + it "is a public instance method" do + BasicObject.should have_public_instance_method(:instance_exec) + end + + it "sets self to the receiver in the context of the passed block" do + a = BasicObject.new + a.instance_exec { self }.equal?(a).should be_true + end + + it "passes arguments to the block" do + a = BasicObject.new + a.instance_exec(1) { |b| b }.should equal(1) + end + + it "raises a LocalJumpError unless given a block" do + -> { "hola".instance_exec }.should raise_error(LocalJumpError) + end + + it "has an arity of -1" do + Object.new.method(:instance_exec).arity.should == -1 + end + + it "accepts arguments with a block" do + -> { "hola".instance_exec(4, 5) { |a,b| a + b } }.should_not raise_error + end + + it "doesn't pass self to the block as an argument" do + "hola".instance_exec { |o| o }.should be_nil + end + + it "passes any arguments to the block" do + Object.new.instance_exec(1,2) {|one, two| one + two}.should == 3 + end + + it "only binds the exec to the receiver" do + f = Object.new + f.instance_exec do + def foo + 1 + end + end + f.foo.should == 1 + -> { Object.new.foo }.should raise_error(NoMethodError) + end + + # TODO: This should probably be replaced with a "should behave like" that uses + # the many scoping/binding specs from kernel/eval_spec, since most of those + # behaviors are the same for instance_exec. See also module_eval/class_eval. + + it "binds self to the receiver" do + s = "hola" + (s == s.instance_exec { self }).should == true + end + + it "binds the block's binding self to the receiver" do + s = "hola" + (s == s.instance_exec { eval "self", binding }).should == true + end + + it "executes in the context of the receiver" do + "Ruby-fu".instance_exec { size }.should == 7 + Object.class_eval { "Ruby-fu".instance_exec{ to_s } }.should == "Ruby-fu" + end + + it "has access to receiver's instance variables" do + BasicObjectSpecs::IVars.new.instance_exec { @secret }.should == 99 + end + + it "sets class variables in the receiver" do + BasicObjectSpecs::InstExec.class_variables.should include(:@@count) + BasicObjectSpecs::InstExec.send(:class_variable_get, :@@count).should == 2 + end + + it "raises a TypeError when defining methods on an immediate" do + -> do + 1.instance_exec { def foo; end } + end.should raise_error(TypeError) + -> do + :foo.instance_exec { def foo; end } + end.should raise_error(TypeError) + end + + quarantine! do # Not clean, leaves cvars lying around to break other specs + it "scopes class var accesses in the caller when called on an Integer" do + # Integer can take instance vars + Integer.class_eval "@@__tmp_instance_exec_spec = 1" + (defined? @@__tmp_instance_exec_spec).should == nil + + @@__tmp_instance_exec_spec = 2 + 1.instance_exec { @@__tmp_instance_exec_spec }.should == 2 + Integer.__send__(:remove_class_variable, :@@__tmp_instance_exec_spec) + end + end + + it "raises a TypeError when defining methods on numerics" do + -> do + (1.0).instance_exec { def foo; end } + end.should raise_error(TypeError) + -> do + (1 << 64).instance_exec { def foo; end } + end.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/basicobject/method_missing_spec.rb b/spec/ruby/core/basicobject/method_missing_spec.rb new file mode 100644 index 0000000000..a29d4375bc --- /dev/null +++ b/spec/ruby/core/basicobject/method_missing_spec.rb @@ -0,0 +1,40 @@ +require_relative "../../spec_helper" +require_relative '../../shared/basicobject/method_missing' + +describe "BasicObject#method_missing" do + it "is a private method" do + BasicObject.should have_private_instance_method(:method_missing) + end +end + +describe "BasicObject#method_missing" do + it_behaves_like :method_missing_class, nil, BasicObject +end + +describe "BasicObject#method_missing" do + it_behaves_like :method_missing_instance, nil, BasicObject +end + +describe "BasicObject#method_missing" do + it_behaves_like :method_missing_defined_module, nil, KernelSpecs::ModuleMM +end + +describe "BasicObject#method_missing" do + it_behaves_like :method_missing_module, nil, KernelSpecs::ModuleNoMM +end + +describe "BasicObject#method_missing" do + it_behaves_like :method_missing_defined_class, nil, KernelSpecs::ClassMM +end + +describe "BasicObject#method_missing" do + it_behaves_like :method_missing_class, nil, KernelSpecs::ClassNoMM +end + +describe "BasicObject#method_missing" do + it_behaves_like :method_missing_defined_instance, nil, KernelSpecs::ClassMM +end + +describe "BasicObject#method_missing" do + it_behaves_like :method_missing_instance, nil, KernelSpecs::ClassNoMM +end diff --git a/spec/ruby/core/basicobject/not_equal_spec.rb b/spec/ruby/core/basicobject/not_equal_spec.rb new file mode 100644 index 0000000000..9329128c43 --- /dev/null +++ b/spec/ruby/core/basicobject/not_equal_spec.rb @@ -0,0 +1,53 @@ +require_relative '../../spec_helper' + +describe "BasicObject#!=" do + it "is a public instance method" do + BasicObject.should have_public_instance_method(:'!=') + end + + it "returns true if other is not identical to self" do + a = BasicObject.new + b = BasicObject.new + (a != b).should be_true + end + + it "returns true if other is an Object" do + a = BasicObject.new + b = Object.new + (a != b).should be_true + end + + it "returns false if other is identical to self" do + a = BasicObject.new + (a != a).should be_false + end + + it "dispatches to #==" do + a = mock("not_equal") + b = BasicObject.new + a.should_receive(:==).and_return(true) + + (a != b).should be_false + end + + describe "when invoked using Kernel#send" do + it "returns true if other is not identical to self" do + a = Object.new + b = Object.new + a.send(:!=, b).should be_true + end + + it "returns false if other is identical to self" do + a = Object.new + a.send(:!=, a).should be_false + end + + it "dispatches to #==" do + a = mock("not_equal") + b = Object.new + a.should_receive(:==).and_return(true) + + a.send(:!=, b).should be_false + end + end +end diff --git a/spec/ruby/core/basicobject/not_spec.rb b/spec/ruby/core/basicobject/not_spec.rb new file mode 100644 index 0000000000..ca4cb6f5ff --- /dev/null +++ b/spec/ruby/core/basicobject/not_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' + +describe "BasicObject#!" do + it "is a public instance method" do + BasicObject.should have_public_instance_method(:'!') + end + + it "returns false" do + (!BasicObject.new).should be_false + end +end diff --git a/spec/ruby/core/basicobject/singleton_method_added_spec.rb b/spec/ruby/core/basicobject/singleton_method_added_spec.rb new file mode 100644 index 0000000000..bd21458ea7 --- /dev/null +++ b/spec/ruby/core/basicobject/singleton_method_added_spec.rb @@ -0,0 +1,147 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/singleton_method' + +describe "BasicObject#singleton_method_added" do + before :each do + ScratchPad.clear + end + + it "is a private method" do + BasicObject.should have_private_instance_method(:singleton_method_added) + end + + it "is called when a singleton method is defined on an object" do + obj = BasicObject.new + + def obj.singleton_method_added(name) + ScratchPad.record [:singleton_method_added, name] + end + + def obj.new_singleton_method + end + + ScratchPad.recorded.should == [:singleton_method_added, :new_singleton_method] + end + + it "is not called for instance methods" do + ScratchPad.record [] + + Module.new do + def self.singleton_method_added(name) + ScratchPad << name + end + + def new_instance_method + end + end + + ScratchPad.recorded.should_not include(:new_instance_method) + end + + it "is called when a singleton method is defined on a module" do + class BasicObjectSpecs::SingletonMethod + def self.new_method_on_self + end + end + ScratchPad.recorded.should == [:singleton_method_added, :new_method_on_self] + end + + it "is called when a method is defined in the singleton class" do + class BasicObjectSpecs::SingletonMethod + class << self + def new_method_on_singleton + end + end + end + ScratchPad.recorded.should == [:singleton_method_added, :new_method_on_singleton] + end + + it "is called when a method is defined with alias_method in the singleton class" do + class BasicObjectSpecs::SingletonMethod + class << self + alias_method :new_method_on_singleton_with_alias_method, :singleton_method_to_alias + end + end + ScratchPad.recorded.should == [:singleton_method_added, :new_method_on_singleton_with_alias_method] + end + + it "is called when a method is defined with syntax alias in the singleton class" do + class BasicObjectSpecs::SingletonMethod + class << self + alias new_method_on_singleton_with_syntax_alias singleton_method_to_alias + end + end + ScratchPad.recorded.should == [:singleton_method_added, :new_method_on_singleton_with_syntax_alias] + end + + it "is called when define_method is used in the singleton class" do + class BasicObjectSpecs::SingletonMethod + class << self + define_method :new_method_with_define_method do + end + end + end + ScratchPad.recorded.should == [:singleton_method_added, :new_method_with_define_method] + end + + describe "when singleton_method_added is undefined" do + it "raises NoMethodError for a metaclass" do + class BasicObjectSpecs::NoSingletonMethodAdded + class << self + undef_method :singleton_method_added + end + + -> { + def self.foo + end + }.should raise_error(NoMethodError, /undefined method [`']singleton_method_added' for/) + end + ensure + BasicObjectSpecs.send(:remove_const, :NoSingletonMethodAdded) + end + + it "raises NoMethodError for a singleton instance" do + object = Object.new + class << object + undef_method :singleton_method_added + + -> { + def foo + end + }.should raise_error(NoMethodError, /undefined method [`']singleton_method_added' for #<Object:/) + + -> { + define_method(:bar) {} + }.should raise_error(NoMethodError, /undefined method [`']singleton_method_added' for #<Object:/) + end + + -> { + object.define_singleton_method(:baz) {} + }.should raise_error(NoMethodError, /undefined method [`']singleton_method_added' for #<Object:/) + end + + it "calls #method_missing" do + ScratchPad.record [] + object = Object.new + class << object + def method_missing(*args) + ScratchPad << args + end + + undef_method :singleton_method_added + + def foo + end + + define_method(:bar) {} + end + object.define_singleton_method(:baz) {} + + ScratchPad.recorded.should == [ + [:singleton_method_added, :foo], + [:singleton_method_added, :bar], + [:singleton_method_added, :baz], + ] + end + end +end diff --git a/spec/ruby/core/basicobject/singleton_method_removed_spec.rb b/spec/ruby/core/basicobject/singleton_method_removed_spec.rb new file mode 100644 index 0000000000..46f9a6894c --- /dev/null +++ b/spec/ruby/core/basicobject/singleton_method_removed_spec.rb @@ -0,0 +1,24 @@ +require_relative '../../spec_helper' + +describe "BasicObject#singleton_method_removed" do + before :each do + ScratchPad.clear + end + + it "is a private method" do + BasicObject.should have_private_instance_method(:singleton_method_removed) + end + + it "is called when a method is removed on self" do + klass = Class.new + def klass.singleton_method_removed(name) + ScratchPad.record [:singleton_method_removed, name] + end + def klass.singleton_method_to_remove + end + class << klass + remove_method :singleton_method_to_remove + end + ScratchPad.recorded.should == [:singleton_method_removed, :singleton_method_to_remove] + end +end diff --git a/spec/ruby/core/basicobject/singleton_method_undefined_spec.rb b/spec/ruby/core/basicobject/singleton_method_undefined_spec.rb new file mode 100644 index 0000000000..7d6c7207db --- /dev/null +++ b/spec/ruby/core/basicobject/singleton_method_undefined_spec.rb @@ -0,0 +1,24 @@ +require_relative '../../spec_helper' + +describe "BasicObject#singleton_method_undefined" do + before :each do + ScratchPad.clear + end + + it "is a private method" do + BasicObject.should have_private_instance_method(:singleton_method_undefined) + end + + it "is called when a method is removed on self" do + klass = Class.new + def klass.singleton_method_undefined(name) + ScratchPad.record [:singleton_method_undefined, name] + end + def klass.singleton_method_to_undefine + end + class << klass + undef_method :singleton_method_to_undefine + end + ScratchPad.recorded.should == [:singleton_method_undefined, :singleton_method_to_undefine] + end +end diff --git a/spec/ruby/core/binding/clone_spec.rb b/spec/ruby/core/binding/clone_spec.rb new file mode 100644 index 0000000000..f1769ac6de --- /dev/null +++ b/spec/ruby/core/binding/clone_spec.rb @@ -0,0 +1,13 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/clone' + +describe "Binding#clone" do + it_behaves_like :binding_clone, :clone + + it "preserves frozen status" do + bind = binding.freeze + bind.frozen?.should == true + bind.clone.frozen?.should == true + end +end diff --git a/spec/ruby/core/binding/dup_spec.rb b/spec/ruby/core/binding/dup_spec.rb new file mode 100644 index 0000000000..4eff66bd9a --- /dev/null +++ b/spec/ruby/core/binding/dup_spec.rb @@ -0,0 +1,30 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/clone' + +describe "Binding#dup" do + it_behaves_like :binding_clone, :dup + + it "resets frozen status" do + bind = binding.freeze + bind.frozen?.should == true + bind.dup.frozen?.should == false + end + + it "retains original binding variables but the list is distinct" do + bind1 = binding + eval "a = 1", bind1 + + bind2 = bind1.dup + eval("a = 2", bind2) + eval("a", bind1).should == 2 + eval("a", bind2).should == 2 + + eval("b = 2", bind2) + -> { eval("b", bind1) }.should raise_error(NameError) + eval("b", bind2).should == 2 + + bind1.local_variables.sort.should == [:a, :bind1, :bind2] + bind2.local_variables.sort.should == [:a, :b, :bind1, :bind2] + end +end diff --git a/spec/ruby/core/binding/eval_spec.rb b/spec/ruby/core/binding/eval_spec.rb new file mode 100644 index 0000000000..bb2036f739 --- /dev/null +++ b/spec/ruby/core/binding/eval_spec.rb @@ -0,0 +1,115 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Binding#eval" do + it "behaves like Kernel.eval(..., self)" do + obj = BindingSpecs::Demo.new(1) + bind = obj.get_binding + + bind.eval("@secret += square(3)").should == 10 + bind.eval("a").should be_true + + bind.eval("class Inside; end") + bind.eval("Inside.name").should == "BindingSpecs::Demo::Inside" + end + + it "does not leak variables to cloned bindings" do + obj = BindingSpecs::Demo.new(1) + bind = obj.get_empty_binding + bind2 = bind.dup + + bind.eval("x = 72") + bind.local_variables.should == [:x] + bind2.local_variables.should == [] + end + + it "starts with line 1 if single argument is given" do + obj = BindingSpecs::Demo.new(1) + bind = obj.get_binding + bind.eval("__LINE__").should == 1 + end + + it "preserves __LINE__ across multiple calls to eval" do + obj = BindingSpecs::Demo.new(1) + bind = obj.get_binding + bind.eval("__LINE__").should == 1 + bind.eval("__LINE__").should == 1 + end + + it "increments __LINE__ on each line of a multiline eval" do + obj = BindingSpecs::Demo.new(1) + bind = obj.get_binding + bind.eval("#foo\n__LINE__").should == 2 + end + + it "starts with line 1 if the Binding is created with #send" do + obj = BindingSpecs::Demo.new(1) + bind, line = obj.get_binding_with_send_and_line + bind.eval("__LINE__").should == 1 + end + + it "starts with a __LINE__ of 1 if a filename is passed" do + bind = BindingSpecs::Demo.new(1).get_binding + bind.eval("__LINE__", "(test)").should == 1 + bind.eval("#foo\n__LINE__", "(test)").should == 2 + end + + it "starts with a __LINE__ from the third argument if passed" do + bind = BindingSpecs::Demo.new(1).get_binding + bind.eval("__LINE__", "(test)", 88).should == 88 + bind.eval("#foo\n__LINE__", "(test)", 88).should == 89 + end + + ruby_version_is ""..."3.3" do + it "uses (eval) as __FILE__ if single argument given" do + obj = BindingSpecs::Demo.new(1) + bind = obj.get_binding + bind.eval("__FILE__").should == '(eval)' + end + end + + it "uses 1 as __LINE__" do + obj = BindingSpecs::Demo.new(1) + bind = obj.get_binding + suppress_warning { bind.eval("__LINE__") }.should == 1 + end + + it "uses the __FILE__ that is passed in" do + bind = BindingSpecs::Demo.new(1).get_binding + bind.eval("__FILE__", "(test)").should == "(test)" + end + + describe "with a file given" do + it "does not store the filename permanently" do + obj = BindingSpecs::Demo.new(1) + bind = obj.get_binding + + bind.eval("__FILE__", "test.rb").should == "test.rb" + suppress_warning {bind.eval("__FILE__")}.should_not == "test.rb" + end + end + + it "with __method__ returns the method where the Binding was created" do + obj = BindingSpecs::Demo.new(1) + bind, meth = obj.get_binding_and_method + bind.eval("__method__").should == meth + end + + it "with __method__ returns the method where the Binding was created, ignoring #send" do + obj = BindingSpecs::Demo.new(1) + bind, meth = obj.get_binding_with_send_and_method + bind.eval("__method__").should == meth + end + + it "reflects refinements activated in the binding scope" do + bind = BindingSpecs::Refined.refined_binding + + bind.eval("'bar'.foo").should == "foo" + end + + ruby_version_is "3.3" do + it "uses the caller location as default filename" do + binding.eval("[__FILE__, __LINE__]").should == ["(eval at #{__FILE__}:#{__LINE__})", 1] + end + end +end diff --git a/spec/ruby/core/binding/fixtures/classes.rb b/spec/ruby/core/binding/fixtures/classes.rb new file mode 100644 index 0000000000..b5f3ce9008 --- /dev/null +++ b/spec/ruby/core/binding/fixtures/classes.rb @@ -0,0 +1,66 @@ +module BindingSpecs + class Demo + def initialize(n) + @secret = n + end + + def square(n) + n * n + end + + def get_binding_and_line + a = true + [binding, __LINE__] + end + + def get_binding + get_binding_and_line[0] + end + + def get_line_of_binding + get_binding_and_line[1] + end + + def get_file_of_binding + __FILE__ + end + + def get_binding_with_send_and_line + [send(:binding), __LINE__] + end + + def get_binding_and_method + [binding, :get_binding_and_method] + end + + def get_binding_with_send_and_method + [send(:binding), :get_binding_with_send_and_method] + end + + def get_empty_binding + binding + end + + def get_binding_in_block + a = true + 1.times do + b = false + return binding + end + end + end + + module AddFooToString + refine(String) do + def foo + "foo" + end + end + end + class Refined + using AddFooToString + def self.refined_binding + binding + end + end +end diff --git a/spec/ruby/core/binding/fixtures/location.rb b/spec/ruby/core/binding/fixtures/location.rb new file mode 100644 index 0000000000..a78ae75731 --- /dev/null +++ b/spec/ruby/core/binding/fixtures/location.rb @@ -0,0 +1,6 @@ +module BindingSpecs + module LocationMethod + FILE_PATH = __FILE__ + TEST_BINDING = binding + end +end diff --git a/spec/ruby/core/binding/local_variable_defined_spec.rb b/spec/ruby/core/binding/local_variable_defined_spec.rb new file mode 100644 index 0000000000..2fc6504ee5 --- /dev/null +++ b/spec/ruby/core/binding/local_variable_defined_spec.rb @@ -0,0 +1,46 @@ +require_relative '../../spec_helper' + +describe 'Binding#local_variable_defined?' do + it 'returns false when a variable is not defined' do + binding.local_variable_defined?(:foo).should == false + end + + it 'returns true when a regular local variable is defined' do + foo = 10 + binding.local_variable_defined?(:foo).should == true + end + + it 'returns true when a local variable is defined using eval()' do + bind = binding + bind.eval('foo = 10') + + bind.local_variable_defined?(:foo).should == true + end + + it 'returns true when a local variable is defined using Binding#local_variable_set' do + bind = binding + bind.local_variable_set(:foo, 10) + + bind.local_variable_defined?(:foo).should == true + end + + it 'returns true when a local variable is defined in a parent scope' do + foo = 10 + -> { + binding.local_variable_defined?(:foo) + }.call.should == true + end + + it 'allows usage of a String as the variable name' do + foo = 10 + binding.local_variable_defined?('foo').should == true + end + + it 'allows usage of an object responding to #to_str as the variable name' do + foo = 10 + name = mock(:obj) + name.stub!(:to_str).and_return('foo') + + binding.local_variable_defined?(name).should == true + end +end diff --git a/spec/ruby/core/binding/local_variable_get_spec.rb b/spec/ruby/core/binding/local_variable_get_spec.rb new file mode 100644 index 0000000000..005670becc --- /dev/null +++ b/spec/ruby/core/binding/local_variable_get_spec.rb @@ -0,0 +1,56 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Binding#local_variable_get" do + it "reads local variables captured in the binding" do + a = 42 + bind = binding + bind.local_variable_get(:a).should == 42 + end + + it "raises a NameError for missing variables" do + bind = BindingSpecs::Demo.new(1).get_empty_binding + + -> { + bind.local_variable_get(:no_such_variable) + }.should raise_error(NameError) + end + + it "reads variables added later to the binding" do + bind = BindingSpecs::Demo.new(1).get_empty_binding + + -> { + bind.local_variable_get(:a) + }.should raise_error(NameError) + + bind.local_variable_set(:a, 42) + + bind.local_variable_get(:a).should == 42 + end + + it 'gets a local variable defined in a parent scope' do + number = 10 + + -> { + binding.local_variable_get(:number) + }.call.should == 10 + end + + it 'gets a local variable defined using eval()' do + bind = binding + bind.eval('number = 10') + + bind.local_variable_get(:number).should == 10 + end + + it "raises a NameError on global access" do + bind = binding + -> { bind.local_variable_get(:$0) }.should raise_error(NameError) + end + + it "raises a NameError on special variable access" do + bind = binding + -> { bind.local_variable_get(:$~) }.should raise_error(NameError) + -> { bind.local_variable_get(:$_) }.should raise_error(NameError) + end +end diff --git a/spec/ruby/core/binding/local_variable_set_spec.rb b/spec/ruby/core/binding/local_variable_set_spec.rb new file mode 100644 index 0000000000..1456c6dda1 --- /dev/null +++ b/spec/ruby/core/binding/local_variable_set_spec.rb @@ -0,0 +1,71 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Binding#local_variable_set" do + it "adds nonexistent variables to the binding's eval scope" do + obj = BindingSpecs::Demo.new(1) + bind = obj.get_empty_binding + bind.eval('local_variables').should == [] + bind.local_variable_set :foo, 1 + bind.eval('local_variables').should == [:foo] + bind.eval('foo').should == 1 + end + + it 'sets a new local variable' do + bind = binding + + bind.local_variable_set(:number, 10) + bind.local_variable_get(:number).should == 10 + end + + it 'sets a local variable using a String as the variable name' do + bind = binding + + bind.local_variable_set('number', 10) + bind.local_variable_get('number').should == 10 + end + + it 'sets a local variable using an object responding to #to_str as the variable name' do + bind = binding + name = mock(:obj) + name.stub!(:to_str).and_return('number') + + bind.local_variable_set(name, 10) + bind.local_variable_get(name).should == 10 + end + + it 'scopes new local variables to the receiving Binding' do + bind = binding + bind.local_variable_set(:number, 10) + + -> { number }.should raise_error(NameError) + end + + it 'overwrites an existing local variable defined before a Binding' do + number = 10 + bind = binding + + bind.local_variable_set(:number, 20) + number.should == 20 + end + + it 'overwrites a local variable defined using eval()' do + bind = binding + bind.eval('number = 10') + + bind.local_variable_set(:number, 20) + bind.local_variable_get(:number).should == 20 + end + + it "raises a NameError on global access" do + bind = binding + -> { bind.local_variable_set(:$0, "") }.should raise_error(NameError) + end + + it "raises a NameError on special variable access" do + bind = binding + -> { bind.local_variable_set(:$~, "") }.should raise_error(NameError) + -> { bind.local_variable_set(:$_, "") }.should raise_error(NameError) + end + +end diff --git a/spec/ruby/core/binding/local_variables_spec.rb b/spec/ruby/core/binding/local_variables_spec.rb new file mode 100644 index 0000000000..92c817b9a8 --- /dev/null +++ b/spec/ruby/core/binding/local_variables_spec.rb @@ -0,0 +1,35 @@ +require_relative '../../spec_helper' + +describe "Binding#local_variables" do + it "returns an Array" do + binding.local_variables.should be_kind_of(Array) + end + + it "includes local variables in the current scope" do + a = 1 + b = nil + binding.local_variables.should == [:a, :b] + end + + it "includes local variables defined after calling binding.local_variables" do + binding.local_variables.should == [:a, :b] + a = 1 + b = 2 + end + + it "includes local variables of inherited scopes and eval'ed context" do + p = proc { |a| b = 1; eval("c = 2; binding.local_variables") } + p.call.should == [:c, :a, :b, :p] + end + + it "includes shadowed local variables only once" do + a = 1 + proc { |a| binding.local_variables }.call(2).should == [:a] + end + + it "includes new variables defined in the binding" do + b = binding + b.local_variable_set :a, 42 + b.local_variables.should == [:a, :b] + end +end diff --git a/spec/ruby/core/binding/receiver_spec.rb b/spec/ruby/core/binding/receiver_spec.rb new file mode 100644 index 0000000000..4bf5e7a7bd --- /dev/null +++ b/spec/ruby/core/binding/receiver_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Binding#receiver" do + it "returns the object to which binding is bound" do + obj = BindingSpecs::Demo.new(1) + obj.get_binding.receiver.should == obj + + binding.receiver.should == self + end +end diff --git a/spec/ruby/core/binding/shared/clone.rb b/spec/ruby/core/binding/shared/clone.rb new file mode 100644 index 0000000000..2d854fce96 --- /dev/null +++ b/spec/ruby/core/binding/shared/clone.rb @@ -0,0 +1,56 @@ +describe :binding_clone, shared: true do + before :each do + @b1 = BindingSpecs::Demo.new(99).get_binding + @b2 = @b1.send(@method) + @b3 = BindingSpecs::Demo.new(99).get_binding_in_block + @b4 = @b3.send(@method) + end + + it "returns a copy of the Binding object" do + [[@b1, @b2, "a"], + [@b3, @b4, "a", "b"]].each do |b1, b2, *vars| + b1.should_not == b2 + + eval("@secret", b1).should == eval("@secret", b2) + eval("square(2)", b1).should == eval("square(2)", b2) + eval("self.square(2)", b1).should == eval("self.square(2)", b2) + vars.each do |v| + eval("#{v}", b1).should == eval("#{v}", b2) + end + end + end + + it "is a shallow copy of the Binding object" do + [[@b1, @b2, "a"], + [@b3, @b4, "a", "b"]].each do |b1, b2, *vars| + vars.each do |v| + eval("#{v} = false", b1) + eval("#{v}", b2).should == false + end + b1.local_variable_set(:x, 37) + b2.local_variable_defined?(:x).should == false + end + end + + ruby_version_is "3.4" do + it "copies instance variables" do + @b1.instance_variable_set(:@ivar, 1) + cl = @b1.send(@method) + cl.instance_variables.should == [:@ivar] + end + + it "copies the finalizer" do + code = <<-'RUBY' + obj = binding + + ObjectSpace.define_finalizer(obj, Proc.new { STDOUT.write "finalized\n" }) + + obj.clone + + exit 0 + RUBY + + ruby_exe(code).lines.sort.should == ["finalized\n", "finalized\n"] + end + end +end diff --git a/spec/ruby/core/binding/source_location_spec.rb b/spec/ruby/core/binding/source_location_spec.rb new file mode 100644 index 0000000000..d1c8191ea8 --- /dev/null +++ b/spec/ruby/core/binding/source_location_spec.rb @@ -0,0 +1,14 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/location' + +describe "Binding#source_location" do + it "returns an [file, line] pair" do + b = BindingSpecs::LocationMethod::TEST_BINDING + b.source_location.should == [BindingSpecs::LocationMethod::FILE_PATH, 4] + end + + it "works for eval with a given line" do + b = eval('binding', nil, "foo", 100) + b.source_location.should == ["foo", 100] + end +end diff --git a/spec/ruby/core/builtin_constants/builtin_constants_spec.rb b/spec/ruby/core/builtin_constants/builtin_constants_spec.rb new file mode 100644 index 0000000000..13e066cc7f --- /dev/null +++ b/spec/ruby/core/builtin_constants/builtin_constants_spec.rb @@ -0,0 +1,151 @@ +require_relative '../../spec_helper' + +describe "RUBY_VERSION" do + it "is a String" do + RUBY_VERSION.should be_kind_of(String) + end + + it "is frozen" do + RUBY_VERSION.should.frozen? + end +end + +describe "RUBY_PATCHLEVEL" do + it "is an Integer" do + RUBY_PATCHLEVEL.should be_kind_of(Integer) + end +end + +describe "RUBY_COPYRIGHT" do + it "is a String" do + RUBY_COPYRIGHT.should be_kind_of(String) + end + + it "is frozen" do + RUBY_COPYRIGHT.should.frozen? + end +end + +describe "RUBY_DESCRIPTION" do + it "is a String" do + RUBY_DESCRIPTION.should be_kind_of(String) + end + + it "is frozen" do + RUBY_DESCRIPTION.should.frozen? + end +end + +describe "RUBY_ENGINE" do + it "is a String" do + RUBY_ENGINE.should be_kind_of(String) + end + + it "is frozen" do + RUBY_ENGINE.should.frozen? + end +end + +describe "RUBY_ENGINE_VERSION" do + it "is a String" do + RUBY_ENGINE_VERSION.should be_kind_of(String) + end + + it "is frozen" do + RUBY_ENGINE_VERSION.should.frozen? + end +end + +describe "RUBY_PLATFORM" do + it "is a String" do + RUBY_PLATFORM.should be_kind_of(String) + end + + it "is frozen" do + RUBY_PLATFORM.should.frozen? + end +end + +describe "RUBY_RELEASE_DATE" do + it "is a String" do + RUBY_RELEASE_DATE.should be_kind_of(String) + end + + it "is frozen" do + RUBY_RELEASE_DATE.should.frozen? + end +end + +describe "RUBY_REVISION" do + it "is a String" do + RUBY_REVISION.should be_kind_of(String) + end + + it "is frozen" do + RUBY_REVISION.should.frozen? + end +end + +ruby_version_is "4.0" do + context "The constant" do + describe "Ruby" do + it "is a Module" do + Ruby.should.instance_of?(Module) + end + end + + describe "Ruby::VERSION" do + it "is equal to RUBY_VERSION" do + Ruby::VERSION.should equal(RUBY_VERSION) + end + end + + describe "RUBY::PATCHLEVEL" do + it "is equal to RUBY_PATCHLEVEL" do + Ruby::PATCHLEVEL.should equal(RUBY_PATCHLEVEL) + end + end + + describe "Ruby::COPYRIGHT" do + it "is equal to RUBY_COPYRIGHT" do + Ruby::COPYRIGHT.should equal(RUBY_COPYRIGHT) + end + end + + describe "Ruby::DESCRIPTION" do + it "is equal to RUBY_DESCRIPTION" do + Ruby::DESCRIPTION.should equal(RUBY_DESCRIPTION) + end + end + + describe "Ruby::ENGINE" do + it "is equal to RUBY_ENGINE" do + Ruby::ENGINE.should equal(RUBY_ENGINE) + end + end + + describe "Ruby::ENGINE_VERSION" do + it "is equal to RUBY_ENGINE_VERSION" do + Ruby::ENGINE_VERSION.should equal(RUBY_ENGINE_VERSION) + end + end + + describe "Ruby::PLATFORM" do + it "is equal to RUBY_PLATFORM" do + Ruby::PLATFORM.should equal(RUBY_PLATFORM) + end + end + + describe "Ruby::RELEASE_DATE" do + it "is equal to RUBY_RELEASE_DATE" do + Ruby::RELEASE_DATE.should equal(RUBY_RELEASE_DATE) + end + end + + describe "Ruby::REVISION" do + it "is equal to RUBY_REVISION" do + Ruby::REVISION.should equal(RUBY_REVISION) + end + end + end +end diff --git a/spec/ruby/core/class/allocate_spec.rb b/spec/ruby/core/class/allocate_spec.rb new file mode 100644 index 0000000000..b39622e06a --- /dev/null +++ b/spec/ruby/core/class/allocate_spec.rb @@ -0,0 +1,41 @@ +require_relative '../../spec_helper' + +describe "Class#allocate" do + it "returns an instance of self" do + klass = Class.new + klass.allocate.should be_an_instance_of(klass) + end + + it "returns a fully-formed instance of Module" do + klass = Class.allocate + klass.constants.should_not == nil + klass.methods.should_not == nil + end + + it "throws an exception when calling a method on a new instance" do + klass = Class.allocate + -> do + klass.new + end.should raise_error(Exception) + end + + it "does not call initialize on the new instance" do + klass = Class.new do + def initialize(*args) + @initialized = true + end + + def initialized? + @initialized || false + end + end + + klass.allocate.should_not.initialized? + end + + it "raises TypeError for #superclass" do + -> do + Class.allocate.superclass + end.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/class/attached_object_spec.rb b/spec/ruby/core/class/attached_object_spec.rb new file mode 100644 index 0000000000..8f8a0734c6 --- /dev/null +++ b/spec/ruby/core/class/attached_object_spec.rb @@ -0,0 +1,29 @@ +require_relative '../../spec_helper' + +describe "Class#attached_object" do + it "returns the object that is attached to a singleton class" do + a = Class.new + + a_obj = a.new + a_obj.singleton_class.attached_object.should == a_obj + end + + it "returns the class object that is attached to a class's singleton class" do + a = Class.new + singleton_class = (class << a; self; end) + + singleton_class.attached_object.should == a + end + + it "raises TypeError if the class is not a singleton class" do + a = Class.new + + -> { a.attached_object }.should raise_error(TypeError, /is not a singleton class/) + end + + it "raises TypeError for special singleton classes" do + -> { nil.singleton_class.attached_object }.should raise_error(TypeError, /[`']NilClass' is not a singleton class/) + -> { true.singleton_class.attached_object }.should raise_error(TypeError, /[`']TrueClass' is not a singleton class/) + -> { false.singleton_class.attached_object }.should raise_error(TypeError, /[`']FalseClass' is not a singleton class/) + end +end diff --git a/spec/ruby/core/class/dup_spec.rb b/spec/ruby/core/class/dup_spec.rb new file mode 100644 index 0000000000..1ff9abf7b4 --- /dev/null +++ b/spec/ruby/core/class/dup_spec.rb @@ -0,0 +1,69 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +# NOTE: This is actually implemented by Module#initialize_copy +describe "Class#dup" do + it "duplicates both the class and the singleton class" do + klass = Class.new do + def hello + "hello" + end + + def self.message + "text" + end + end + + klass_dup = klass.dup + + klass_dup.new.hello.should == "hello" + klass_dup.message.should == "text" + end + + it "retains an included module in the ancestor chain for the singleton class" do + klass = Class.new + mod = Module.new do + def hello + "hello" + end + end + + klass.extend(mod) + klass_dup = klass.dup + klass_dup.hello.should == "hello" + end + + it "retains the correct ancestor chain for the singleton class" do + super_klass = Class.new do + def hello + "hello" + end + + def self.message + "text" + end + end + + klass = Class.new(super_klass) + klass_dup = klass.dup + + klass_dup.new.hello.should == "hello" + klass_dup.message.should == "text" + end + + it "sets the name from the class to nil if not assigned to a constant" do + copy = CoreClassSpecs::Record.dup + copy.name.should be_nil + end + + it "stores the new name if assigned to a constant" do + CoreClassSpecs::RecordCopy = CoreClassSpecs::Record.dup + CoreClassSpecs::RecordCopy.name.should == "CoreClassSpecs::RecordCopy" + ensure + CoreClassSpecs.send(:remove_const, :RecordCopy) + end + + it "raises TypeError if called on BasicObject" do + -> { BasicObject.dup }.should raise_error(TypeError, "can't copy the root class") + end +end diff --git a/spec/ruby/core/class/fixtures/classes.rb b/spec/ruby/core/class/fixtures/classes.rb new file mode 100644 index 0000000000..f96db90795 --- /dev/null +++ b/spec/ruby/core/class/fixtures/classes.rb @@ -0,0 +1,47 @@ +module CoreClassSpecs + class Record + end + + module M + def inherited(klass) + ScratchPad.record klass + super + end + end + + class F; end + class << F + include M + end + + class A + def self.inherited(klass) + ScratchPad.record klass + end + end + + class H < A + def self.inherited(klass) + super + end + end + + module Inherited + class A + SUBCLASSES = [] + def self.inherited(subclass) + SUBCLASSES << [self, subclass] + end + end + + class B < A; end + class B < A; end # reopen + class C < B; end + + class D + def self.inherited(subclass) + ScratchPad << self + end + end + end +end diff --git a/spec/ruby/core/class/inherited_spec.rb b/spec/ruby/core/class/inherited_spec.rb new file mode 100644 index 0000000000..2a8d1ff813 --- /dev/null +++ b/spec/ruby/core/class/inherited_spec.rb @@ -0,0 +1,118 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Class.inherited" do + + before :each do + ScratchPad.record nil + end + + it "is invoked with the child Class when self is subclassed" do + begin + top = Class.new do + def self.inherited(cls) + $child_class = cls + end + end + + child = Class.new(top) + $child_class.should == child + + other_child = Class.new(top) + $child_class.should == other_child + ensure + $child_class = nil + end + end + + it "is invoked only once per subclass" do + expected = [ + [CoreClassSpecs::Inherited::A, CoreClassSpecs::Inherited::B], + [CoreClassSpecs::Inherited::B, CoreClassSpecs::Inherited::C], + ] + + CoreClassSpecs::Inherited::A::SUBCLASSES.should == expected + end + + it "is called when marked as a private class method" do + a = Class.new do + def self.inherited(klass) + ScratchPad.record klass + end + end + a.private_class_method :inherited + ScratchPad.recorded.should == nil + b = Class.new(a) + ScratchPad.recorded.should == b + end + + it "is called when marked as a protected class method" do + a = Class.new + class << a + def inherited(klass) + ScratchPad.record klass + end + protected :inherited + end + ScratchPad.recorded.should == nil + b = Class.new(a) + ScratchPad.recorded.should == b + end + + it "is called when marked as a public class method" do + a = Class.new do + def self.inherited(klass) + ScratchPad.record klass + end + end + a.public_class_method :inherited + ScratchPad.recorded.should == nil + b = Class.new(a) + ScratchPad.recorded.should == b + end + + it "is called by super from a method provided by an included module" do + ScratchPad.recorded.should == nil + e = Class.new(CoreClassSpecs::F) + ScratchPad.recorded.should == e + end + + it "is called by super even when marked as a private class method" do + ScratchPad.recorded.should == nil + CoreClassSpecs::H.private_class_method :inherited + i = Class.new(CoreClassSpecs::H) + ScratchPad.recorded.should == i + end + + it "will be invoked by child class regardless of visibility" do + top = Class.new do + class << self + def inherited(cls); end + end + end + + class << top; private :inherited; end + -> { Class.new(top) }.should_not raise_error + + class << top; protected :inherited; end + -> { Class.new(top) }.should_not raise_error + end + + it "if the subclass is assigned to a constant, it is all set" do + ScratchPad.record [] + + parent = Class.new do + def self.inherited(subclass) + ScratchPad << defined?(self::C) + ScratchPad << const_defined?(:C) + ScratchPad << constants + ScratchPad << const_get(:C) + ScratchPad << subclass.name.match?(/\A#<Class:0x\w+>::C\z/) + end + end + + class parent::C < parent; end + + ScratchPad.recorded.should == ["constant", true, [:C], parent::C, true] + end +end diff --git a/spec/ruby/core/class/initialize_spec.rb b/spec/ruby/core/class/initialize_spec.rb new file mode 100644 index 0000000000..6348758485 --- /dev/null +++ b/spec/ruby/core/class/initialize_spec.rb @@ -0,0 +1,34 @@ +require_relative '../../spec_helper' + +describe "Class#initialize" do + it "is private" do + Class.should have_private_method(:initialize) + end + + it "raises a TypeError when called on already initialized classes" do + ->{ + Integer.send :initialize + }.should raise_error(TypeError) + + ->{ + Object.send :initialize + }.should raise_error(TypeError) + end + + # See [redmine:2601] + it "raises a TypeError when called on BasicObject" do + ->{ + BasicObject.send :initialize + }.should raise_error(TypeError) + end + + describe "when given the Class" do + before :each do + @uninitialized = Class.allocate + end + + it "raises a TypeError" do + ->{@uninitialized.send(:initialize, Class)}.should raise_error(TypeError) + end + end +end diff --git a/spec/ruby/core/class/new_spec.rb b/spec/ruby/core/class/new_spec.rb new file mode 100644 index 0000000000..6fe54c3209 --- /dev/null +++ b/spec/ruby/core/class/new_spec.rb @@ -0,0 +1,157 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Class.new with a block given" do + it "yields the new class as self in the block" do + self_in_block = nil + klass = Class.new do + self_in_block = self + end + self_in_block.should equal klass + end + + it "uses the given block as the class' body" do + klass = Class.new do + def self.message + "text" + end + + def hello + "hello again" + end + end + + klass.message.should == "text" + klass.new.hello.should == "hello again" + end + + it "creates a subclass of the given superclass" do + sc = Class.new do + def self.body + @body + end + @body = self + def message; "text"; end + end + klass = Class.new(sc) do + def self.body + @body + end + @body = self + def message2; "hello"; end + end + + klass.body.should == klass + sc.body.should == sc + klass.superclass.should == sc + klass.new.message.should == "text" + klass.new.message2.should == "hello" + end + + it "runs the inherited hook after yielding the block" do + ScratchPad.record [] + klass = Class.new(CoreClassSpecs::Inherited::D) do + ScratchPad << self + end + + ScratchPad.recorded.should == [CoreClassSpecs::Inherited::D, klass] + end +end + +describe "Class.new" do + it "creates a new anonymous class" do + klass = Class.new + klass.is_a?(Class).should == true + + klass_instance = klass.new + klass_instance.is_a?(klass).should == true + end + + it "raises a TypeError if passed a metaclass" do + obj = mock("Class.new metaclass") + meta = obj.singleton_class + -> { Class.new meta }.should raise_error(TypeError) + end + + it "creates a class without a name" do + Class.new.name.should be_nil + end + + it "creates a class that can be given a name by assigning it to a constant" do + ::MyClass = Class.new + ::MyClass.name.should == "MyClass" + a = Class.new + MyClass::NestedClass = a + MyClass::NestedClass.name.should == "MyClass::NestedClass" + ensure + Object.send(:remove_const, :MyClass) + end + + it "sets the new class' superclass to the given class" do + top = Class.new + Class.new(top).superclass.should == top + end + + it "sets the new class' superclass to Object when no class given" do + Class.new.superclass.should == Object + end + + it "raises a TypeError when given a non-Class" do + error_msg = /superclass must be a.*Class/ + -> { Class.new("") }.should raise_error(TypeError, error_msg) + -> { Class.new(1) }.should raise_error(TypeError, error_msg) + -> { Class.new(:symbol) }.should raise_error(TypeError, error_msg) + -> { Class.new(mock('o')) }.should raise_error(TypeError, error_msg) + -> { Class.new(Module.new) }.should raise_error(TypeError, error_msg) + -> { Class.new(BasicObject.new) }.should raise_error(TypeError, error_msg) + end +end + +describe "Class#new" do + it "returns a new instance of self" do + klass = Class.new + klass.new.is_a?(klass).should == true + end + + it "invokes #initialize on the new instance with the given args" do + klass = Class.new do + def initialize(*args) + @initialized = true + @args = args + end + + def args + @args + end + + def initialized? + @initialized || false + end + end + + klass.new.should.initialized? + klass.new(1, 2, 3).args.should == [1, 2, 3] + end + + it "uses the internal allocator and does not call #allocate" do + klass = Class.new do + def self.allocate + raise "allocate should not be called" + end + end + + instance = klass.new + instance.should be_kind_of klass + instance.class.should equal klass + end + + it "passes the block to #initialize" do + klass = Class.new do + def initialize + yield + end + end + + klass.new { break 42 }.should == 42 + end +end diff --git a/spec/ruby/core/class/subclasses_spec.rb b/spec/ruby/core/class/subclasses_spec.rb new file mode 100644 index 0000000000..f692152787 --- /dev/null +++ b/spec/ruby/core/class/subclasses_spec.rb @@ -0,0 +1,85 @@ +require_relative '../../spec_helper' +require_relative '../module/fixtures/classes' + +describe "Class#subclasses" do + it "returns a list of classes directly inheriting from self" do + assert_subclasses(ModuleSpecs::Parent, [ModuleSpecs::Child, ModuleSpecs::Child2]) + end + + it "does not return included modules from the parent" do + parent = Class.new + child = Class.new(parent) + mod = Module.new + parent.include(mod) + + assert_subclasses(parent, [child]) + end + + it "does not return included modules from the child" do + parent = Class.new + child = Class.new(parent) + mod = Module.new + parent.include(mod) + + assert_subclasses(parent, [child]) + end + + it "does not return prepended modules from the parent" do + parent = Class.new + child = Class.new(parent) + mod = Module.new + parent.prepend(mod) + + assert_subclasses(parent, [child]) + end + + it "does not return prepended modules from the child" do + parent = Class.new + child = Class.new(parent) + mod = Module.new + child.prepend(mod) + + assert_subclasses(parent, [child]) + end + + it "does not return singleton classes" do + a = Class.new + + a_obj = a.new + def a_obj.force_singleton_class + 42 + end + + a.subclasses.should_not include(a_obj.singleton_class) + end + + it "has 1 entry per module or class" do + ModuleSpecs::Parent.subclasses.should == ModuleSpecs::Parent.subclasses.uniq + end + + it "works when creating subclasses concurrently" do + t = 16 + n = 1000 + go = false + superclass = Class.new + + threads = t.times.map do + Thread.new do + Thread.pass until go + n.times.map do + Class.new(superclass) + end + end + end + + go = true + classes = threads.map(&:value) + + superclass.subclasses.size.should == t * n + superclass.subclasses.each { |c| c.should be_kind_of(Class) } + end + + def assert_subclasses(mod,subclasses) + mod.subclasses.sort_by(&:inspect).should == subclasses.sort_by(&:inspect) + end +end diff --git a/spec/ruby/core/class/superclass_spec.rb b/spec/ruby/core/class/superclass_spec.rb new file mode 100644 index 0000000000..00579238a6 --- /dev/null +++ b/spec/ruby/core/class/superclass_spec.rb @@ -0,0 +1,27 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Class#superclass" do + it "returns the superclass of self" do + BasicObject.superclass.should be_nil + Object.superclass.should == BasicObject + Class.superclass.should == Module + Class.new.superclass.should == Object + Class.new(String).superclass.should == String + Class.new(Integer).superclass.should == Integer + end + + # redmine:567 + describe "for a singleton class" do + it "of an object returns the class of the object" do + a = CoreClassSpecs::A.new + sc = class << a; self; end + sc.superclass.should == CoreClassSpecs::A + end + + it "of a class returns the singleton class of its superclass" do # sorry, can't find a simpler way to express this... + sc = class << CoreClassSpecs::H; self; end + sc.superclass.should == class << CoreClassSpecs::A; self; end + end + end +end diff --git a/spec/ruby/core/comparable/between_spec.rb b/spec/ruby/core/comparable/between_spec.rb new file mode 100644 index 0000000000..fd79bb9b4c --- /dev/null +++ b/spec/ruby/core/comparable/between_spec.rb @@ -0,0 +1,25 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Comparable#between?" do + it "returns true if self is greater than or equal to the first and less than or equal to the second argument" do + a = ComparableSpecs::Weird.new(-1) + b = ComparableSpecs::Weird.new(0) + c = ComparableSpecs::Weird.new(1) + d = ComparableSpecs::Weird.new(2) + + a.between?(a, a).should == true + a.between?(a, b).should == true + a.between?(a, c).should == true + a.between?(a, d).should == true + c.between?(c, d).should == true + d.between?(d, d).should == true + c.between?(a, d).should == true + + a.between?(b, b).should == false + a.between?(b, c).should == false + a.between?(b, d).should == false + c.between?(a, a).should == false + c.between?(a, b).should == false + end +end diff --git a/spec/ruby/core/comparable/clamp_spec.rb b/spec/ruby/core/comparable/clamp_spec.rb new file mode 100644 index 0000000000..18f616a997 --- /dev/null +++ b/spec/ruby/core/comparable/clamp_spec.rb @@ -0,0 +1,223 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe 'Comparable#clamp' do + it 'raises an Argument error unless the 2 parameters are correctly ordered' do + one = ComparableSpecs::WithOnlyCompareDefined.new(1) + two = ComparableSpecs::WithOnlyCompareDefined.new(2) + c = ComparableSpecs::Weird.new(3) + + -> { c.clamp(two, one) }.should raise_error(ArgumentError) + one.should_receive(:<=>).any_number_of_times.and_return(nil) + -> { c.clamp(one, two) }.should raise_error(ArgumentError) + end + + it 'returns self if within the given parameters' do + one = ComparableSpecs::WithOnlyCompareDefined.new(1) + two = ComparableSpecs::WithOnlyCompareDefined.new(2) + three = ComparableSpecs::WithOnlyCompareDefined.new(3) + c = ComparableSpecs::Weird.new(2) + + c.clamp(one, two).should equal(c) + c.clamp(two, two).should equal(c) + c.clamp(one, three).should equal(c) + c.clamp(two, three).should equal(c) + end + + it 'returns the min parameter if less than it' do + one = ComparableSpecs::WithOnlyCompareDefined.new(1) + two = ComparableSpecs::WithOnlyCompareDefined.new(2) + c = ComparableSpecs::Weird.new(0) + + c.clamp(one, two).should equal(one) + end + + it 'returns the max parameter if greater than it' do + one = ComparableSpecs::WithOnlyCompareDefined.new(1) + two = ComparableSpecs::WithOnlyCompareDefined.new(2) + c = ComparableSpecs::Weird.new(3) + + c.clamp(one, two).should equal(two) + end + + context 'max is nil' do + it 'returns min if less than it' do + one = ComparableSpecs::WithOnlyCompareDefined.new(1) + c = ComparableSpecs::Weird.new(0) + c.clamp(one, nil).should equal(one) + end + + it 'always returns self if greater than min' do + one = ComparableSpecs::WithOnlyCompareDefined.new(1) + c = ComparableSpecs::Weird.new(2) + c.clamp(one, nil).should equal(c) + end + end + + context 'min is nil' do + it 'returns max if greater than it' do + one = ComparableSpecs::WithOnlyCompareDefined.new(1) + c = ComparableSpecs::Weird.new(2) + c.clamp(nil, one).should equal(one) + end + + it 'always returns self if less than max' do + one = ComparableSpecs::WithOnlyCompareDefined.new(1) + c = ComparableSpecs::Weird.new(0) + c.clamp(nil, one).should equal(c) + end + end + + it 'always returns self when min is nil and max is nil' do + c = ComparableSpecs::Weird.new(1) + c.clamp(nil, nil).should equal(c) + end + + it 'returns self if within the given range parameters' do + one = ComparableSpecs::WithOnlyCompareDefined.new(1) + two = ComparableSpecs::WithOnlyCompareDefined.new(2) + three = ComparableSpecs::WithOnlyCompareDefined.new(3) + c = ComparableSpecs::Weird.new(2) + + c.clamp(one..two).should equal(c) + c.clamp(two..two).should equal(c) + c.clamp(one..three).should equal(c) + c.clamp(two..three).should equal(c) + end + + it 'returns the minimum value of the range parameters if less than it' do + one = ComparableSpecs::WithOnlyCompareDefined.new(1) + two = ComparableSpecs::WithOnlyCompareDefined.new(2) + c = ComparableSpecs::Weird.new(0) + + c.clamp(one..two).should equal(one) + end + + it 'returns the maximum value of the range parameters if greater than it' do + one = ComparableSpecs::WithOnlyCompareDefined.new(1) + two = ComparableSpecs::WithOnlyCompareDefined.new(2) + c = ComparableSpecs::Weird.new(3) + + c.clamp(one..two).should equal(two) + end + + it 'raises an Argument error if the range parameter is exclusive' do + one = ComparableSpecs::WithOnlyCompareDefined.new(1) + two = ComparableSpecs::WithOnlyCompareDefined.new(2) + c = ComparableSpecs::Weird.new(3) + + -> { c.clamp(one...two) }.should raise_error(ArgumentError) + end + + context 'with nil as the max argument' do + it 'returns min argument if less than it' do + one = ComparableSpecs::WithOnlyCompareDefined.new(1) + zero = ComparableSpecs::WithOnlyCompareDefined.new(0) + c = ComparableSpecs::Weird.new(0) + + c.clamp(one, nil).should equal(one) + c.clamp(zero, nil).should equal(c) + end + + it 'always returns self if greater than min argument' do + one = ComparableSpecs::WithOnlyCompareDefined.new(1) + two = ComparableSpecs::WithOnlyCompareDefined.new(2) + c = ComparableSpecs::Weird.new(2) + + c.clamp(one, nil).should equal(c) + c.clamp(two, nil).should equal(c) + end + end + + context 'with endless range' do + it 'returns minimum value of the range parameters if less than it' do + one = ComparableSpecs::WithOnlyCompareDefined.new(1) + zero = ComparableSpecs::WithOnlyCompareDefined.new(0) + c = ComparableSpecs::Weird.new(0) + + c.clamp(one..).should equal(one) + c.clamp(zero..).should equal(c) + end + + it 'always returns self if greater than minimum value of the range parameters' do + one = ComparableSpecs::WithOnlyCompareDefined.new(1) + two = ComparableSpecs::WithOnlyCompareDefined.new(2) + c = ComparableSpecs::Weird.new(2) + + c.clamp(one..).should equal(c) + c.clamp(two..).should equal(c) + end + + it 'works with exclusive range' do + one = ComparableSpecs::WithOnlyCompareDefined.new(1) + c = ComparableSpecs::Weird.new(2) + + c.clamp(one...).should equal(c) + end + end + + context 'with nil as the min argument' do + it 'returns max argument if greater than it' do + one = ComparableSpecs::WithOnlyCompareDefined.new(1) + c = ComparableSpecs::Weird.new(2) + + c.clamp(nil, one).should equal(one) + end + + it 'always returns self if less than max argument' do + one = ComparableSpecs::WithOnlyCompareDefined.new(1) + zero = ComparableSpecs::WithOnlyCompareDefined.new(0) + c = ComparableSpecs::Weird.new(0) + + c.clamp(nil, one).should equal(c) + c.clamp(nil, zero).should equal(c) + end + end + + context 'with beginless range' do + it 'returns maximum value of the range parameters if greater than it' do + one = ComparableSpecs::WithOnlyCompareDefined.new(1) + c = ComparableSpecs::Weird.new(2) + + c.clamp(..one).should equal(one) + end + + it 'always returns self if less than maximum value of the range parameters' do + one = ComparableSpecs::WithOnlyCompareDefined.new(1) + zero = ComparableSpecs::WithOnlyCompareDefined.new(0) + c = ComparableSpecs::Weird.new(0) + + c.clamp(..one).should equal(c) + c.clamp(..zero).should equal(c) + end + + it 'raises an Argument error if the range parameter is exclusive' do + one = ComparableSpecs::WithOnlyCompareDefined.new(1) + c = ComparableSpecs::Weird.new(0) + + -> { c.clamp(...one) }.should raise_error(ArgumentError) + end + end + + context 'with nil as the min and the max argument' do + it 'always returns self' do + c = ComparableSpecs::Weird.new(1) + + c.clamp(nil, nil).should equal(c) + end + end + + context 'with beginless-and-endless range' do + it 'always returns self' do + c = ComparableSpecs::Weird.new(1) + + c.clamp(nil..nil).should equal(c) + end + + it 'works with exclusive range' do + c = ComparableSpecs::Weird.new(2) + + c.clamp(nil...nil).should equal(c) + end + end +end diff --git a/spec/ruby/core/comparable/equal_value_spec.rb b/spec/ruby/core/comparable/equal_value_spec.rb new file mode 100644 index 0000000000..ddcc03cb41 --- /dev/null +++ b/spec/ruby/core/comparable/equal_value_spec.rb @@ -0,0 +1,114 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Comparable#==" do + a = b = nil + before :each do + a = ComparableSpecs::Weird.new(0) + b = ComparableSpecs::Weird.new(10) + end + + it "returns true if other is the same as self" do + (a == a).should == true + (b == b).should == true + end + + it "calls #<=> on self with other and returns true if #<=> returns 0" do + a.should_receive(:<=>).once.and_return(0) + (a == b).should == true + end + + it "calls #<=> on self with other and returns true if #<=> returns 0.0" do + a.should_receive(:<=>).once.and_return(0.0) + (a == b).should == true + end + + it "returns false if calling #<=> on self returns a positive Integer" do + a.should_receive(:<=>).once.and_return(1) + (a == b).should == false + end + + it "returns false if calling #<=> on self returns a negative Integer" do + a.should_receive(:<=>).once.and_return(-1) + (a == b).should == false + end + + context "when #<=> returns nil" do + before :each do + a.should_receive(:<=>).once.and_return(nil) + end + + it "returns false" do + (a == b).should be_false + end + end + + context "when #<=> returns nor nil neither an Integer" do + before :each do + a.should_receive(:<=>).once.and_return("abc") + end + + it "raises an ArgumentError" do + -> { (a == b) }.should raise_error(ArgumentError) + end + end + + context "when #<=> raises an exception" do + context "if it is a StandardError" do + before :each do + a.should_receive(:<=>).once.and_raise(StandardError) + end + + it "lets it go through" do + -> { (a == b) }.should raise_error(StandardError) + end + end + + context "if it is a subclass of StandardError" do + # TypeError < StandardError + before :each do + a.should_receive(:<=>).once.and_raise(TypeError) + end + + it "lets it go through" do + -> { (a == b) }.should raise_error(TypeError) + end + end + + it "lets it go through if it is not a StandardError" do + a.should_receive(:<=>).once.and_raise(Exception) + -> { (a == b) }.should raise_error(Exception) + end + end + + context "when #<=> is not defined" do + before :each do + @a = ComparableSpecs::WithoutCompareDefined.new + @b = ComparableSpecs::WithoutCompareDefined.new + end + + it "returns true for identical objects" do + @a.should == @a + end + + it "returns false and does not recurse infinitely" do + @a.should_not == @b + end + end + + context "when #<=> calls super" do + before :each do + @a = ComparableSpecs::CompareCallingSuper.new + @b = ComparableSpecs::CompareCallingSuper.new + end + + it "returns true for identical objects" do + @a.should == @a + end + + it "calls the defined #<=> only once for different objects" do + @a.should_not == @b + @a.calls.should == 1 + end + end +end diff --git a/spec/ruby/core/comparable/fixtures/classes.rb b/spec/ruby/core/comparable/fixtures/classes.rb new file mode 100644 index 0000000000..2bdabbf014 --- /dev/null +++ b/spec/ruby/core/comparable/fixtures/classes.rb @@ -0,0 +1,37 @@ +module ComparableSpecs + class WithOnlyCompareDefined + attr_reader :value + + def initialize(value) + @value = value + end + + def <=>(other) + return nil if other.nil? + self.value <=> other.value + end + end + + class Weird < WithOnlyCompareDefined + include Comparable + end + + class WithoutCompareDefined + include Comparable + end + + class CompareCallingSuper + include Comparable + + attr_reader :calls + + def initialize + @calls = 0 + end + + def <=>(other) + @calls += 1 + super(other) + end + end +end diff --git a/spec/ruby/core/comparable/gt_spec.rb b/spec/ruby/core/comparable/gt_spec.rb new file mode 100644 index 0000000000..150e653dc7 --- /dev/null +++ b/spec/ruby/core/comparable/gt_spec.rb @@ -0,0 +1,43 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Comparable#>" do + it "calls #<=> on self with other and returns true if #<=> returns any Integer greater than 0" do + a = ComparableSpecs::Weird.new(0) + b = ComparableSpecs::Weird.new(20) + + a.should_receive(:<=>).any_number_of_times.and_return(1) + (a > b).should == true + + a.should_receive(:<=>).any_number_of_times.and_return(0.1) + (a > b).should == true + + a.should_receive(:<=>).any_number_of_times.and_return(10000000) + (a > b).should == true + end + + it "returns false if calling #<=> on self returns 0 or any Integer less than 0" do + a = ComparableSpecs::Weird.new(0) + b = ComparableSpecs::Weird.new(10) + + a.should_receive(:<=>).any_number_of_times.and_return(0) + (a > b).should == false + + a.should_receive(:<=>).any_number_of_times.and_return(0.0) + (a > b).should == false + + a.should_receive(:<=>).any_number_of_times.and_return(-1.0) + (a > b).should == false + + a.should_receive(:<=>).any_number_of_times.and_return(-10000000) + (a > b).should == false + end + + it "raises an ArgumentError if calling #<=> on self returns nil" do + a = ComparableSpecs::Weird.new(0) + b = ComparableSpecs::Weird.new(20) + + a.should_receive(:<=>).any_number_of_times.and_return(nil) + -> { (a > b) }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/comparable/gte_spec.rb b/spec/ruby/core/comparable/gte_spec.rb new file mode 100644 index 0000000000..328f58c66c --- /dev/null +++ b/spec/ruby/core/comparable/gte_spec.rb @@ -0,0 +1,47 @@ +require_relative 'fixtures/classes' +require_relative '../../spec_helper' + +describe "Comparable#>=" do + it "calls #<=> on self with other and returns true if #<=> returns 0 or any Integer greater than 0" do + a = ComparableSpecs::Weird.new(0) + b = ComparableSpecs::Weird.new(20) + + a.should_receive(:<=>).any_number_of_times.and_return(0) + (a >= b).should == true + + a.should_receive(:<=>).any_number_of_times.and_return(0.0) + (a >= b).should == true + + a.should_receive(:<=>).any_number_of_times.and_return(1) + (a >= b).should == true + + a.should_receive(:<=>).any_number_of_times.and_return(0.1) + (a >= b).should == true + + a.should_receive(:<=>).any_number_of_times.and_return(10000000) + (a >= b).should == true + end + + it "returns false if calling #<=> on self returns any Integer less than 0" do + a = ComparableSpecs::Weird.new(0) + b = ComparableSpecs::Weird.new(10) + + + a.should_receive(:<=>).any_number_of_times.and_return(-0.1) + (a >= b).should == false + + a.should_receive(:<=>).any_number_of_times.and_return(-1.0) + (a >= b).should == false + + a.should_receive(:<=>).any_number_of_times.and_return(-10000000) + (a >= b).should == false + end + + it "raises an ArgumentError if calling #<=> on self returns nil" do + a = ComparableSpecs::Weird.new(0) + b = ComparableSpecs::Weird.new(20) + + a.should_receive(:<=>).any_number_of_times.and_return(nil) + -> { (a >= b) }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/comparable/lt_spec.rb b/spec/ruby/core/comparable/lt_spec.rb new file mode 100644 index 0000000000..bca95f8d25 --- /dev/null +++ b/spec/ruby/core/comparable/lt_spec.rb @@ -0,0 +1,49 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Comparable#<" do + it "calls #<=> on self with other and returns true if #<=> returns any Integer less than 0" do + a = ComparableSpecs::Weird.new(0) + b = ComparableSpecs::Weird.new(20) + + a.should_receive(:<=>).any_number_of_times.and_return(-1) + (a < b).should == true + + a.should_receive(:<=>).any_number_of_times.and_return(-0.1) + (a < b).should == true + + a.should_receive(:<=>).any_number_of_times.and_return(-10000000) + (a < b).should == true + end + + it "returns false if calling #<=> on self returns 0 or any Integer greater than 0" do + a = ComparableSpecs::Weird.new(0) + b = ComparableSpecs::Weird.new(10) + + a.should_receive(:<=>).any_number_of_times.and_return(0) + (a < b).should == false + + a.should_receive(:<=>).any_number_of_times.and_return(0.0) + (a < b).should == false + + a.should_receive(:<=>).any_number_of_times.and_return(1.0) + (a < b).should == false + + a.should_receive(:<=>).any_number_of_times.and_return(10000000) + (a < b).should == false + end + + it "raises an ArgumentError if calling #<=> on self returns nil" do + a = ComparableSpecs::Weird.new(0) + b = ComparableSpecs::Weird.new(20) + + a.should_receive(:<=>).any_number_of_times.and_return(nil) + -> { (a < b) }.should raise_error(ArgumentError) + end + + it "raises an argument error with a message containing the value" do + -> { ("foo" < 7) }.should raise_error(ArgumentError) { |e| + e.message.should == "comparison of String with 7 failed" + } + end +end diff --git a/spec/ruby/core/comparable/lte_spec.rb b/spec/ruby/core/comparable/lte_spec.rb new file mode 100644 index 0000000000..b5cb9cc4e7 --- /dev/null +++ b/spec/ruby/core/comparable/lte_spec.rb @@ -0,0 +1,46 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Comparable#<=" do + it "calls #<=> on self with other and returns true if #<=> returns 0 or any Integer less than 0" do + a = ComparableSpecs::Weird.new(0) + b = ComparableSpecs::Weird.new(20) + + a.should_receive(:<=>).any_number_of_times.and_return(0) + (a <= b).should == true + + a.should_receive(:<=>).any_number_of_times.and_return(0.0) + (a <= b).should == true + + a.should_receive(:<=>).any_number_of_times.and_return(-1) + (a <= b).should == true + + a.should_receive(:<=>).any_number_of_times.and_return(-0.1) + (a <= b).should == true + + a.should_receive(:<=>).any_number_of_times.and_return(-10000000) + (a <= b).should == true + end + + it "returns false if calling #<=> on self returns any Integer greater than 0" do + a = ComparableSpecs::Weird.new(0) + b = ComparableSpecs::Weird.new(10) + + a.should_receive(:<=>).any_number_of_times.and_return(0.1) + (a <= b).should == false + + a.should_receive(:<=>).any_number_of_times.and_return(1.0) + (a <= b).should == false + + a.should_receive(:<=>).any_number_of_times.and_return(10000000) + (a <= b).should == false + end + + it "raises an ArgumentError if calling #<=> on self returns nil" do + a = ComparableSpecs::Weird.new(0) + b = ComparableSpecs::Weird.new(20) + + a.should_receive(:<=>).any_number_of_times.and_return(nil) + -> { (a <= b) }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/complex/abs2_spec.rb b/spec/ruby/core/complex/abs2_spec.rb new file mode 100644 index 0000000000..3e5c5fd225 --- /dev/null +++ b/spec/ruby/core/complex/abs2_spec.rb @@ -0,0 +1,9 @@ +require_relative '../../spec_helper' + +describe "Complex#abs2" do + it "returns the sum of the squares of the real and imaginary parts" do + Complex(1, -2).abs2.should == 1 + 4 + Complex(-0.1, 0.2).abs2.should be_close(0.01 + 0.04, TOLERANCE) + Complex(0).abs2.should == 0 + end +end diff --git a/spec/ruby/core/complex/abs_spec.rb b/spec/ruby/core/complex/abs_spec.rb new file mode 100644 index 0000000000..43912c517f --- /dev/null +++ b/spec/ruby/core/complex/abs_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/abs' + +describe "Complex#abs" do + it_behaves_like :complex_abs, :abs +end diff --git a/spec/ruby/core/complex/angle_spec.rb b/spec/ruby/core/complex/angle_spec.rb new file mode 100644 index 0000000000..4aa176956f --- /dev/null +++ b/spec/ruby/core/complex/angle_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/arg' + +describe "Complex#angle" do + it_behaves_like :complex_arg, :angle +end diff --git a/spec/ruby/core/complex/arg_spec.rb b/spec/ruby/core/complex/arg_spec.rb new file mode 100644 index 0000000000..009f19429f --- /dev/null +++ b/spec/ruby/core/complex/arg_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/arg' + +describe "Complex#arg" do + it_behaves_like :complex_arg, :arg +end diff --git a/spec/ruby/core/complex/coerce_spec.rb b/spec/ruby/core/complex/coerce_spec.rb new file mode 100644 index 0000000000..a30a6c1d5f --- /dev/null +++ b/spec/ruby/core/complex/coerce_spec.rb @@ -0,0 +1,70 @@ +require_relative '../../spec_helper' + +describe "Complex#coerce" do + before :each do + @one = Complex(1) + end + + it "returns an array containing other and self as Complex when other is an Integer" do + result = @one.coerce(2) + result.should == [2, 1] + result.first.should be_kind_of(Complex) + result.last.should be_kind_of(Complex) + end + + it "returns an array containing other and self as Complex when other is a Float" do + result = @one.coerce(20.5) + result.should == [20.5, 1] + result.first.should be_kind_of(Complex) + result.last.should be_kind_of(Complex) + end + + it "returns an array containing other and self as Complex when other is a Bignum" do + result = @one.coerce(4294967296) + result.should == [4294967296, 1] + result.first.should be_kind_of(Complex) + result.last.should be_kind_of(Complex) + end + + it "returns an array containing other and self as Complex when other is a Rational" do + result = @one.coerce(Rational(5,6)) + result.should == [Rational(5,6), 1] + result.first.should be_kind_of(Complex) + result.last.should be_kind_of(Complex) + end + + it "returns an array containing other and self when other is a Complex" do + other = Complex(2) + result = @one.coerce(other) + result.should == [other, @one] + result.first.should equal(other) + result.last.should equal(@one) + end + + it "returns an array containing other as Complex and self when other is a Numeric which responds to #real? with true" do + other = mock_numeric('other') + other.should_receive(:real?).any_number_of_times.and_return(true) + result = @one.coerce(other) + result.should == [other, @one] + result.first.should eql(Complex(other)) + result.last.should equal(@one) + end + + it "raises TypeError when other is a Numeric which responds to #real? with false" do + other = mock_numeric('other') + other.should_receive(:real?).any_number_of_times.and_return(false) + -> { @one.coerce(other) }.should raise_error(TypeError) + end + + it "raises a TypeError when other is a String" do + -> { @one.coerce("20") }.should raise_error(TypeError) + end + + it "raises a TypeError when other is nil" do + -> { @one.coerce(nil) }.should raise_error(TypeError) + end + + it "raises a TypeError when other is false" do + -> { @one.coerce(false) }.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/complex/comparison_spec.rb b/spec/ruby/core/complex/comparison_spec.rb new file mode 100644 index 0000000000..3a3142f234 --- /dev/null +++ b/spec/ruby/core/complex/comparison_spec.rb @@ -0,0 +1,25 @@ +require_relative '../../spec_helper' + +describe "Complex#<=>" do + it "returns nil if either self or argument has imaginary part" do + (Complex(5, 1) <=> Complex(2)).should be_nil + (Complex(1) <=> Complex(2, 1)).should be_nil + (5 <=> Complex(2, 1)).should be_nil + end + + it "returns nil if argument is not numeric" do + (Complex(5, 1) <=> "cmp").should be_nil + (Complex(1) <=> "cmp").should be_nil + (Complex(1) <=> Object.new).should be_nil + end + + it "returns 0, 1, or -1 if self and argument do not have imaginary part" do + (Complex(5) <=> Complex(2)).should == 1 + (Complex(2) <=> Complex(3)).should == -1 + (Complex(2) <=> Complex(2)).should == 0 + + (Complex(5) <=> 2).should == 1 + (Complex(2) <=> 3).should == -1 + (Complex(2) <=> 2).should == 0 + end +end diff --git a/spec/ruby/core/complex/conj_spec.rb b/spec/ruby/core/complex/conj_spec.rb new file mode 100644 index 0000000000..5e3bc1acb8 --- /dev/null +++ b/spec/ruby/core/complex/conj_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/conjugate' + +describe "Complex#conj" do + it_behaves_like :complex_conjugate, :conj +end diff --git a/spec/ruby/core/complex/conjugate_spec.rb b/spec/ruby/core/complex/conjugate_spec.rb new file mode 100644 index 0000000000..f658bab4da --- /dev/null +++ b/spec/ruby/core/complex/conjugate_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/conjugate' + +describe "Complex#conjugate" do + it_behaves_like :complex_conjugate, :conjugate +end diff --git a/spec/ruby/core/complex/constants_spec.rb b/spec/ruby/core/complex/constants_spec.rb new file mode 100644 index 0000000000..50303de16c --- /dev/null +++ b/spec/ruby/core/complex/constants_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' + +describe "Complex::I" do + it "is Complex(0, 1)" do + Complex::I.should eql(Complex(0, 1)) + end +end diff --git a/spec/ruby/core/complex/denominator_spec.rb b/spec/ruby/core/complex/denominator_spec.rb new file mode 100644 index 0000000000..c1a2003820 --- /dev/null +++ b/spec/ruby/core/complex/denominator_spec.rb @@ -0,0 +1,13 @@ +require_relative '../../spec_helper' + +describe "Complex#denominator" do + it "returns the least common multiple denominator of the real and imaginary parts" do + Complex(3, 4).denominator.should == 1 + Complex(3, bignum_value).denominator.should == 1 + + Complex(3, Rational(3,4)).denominator.should == 4 + + Complex(Rational(4,8), Rational(3,4)).denominator.should == 4 + Complex(Rational(3,8), Rational(3,4)).denominator.should == 8 + end +end diff --git a/spec/ruby/core/complex/divide_spec.rb b/spec/ruby/core/complex/divide_spec.rb new file mode 100644 index 0000000000..bebf862312 --- /dev/null +++ b/spec/ruby/core/complex/divide_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/divide' + +describe "Complex#/" do + it_behaves_like :complex_divide, :/ +end diff --git a/spec/ruby/core/complex/eql_spec.rb b/spec/ruby/core/complex/eql_spec.rb new file mode 100644 index 0000000000..9194efc074 --- /dev/null +++ b/spec/ruby/core/complex/eql_spec.rb @@ -0,0 +1,31 @@ +require_relative '../../spec_helper' + +describe "Complex#eql?" do + it "returns false if other is not Complex" do + Complex(1).eql?(1).should be_false + end + + it "returns true when the respective parts are of the same classes and self == other" do + Complex(1, 2).eql?(Complex(1, 2)).should be_true + end + + it "returns false when the real parts are of different classes" do + Complex(1).eql?(Complex(1.0)).should be_false + end + + it "returns false when the imaginary parts are of different classes" do + Complex(1, 2).eql?(Complex(1, 2.0)).should be_false + end + + it "returns false when self == other is false" do + Complex(1, 2).eql?(Complex(2, 3)).should be_false + end + + it "does NOT send #eql? to real or imaginary parts" do + real = mock_numeric('real') + imag = mock_numeric('imag') + real.should_not_receive(:eql?) + imag.should_not_receive(:eql?) + Complex(real, imag).eql?(Complex(real, imag)).should be_true + end +end diff --git a/spec/ruby/core/complex/equal_value_spec.rb b/spec/ruby/core/complex/equal_value_spec.rb new file mode 100644 index 0000000000..97c486d820 --- /dev/null +++ b/spec/ruby/core/complex/equal_value_spec.rb @@ -0,0 +1,93 @@ +require_relative '../../spec_helper' + +describe "Complex#==" do + describe "with Complex" do + it "returns true when self and other have numerical equality" do + Complex(1, 2).should == Complex(1, 2) + Complex(3, 9).should == Complex(3, 9) + Complex(-3, -9).should == Complex(-3, -9) + + Complex(1, 2).should_not == Complex(3, 4) + Complex(3, 9).should_not == Complex(9, 3) + + Complex(1.0, 2.0).should == Complex(1, 2) + Complex(3.0, 9.0).should_not == Complex(9.0, 3.0) + + Complex(1.5, 2.5).should == Complex(1.5, 2.5) + Complex(1.5, 2.5).should == Complex(1.5, 2.5) + Complex(-1.5, 2.5).should == Complex(-1.5, 2.5) + + Complex(1.5, 2.5).should_not == Complex(2.5, 1.5) + Complex(3.75, 2.5).should_not == Complex(1.5, 2.5) + + Complex(bignum_value, 2.5).should == Complex(bignum_value, 2.5) + Complex(3.75, bignum_value).should_not == Complex(1.5, bignum_value) + + Complex(nan_value).should_not == Complex(nan_value) + end + end + + describe "with Numeric" do + it "returns true when self's imaginary part is 0 and the real part and other have numerical equality" do + Complex(3, 0).should == 3 + Complex(-3, 0).should == -3 + + Complex(3.5, 0).should == 3.5 + Complex(-3.5, 0).should == -3.5 + + Complex(bignum_value, 0).should == bignum_value + Complex(-bignum_value, 0).should == -bignum_value + + Complex(3.0, 0).should == 3 + Complex(-3.0, 0).should == -3 + + Complex(3, 0).should_not == 4 + Complex(-3, 0).should_not == -4 + + Complex(3.5, 0).should_not == -4.5 + Complex(-3.5, 0).should_not == 2.5 + + Complex(bignum_value, 0).should_not == bignum_value(10) + Complex(-bignum_value, 0).should_not == -bignum_value(20) + end + end + + describe "with Object" do + # Integer#== and Float#== only return booleans - Bug? + it "calls other#== with self" do + value = Complex(3, 0) + + obj = mock("Object") + obj.should_receive(:==).with(value).and_return(:expected) + + (value == obj).should_not be_false + end + end + + describe "with a Numeric which responds to #real? with true" do + before do + @other = mock_numeric('other') + @other.should_receive(:real?).any_number_of_times.and_return(true) + end + + it "returns real == other when the imaginary part is zero" do + real = mock_numeric('real') + real.should_receive(:==).with(@other).and_return(true) + (Complex(real, 0) == @other).should be_true + end + + it "returns false when the imaginary part is not zero" do + (Complex(3, 1) == @other).should be_false + end + end + + describe "with a Numeric which responds to #real? with false" do + it "returns other == self" do + complex = Complex(3, 0) + other = mock_numeric('other') + other.should_receive(:real?).any_number_of_times.and_return(false) + other.should_receive(:==).with(complex).and_return(true) + (complex == other).should be_true + end + end +end diff --git a/spec/ruby/core/complex/exponent_spec.rb b/spec/ruby/core/complex/exponent_spec.rb new file mode 100644 index 0000000000..86f827aece --- /dev/null +++ b/spec/ruby/core/complex/exponent_spec.rb @@ -0,0 +1,61 @@ +require_relative '../../spec_helper' + +describe "Complex#**" do + describe "with Integer 0" do + it "returns Complex(1)" do + (Complex(3, 4) ** 0).should eql(Complex(1)) + end + end + + describe "with Float 0.0" do + it "returns Complex(1.0, 0.0)" do + (Complex(3, 4) ** 0.0).should eql(Complex(1.0, 0.0)) + end + end + + describe "with Complex" do + it "returns self raised to the given power" do + (Complex(2, 1) ** Complex(2, 1)).should be_close(Complex(-0.504824688978319, 3.10414407699553), TOLERANCE) + (Complex(2, 1) ** Complex(3, 4)).should be_close(Complex(-0.179174656916581, -1.74071656397662), TOLERANCE) + + (Complex(2, 1) ** Complex(-2, -1)).should be_close(Complex(-0.051041070450869, -0.313849223270419), TOLERANCE) + (Complex(-2, -1) ** Complex(2, 1)).should be_close(Complex(-11.6819929610857, 71.8320439736158), TOLERANCE) + end + end + + describe "with Integer" do + it "returns self raised to the given power" do + (Complex(2, 1) ** 2).should == Complex(3, 4) + (Complex(3, 4) ** 2).should == Complex(-7, 24) + (Complex(3, 4) ** -2).should be_close(Complex(-0.0112, -0.0384), TOLERANCE) + + + (Complex(2, 1) ** 2.5).should be_close(Complex(2.99179707178602, 6.85206901006896), TOLERANCE) + (Complex(3, 4) ** 2.5).should be_close(Complex(-38.0, 41.0), TOLERANCE) + (Complex(3, 4) ** -2.5).should be_close(Complex(-0.01216, -0.01312), TOLERANCE) + + (Complex(1) ** 1).should == Complex(1) + + # NOTE: Takes way too long... + #(Complex(2, 1) ** bignum_value) + end + end + + describe "with Rational" do + it "returns self raised to the given power" do + (Complex(2, 1) ** Rational(3, 4)).should be_close(Complex(1.71913265276568, 0.623124744394697), TOLERANCE) + (Complex(2, 1) ** Rational(4, 3)).should be_close(Complex(2.3828547125173, 1.69466313833091), TOLERANCE) + (Complex(2, 1) ** Rational(-4, 3)).should be_close(Complex(0.278700377879388, -0.198209003071003), TOLERANCE) + end + end + + describe "with Object" do + it "tries to coerce self into other" do + value = Complex(3, 9) + + obj = mock("Object") + obj.should_receive(:coerce).with(value).and_return([2, 5]) + (value ** obj).should == 2 ** 5 + end + end +end diff --git a/spec/ruby/core/complex/fdiv_spec.rb b/spec/ruby/core/complex/fdiv_spec.rb new file mode 100644 index 0000000000..68f7d1b309 --- /dev/null +++ b/spec/ruby/core/complex/fdiv_spec.rb @@ -0,0 +1,129 @@ +require_relative '../../spec_helper' + +describe "Complex#fdiv" do + it "accepts a numeric argument" do + -> { Complex(20).fdiv(2) }.should_not raise_error(TypeError) + -> { Complex(20).fdiv(2.0) }.should_not raise_error(TypeError) + -> { Complex(20).fdiv(bignum_value) }.should_not raise_error(TypeError) + end + + it "accepts a negative numeric argument" do + -> { Complex(20).fdiv(-2) }.should_not raise_error(TypeError) + -> { Complex(20).fdiv(-2.0) }.should_not raise_error(TypeError) + -> { Complex(20).fdiv(-bignum_value) }.should_not raise_error(TypeError) + end + + it "raises a TypeError if passed a non-numeric argument" do + -> { Complex(20).fdiv([]) }.should raise_error(TypeError) + -> { Complex(20).fdiv(:sym) }.should raise_error(TypeError) + -> { Complex(20).fdiv('s') }.should raise_error(TypeError) + end + + it "sets the real part to NaN if self's real part is NaN" do + Complex(nan_value).fdiv(2).real.nan?.should be_true + end + + it "sets the imaginary part to NaN if self's imaginary part is NaN" do + Complex(2, nan_value).fdiv(2).imag.nan?.should be_true + end + + it "sets the real and imaginary part to NaN if self's real and imaginary parts are NaN" do + Complex(nan_value, nan_value).fdiv(2).imag.nan?.should be_true + Complex(nan_value, nan_value).fdiv(2).real.nan?.should be_true + end + + it "sets the real and imaginary part to NaN if self's real part and the argument are both NaN" do + Complex(nan_value, 2).fdiv(nan_value).imag.nan?.should be_true + Complex(nan_value, 2).fdiv(nan_value).real.nan?.should be_true + end + + it "sets the real and imaginary part to NaN if self's real part, self's imaginary part, and the argument are NaN" do + Complex(nan_value, nan_value).fdiv(nan_value).imag.nan?.should be_true + Complex(nan_value, nan_value).fdiv(nan_value).real.nan?.should be_true + end + + it "sets the real part to Infinity if self's real part is Infinity" do + Complex(infinity_value).fdiv(2).real.infinite?.should == 1 + Complex(infinity_value,2).fdiv(2).real.infinite?.should == 1 + end + + it "sets the imaginary part to Infinity if self's imaginary part is Infinity" do + Complex(2, infinity_value).fdiv(2).imag.infinite?.should == 1 + Complex(2, infinity_value).fdiv(2).imag.infinite?.should == 1 + end + + it "sets the imaginary and real part to Infinity if self's imaginary and real parts are Infinity" do + Complex(infinity_value, infinity_value).fdiv(2).real.infinite?.should == 1 + Complex(infinity_value, infinity_value).fdiv(2).imag.infinite?.should == 1 + end + + it "sets the real part to NaN and the imaginary part to NaN if self's imaginary part, self's real part, and the argument are Infinity" do + Complex(infinity_value, infinity_value).fdiv(infinity_value).real.nan?.should be_true + Complex(infinity_value, infinity_value).fdiv(infinity_value).imag.nan?.should be_true + end +end + +describe "Complex#fdiv with no imaginary part" do + before :each do + @numbers = [1, 5.43, 10, bignum_value, 99872.2918710].map{|n| [n,-n]}.flatten + end + + it "returns a Complex number" do + @numbers.each do |real| + @numbers.each do |other| + Complex(real).fdiv(other).should be_an_instance_of(Complex) + end + end + end + + it "sets the real part to self's real part fdiv'd with the argument" do + @numbers.each do |real| + @numbers.each do |other| + Complex(real).fdiv(other).real.should == real.fdiv(other) + end + end + end + + it "sets the imaginary part to 0.0" do + @numbers.each do |real| + @numbers.each do |other| + Complex(real).fdiv(other).imaginary.should == 0.0 + end + end + end +end + +describe "Complex#fdiv with an imaginary part" do + before :each do + @numbers = [1, 5.43, 10, bignum_value, 99872.2918710].map{|n| [n,-n]}.flatten + end + + it "returns a Complex number" do + @numbers.each do |real| + @numbers.each_with_index do |other,idx| + Complex( + real,@numbers[idx == 0 ? -1 : idx-1] + ).fdiv(other).should be_an_instance_of(Complex) + end + end + end + + it "sets the real part to self's real part fdiv'd with the argument" do + @numbers.each do |real| + @numbers.each_with_index do |other,idx| + Complex( + real,@numbers[idx == 0 ? -1 : idx-1] + ).fdiv(other).real.should == real.fdiv(other) + end + end + end + + it "sets the imaginary part to the imaginary part fdiv'd with the argument" do + @numbers.each do |real| + @numbers.each_with_index do |other,idx| + im = @numbers[idx == 0 ? -1 : idx-1] + Complex(real, im).fdiv(other).imag.should == im.fdiv(other) + end + end + end +end diff --git a/spec/ruby/core/complex/finite_spec.rb b/spec/ruby/core/complex/finite_spec.rb new file mode 100644 index 0000000000..7d9f82404e --- /dev/null +++ b/spec/ruby/core/complex/finite_spec.rb @@ -0,0 +1,32 @@ +require_relative '../../spec_helper' + +describe "Complex#finite?" do + it "returns true if magnitude is finite" do + (1+1i).should.finite? + end + + it "returns false for positive infinity" do + value = Complex(Float::INFINITY, 42) + value.should_not.finite? + end + + it "returns false for positive complex with infinite imaginary" do + value = Complex(1, Float::INFINITY) + value.should_not.finite? + end + + it "returns false for negative infinity" do + value = -Complex(Float::INFINITY, 42) + value.should_not.finite? + end + + it "returns false for negative complex with infinite imaginary" do + value = -Complex(1, Float::INFINITY) + value.should_not.finite? + end + + it "returns false for NaN" do + value = Complex(Float::NAN, Float::NAN) + value.should_not.finite? + end +end diff --git a/spec/ruby/core/complex/hash_spec.rb b/spec/ruby/core/complex/hash_spec.rb new file mode 100644 index 0000000000..cad283309d --- /dev/null +++ b/spec/ruby/core/complex/hash_spec.rb @@ -0,0 +1,16 @@ +require_relative '../../spec_helper' + +describe "Complex#hash" do + it "is static" do + Complex(1).hash.should == Complex(1).hash + Complex(1, 0).hash.should == Complex(1).hash + Complex(1, 1).hash.should == Complex(1, 1).hash + end + + it "is different for different instances" do + Complex(1, 2).hash.should_not == Complex(1, 1).hash + Complex(2, 1).hash.should_not == Complex(1, 1).hash + + Complex(1, 2).hash.should_not == Complex(2, 1).hash + end +end diff --git a/spec/ruby/core/complex/imag_spec.rb b/spec/ruby/core/complex/imag_spec.rb new file mode 100644 index 0000000000..2bafd1ab54 --- /dev/null +++ b/spec/ruby/core/complex/imag_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/image' + +describe "Complex#imag" do + it_behaves_like :complex_image, :imag +end diff --git a/spec/ruby/core/complex/imaginary_spec.rb b/spec/ruby/core/complex/imaginary_spec.rb new file mode 100644 index 0000000000..a8a1bfea90 --- /dev/null +++ b/spec/ruby/core/complex/imaginary_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/image' + +describe "Complex#imaginary" do + it_behaves_like :complex_image, :imaginary +end diff --git a/spec/ruby/core/complex/infinite_spec.rb b/spec/ruby/core/complex/infinite_spec.rb new file mode 100644 index 0000000000..9e48860dee --- /dev/null +++ b/spec/ruby/core/complex/infinite_spec.rb @@ -0,0 +1,32 @@ +require_relative '../../spec_helper' + +describe "Complex#infinite?" do + it "returns nil if magnitude is finite" do + (1+1i).infinite?.should == nil + end + + it "returns 1 for positive infinity" do + value = Complex(Float::INFINITY, 42).infinite? + value.should == 1 + end + + it "returns 1 for positive complex with infinite imaginary" do + value = Complex(1, Float::INFINITY).infinite? + value.should == 1 + end + + it "returns -1 for negative infinity" do + value = -Complex(Float::INFINITY, 42).infinite? + value.should == -1 + end + + it "returns -1 for negative complex with infinite imaginary" do + value = -Complex(1, Float::INFINITY).infinite? + value.should == -1 + end + + it "returns nil for NaN" do + value = Complex(0, Float::NAN).infinite? + value.should == nil + end +end diff --git a/spec/ruby/core/complex/inspect_spec.rb b/spec/ruby/core/complex/inspect_spec.rb new file mode 100644 index 0000000000..045be94b22 --- /dev/null +++ b/spec/ruby/core/complex/inspect_spec.rb @@ -0,0 +1,37 @@ +require_relative '../../spec_helper' +require_relative '../numeric/fixtures/classes' + +describe "Complex#inspect" do + it "returns (${real}+${image}i) for positive imaginary parts" do + Complex(1).inspect.should == "(1+0i)" + Complex(7).inspect.should == "(7+0i)" + Complex(-1, 4).inspect.should == "(-1+4i)" + Complex(-7, 6.7).inspect.should == "(-7+6.7i)" + end + + it "returns (${real}-${image}i) for negative imaginary parts" do + Complex(0, -1).inspect.should == "(0-1i)" + Complex(-1, -4).inspect.should == "(-1-4i)" + Complex(-7, -6.7).inspect.should == "(-7-6.7i)" + end + + it "calls #inspect on real and imaginary" do + real = NumericSpecs::Subclass.new + # + because of https://bugs.ruby-lang.org/issues/20337 + real.should_receive(:inspect).and_return(+"1") + imaginary = NumericSpecs::Subclass.new + imaginary.should_receive(:inspect).and_return("2") + imaginary.should_receive(:<).any_number_of_times.and_return(false) + Complex(real, imaginary).inspect.should == "(1+2i)" + end + + it "adds an `*' before the `i' if the last character of the imaginary part is not numeric" do + real = NumericSpecs::Subclass.new + # + because of https://bugs.ruby-lang.org/issues/20337 + real.should_receive(:inspect).and_return(+"(1)") + imaginary = NumericSpecs::Subclass.new + imaginary.should_receive(:inspect).and_return("(2)") + imaginary.should_receive(:<).any_number_of_times.and_return(false) + Complex(real, imaginary).inspect.should == "((1)+(2)*i)" + end +end diff --git a/spec/ruby/core/complex/integer_spec.rb b/spec/ruby/core/complex/integer_spec.rb new file mode 100644 index 0000000000..0957accb70 --- /dev/null +++ b/spec/ruby/core/complex/integer_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' + +describe "Complex#integer?" do + it "returns false for a Complex with no imaginary part" do + Complex(20).integer?.should be_false + end + + it "returns false for a Complex with an imaginary part" do + Complex(20,3).integer?.should be_false + end +end diff --git a/spec/ruby/core/complex/magnitude_spec.rb b/spec/ruby/core/complex/magnitude_spec.rb new file mode 100644 index 0000000000..86f3b29868 --- /dev/null +++ b/spec/ruby/core/complex/magnitude_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/abs' + +describe "Complex#magnitude" do + it_behaves_like :complex_abs, :magnitude +end diff --git a/spec/ruby/core/complex/marshal_dump_spec.rb b/spec/ruby/core/complex/marshal_dump_spec.rb new file mode 100644 index 0000000000..116899b0ad --- /dev/null +++ b/spec/ruby/core/complex/marshal_dump_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' + +describe "Complex#marshal_dump" do + it "is a private method" do + Complex.should have_private_instance_method(:marshal_dump, false) + end + + it "dumps real and imaginary parts" do + Complex(1, 2).send(:marshal_dump).should == [1, 2] + end +end diff --git a/spec/ruby/core/complex/minus_spec.rb b/spec/ruby/core/complex/minus_spec.rb new file mode 100644 index 0000000000..7c104ce784 --- /dev/null +++ b/spec/ruby/core/complex/minus_spec.rb @@ -0,0 +1,45 @@ +require_relative '../../spec_helper' + +describe "Complex#-" do + describe "with Complex" do + it "subtracts both the real and imaginary components" do + (Complex(1, 2) - Complex(10, 20)).should == Complex(1 - 10, 2 - 20) + (Complex(1.5, 2.1) - Complex(100.2, -30.3)).should == Complex(1.5 - 100.2, 2.1 - (-30.3)) + end + end + + describe "with Integer" do + it "subtracts the real number from the real component of self" do + (Complex(1, 2) - 50).should == Complex(-49, 2) + (Complex(1, 2) - 50.5).should == Complex(-49.5, 2) + end + end + + describe "with Object" do + it "tries to coerce self into other" do + value = Complex(3, 9) + + obj = mock("Object") + obj.should_receive(:coerce).with(value).and_return([2, 5]) + (value - obj).should == 2 - 5 + end + end + + describe "passed Numeric which responds to #real? with true" do + it "coerces the passed argument to the type of the real part and subtracts the resulting elements" do + n = mock_numeric('n') + n.should_receive(:real?).and_return(true) + n.should_receive(:coerce).with(1).and_return([1, 4]) + (Complex(1, 2) - n).should == Complex(-3, 2) + end + end + + describe "passed Numeric which responds to #real? with false" do + it "coerces the passed argument to Complex and subtracts the resulting elements" do + n = mock_numeric('n') + n.should_receive(:real?).and_return(false) + n.should_receive(:coerce).with(Complex(1, 2)).and_return([Complex(1, 2), Complex(3, 4)]) + (Complex(1, 2) - n).should == Complex(-2, -2) + end + end +end diff --git a/spec/ruby/core/complex/multiply_spec.rb b/spec/ruby/core/complex/multiply_spec.rb new file mode 100644 index 0000000000..35bf7c8455 --- /dev/null +++ b/spec/ruby/core/complex/multiply_spec.rb @@ -0,0 +1,49 @@ +require_relative '../../spec_helper' + +describe "Complex#*" do + describe "with Complex" do + it "multiplies according to the usual rule for complex numbers: (a + bi) * (c + di) = ac - bd + (ad + bc)i" do + (Complex(1, 2) * Complex(10, 20)).should == Complex((1 * 10) - (2 * 20), (1 * 20) + (2 * 10)) + (Complex(1.5, 2.1) * Complex(100.2, -30.3)).should == Complex((1.5 * 100.2) - (2.1 * -30.3), (1.5 * -30.3) + (2.1 * 100.2)) + end + end + + describe "with Integer" do + it "multiplies both parts of self by the given Integer" do + (Complex(3, 2) * 50).should == Complex(150, 100) + (Complex(-3, 2) * 50.5).should == Complex(-151.5, 101) + end + end + + describe "with Object" do + it "tries to coerce self into other" do + value = Complex(3, 9) + + obj = mock("Object") + obj.should_receive(:coerce).with(value).and_return([2, 5]) + (value * obj).should == 2 * 5 + end + end + + describe "with a Numeric which responds to #real? with true" do + it "multiples both parts of self by other" do + other = mock_numeric('other') + real = mock_numeric('real') + imag = mock_numeric('imag') + other.should_receive(:real?).and_return(true) + real.should_receive(:*).with(other).and_return(1) + imag.should_receive(:*).with(other).and_return(2) + (Complex(real, imag) * other).should == Complex(1, 2) + end + + describe "with a Numeric which responds to #real? with false" do + it "coerces the passed argument to Complex and multiplies the resulting elements" do + complex = Complex(3, 0) + other = mock_numeric('other') + other.should_receive(:real?).any_number_of_times.and_return(false) + other.should_receive(:coerce).with(complex).and_return([5, 2]) + (complex * other).should == 10 + end + end + end +end diff --git a/spec/ruby/core/complex/negative_spec.rb b/spec/ruby/core/complex/negative_spec.rb new file mode 100644 index 0000000000..62ab89c04a --- /dev/null +++ b/spec/ruby/core/complex/negative_spec.rb @@ -0,0 +1,13 @@ +require_relative '../../spec_helper' + +describe "Complex#negative?" do + it "is undefined" do + c = Complex(1) + + c.methods.should_not include(:negative?) + + -> { + c.negative? + }.should raise_error(NoMethodError) + end +end diff --git a/spec/ruby/core/complex/numerator_spec.rb b/spec/ruby/core/complex/numerator_spec.rb new file mode 100644 index 0000000000..7ab66e6a61 --- /dev/null +++ b/spec/ruby/core/complex/numerator_spec.rb @@ -0,0 +1,19 @@ +require_relative '../../spec_helper' + +describe "Complex#numerator" do + it "returns self's numerator" do + Complex(2).numerator.should == Complex(2) + Complex(3, 4).numerator.should == Complex(3, 4) + + Complex(Rational(3, 4), Rational(3, 4)).numerator.should == Complex(3, 3) + Complex(Rational(7, 4), Rational(8, 4)).numerator.should == Complex(7, 8) + + Complex(Rational(7, 8), Rational(8, 4)).numerator.should == Complex(7, 16) + Complex(Rational(7, 4), Rational(8, 8)).numerator.should == Complex(7, 4) + + # NOTE: + # Bug? - Fails with a MethodMissingError + # (undefined method `denominator' for 3.5:Float) + # Complex(3.5, 3.7).numerator + end +end diff --git a/spec/ruby/core/complex/phase_spec.rb b/spec/ruby/core/complex/phase_spec.rb new file mode 100644 index 0000000000..89574bf533 --- /dev/null +++ b/spec/ruby/core/complex/phase_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/arg' + +describe "Complex#phase" do + it_behaves_like :complex_arg, :phase +end diff --git a/spec/ruby/core/complex/plus_spec.rb b/spec/ruby/core/complex/plus_spec.rb new file mode 100644 index 0000000000..2056ca786c --- /dev/null +++ b/spec/ruby/core/complex/plus_spec.rb @@ -0,0 +1,45 @@ +require_relative '../../spec_helper' + +describe "Complex#+" do + describe "with Complex" do + it "adds both the real and imaginary components" do + (Complex(1, 2) + Complex(10, 20)).should == Complex(1 + 10, 2 + 20) + (Complex(1.5, 2.1) + Complex(100.2, -30.3)).should == Complex(1.5 + 100.2, 2.1 + (-30.3)) + end + end + + describe "with Integer" do + it "adds the real number to the real component of self" do + (Complex(1, 2) + 50).should == Complex(51, 2) + (Complex(1, 2) + 50.5).should == Complex(51.5, 2) + end + end + + describe "with Object" do + it "tries to coerce self into other" do + value = Complex(3, 9) + + obj = mock("Object") + obj.should_receive(:coerce).with(value).and_return([2, 5]) + (value + obj).should == 2 + 5 + end + end + + describe "passed Numeric which responds to #real? with true" do + it "coerces the passed argument to the type of the real part and adds the resulting elements" do + n = mock_numeric('n') + n.should_receive(:real?).and_return(true) + n.should_receive(:coerce).with(1).and_return([1, 4]) + (Complex(1, 2) + n).should == Complex(5, 2) + end + end + + describe "passed Numeric which responds to #real? with false" do + it "coerces the passed argument to Complex and adds the resulting elements" do + n = mock_numeric('n') + n.should_receive(:real?).and_return(false) + n.should_receive(:coerce).with(Complex(1, 2)).and_return([Complex(1, 2), Complex(3, 4)]) + (Complex(1, 2) + n).should == Complex(4, 6) + end + end +end diff --git a/spec/ruby/core/complex/polar_spec.rb b/spec/ruby/core/complex/polar_spec.rb new file mode 100644 index 0000000000..56335584ef --- /dev/null +++ b/spec/ruby/core/complex/polar_spec.rb @@ -0,0 +1,41 @@ +require_relative '../../spec_helper' + +describe "Complex.polar" do + it "returns a complex number in terms of radius and angle" do + Complex.polar(50, 60).should be_close(Complex(-47.6206490207578, -15.2405310551108), TOLERANCE) + Complex.polar(-10, -20).should be_close(Complex(-4.08082061813392, 9.12945250727628), TOLERANCE) + end + + it "raises a TypeError when given non real arguments" do + ->{ Complex.polar(nil) }.should raise_error(TypeError) + ->{ Complex.polar(nil, nil) }.should raise_error(TypeError) + end + + it "computes the real values of the real & imaginary parts from the polar form" do + a = Complex.polar(1.0+0.0i, Math::PI/2+0.0i) + a.real.should be_close(0.0, TOLERANCE) + a.imag.should be_close(1.0, TOLERANCE) + a.real.real?.should be_true + a.imag.real?.should be_true + + b = Complex.polar(1+0.0i) + b.real.should be_close(1.0, TOLERANCE) + b.imag.should be_close(0.0, TOLERANCE) + b.real.real?.should be_true + b.imag.real?.should be_true + end +end + +describe "Complex#polar" do + it "returns the absolute value and the argument" do + a = Complex(3, 4) + a.polar.size.should == 2 + a.polar.first.should == 5.0 + a.polar.last.should be_close(0.927295218001612, TOLERANCE) + + b = Complex(-3.5, 4.7) + b.polar.size.should == 2 + b.polar.first.should be_close(5.86003412959345, TOLERANCE) + b.polar.last.should be_close(2.21088447955664, TOLERANCE) + end +end diff --git a/spec/ruby/core/complex/positive_spec.rb b/spec/ruby/core/complex/positive_spec.rb new file mode 100644 index 0000000000..f1bad8608c --- /dev/null +++ b/spec/ruby/core/complex/positive_spec.rb @@ -0,0 +1,13 @@ +require_relative '../../spec_helper' + +describe "Complex#positive?" do + it "is undefined" do + c = Complex(1) + + c.methods.should_not include(:positive?) + + -> { + c.positive? + }.should raise_error(NoMethodError) + end +end diff --git a/spec/ruby/core/complex/quo_spec.rb b/spec/ruby/core/complex/quo_spec.rb new file mode 100644 index 0000000000..ee6fd65c79 --- /dev/null +++ b/spec/ruby/core/complex/quo_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/divide' + +describe "Complex#quo" do + it_behaves_like :complex_divide, :quo +end diff --git a/spec/ruby/core/complex/rationalize_spec.rb b/spec/ruby/core/complex/rationalize_spec.rb new file mode 100644 index 0000000000..043b8ddf2a --- /dev/null +++ b/spec/ruby/core/complex/rationalize_spec.rb @@ -0,0 +1,31 @@ +require_relative '../../spec_helper' + +describe "Complex#rationalize" do + it "raises RangeError if self has non-zero imaginary part" do + -> { Complex(1,5).rationalize }.should raise_error(RangeError) + end + + it "raises RangeError if self has 0.0 imaginary part" do + -> { Complex(1,0.0).rationalize }.should raise_error(RangeError) + end + + it "returns a Rational if self has zero imaginary part" do + Complex(1,0).rationalize.should == Rational(1,1) + Complex(2<<63+5).rationalize.should == Rational(2<<63+5,1) + end + + it "sends #rationalize to the real part" do + real = mock_numeric('real') + real.should_receive(:rationalize).with(0.1).and_return(:result) + Complex(real, 0).rationalize(0.1).should == :result + end + + it "ignores a single argument" do + Complex(1,0).rationalize(0.1).should == Rational(1,1) + end + + it "raises ArgumentError when passed more than one argument" do + -> { Complex(1,0).rationalize(0.1, 0.1) }.should raise_error(ArgumentError) + -> { Complex(1,0).rationalize(0.1, 0.1, 2) }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/complex/real_spec.rb b/spec/ruby/core/complex/real_spec.rb new file mode 100644 index 0000000000..2ea791c005 --- /dev/null +++ b/spec/ruby/core/complex/real_spec.rb @@ -0,0 +1,28 @@ +require_relative '../../spec_helper' + +describe "Complex#real" do + it "returns the real part of self" do + Complex(1, 0).real.should == 1 + Complex(2, 1).real.should == 2 + Complex(6.7, 8.9).real.should == 6.7 + Complex(bignum_value, 3).real.should == bignum_value + end +end + +describe "Complex#real?" do + it "returns false if there is an imaginary part" do + Complex(2,3).real?.should be_false + end + + it "returns false if there is not an imaginary part" do + Complex(2).real?.should be_false + end + + it "returns false if the real part is Infinity" do + Complex(infinity_value).real?.should be_false + end + + it "returns false if the real part is NaN" do + Complex(nan_value).real?.should be_false + end +end diff --git a/spec/ruby/core/complex/rect_spec.rb b/spec/ruby/core/complex/rect_spec.rb new file mode 100644 index 0000000000..9e95f3efc2 --- /dev/null +++ b/spec/ruby/core/complex/rect_spec.rb @@ -0,0 +1,10 @@ +require_relative '../../spec_helper' +require_relative 'shared/rect' + +describe "Complex#rect" do + it_behaves_like :complex_rect, :rect +end + +describe "Complex.rect" do + it_behaves_like :complex_rect_class, :rect +end diff --git a/spec/ruby/core/complex/rectangular_spec.rb b/spec/ruby/core/complex/rectangular_spec.rb new file mode 100644 index 0000000000..d4b8ad9782 --- /dev/null +++ b/spec/ruby/core/complex/rectangular_spec.rb @@ -0,0 +1,10 @@ +require_relative '../../spec_helper' +require_relative 'shared/rect' + +describe "Complex#rectangular" do + it_behaves_like :complex_rect, :rectangular +end + +describe "Complex.rectangular" do + it_behaves_like :complex_rect_class, :rectangular +end diff --git a/spec/ruby/core/complex/shared/abs.rb b/spec/ruby/core/complex/shared/abs.rb new file mode 100644 index 0000000000..2299479341 --- /dev/null +++ b/spec/ruby/core/complex/shared/abs.rb @@ -0,0 +1,10 @@ +describe :complex_abs, shared: true do + it "returns the modulus: |a + bi| = sqrt((a ^ 2) + (b ^ 2))" do + Complex(0, 0).send(@method).should == 0 + Complex(3, 4).send(@method).should == 5 # well-known integer case + Complex(-3, 4).send(@method).should == 5 + Complex(1, -1).send(@method).should be_close(Math.sqrt(2), TOLERANCE) + Complex(6.5, 0).send(@method).should be_close(6.5, TOLERANCE) + Complex(0, -7.2).send(@method).should be_close(7.2, TOLERANCE) + end +end diff --git a/spec/ruby/core/complex/shared/arg.rb b/spec/ruby/core/complex/shared/arg.rb new file mode 100644 index 0000000000..c81f197433 --- /dev/null +++ b/spec/ruby/core/complex/shared/arg.rb @@ -0,0 +1,9 @@ +describe :complex_arg, shared: true do + it "returns the argument -- i.e., the angle from (1, 0) in the complex plane" do + two_pi = 2 * Math::PI + (Complex(1, 0).send(@method) % two_pi).should be_close(0, TOLERANCE) + (Complex(0, 2).send(@method) % two_pi).should be_close(Math::PI * 0.5, TOLERANCE) + (Complex(-100, 0).send(@method) % two_pi).should be_close(Math::PI, TOLERANCE) + (Complex(0, -75.3).send(@method) % two_pi).should be_close(Math::PI * 1.5, TOLERANCE) + end +end diff --git a/spec/ruby/core/complex/shared/conjugate.rb b/spec/ruby/core/complex/shared/conjugate.rb new file mode 100644 index 0000000000..d1ae47bcb6 --- /dev/null +++ b/spec/ruby/core/complex/shared/conjugate.rb @@ -0,0 +1,8 @@ +describe :complex_conjugate, shared: true do + it "returns the complex conjugate: conj a + bi = a - bi" do + Complex(3, 5).send(@method).should == Complex(3, -5) + Complex(3, -5).send(@method).should == Complex(3, 5) + Complex(-3.0, 5.2).send(@method).should be_close(Complex(-3.0, -5.2), TOLERANCE) + Complex(3.0, -5.2).send(@method).should be_close(Complex(3.0, 5.2), TOLERANCE) + end +end diff --git a/spec/ruby/core/complex/shared/divide.rb b/spec/ruby/core/complex/shared/divide.rb new file mode 100644 index 0000000000..a60802c74c --- /dev/null +++ b/spec/ruby/core/complex/shared/divide.rb @@ -0,0 +1,82 @@ +describe :complex_divide, shared: true do + describe "with Complex" do + it "divides according to the usual rule for complex numbers" do + a = Complex((1 * 10) - (2 * 20), (1 * 20) + (2 * 10)) + b = Complex(1, 2) + a.send(@method, b).should == Complex(10, 20) + + c = Complex((1.5 * 100.2) - (2.1 * -30.3), (1.5 * -30.3) + (2.1 * 100.2)) + d = Complex(1.5, 2.1) + # remember the floating-point arithmetic + c.send(@method, d).should be_close(Complex(100.2, -30.3), TOLERANCE) + end + end + + describe "with Fixnum" do + it "divides both parts of the Complex number" do + Complex(20, 40).send(@method, 2).should == Complex(10, 20) + Complex(30, 30).send(@method, 10).should == Complex(3, 3) + end + + it "raises a ZeroDivisionError when given zero" do + -> { Complex(20, 40).send(@method, 0) }.should raise_error(ZeroDivisionError) + end + + it "produces Rational parts" do + Complex(5, 9).send(@method, 2).should eql(Complex(Rational(5,2), Rational(9,2))) + end + end + + describe "with Bignum" do + it "divides both parts of the Complex number" do + Complex(20, 40).send(@method, 2).should == Complex(10, 20) + Complex(15, 16).send(@method, 2.0).should be_close(Complex(7.5, 8), TOLERANCE) + end + end + + describe "with Float" do + it "divides both parts of the Complex number" do + Complex(3, 9).send(@method, 1.5).should == Complex(2, 6) + Complex(15, 16).send(@method, 2.0).should be_close(Complex(7.5, 8), TOLERANCE) + end + + it "returns Complex(Infinity, Infinity) when given zero" do + Complex(20, 40).send(@method, 0.0).real.infinite?.should == 1 + Complex(20, 40).send(@method, 0.0).imag.infinite?.should == 1 + Complex(-20, 40).send(@method, 0.0).real.infinite?.should == -1 + Complex(-20, 40).send(@method, 0.0).imag.infinite?.should == 1 + end + end + + describe "with Object" do + it "tries to coerce self into other" do + value = Complex(3, 9) + + obj = mock("Object") + obj.should_receive(:coerce).with(value).and_return([4, 2]) + value.send(@method, obj).should == 2 + end + end + + describe "with a Numeric which responds to #real? with true" do + it "returns Complex(real.quo(other), imag.quo(other))" do + other = mock_numeric('other') + real = mock_numeric('real') + imag = mock_numeric('imag') + other.should_receive(:real?).and_return(true) + real.should_receive(:quo).with(other).and_return(1) + imag.should_receive(:quo).with(other).and_return(2) + Complex(real, imag).send(@method, other).should == Complex(1, 2) + end + end + + describe "with a Numeric which responds to #real? with false" do + it "coerces the passed argument to Complex and divides the resulting elements" do + complex = Complex(3, 0) + other = mock_numeric('other') + other.should_receive(:real?).any_number_of_times.and_return(false) + other.should_receive(:coerce).with(complex).and_return([5, 2]) + complex.send(@method, other).should eql(Rational(5, 2)) + end + end +end diff --git a/spec/ruby/core/complex/shared/image.rb b/spec/ruby/core/complex/shared/image.rb new file mode 100644 index 0000000000..f839dbcaf9 --- /dev/null +++ b/spec/ruby/core/complex/shared/image.rb @@ -0,0 +1,8 @@ +describe :complex_image, shared: true do + it "returns the imaginary part of self" do + Complex(1, 0).send(@method).should == 0 + Complex(2, 1).send(@method).should == 1 + Complex(6.7, 8.9).send(@method).should == 8.9 + Complex(1, bignum_value).send(@method).should == bignum_value + end +end diff --git a/spec/ruby/core/complex/shared/rect.rb b/spec/ruby/core/complex/shared/rect.rb new file mode 100644 index 0000000000..4ac294e771 --- /dev/null +++ b/spec/ruby/core/complex/shared/rect.rb @@ -0,0 +1,94 @@ +describe :complex_rect, shared: true do + before :each do + @numbers = [ + Complex(1), + Complex(0, 20), + Complex(0, 0), + Complex(0.0), + Complex(9999999**99), + Complex(-20), + Complex.polar(76, 10) + ] + end + + it "returns an Array" do + @numbers.each do |number| + number.send(@method).should be_an_instance_of(Array) + end + end + + it "returns a two-element Array" do + @numbers.each do |number| + number.send(@method).size.should == 2 + end + end + + it "returns the real part of self as the first element" do + @numbers.each do |number| + number.send(@method).first.should == number.real + end + end + + it "returns the imaginary part of self as the last element" do + @numbers.each do |number| + number.send(@method).last.should == number.imaginary + end + end + + it "raises an ArgumentError if given any arguments" do + @numbers.each do |number| + -> { number.send(@method, number) }.should raise_error(ArgumentError) + end + end +end + +describe :complex_rect_class, shared: true do + describe "passed a Numeric n which responds to #real? with true" do + it "returns a Complex with real part n and imaginary part 0" do + n = mock_numeric('n') + n.should_receive(:real?).any_number_of_times.and_return(true) + result = Complex.send(@method, n) + result.real.should == n + result.imag.should == 0 + end + end + + describe "passed a Numeric which responds to #real? with false" do + it "raises TypeError" do + n = mock_numeric('n') + n.should_receive(:real?).any_number_of_times.and_return(false) + -> { Complex.send(@method, n) }.should raise_error(TypeError) + end + end + + describe "passed Numerics n1 and n2 and at least one responds to #real? with false" do + [[false, false], [false, true], [true, false]].each do |r1, r2| + it "raises TypeError" do + n1 = mock_numeric('n1') + n2 = mock_numeric('n2') + n1.should_receive(:real?).any_number_of_times.and_return(r1) + n2.should_receive(:real?).any_number_of_times.and_return(r2) + -> { Complex.send(@method, n1, n2) }.should raise_error(TypeError) + end + end + end + + describe "passed Numerics n1 and n2 and both respond to #real? with true" do + it "returns a Complex with real part n1 and imaginary part n2" do + n1 = mock_numeric('n1') + n2 = mock_numeric('n2') + n1.should_receive(:real?).any_number_of_times.and_return(true) + n2.should_receive(:real?).any_number_of_times.and_return(true) + result = Complex.send(@method, n1, n2) + result.real.should == n1 + result.imag.should == n2 + end + end + + describe "passed a non-Numeric" do + it "raises TypeError" do + -> { Complex.send(@method, :sym) }.should raise_error(TypeError) + -> { Complex.send(@method, 0, :sym) }.should raise_error(TypeError) + end + end +end diff --git a/spec/ruby/core/complex/to_c_spec.rb b/spec/ruby/core/complex/to_c_spec.rb new file mode 100644 index 0000000000..5ce01d9d4e --- /dev/null +++ b/spec/ruby/core/complex/to_c_spec.rb @@ -0,0 +1,12 @@ +require_relative '../../spec_helper' + +describe "Complex#to_c" do + it "returns self" do + value = Complex(1, 5) + value.to_c.should equal(value) + end + + it 'returns the same value' do + Complex(1, 5).to_c.should == Complex(1, 5) + end +end diff --git a/spec/ruby/core/complex/to_f_spec.rb b/spec/ruby/core/complex/to_f_spec.rb new file mode 100644 index 0000000000..b53471c1fc --- /dev/null +++ b/spec/ruby/core/complex/to_f_spec.rb @@ -0,0 +1,41 @@ +require_relative '../../spec_helper' + +describe "Complex#to_f" do + describe "when the imaginary part is Integer 0" do + it "returns the result of sending #to_f to the real part" do + real = mock_numeric('real') + real.should_receive(:to_f).and_return(:f) + Complex(real, 0).to_f.should == :f + end + end + + describe "when the imaginary part is Rational 0" do + it "returns the result of sending #to_f to the real part" do + real = mock_numeric('real') + real.should_receive(:to_f).and_return(:f) + Complex(real, Rational(0)).to_f.should == :f + end + end + + describe "when the imaginary part responds to #== 0 with true" do + it "returns the result of sending #to_f to the real part" do + real = mock_numeric('real') + real.should_receive(:to_f).and_return(:f) + imag = mock_numeric('imag') + imag.should_receive(:==).with(0).any_number_of_times.and_return(true) + Complex(real, imag).to_f.should == :f + end + end + + describe "when the imaginary part is non-zero" do + it "raises RangeError" do + -> { Complex(0, 1).to_f }.should raise_error(RangeError) + end + end + + describe "when the imaginary part is Float 0.0" do + it "raises RangeError" do + -> { Complex(0, 0.0).to_f }.should raise_error(RangeError) + end + end +end diff --git a/spec/ruby/core/complex/to_i_spec.rb b/spec/ruby/core/complex/to_i_spec.rb new file mode 100644 index 0000000000..1e78f5ec0e --- /dev/null +++ b/spec/ruby/core/complex/to_i_spec.rb @@ -0,0 +1,41 @@ +require_relative '../../spec_helper' + +describe "Complex#to_i" do + describe "when the imaginary part is Integer 0" do + it "returns the result of sending #to_i to the real part" do + real = mock_numeric('real') + real.should_receive(:to_i).and_return(:i) + Complex(real, 0).to_i.should == :i + end + end + + describe "when the imaginary part is Rational 0" do + it "returns the result of sending #to_i to the real part" do + real = mock_numeric('real') + real.should_receive(:to_i).and_return(:i) + Complex(real, Rational(0)).to_i.should == :i + end + end + + describe "when the imaginary part responds to #== 0 with true" do + it "returns the result of sending #to_i to the real part" do + real = mock_numeric('real') + real.should_receive(:to_i).and_return(:i) + imag = mock_numeric('imag') + imag.should_receive(:==).with(0).any_number_of_times.and_return(true) + Complex(real, imag).to_i.should == :i + end + end + + describe "when the imaginary part is non-zero" do + it "raises RangeError" do + -> { Complex(0, 1).to_i }.should raise_error(RangeError) + end + end + + describe "when the imaginary part is Float 0.0" do + it "raises RangeError" do + -> { Complex(0, 0.0).to_i }.should raise_error(RangeError) + end + end +end diff --git a/spec/ruby/core/complex/to_r_spec.rb b/spec/ruby/core/complex/to_r_spec.rb new file mode 100644 index 0000000000..788027a500 --- /dev/null +++ b/spec/ruby/core/complex/to_r_spec.rb @@ -0,0 +1,49 @@ +require_relative '../../spec_helper' + +describe "Complex#to_r" do + describe "when the imaginary part is Integer 0" do + it "returns the result of sending #to_r to the real part" do + real = mock_numeric('real') + real.should_receive(:to_r).and_return(:r) + Complex(real, 0).to_r.should == :r + end + end + + describe "when the imaginary part is Rational 0" do + it "returns the result of sending #to_r to the real part" do + real = mock_numeric('real') + real.should_receive(:to_r).and_return(:r) + Complex(real, Rational(0)).to_r.should == :r + end + end + + describe "when the imaginary part responds to #== 0 with true" do + it "returns the result of sending #to_r to the real part" do + real = mock_numeric('real') + real.should_receive(:to_r).and_return(:r) + imag = mock_numeric('imag') + imag.should_receive(:==).with(0).any_number_of_times.and_return(true) + Complex(real, imag).to_r.should == :r + end + end + + describe "when the imaginary part is non-zero" do + it "raises RangeError" do + -> { Complex(0, 1).to_r }.should raise_error(RangeError) + end + end + + describe "when the imaginary part is Float 0.0" do + ruby_version_is ''...'3.4' do + it "raises RangeError" do + -> { Complex(0, 0.0).to_r }.should raise_error(RangeError) + end + end + + ruby_version_is '3.4' do + it "returns a Rational" do + Complex(0, 0.0).to_r.should == 0r + end + end + end +end diff --git a/spec/ruby/core/complex/to_s_spec.rb b/spec/ruby/core/complex/to_s_spec.rb new file mode 100644 index 0000000000..ceccffe470 --- /dev/null +++ b/spec/ruby/core/complex/to_s_spec.rb @@ -0,0 +1,55 @@ +require_relative '../../spec_helper' +require_relative '../numeric/fixtures/classes' + +describe "Complex#to_s" do + describe "when self's real component is 0" do + it "returns both the real and imaginary component even when the real is 0" do + Complex(0, 5).to_s.should == "0+5i" + Complex(0, -3.2).to_s.should == "0-3.2i" + end + end + + it "returns self as String" do + Complex(1, 5).to_s.should == "1+5i" + Complex(-2.5, 1.5).to_s.should == "-2.5+1.5i" + + Complex(1, -5).to_s.should == "1-5i" + Complex(-2.5, -1.5).to_s.should == "-2.5-1.5i" + + # Guard against the Mathn library + guard -> { !defined?(Math.rsqrt) } do + Complex(1, 0).to_s.should == "1+0i" + Complex(1, -0).to_s.should == "1+0i" + end + end + + it "returns 1+0.0i for Complex(1, 0.0)" do + Complex(1, 0.0).to_s.should == "1+0.0i" + end + + it "returns 1-0.0i for Complex(1, -0.0)" do + Complex(1, -0.0).to_s.should == "1-0.0i" + end + + it "returns 1+Infinity*i for Complex(1, Infinity)" do + Complex(1, infinity_value).to_s.should == "1+Infinity*i" + end + + it "returns 1-Infinity*i for Complex(1, -Infinity)" do + Complex(1, -infinity_value).to_s.should == "1-Infinity*i" + end + + it "returns 1+NaN*i for Complex(1, NaN)" do + Complex(1, nan_value).to_s.should == "1+NaN*i" + end + + it "treats real and imaginary parts as strings" do + real = NumericSpecs::Subclass.new + # + because of https://bugs.ruby-lang.org/issues/20337 + real.should_receive(:to_s).and_return(+"1") + imaginary = NumericSpecs::Subclass.new + imaginary.should_receive(:to_s).and_return("2") + imaginary.should_receive(:<).any_number_of_times.and_return(false) + Complex(real, imaginary).to_s.should == "1+2i" + end +end diff --git a/spec/ruby/core/complex/uminus_spec.rb b/spec/ruby/core/complex/uminus_spec.rb new file mode 100644 index 0000000000..c0184e11de --- /dev/null +++ b/spec/ruby/core/complex/uminus_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' + +describe "Complex#-@" do + it "sends #-@ to the real and imaginary parts and returns a Complex with the resulting respective parts" do + real = mock_numeric('real') + imag = mock_numeric('imag') + real.should_receive(:-@).and_return(-1) + imag.should_receive(:-@).and_return(-2) + Complex(real, imag).send(:-@).should == Complex(-1, -2) + end +end diff --git a/spec/ruby/core/conditionvariable/broadcast_spec.rb b/spec/ruby/core/conditionvariable/broadcast_spec.rb new file mode 100644 index 0000000000..55a7b89c72 --- /dev/null +++ b/spec/ruby/core/conditionvariable/broadcast_spec.rb @@ -0,0 +1,39 @@ +require_relative '../../spec_helper' + +describe "ConditionVariable#broadcast" do + it "releases all threads waiting in line for this resource" do + m = Mutex.new + cv = ConditionVariable.new + threads = [] + r1 = [] + r2 = [] + + # large number to attempt to cause race conditions + 100.times do |i| + threads << Thread.new(i) do |tid| + m.synchronize do + r1 << tid + cv.wait(m) + r2 << tid + end + end + end + + # wait for all threads to acquire the mutex the first time + Thread.pass until m.synchronize { r1.size == threads.size } + # wait until all threads are sleeping (ie waiting) + Thread.pass until threads.all?(&:stop?) + + r2.should be_empty + m.synchronize do + cv.broadcast + end + + threads.each {|t| t.join } + + # ensure that all threads that enter cv.wait are released + r2.sort.should == r1.sort + # note that order is not specified as broadcast results in a race + # condition on regaining the lock m + end +end diff --git a/spec/ruby/core/conditionvariable/marshal_dump_spec.rb b/spec/ruby/core/conditionvariable/marshal_dump_spec.rb new file mode 100644 index 0000000000..88b1cc38c1 --- /dev/null +++ b/spec/ruby/core/conditionvariable/marshal_dump_spec.rb @@ -0,0 +1,8 @@ +require_relative '../../spec_helper' + +describe "ConditionVariable#marshal_dump" do + it "raises a TypeError" do + cv = ConditionVariable.new + -> { cv.marshal_dump }.should raise_error(TypeError, /can't dump/) + end +end diff --git a/spec/ruby/core/conditionvariable/signal_spec.rb b/spec/ruby/core/conditionvariable/signal_spec.rb new file mode 100644 index 0000000000..43a9cc611b --- /dev/null +++ b/spec/ruby/core/conditionvariable/signal_spec.rb @@ -0,0 +1,76 @@ +require_relative '../../spec_helper' + +describe "ConditionVariable#signal" do + it "releases the first thread waiting in line for this resource" do + m = Mutex.new + cv = ConditionVariable.new + threads = [] + r1 = [] + r2 = [] + + # large number to attempt to cause race conditions + 100.times do |i| + threads << Thread.new(i) do |tid| + m.synchronize do + r1 << tid + cv.wait(m) + r2 << tid + end + end + end + + # wait for all threads to acquire the mutex the first time + Thread.pass until m.synchronize { r1.size == threads.size } + # wait until all threads are sleeping (ie waiting) + Thread.pass until threads.all?(&:stop?) + + r2.should be_empty + 100.times do |i| + m.synchronize do + cv.signal + end + Thread.pass until r2.size == i+1 + end + + threads.each {|t| t.join } + + # ensure that all the threads that went into the cv.wait are + # released in the same order + r2.should == r1 + end + + it "allows control to be passed between a pair of threads" do + m = Mutex.new + cv = ConditionVariable.new + repeats = 100 + in_synchronize = false + + t1 = Thread.new do + m.synchronize do + in_synchronize = true + repeats.times do + cv.wait(m) + cv.signal + end + end + end + + # Make sure t1 is waiting for a signal before launching t2. + Thread.pass until in_synchronize + Thread.pass until t1.stop? + + t2 = Thread.new do + m.synchronize do + repeats.times do + cv.signal + cv.wait(m) + end + end + end + + # Check that both threads terminated without exception + t1.join + t2.join + m.should_not.locked? + end +end diff --git a/spec/ruby/core/conditionvariable/wait_spec.rb b/spec/ruby/core/conditionvariable/wait_spec.rb new file mode 100644 index 0000000000..fe73e513c0 --- /dev/null +++ b/spec/ruby/core/conditionvariable/wait_spec.rb @@ -0,0 +1,174 @@ +require_relative '../../spec_helper' + +describe "ConditionVariable#wait" do + it "calls #sleep on the given object" do + o = Object.new + o.should_receive(:sleep).with(1234) + + cv = ConditionVariable.new + + cv.wait(o, 1234) + end + + it "can be woken up by ConditionVariable#signal" do + m = Mutex.new + cv = ConditionVariable.new + in_synchronize = false + + th = Thread.new do + m.synchronize do + in_synchronize = true + cv.wait(m) + end + :success + end + + # wait for m to acquire the mutex + Thread.pass until in_synchronize + # wait until th is sleeping (ie waiting) + Thread.pass until th.stop? + + m.synchronize { cv.signal } + th.value.should == :success + end + + it "can be interrupted by Thread#run" do + m = Mutex.new + cv = ConditionVariable.new + in_synchronize = false + + th = Thread.new do + m.synchronize do + in_synchronize = true + cv.wait(m) + end + :success + end + + # wait for m to acquire the mutex + Thread.pass until in_synchronize + # wait until th is sleeping (ie waiting) + Thread.pass until th.stop? + + th.run + th.value.should == :success + end + + it "can be interrupted by Thread#wakeup" do + m = Mutex.new + cv = ConditionVariable.new + in_synchronize = false + + th = Thread.new do + m.synchronize do + in_synchronize = true + cv.wait(m) + end + :success + end + + # wait for m to acquire the mutex + Thread.pass until in_synchronize + # wait until th is sleeping (ie waiting) + Thread.pass until th.stop? + + th.wakeup + th.value.should == :success + end + + it "reacquires the lock even if the thread is killed" do + m = Mutex.new + cv = ConditionVariable.new + in_synchronize = false + owned = nil + + th = Thread.new do + m.synchronize do + in_synchronize = true + begin + cv.wait(m) + ensure + owned = m.owned? + $stderr.puts "\nThe Thread doesn't own the Mutex!" unless owned + end + end + end + + # wait for m to acquire the mutex + Thread.pass until in_synchronize + # wait until th is sleeping (ie waiting) + Thread.pass until th.stop? + + th.kill + th.join + + owned.should == true + end + + it "reacquires the lock even if the thread is killed after being signaled" do + m = Mutex.new + cv = ConditionVariable.new + in_synchronize = false + owned = nil + + th = Thread.new do + m.synchronize do + in_synchronize = true + begin + cv.wait(m) + ensure + owned = m.owned? + $stderr.puts "\nThe Thread doesn't own the Mutex!" unless owned + end + end + end + + # wait for m to acquire the mutex + Thread.pass until in_synchronize + # wait until th is sleeping (ie waiting) + Thread.pass until th.stop? + + m.synchronize { + cv.signal + # Wait that the thread is blocked on acquiring the Mutex + sleep 0.001 + # Kill the thread, yet the thread should first acquire the Mutex before going on + th.kill + } + + th.join + owned.should == true + end + + it "supports multiple Threads waiting on the same ConditionVariable and Mutex" do + m = Mutex.new + cv = ConditionVariable.new + n_threads = 4 + events = [] + + threads = n_threads.times.map { + Thread.new { + m.synchronize { + events << :t_in_synchronize + cv.wait(m) + } + } + } + + Thread.pass until m.synchronize { events.size } == n_threads + Thread.pass until threads.any?(&:stop?) + m.synchronize do + threads.each { |t| + # Cause interactions with the waiting threads. + # On TruffleRuby, this causes a safepoint which has interesting + # interactions with the ConditionVariable. + bt = t.backtrace + bt.should be_kind_of(Array) + bt.size.should >= 2 + } + end + + cv.broadcast + threads.each(&:join) + end +end diff --git a/spec/ruby/core/data/constants_spec.rb b/spec/ruby/core/data/constants_spec.rb new file mode 100644 index 0000000000..ad0b1ddea7 --- /dev/null +++ b/spec/ruby/core/data/constants_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' + +describe "Data" do + it "is a new constant" do + Data.superclass.should == Object + end + + it "is not deprecated" do + -> { Data }.should_not complain + end +end diff --git a/spec/ruby/core/data/deconstruct_keys_spec.rb b/spec/ruby/core/data/deconstruct_keys_spec.rb new file mode 100644 index 0000000000..df378f8b98 --- /dev/null +++ b/spec/ruby/core/data/deconstruct_keys_spec.rb @@ -0,0 +1,130 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Data#deconstruct_keys" do + it "returns a hash of attributes" do + klass = Data.define(:x, :y) + d = klass.new(1, 2) + + d.deconstruct_keys([:x, :y]).should == {x: 1, y: 2} + end + + it "requires one argument" do + klass = Data.define(:x, :y) + d = klass.new(1, 2) + + -> { + d.deconstruct_keys + }.should raise_error(ArgumentError, /wrong number of arguments \(given 0, expected 1\)/) + end + + it "returns only specified keys" do + klass = Data.define(:x, :y) + d = klass.new(1, 2) + + d.deconstruct_keys([:x, :y]).should == {x: 1, y: 2} + d.deconstruct_keys([:x] ).should == {x: 1} + d.deconstruct_keys([] ).should == {} + end + + it "accepts string attribute names" do + klass = Data.define(:x, :y) + d = klass.new(1, 2) + + d.deconstruct_keys(['x', 'y']).should == {'x' => 1, 'y' => 2} + end + + it "accepts argument position number as well but returns them as keys" do + klass = Data.define(:x, :y) + d = klass.new(1, 2) + + d.deconstruct_keys([0, 1]).should == {0 => 1, 1 => 2} + d.deconstruct_keys([0] ).should == {0 => 1} + d.deconstruct_keys([-1] ).should == {-1 => 2} + end + + it "ignores incorrect position numbers" do + klass = Data.define(:x, :y) + d = klass.new(1, 2) + + d.deconstruct_keys([0, 3]).should == {0 => 1} + end + + it "support mixing attribute names and argument position numbers" do + klass = Data.define(:x, :y) + d = klass.new(1, 2) + + d.deconstruct_keys([0, :x]).should == {0 => 1, :x => 1} + end + + it "returns an empty hash when there are more keys than attributes" do + klass = Data.define(:x, :y) + d = klass.new(1, 2) + + d.deconstruct_keys([:x, :y, :x]).should == {} + end + + it "returns at first not existing attribute name" do + klass = Data.define(:x, :y) + d = klass.new(1, 2) + + d.deconstruct_keys([:a, :x]).should == {} + d.deconstruct_keys([:x, :a]).should == {x: 1} + end + + it "returns at first not existing argument position number" do + klass = Data.define(:x, :y) + d = klass.new(1, 2) + + d.deconstruct_keys([3, 0]).should == {} + d.deconstruct_keys([0, 3]).should == {0 => 1} + end + + it "accepts nil argument and return all the attributes" do + klass = Data.define(:x, :y) + d = klass.new(1, 2) + + d.deconstruct_keys(nil).should == {x: 1, y: 2} + end + + it "tries to convert a key with #to_int if index is not a String nor a Symbol, but responds to #to_int" do + klass = Data.define(:x, :y) + d = klass.new(1, 2) + + key = mock("to_int") + key.should_receive(:to_int).and_return(1) + + d.deconstruct_keys([key]).should == { key => 2 } + end + + it "raises a TypeError if the conversion with #to_int does not return an Integer" do + klass = Data.define(:x, :y) + d = klass.new(1, 2) + + key = mock("to_int") + key.should_receive(:to_int).and_return("not an Integer") + + -> { + d.deconstruct_keys([key]) + }.should raise_error(TypeError, /can't convert MockObject to Integer/) + end + + it "raises TypeError if index is not a String, a Symbol and not convertible to Integer " do + klass = Data.define(:x, :y) + d = klass.new(1, 2) + + -> { + d.deconstruct_keys([0, []]) + }.should raise_error(TypeError, "no implicit conversion of Array into Integer") + end + + it "raise TypeError if passed anything except nil or array" do + klass = Data.define(:x, :y) + d = klass.new(1, 2) + + -> { d.deconstruct_keys('x') }.should raise_error(TypeError, /expected Array or nil/) + -> { d.deconstruct_keys(1) }.should raise_error(TypeError, /expected Array or nil/) + -> { d.deconstruct_keys(:x) }.should raise_error(TypeError, /expected Array or nil/) + -> { d.deconstruct_keys({}) }.should raise_error(TypeError, /expected Array or nil/) + end +end diff --git a/spec/ruby/core/data/deconstruct_spec.rb b/spec/ruby/core/data/deconstruct_spec.rb new file mode 100644 index 0000000000..4ca0b87039 --- /dev/null +++ b/spec/ruby/core/data/deconstruct_spec.rb @@ -0,0 +1,8 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Data#deconstruct" do + it "returns an array of attribute values" do + DataSpecs::Measure.new(42, "km").deconstruct.should == [42, "km"] + end +end diff --git a/spec/ruby/core/data/define_spec.rb b/spec/ruby/core/data/define_spec.rb new file mode 100644 index 0000000000..c0b4671e39 --- /dev/null +++ b/spec/ruby/core/data/define_spec.rb @@ -0,0 +1,34 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Data.define" do + it "accepts no arguments" do + empty_data = Data.define + empty_data.members.should == [] + end + + it "accepts symbols" do + movie = Data.define(:title, :year) + movie.members.should == [:title, :year] + end + + it "accepts strings" do + movie = Data.define("title", "year") + movie.members.should == [:title, :year] + end + + it "accepts a mix of strings and symbols" do + movie = Data.define("title", :year, "genre") + movie.members.should == [:title, :year, :genre] + end + + it "accepts a block" do + movie = Data.define(:title, :year) do + def title_with_year + "#{title} (#{year})" + end + end + movie.members.should == [:title, :year] + movie.new("Matrix", 1999).title_with_year.should == "Matrix (1999)" + end +end diff --git a/spec/ruby/core/data/eql_spec.rb b/spec/ruby/core/data/eql_spec.rb new file mode 100644 index 0000000000..6958d5de4a --- /dev/null +++ b/spec/ruby/core/data/eql_spec.rb @@ -0,0 +1,63 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Data#eql?" do + it "returns true if the other is the same object" do + a = DataSpecs::Measure.new(42, "km") + a.should.eql?(a) + end + + it "returns true if the other has all the same fields" do + a = DataSpecs::Measure.new(42, "km") + b = DataSpecs::Measure.new(42, "km") + a.should.eql?(b) + end + + it "returns false if the other is a different object or has different fields" do + a = DataSpecs::Measure.new(42, "km") + b = DataSpecs::Measure.new(42, "mi") + a.should_not.eql?(b) + end + + it "returns false if other is of a different class" do + a = DataSpecs::Measure.new(42, "km") + klass = Data.define(*DataSpecs::Measure.members) + b = klass.new(42, "km") + a.should_not.eql?(b) + end + + it "returns false if any corresponding elements are not equal with #eql?" do + a = DataSpecs::Measure.new(42, "km") + b = DataSpecs::Measure.new(42.0, "mi") + a.should_not.eql?(b) + end + + context "recursive structure" do + it "returns true the other is the same object" do + a = DataSpecs::Measure.allocate + a.send(:initialize, amount: 42, unit: a) + + a.should.eql?(a) + end + + it "returns true if the other has all the same fields" do + a = DataSpecs::Measure.allocate + a.send(:initialize, amount: 42, unit: a) + + b = DataSpecs::Measure.allocate + b.send(:initialize, amount: 42, unit: b) + + a.should.eql?(b) + end + + it "returns false if any corresponding elements are not equal with #eql?" do + a = DataSpecs::Measure.allocate + a.send(:initialize, amount: a, unit: "km") + + b = DataSpecs::Measure.allocate + b.send(:initialize, amount: b, unit: "mi") + + a.should_not.eql?(b) + end + end +end diff --git a/spec/ruby/core/data/equal_value_spec.rb b/spec/ruby/core/data/equal_value_spec.rb new file mode 100644 index 0000000000..d9a0dcff3e --- /dev/null +++ b/spec/ruby/core/data/equal_value_spec.rb @@ -0,0 +1,63 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Data#==" do + it "returns true if the other is the same object" do + a = DataSpecs::Measure.new(42, "km") + a.should == a + end + + it "returns true if the other has all the same fields" do + a = DataSpecs::Measure.new(42, "km") + b = DataSpecs::Measure.new(42, "km") + a.should == b + end + + it "returns false if the other is a different object or has different fields" do + a = DataSpecs::Measure.new(42, "km") + b = DataSpecs::Measure.new(42, "mi") + a.should_not == b + end + + it "returns false if other is of a different class" do + a = DataSpecs::Measure.new(42, "km") + klass = Data.define(*DataSpecs::Measure.members) + b = klass.new(42, "km") + a.should_not == b + end + + it "returns false if any corresponding elements are not equal with #==" do + a = DataSpecs::Measure.new(42, "km") + b = DataSpecs::Measure.new(42.0, "mi") + a.should_not == b + end + + context "recursive structure" do + it "returns true the other is the same object" do + a = DataSpecs::Measure.allocate + a.send(:initialize, amount: 42, unit: a) + + a.should == a + end + + it "returns true if the other has all the same fields" do + a = DataSpecs::Measure.allocate + a.send(:initialize, amount: 42, unit: a) + + b = DataSpecs::Measure.allocate + b.send(:initialize, amount: 42, unit: b) + + a.should == b + end + + it "returns false if any corresponding elements are not equal with #==" do + a = DataSpecs::Measure.allocate + a.send(:initialize, amount: a, unit: "km") + + b = DataSpecs::Measure.allocate + b.send(:initialize, amount: b, unit: "mi") + + a.should_not == b + end + end +end diff --git a/spec/ruby/core/data/fixtures/classes.rb b/spec/ruby/core/data/fixtures/classes.rb new file mode 100644 index 0000000000..650c0b2a62 --- /dev/null +++ b/spec/ruby/core/data/fixtures/classes.rb @@ -0,0 +1,34 @@ +module DataSpecs + if Data.respond_to?(:define) + Measure = Data.define(:amount, :unit) + + class MeasureWithOverriddenName < Measure + def self.name + "A" + end + end + + class DataSubclass < Data; end + + MeasureSubclass = Class.new(Measure) do + def initialize(amount:, unit:) + super + end + end + + Empty = Data.define() + + DataWithOverriddenInitialize = Data.define(:amount, :unit) do + def initialize(*rest, **kw) + super + ScratchPad.record [:initialize, rest, kw] + end + end + + Area = Data.define(:width, :height, :area) do + def initialize(width:, height:) + super(width: width, height: height, area: width * height) + end + end + end +end diff --git a/spec/ruby/core/data/hash_spec.rb b/spec/ruby/core/data/hash_spec.rb new file mode 100644 index 0000000000..c23f08a71d --- /dev/null +++ b/spec/ruby/core/data/hash_spec.rb @@ -0,0 +1,25 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Data#hash" do + it "returns the same integer for objects with the same content" do + a = DataSpecs::Measure.new(42, "km") + b = DataSpecs::Measure.new(42, "km") + a.hash.should == b.hash + a.hash.should be_an_instance_of(Integer) + end + + it "returns different hashes for objects with different values" do + a = DataSpecs::Measure.new(42, "km") + b = DataSpecs::Measure.new(42, "ml") + a.hash.should_not == b.hash + + a = DataSpecs::Measure.new(42, "km") + b = DataSpecs::Measure.new(13, "km") + a.hash.should_not == b.hash + end + + it "returns different hashes for different classes" do + Data.define(:x).new(1).hash.should != Data.define(:x).new(1).hash + end +end diff --git a/spec/ruby/core/data/initialize_spec.rb b/spec/ruby/core/data/initialize_spec.rb new file mode 100644 index 0000000000..010c73b91b --- /dev/null +++ b/spec/ruby/core/data/initialize_spec.rb @@ -0,0 +1,124 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Data#initialize" do + it "accepts positional arguments" do + data = DataSpecs::Measure.new(42, "km") + + data.amount.should == 42 + data.unit.should == "km" + end + + it "accepts alternative positional arguments" do + data = DataSpecs::Measure[42, "km"] + + data.amount.should == 42 + data.unit.should == "km" + end + + it "accepts keyword arguments" do + data = DataSpecs::Measure.new(amount: 42, unit: "km") + + data.amount.should == 42 + data.unit.should == "km" + end + + it "accepts alternative keyword arguments" do + data = DataSpecs::Measure[amount: 42, unit: "km"] + + data.amount.should == 42 + data.unit.should == "km" + end + + it "accepts String keyword arguments" do + data = DataSpecs::Measure.new("amount" => 42, "unit" => "km") + + data.amount.should == 42 + data.unit.should == "km" + end + + it "raises ArgumentError if no arguments are given" do + -> { + DataSpecs::Measure.new + }.should raise_error(ArgumentError) { |e| + e.message.should.include?("missing keywords: :amount, :unit") + } + end + + it "raises ArgumentError if at least one argument is missing" do + -> { + DataSpecs::Measure.new(unit: "km") + }.should raise_error(ArgumentError) { |e| + e.message.should.include?("missing keyword: :amount") + } + end + + it "raises ArgumentError if unknown keyword is given" do + -> { + DataSpecs::Measure.new(amount: 42, unit: "km", system: "metric") + }.should raise_error(ArgumentError) { |e| + e.message.should.include?("unknown keyword: :system") + } + end + + it "supports super from a subclass" do + ms = DataSpecs::MeasureSubclass.new(amount: 1, unit: "km") + + ms.amount.should == 1 + ms.unit.should == "km" + end + + it "supports Data with no fields" do + -> { DataSpecs::Empty.new }.should_not raise_error + end + + it "can be overridden" do + ScratchPad.record [] + + measure_class = Data.define(:amount, :unit) do + def initialize(*, **) + super + ScratchPad << :initialize + end + end + + measure_class.new(42, "m") + ScratchPad.recorded.should == [:initialize] + end + + context "when it is overridden" do + it "is called with keyword arguments when given positional arguments" do + ScratchPad.clear + DataSpecs::DataWithOverriddenInitialize.new(42, "m") + ScratchPad.recorded.should == [:initialize, [], {amount: 42, unit: "m"}] + end + + it "is called with keyword arguments when given keyword arguments" do + ScratchPad.clear + DataSpecs::DataWithOverriddenInitialize.new(amount: 42, unit: "m") + ScratchPad.recorded.should == [:initialize, [], {amount: 42, unit: "m"}] + end + + it "is called with keyword arguments when given alternative positional arguments" do + ScratchPad.clear + DataSpecs::DataWithOverriddenInitialize[42, "m"] + ScratchPad.recorded.should == [:initialize, [], {amount: 42, unit: "m"}] + end + + it "is called with keyword arguments when given alternative keyword arguments" do + ScratchPad.clear + DataSpecs::DataWithOverriddenInitialize[amount: 42, unit: "m"] + ScratchPad.recorded.should == [:initialize, [], {amount: 42, unit: "m"}] + end + + # See https://github.com/ruby/psych/pull/765 + it "can be deserialized by calling Data.instance_method(:initialize)" do + d1 = DataSpecs::Area.new(width: 2, height: 3) + d1.area.should == 6 + + d2 = DataSpecs::Area.allocate + Data.instance_method(:initialize).bind_call(d2, **d1.to_h) + d2.should == d1 + end + end +end diff --git a/spec/ruby/core/data/inspect_spec.rb b/spec/ruby/core/data/inspect_spec.rb new file mode 100644 index 0000000000..38642910a0 --- /dev/null +++ b/spec/ruby/core/data/inspect_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/inspect' + +describe "Data#inspect" do + it_behaves_like :data_inspect, :inspect +end diff --git a/spec/ruby/core/data/members_spec.rb b/spec/ruby/core/data/members_spec.rb new file mode 100644 index 0000000000..457a90a0d6 --- /dev/null +++ b/spec/ruby/core/data/members_spec.rb @@ -0,0 +1,21 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Data#members" do + it "returns an array of attribute names" do + measure = DataSpecs::Measure.new(amount: 42, unit: 'km') + measure.members.should == [:amount, :unit] + end +end + +describe "DataClass#members" do + it "returns an array of attribute names" do + DataSpecs::Measure.members.should == [:amount, :unit] + end + + context "class inheriting Data" do + it "isn't available in a subclass" do + DataSpecs::DataSubclass.should_not.respond_to?(:members) + end + end +end diff --git a/spec/ruby/core/data/shared/inspect.rb b/spec/ruby/core/data/shared/inspect.rb new file mode 100644 index 0000000000..6cd5664da7 --- /dev/null +++ b/spec/ruby/core/data/shared/inspect.rb @@ -0,0 +1,62 @@ +require_relative '../fixtures/classes' + +describe :data_inspect, shared: true do + it "returns a string representation showing members and values" do + a = DataSpecs::Measure.new(42, "km") + a.send(@method).should == '#<data DataSpecs::Measure amount=42, unit="km">' + end + + it "returns a string representation without the class name for anonymous structs" do + Data.define(:a).new("").send(@method).should == '#<data a="">' + end + + it "returns a string representation without the class name for structs nested in anonymous classes" do + c = Class.new + c.class_eval <<~DOC + Foo = Data.define(:a) + DOC + + c::Foo.new("").send(@method).should == '#<data a="">' + end + + it "returns a string representation without the class name for structs nested in anonymous modules" do + m = Module.new + m.class_eval <<~DOC + Foo = Data.define(:a) + DOC + + m::Foo.new("").send(@method).should == '#<data a="">' + end + + it "does not call #name method" do + struct = DataSpecs::MeasureWithOverriddenName.new(42, "km") + struct.send(@method).should == '#<data DataSpecs::MeasureWithOverriddenName amount=42, unit="km">' + end + + it "does not call #name method when struct is anonymous" do + klass = Class.new(DataSpecs::Measure) do + def self.name + "A" + end + end + struct = klass.new(42, "km") + struct.send(@method).should == '#<data amount=42, unit="km">' + end + + context "recursive structure" do + it "returns string representation with recursive attribute replaced with ..." do + a = DataSpecs::Measure.allocate + a.send(:initialize, amount: 42, unit: a) + + a.send(@method).should == "#<data DataSpecs::Measure amount=42, unit=#<data DataSpecs::Measure:...>>" + end + + it "returns string representation with recursive attribute replaced with ... when an anonymous class" do + klass = Class.new(DataSpecs::Measure) + a = klass.allocate + a.send(:initialize, amount: 42, unit: a) + + a.send(@method).should =~ /#<data amount=42, unit=#<data #<Class:0x.+?>:\.\.\.>>/ + end + end +end diff --git a/spec/ruby/core/data/to_h_spec.rb b/spec/ruby/core/data/to_h_spec.rb new file mode 100644 index 0000000000..64816b7251 --- /dev/null +++ b/spec/ruby/core/data/to_h_spec.rb @@ -0,0 +1,63 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Data#to_h" do + it "transforms the data object into a hash" do + data = DataSpecs::Measure.new(amount: 42, unit: 'km') + data.to_h.should == { amount: 42, unit: 'km' } + end + + context "with block" do + it "transforms [key, value] pairs returned by the block into a hash" do + data = DataSpecs::Measure.new(amount: 42, unit: 'km') + data.to_h { |key, value| [value, key] }.should == { 42 => :amount, 'km' => :unit } + end + + it "passes to a block each pair's key and value as separate arguments" do + ScratchPad.record [] + data = DataSpecs::Measure.new(amount: 42, unit: 'km') + data.to_h { |k, v| ScratchPad << [k, v]; [k, v] } + ScratchPad.recorded.sort.should == [[:amount, 42], [:unit, 'km']] + + ScratchPad.record [] + data.to_h { |*args| ScratchPad << args; [args[0], args[1]] } + ScratchPad.recorded.sort.should == [[:amount, 42], [:unit, 'km']] + end + + it "raises ArgumentError if block returns longer or shorter array" do + data = DataSpecs::Measure.new(amount: 42, unit: 'km') + -> do + data.to_h { |k, v| [k.to_s, v*v, 1] } + end.should raise_error(ArgumentError, /element has wrong array length/) + + -> do + data.to_h { |k, v| [k] } + end.should raise_error(ArgumentError, /element has wrong array length/) + end + + it "raises TypeError if block returns something other than Array" do + data = DataSpecs::Measure.new(amount: 42, unit: 'km') + -> do + data.to_h { |k, v| "not-array" } + end.should raise_error(TypeError, /wrong element type String/) + end + + it "coerces returned pair to Array with #to_ary" do + x = mock('x') + x.stub!(:to_ary).and_return([:b, 'b']) + data = DataSpecs::Measure.new(amount: 42, unit: 'km') + + data.to_h { |k| x }.should == { :b => 'b' } + end + + it "does not coerce returned pair to Array with #to_a" do + x = mock('x') + x.stub!(:to_a).and_return([:b, 'b']) + data = DataSpecs::Measure.new(amount: 42, unit: 'km') + + -> do + data.to_h { |k| x } + end.should raise_error(TypeError, /wrong element type MockObject/) + end + end +end diff --git a/spec/ruby/core/data/to_s_spec.rb b/spec/ruby/core/data/to_s_spec.rb new file mode 100644 index 0000000000..2b4a670e8e --- /dev/null +++ b/spec/ruby/core/data/to_s_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/inspect' + +describe "Data#to_s" do + it_behaves_like :data_inspect, :to_s +end diff --git a/spec/ruby/core/data/with_spec.rb b/spec/ruby/core/data/with_spec.rb new file mode 100644 index 0000000000..fd0a99d1fa --- /dev/null +++ b/spec/ruby/core/data/with_spec.rb @@ -0,0 +1,57 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Data#with" do + it "returns self if given no arguments" do + data = DataSpecs::Measure.new(amount: 42, unit: "km") + data = data.with.should.equal?(data) + end + + it "accepts keyword arguments" do + data = DataSpecs::Measure.new(amount: 42, unit: "km") + data = data.with(amount: 4, unit: "m") + + data.amount.should == 4 + data.unit.should == "m" + end + + it "accepts String keyword arguments" do + data = DataSpecs::Measure.new(amount: 42, unit: "km") + data = data.with("amount" => 4, "unit" => "m") + + data.amount.should == 4 + data.unit.should == "m" + end + + it "raises ArgumentError if no keyword arguments are given" do + data = DataSpecs::Measure.new(amount: 42, unit: "km") + + -> { + data.with(4, "m") + }.should raise_error(ArgumentError, "wrong number of arguments (given 2, expected 0)") + end + + it "does not depend on the Data.new method" do + subclass = Class.new(DataSpecs::Measure) + data = subclass.new(amount: 42, unit: "km") + + def subclass.new(*) + raise "Data.new is called" + end + + data_copy = data.with(unit: "m") + data_copy.amount.should == 42 + data_copy.unit.should == "m" + end + + ruby_version_is "3.3" do + it "calls #initialize" do + data = DataSpecs::DataWithOverriddenInitialize.new(42, "m") + ScratchPad.clear + + data.with(amount: 0) + + ScratchPad.recorded.should == [:initialize, [], {amount: 0, unit: "m"}] + end + end +end diff --git a/spec/ruby/core/dir/chdir_spec.rb b/spec/ruby/core/dir/chdir_spec.rb new file mode 100644 index 0000000000..015386a902 --- /dev/null +++ b/spec/ruby/core/dir/chdir_spec.rb @@ -0,0 +1,220 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' + +describe "Dir.chdir" do + before :all do + DirSpecs.create_mock_dirs + end + + after :all do + DirSpecs.delete_mock_dirs + end + + before :each do + @original = Dir.pwd + end + + after :each do + Dir.chdir(@original) + end + + it "defaults to $HOME with no arguments" do + skip "$HOME not valid directory" unless ENV['HOME'] && File.directory?(ENV['HOME']) + + Dir.chdir + current_dir = Dir.pwd + + Dir.chdir(ENV['HOME']) + home = Dir.pwd + current_dir.should == home + end + + it "changes to the specified directory" do + Dir.chdir DirSpecs.mock_dir + Dir.pwd.should == DirSpecs.mock_dir + end + + it "returns 0 when successfully changing directory" do + Dir.chdir(@original).should == 0 + end + + it "calls #to_str on the argument if it's not a String" do + obj = mock('path') + obj.should_receive(:to_str).and_return(Dir.pwd) + Dir.chdir(obj) + end + + it "calls #to_str on the argument if it's not a String and a block is given" do + obj = mock('path') + obj.should_receive(:to_str).and_return(Dir.pwd) + Dir.chdir(obj) { } + end + + it "calls #to_path on the argument if it's not a String" do + obj = mock('path') + obj.should_receive(:to_path).and_return(Dir.pwd) + Dir.chdir(obj) + end + + it "prefers #to_path over #to_str" do + obj = Class.new do + def to_path; Dir.pwd; end + def to_str; DirSpecs.mock_dir; end + end + Dir.chdir(obj.new) + Dir.pwd.should == @original + end + + it "returns the value of the block when a block is given" do + Dir.chdir(@original) { :block_value }.should == :block_value + end + + it "defaults to the home directory when given a block but no argument" do + skip "$HOME not valid directory" unless ENV['HOME'] && File.directory?(ENV['HOME']) + + # Windows will return a path with forward slashes for ENV["HOME"] so we have + # to compare the route representations returned by Dir.chdir. + current_dir = "" + Dir.chdir { current_dir = Dir.pwd } + + Dir.chdir(ENV['HOME']) + home = Dir.pwd + current_dir.should == home + end + + it "changes to the specified directory for the duration of the block" do + ar = Dir.chdir(DirSpecs.mock_dir) { |dir| [dir, Dir.pwd] } + ar.should == [DirSpecs.mock_dir, DirSpecs.mock_dir] + + Dir.pwd.should == @original + end + + it "raises an Errno::ENOENT if the directory does not exist" do + -> { Dir.chdir DirSpecs.nonexistent }.should raise_error(Errno::ENOENT) + -> { Dir.chdir(DirSpecs.nonexistent) { } }.should raise_error(Errno::ENOENT) + end + + it "raises an Errno::ENOENT if the original directory no longer exists" do + dir1 = tmp('testdir1') + dir2 = tmp('testdir2') + Dir.should_not.exist?(dir1) + Dir.should_not.exist?(dir2) + Dir.mkdir dir1 + Dir.mkdir dir2 + begin + -> { + Dir.chdir dir1 do + Dir.chdir(dir2) { Dir.unlink dir1 } + end + }.should raise_error(Errno::ENOENT) + ensure + Dir.unlink dir1 if Dir.exist?(dir1) + Dir.unlink dir2 if Dir.exist?(dir2) + end + end + + it "always returns to the original directory when given a block" do + begin + Dir.chdir(DirSpecs.mock_dir) do + raise StandardError, "something bad happened" + end + rescue StandardError + end + + Dir.pwd.should == @original + end +end + +ruby_version_is '3.3' do + describe "Dir#chdir" do + before :all do + DirSpecs.create_mock_dirs + end + + after :all do + DirSpecs.delete_mock_dirs + end + + before :each do + @original = Dir.pwd + end + + after :each do + Dir.chdir(@original) + end + + it "changes the current working directory to self" do + dir = Dir.new(DirSpecs.mock_dir) + dir.chdir + Dir.pwd.should == DirSpecs.mock_dir + ensure + dir.close + end + + it "changes the current working directory to self for duration of the block when a block is given" do + dir = Dir.new(DirSpecs.mock_dir) + pwd_in_block = nil + + dir.chdir { pwd_in_block = Dir.pwd } + + pwd_in_block.should == DirSpecs.mock_dir + Dir.pwd.should == @original + ensure + dir.close + end + + it "returns 0 when successfully changing directory" do + dir = Dir.new(DirSpecs.mock_dir) + dir.chdir.should == 0 + ensure + dir.close + end + + it "returns the value of the block when a block is given" do + dir = Dir.new(DirSpecs.mock_dir) + dir.chdir { :block_value }.should == :block_value + ensure + dir.close + end + + platform_is_not :windows do + it "does not raise an Errno::ENOENT if the original directory no longer exists" do + dir_name1 = tmp('testdir1') + dir_name2 = tmp('testdir2') + Dir.should_not.exist?(dir_name1) + Dir.should_not.exist?(dir_name2) + Dir.mkdir dir_name1 + Dir.mkdir dir_name2 + + dir2 = Dir.new(dir_name2) + + begin + Dir.chdir(dir_name1) do + dir2.chdir { Dir.unlink dir_name1 } + end + Dir.pwd.should == @original + ensure + Dir.unlink dir_name1 if Dir.exist?(dir_name1) + Dir.unlink dir_name2 if Dir.exist?(dir_name2) + end + ensure + dir2.close + end + end + + it "always returns to the original directory when given a block" do + dir = Dir.new(DirSpecs.mock_dir) + + begin + dir.chdir do + raise StandardError, "something bad happened" + end + rescue StandardError + end + + Dir.pwd.should == @original + ensure + dir.close + end + end +end diff --git a/spec/ruby/core/dir/children_spec.rb b/spec/ruby/core/dir/children_spec.rb new file mode 100644 index 0000000000..0ad3df4669 --- /dev/null +++ b/spec/ruby/core/dir/children_spec.rb @@ -0,0 +1,147 @@ +# encoding: utf-8 + +require_relative '../../spec_helper' +require_relative 'fixtures/common' + +describe "Dir.children" do + before :all do + DirSpecs.create_mock_dirs + end + + before :each do + @internal = Encoding.default_internal + end + + after :all do + DirSpecs.delete_mock_dirs + end + + after :each do + Encoding.default_internal = @internal + end + + it "returns an Array of filenames in an existing directory including dotfiles" do + a = Dir.children(DirSpecs.mock_dir).sort + + a.should == DirSpecs.expected_paths - %w[. ..] + + a = Dir.children("#{DirSpecs.mock_dir}/deeply/nested").sort + a.should == %w|.dotfile.ext directory| + end + + it "calls #to_path on non-String arguments" do + p = mock('path') + p.should_receive(:to_path).and_return(DirSpecs.mock_dir) + Dir.children(p) + end + + it "accepts an options Hash" do + a = Dir.children("#{DirSpecs.mock_dir}/deeply/nested", encoding: "utf-8").sort + a.should == %w|.dotfile.ext directory| + end + + it "returns children encoded with the filesystem encoding by default" do + # This spec depends on the locale not being US-ASCII because if it is, the + # children that are not ascii_only? will be BINARY encoded. + children = Dir.children(File.join(DirSpecs.mock_dir, 'special')).sort + encoding = Encoding.find("filesystem") + encoding = Encoding::BINARY if encoding == Encoding::US_ASCII + platform_is_not :windows do + children.should include("ã“ã‚“ã«ã¡ã¯.txt".dup.force_encoding(encoding)) + end + children.first.encoding.should equal(Encoding.find("filesystem")) + end + + it "returns children encoded with the specified encoding" do + dir = File.join(DirSpecs.mock_dir, 'special') + children = Dir.children(dir, encoding: "euc-jp").sort + children.first.encoding.should equal(Encoding::EUC_JP) + end + + it "returns children transcoded to the default internal encoding" do + Encoding.default_internal = Encoding::EUC_KR + children = Dir.children(File.join(DirSpecs.mock_dir, 'special')).sort + children.first.encoding.should equal(Encoding::EUC_KR) + end + + it "raises a SystemCallError if called with a nonexistent directory" do + -> { Dir.children DirSpecs.nonexistent }.should raise_error(SystemCallError) + end +end + +describe "Dir#children" do + before :all do + DirSpecs.create_mock_dirs + end + + before :each do + @internal = Encoding.default_internal + end + + after :all do + DirSpecs.delete_mock_dirs + end + + after :each do + Encoding.default_internal = @internal + @dir.close if @dir + end + + it "returns an Array of filenames in an existing directory including dotfiles" do + @dir = Dir.new(DirSpecs.mock_dir) + a = @dir.children.sort + @dir.close + + a.should == DirSpecs.expected_paths - %w[. ..] + + @dir = Dir.new("#{DirSpecs.mock_dir}/deeply/nested") + a = @dir.children.sort + a.should == %w|.dotfile.ext directory| + end + + it "accepts an encoding keyword for the encoding of the entries" do + @dir = Dir.new("#{DirSpecs.mock_dir}/deeply/nested", encoding: "utf-8") + dirs = @dir.to_a.sort + dirs.each { |d| d.encoding.should == Encoding::UTF_8 } + end + + it "returns children encoded with the filesystem encoding by default" do + # This spec depends on the locale not being US-ASCII because if it is, the + # children that are not ascii_only? will be BINARY encoded. + @dir = Dir.new(File.join(DirSpecs.mock_dir, 'special')) + children = @dir.children.sort + encoding = Encoding.find("filesystem") + encoding = Encoding::BINARY if encoding == Encoding::US_ASCII + platform_is_not :windows do + children.should include("ã“ã‚“ã«ã¡ã¯.txt".dup.force_encoding(encoding)) + end + children.first.encoding.should equal(Encoding.find("filesystem")) + end + + it "returns children encoded with the specified encoding" do + path = File.join(DirSpecs.mock_dir, 'special') + @dir = Dir.new(path, encoding: "euc-jp") + children = @dir.children.sort + children.first.encoding.should equal(Encoding::EUC_JP) + end + + it "returns children transcoded to the default internal encoding" do + Encoding.default_internal = Encoding::EUC_KR + @dir = Dir.new(File.join(DirSpecs.mock_dir, 'special')) + children = @dir.children.sort + children.first.encoding.should equal(Encoding::EUC_KR) + end + + it "returns the same result when called repeatedly" do + @dir = Dir.open DirSpecs.mock_dir + + a = [] + @dir.each {|dir| a << dir} + + b = [] + @dir.each {|dir| b << dir} + + a.sort.should == b.sort + a.sort.should == DirSpecs.expected_paths + end +end diff --git a/spec/ruby/core/dir/chroot_spec.rb b/spec/ruby/core/dir/chroot_spec.rb new file mode 100644 index 0000000000..a5ca8943fc --- /dev/null +++ b/spec/ruby/core/dir/chroot_spec.rb @@ -0,0 +1,47 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' +require_relative 'shared/chroot' + +platform_is_not :windows do + as_superuser do + describe "Dir.chroot as root" do + it_behaves_like :dir_chroot_as_root, :chroot + end + end + + platform_is_not :cygwin, :android do + as_user do + describe "Dir.chroot as regular user" do + before :all do + DirSpecs.create_mock_dirs + end + + after :all do + DirSpecs.delete_mock_dirs + end + + it "raises an Errno::EPERM exception if the directory exists" do + -> { Dir.chroot('.') }.should raise_error(Errno::EPERM) + end + + it "raises a SystemCallError if the directory doesn't exist" do + -> { Dir.chroot('xgwhwhsjai2222jg') }.should raise_error(SystemCallError) + end + + it "calls #to_path on non-String argument" do + p = mock('path') + p.should_receive(:to_path).and_return('.') + -> { Dir.chroot(p) }.should raise_error(Errno::EPERM) + end + end + end + end + + platform_is :cygwin do + as_user do + describe "Dir.chroot as regular user" do + it_behaves_like :dir_chroot_as_root, :chroot + end + end + end +end diff --git a/spec/ruby/core/dir/close_spec.rb b/spec/ruby/core/dir/close_spec.rb new file mode 100644 index 0000000000..f7cce318b8 --- /dev/null +++ b/spec/ruby/core/dir/close_spec.rb @@ -0,0 +1,53 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' +describe "Dir#close" do + before :all do + DirSpecs.create_mock_dirs + end + + after :all do + DirSpecs.delete_mock_dirs + end + + it "does not raise an IOError even if the Dir instance is closed" do + dir = Dir.open DirSpecs.mock_dir + dir.close.should == nil + dir.close.should == nil + + platform_is_not :windows do + -> { dir.fileno }.should raise_error(IOError, /closed directory/) + end + end + + it "returns nil" do + dir = Dir.open DirSpecs.mock_dir + dir.close.should == nil + end + + ruby_version_is '3.3'...'3.4' do + platform_is_not :windows do + it "does not raise an error even if the file descriptor is closed with another Dir instance" do + dir = Dir.open DirSpecs.mock_dir + dir_new = Dir.for_fd(dir.fileno) + + dir.close + dir_new.close + + -> { dir.fileno }.should raise_error(IOError, /closed directory/) + -> { dir_new.fileno }.should raise_error(IOError, /closed directory/) + end + end + end + + ruby_version_is '3.4' do + platform_is_not :windows do + it "raises an error if the file descriptor is closed with another Dir instance" do + dir = Dir.open DirSpecs.mock_dir + dir_new = Dir.for_fd(dir.fileno) + dir.close + + -> { dir_new.close }.should raise_error(Errno::EBADF, 'Bad file descriptor - closedir') + end + end + end +end diff --git a/spec/ruby/core/dir/delete_spec.rb b/spec/ruby/core/dir/delete_spec.rb new file mode 100644 index 0000000000..a0020788ca --- /dev/null +++ b/spec/ruby/core/dir/delete_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' +require_relative 'shared/delete' + +describe "Dir.delete" do + before :all do + DirSpecs.create_mock_dirs + end + + after :all do + DirSpecs.delete_mock_dirs + end + + it_behaves_like :dir_delete, :delete +end diff --git a/spec/ruby/core/dir/dir_spec.rb b/spec/ruby/core/dir/dir_spec.rb new file mode 100644 index 0000000000..7d55ea26d4 --- /dev/null +++ b/spec/ruby/core/dir/dir_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' + +describe "Dir" do + it "includes Enumerable" do + Dir.include?(Enumerable).should == true + end +end diff --git a/spec/ruby/core/dir/each_child_spec.rb b/spec/ruby/core/dir/each_child_spec.rb new file mode 100644 index 0000000000..7194273b95 --- /dev/null +++ b/spec/ruby/core/dir/each_child_spec.rb @@ -0,0 +1,119 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' + +describe "Dir.each_child" do + before :all do + DirSpecs.create_mock_dirs + end + + after :all do + DirSpecs.delete_mock_dirs + end + + it "accepts an encoding keyword for the encoding of the entries" do + dirs = Dir.each_child("#{DirSpecs.mock_dir}/deeply/nested", encoding: "utf-8").to_a.sort + dirs.each {|dir| dir.encoding.should == Encoding::UTF_8} + end + + it "yields all names in an existing directory to the provided block" do + a, b = [], [] + + Dir.each_child(DirSpecs.mock_dir) {|f| a << f} + Dir.each_child("#{DirSpecs.mock_dir}/deeply/nested") {|f| b << f} + + a.sort.should == DirSpecs.expected_paths - %w[. ..] + b.sort.should == %w|.dotfile.ext directory| + end + + it "returns nil when successful" do + Dir.each_child(DirSpecs.mock_dir) {|f| f}.should == nil + end + + it "calls #to_path on non-String arguments" do + p = mock('path') + p.should_receive(:to_path).and_return(DirSpecs.mock_dir) + Dir.each_child(p).to_a + end + + it "raises a SystemCallError if passed a nonexistent directory" do + -> { Dir.each_child(DirSpecs.nonexistent) {} }.should raise_error(SystemCallError) + end + + describe "when no block is given" do + it "returns an Enumerator" do + Dir.each_child(DirSpecs.mock_dir).should be_an_instance_of(Enumerator) + Dir.each_child(DirSpecs.mock_dir).to_a.sort.should == DirSpecs.expected_paths - %w[. ..] + end + + describe "returned Enumerator" do + describe "size" do + it "should return nil" do + Dir.each_child(DirSpecs.mock_dir).size.should == nil + end + end + end + end +end + +describe "Dir#each_child" do + before :all do + DirSpecs.create_mock_dirs + end + + after :all do + DirSpecs.delete_mock_dirs + end + + after :each do + @dir.close if @dir + end + + it "yields all names in an existing directory to the provided block" do + a, b = [], [] + @dir = Dir.new(DirSpecs.mock_dir) + @dir2 = Dir.new("#{DirSpecs.mock_dir}/deeply/nested") + + @dir.each_child { |f| a << f } + @dir2.each_child { |f| b << f } + @dir2.close + + a.sort.should == DirSpecs.expected_paths - %w|. ..| + b.sort.should == %w|.dotfile.ext directory| + end + + it "returns self when successful" do + @dir = Dir.new(DirSpecs.mock_dir) + @dir.each_child { |f| f }.should == @dir + end + + it "returns the same result when called repeatedly" do + @dir = Dir.open DirSpecs.mock_dir + + a = [] + @dir.each {|dir| a << dir} + + b = [] + @dir.each {|dir| b << dir} + + a.sort.should == b.sort + a.sort.should == DirSpecs.expected_paths + end + + describe "when no block is given" do + it "returns an Enumerator" do + @dir = Dir.new(DirSpecs.mock_dir) + + @dir.each_child.should be_an_instance_of(Enumerator) + @dir.each_child.to_a.sort.should == DirSpecs.expected_paths - %w|. ..| + end + + describe "returned Enumerator" do + describe "size" do + it "should return nil" do + @dir = Dir.new(DirSpecs.mock_dir) + @dir.each_child.size.should == nil + end + end + end + end +end diff --git a/spec/ruby/core/dir/each_spec.rb b/spec/ruby/core/dir/each_spec.rb new file mode 100644 index 0000000000..7674663d82 --- /dev/null +++ b/spec/ruby/core/dir/each_spec.rb @@ -0,0 +1,75 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' +require_relative 'shared/closed' + +describe "Dir#each" do + before :all do + DirSpecs.create_mock_dirs + end + + after :all do + DirSpecs.delete_mock_dirs + end + + before :each do + @dir = Dir.open DirSpecs.mock_dir + end + + after :each do + @dir.close + end + + it "yields each directory entry in succession" do + a = [] + @dir.each {|dir| a << dir} + + a.sort.should == DirSpecs.expected_paths + end + + it "returns the directory which remains open" do + # an FS does not necessarily impose order + ls = Dir.entries(DirSpecs.mock_dir) + @dir.each {}.should == @dir + @dir.read.should == nil + @dir.rewind + ls.should include(@dir.read) + end + + it "returns the same result when called repeatedly" do + a = [] + @dir.each {|dir| a << dir} + + b = [] + @dir.each {|dir| b << dir} + + a.sort.should == b.sort + a.sort.should == DirSpecs.expected_paths + end + + describe "when no block is given" do + it "returns an Enumerator" do + @dir.each.should be_an_instance_of(Enumerator) + @dir.each.to_a.sort.should == DirSpecs.expected_paths + end + + describe "returned Enumerator" do + describe "size" do + it "should return nil" do + @dir.each.size.should == nil + end + end + end + end +end + +describe "Dir#each" do + before :all do + DirSpecs.create_mock_dirs + end + + after :all do + DirSpecs.delete_mock_dirs + end + + it_behaves_like :dir_closed, :each +end diff --git a/spec/ruby/core/dir/element_reference_spec.rb b/spec/ruby/core/dir/element_reference_spec.rb new file mode 100644 index 0000000000..092114bed4 --- /dev/null +++ b/spec/ruby/core/dir/element_reference_spec.rb @@ -0,0 +1,33 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' +require_relative 'shared/glob' + +describe "Dir.[]" do + it_behaves_like :dir_glob, :[] +end + +describe "Dir.[]" do + it_behaves_like :dir_glob_recursive, :[] +end + +describe "Dir.[]" do + before :all do + DirSpecs.create_mock_dirs + @cwd = Dir.pwd + Dir.chdir DirSpecs.mock_dir + end + + after :all do + Dir.chdir @cwd + DirSpecs.delete_mock_dirs + end + + it "calls #to_path to convert multiple patterns" do + pat1 = mock('file_one.ext') + pat1.should_receive(:to_path).and_return('file_one.ext') + pat2 = mock('file_two.ext') + pat2.should_receive(:to_path).and_return('file_two.ext') + + Dir[pat1, pat2].should == %w[file_one.ext file_two.ext] + end +end diff --git a/spec/ruby/core/dir/empty_spec.rb b/spec/ruby/core/dir/empty_spec.rb new file mode 100644 index 0000000000..8cc8757798 --- /dev/null +++ b/spec/ruby/core/dir/empty_spec.rb @@ -0,0 +1,31 @@ +require_relative '../../spec_helper' + +describe "Dir.empty?" do + before :all do + @empty_dir = tmp("empty_dir") + mkdir_p @empty_dir + end + + after :all do + rm_r @empty_dir + end + + it "returns true for empty directories" do + result = Dir.empty? @empty_dir + result.should be_true + end + + it "returns false for non-empty directories" do + result = Dir.empty? __dir__ + result.should be_false + end + + it "returns false for a non-directory" do + result = Dir.empty? __FILE__ + result.should be_false + end + + it "raises ENOENT for nonexistent directories" do + -> { Dir.empty? tmp("nonexistent") }.should raise_error(Errno::ENOENT) + end +end diff --git a/spec/ruby/core/dir/entries_spec.rb b/spec/ruby/core/dir/entries_spec.rb new file mode 100644 index 0000000000..7462542acf --- /dev/null +++ b/spec/ruby/core/dir/entries_spec.rb @@ -0,0 +1,70 @@ +# encoding: utf-8 + +require_relative '../../spec_helper' +require_relative 'fixtures/common' + +describe "Dir.entries" do + before :all do + DirSpecs.create_mock_dirs + end + + before :each do + @internal = Encoding.default_internal + end + + after :all do + DirSpecs.delete_mock_dirs + end + + after :each do + Encoding.default_internal = @internal + end + + it "returns an Array of filenames in an existing directory including dotfiles" do + a = Dir.entries(DirSpecs.mock_dir).sort + + a.should == DirSpecs.expected_paths + + a = Dir.entries("#{DirSpecs.mock_dir}/deeply/nested").sort + a.should == %w|. .. .dotfile.ext directory| + end + + it "calls #to_path on non-String arguments" do + p = mock('path') + p.should_receive(:to_path).and_return(DirSpecs.mock_dir) + Dir.entries(p) + end + + it "accepts an encoding keyword for the encoding of the entries" do + dirs = Dir.entries("#{DirSpecs.mock_dir}/deeply/nested", encoding: "utf-8").to_a.sort + dirs.each {|dir| dir.encoding.should == Encoding::UTF_8} + end + + it "returns entries encoded with the filesystem encoding by default" do + # This spec depends on the locale not being US-ASCII because if it is, the + # entries that are not ascii_only? will be BINARY encoded. + entries = Dir.entries(File.join(DirSpecs.mock_dir, 'special')).sort + encoding = Encoding.find("filesystem") + encoding = Encoding::BINARY if encoding == Encoding::US_ASCII + platform_is_not :windows do + entries.should include("ã“ã‚“ã«ã¡ã¯.txt".dup.force_encoding(encoding)) + end + entries.first.encoding.should equal(Encoding.find("filesystem")) + end + + it "returns entries encoded with the specified encoding" do + dir = File.join(DirSpecs.mock_dir, 'special') + entries = Dir.entries(dir, encoding: "euc-jp").sort + entries.first.encoding.should equal(Encoding::EUC_JP) + end + + it "returns entries transcoded to the default internal encoding" do + Encoding.default_internal = Encoding::EUC_KR + entries = Dir.entries(File.join(DirSpecs.mock_dir, 'special')).sort + entries.first.encoding.should equal(Encoding::EUC_KR) + end + + it "raises a SystemCallError if called with a nonexistent directory" do + -> { Dir.entries DirSpecs.nonexistent }.should raise_error(SystemCallError) + end +end diff --git a/spec/ruby/core/dir/exist_spec.rb b/spec/ruby/core/dir/exist_spec.rb new file mode 100644 index 0000000000..0b8e521894 --- /dev/null +++ b/spec/ruby/core/dir/exist_spec.rb @@ -0,0 +1,21 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' +require_relative 'shared/exist' + +describe "Dir.exist?" do + before :all do + DirSpecs.create_mock_dirs + end + + after :all do + DirSpecs.delete_mock_dirs + end + + it_behaves_like :dir_exist, :exist? +end + +describe "Dir.exists?" do + it "has been removed" do + Dir.should_not.respond_to?(:exists?) + end +end diff --git a/spec/ruby/core/dir/fchdir_spec.rb b/spec/ruby/core/dir/fchdir_spec.rb new file mode 100644 index 0000000000..52600a95f2 --- /dev/null +++ b/spec/ruby/core/dir/fchdir_spec.rb @@ -0,0 +1,73 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' + +ruby_version_is '3.3' do + platform_is_not :windows do + describe "Dir.fchdir" do + before :all do + DirSpecs.create_mock_dirs + end + + after :all do + DirSpecs.delete_mock_dirs + end + + before :each do + @original = Dir.pwd + end + + after :each do + Dir.chdir(@original) + end + + it "changes the current working directory to the directory specified by the integer file descriptor" do + dir = Dir.new(DirSpecs.mock_dir) + Dir.fchdir dir.fileno + Dir.pwd.should == DirSpecs.mock_dir + ensure + dir.close + end + + it "returns 0 when successfully changing directory" do + dir = Dir.new(DirSpecs.mock_dir) + Dir.fchdir(dir.fileno).should == 0 + ensure + dir.close + end + + it "returns the value of the block when a block is given" do + dir = Dir.new(DirSpecs.mock_dir) + Dir.fchdir(dir.fileno) { :block_value }.should == :block_value + ensure + dir.close + end + + it "changes to the specified directory for the duration of the block" do + dir = Dir.new(DirSpecs.mock_dir) + Dir.fchdir(dir.fileno) { Dir.pwd }.should == DirSpecs.mock_dir + Dir.pwd.should == @original + ensure + dir.close + end + + it "raises a SystemCallError if the file descriptor given is not valid" do + -> { Dir.fchdir(-1) }.should raise_error(SystemCallError, "Bad file descriptor - fchdir") + -> { Dir.fchdir(-1) { } }.should raise_error(SystemCallError, "Bad file descriptor - fchdir") + end + + it "raises a SystemCallError if the file descriptor given is not for a directory" do + -> { Dir.fchdir $stdout.fileno }.should raise_error(SystemCallError, /(Not a directory|Invalid argument) - fchdir/) + -> { Dir.fchdir($stdout.fileno) { } }.should raise_error(SystemCallError, /(Not a directory|Invalid argument) - fchdir/) + end + end + end + + platform_is :windows do + describe "Dir.fchdir" do + it "raises NotImplementedError" do + -> { Dir.fchdir 1 }.should raise_error(NotImplementedError) + -> { Dir.fchdir(1) { } }.should raise_error(NotImplementedError) + end + end + end +end diff --git a/spec/ruby/core/dir/fileno_spec.rb b/spec/ruby/core/dir/fileno_spec.rb new file mode 100644 index 0000000000..bb84ef5378 --- /dev/null +++ b/spec/ruby/core/dir/fileno_spec.rb @@ -0,0 +1,37 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' + +has_dir_fileno = begin + dir = Dir.new('.') + dir.fileno + true +rescue NotImplementedError + false +rescue Exception + true +ensure + dir.close +end + +describe "Dir#fileno" do + before :each do + @name = tmp("fileno") + mkdir_p @name + @dir = Dir.new(@name) + end + + after :each do + @dir.close + rm_r @name + end + + if has_dir_fileno + it "returns the file descriptor of the dir" do + @dir.fileno.should be_kind_of(Integer) + end + else + it "raises an error when not implemented on the platform" do + -> { @dir.fileno }.should raise_error(NotImplementedError) + end + end +end diff --git a/spec/ruby/core/dir/fixtures/common.rb b/spec/ruby/core/dir/fixtures/common.rb new file mode 100644 index 0000000000..848656c9b9 --- /dev/null +++ b/spec/ruby/core/dir/fixtures/common.rb @@ -0,0 +1,198 @@ +# encoding: utf-8 + +module DirSpecs + def self.mock_dir(dirs = ['dir_specs_mock']) + @mock_dir ||= tmp("") + File.join @mock_dir, dirs + end + + def self.nonexistent + name = File.join mock_dir, "nonexistent00" + name = name.next while File.exist? name + name + end + + # TODO: make these relative to the mock_dir + def self.clear_dirs + [ 'nonexisting', + 'default_perms', + 'reduced', + 'always_returns_0', + '???', + [0xe9].pack('U') + ].each do |dir| + begin + Dir.rmdir mock_dir(dir) + rescue + end + end + end + + # The names of the fixture directories and files used by + # various Dir specs. + def self.mock_dir_files + unless @mock_dir_files + @mock_dir_files = %w[ + .dotfile + .dotsubdir/.dotfile + .dotsubdir/nondotfile + nested/.dotsubir/.dotfile + nested/.dotsubir/nondotfile + + deeply/.dotfile + deeply/nested/.dotfile.ext + deeply/nested/directory/structure/.ext + deeply/nested/directory/structure/bar + deeply/nested/directory/structure/baz + deeply/nested/directory/structure/file_one + deeply/nested/directory/structure/file_one.ext + deeply/nested/directory/structure/foo + deeply/nondotfile + + file_one.ext + file_two.ext + + dir_filename_ordering + dir/filename_ordering + + nondotfile + + subdir_one/.dotfile + subdir_one/nondotfile + subdir_two/nondotfile + subdir_two/nondotfile.ext + + brace/a + brace/a.js + brace/a.erb + brace/a.js.rjs + brace/a.html.erb + + special/+ + + special/^ + special/$ + + special/( + special/) + special/[ + special/] + special/{ + special/} + + special/test{1}/file[1] + special/{}/special + special/test\ +()[]{}/hello_world.erb + ] + + platform_is_not :windows do + @mock_dir_files += %w[ + special/* + special/? + + special/| + + special/ã“ã‚“ã«ã¡ã¯.txt + special/\a + ] + @mock_dir_files << "special/_\u{1f60e}.erb" + end + end + + @mock_dir_files + end + + def self.mock_dir_links + unless @mock_dir_links + @mock_dir_links = [] + platform_is_not :windows do + @mock_dir_links += [ + ['special/ln', 'subdir_one'] + ] + end + end + @mock_dir_links + end + + def self.create_mock_dirs + mock_dir_files.each do |name| + file = File.join mock_dir, name + mkdir_p File.dirname(file) + touch file + end + mock_dir_links.each do |link, target| + full_link = File.join mock_dir, link + full_target = File.join mock_dir, target + + File.symlink full_target, full_link + end + end + + def self.delete_mock_dirs + begin + rm_r mock_dir + rescue Errno::ENOTEMPTY => e + puts Dir["#{mock_dir}/**/*"] + raise e + end + end + + def self.mock_rmdir(*dirs) + mock_dir ['rmdir_dirs'].concat(dirs) + end + + def self.rmdir_dirs(create = true) + dirs = %w[ + empty + nonempty + nonempty/child + noperm + noperm/child + ] + + base_dir = mock_dir ['rmdir_dirs'] + + dirs.reverse_each do |d| + dir = File.join base_dir, d + if File.exist? dir + File.chmod 0777, dir + rm_r dir + end + end + rm_r base_dir + + if create + dirs.each do |d| + dir = File.join base_dir, d + unless File.exist? dir + mkdir_p dir + File.chmod 0777, dir + end + end + end + end + + def self.expected_paths + %w[ + . + .. + .dotfile + .dotsubdir + brace + deeply + dir + dir_filename_ordering + file_one.ext + file_two.ext + nested + nondotfile + special + subdir_one + subdir_two + ] + end + + def self.expected_glob_paths + expected_paths - ['..'] + end +end diff --git a/spec/ruby/core/dir/for_fd_spec.rb b/spec/ruby/core/dir/for_fd_spec.rb new file mode 100644 index 0000000000..ba467f2f86 --- /dev/null +++ b/spec/ruby/core/dir/for_fd_spec.rb @@ -0,0 +1,79 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' + +quarantine! do # leads to "Errno::EBADF: Bad file descriptor - closedir" in DirSpecs.delete_mock_dirs +ruby_version_is '3.3' do + platform_is_not :windows do + describe "Dir.for_fd" do + before :all do + DirSpecs.create_mock_dirs + end + + after :all do + DirSpecs.delete_mock_dirs + end + + before :each do + @original = Dir.pwd + end + + after :each do + Dir.chdir(@original) + end + + it "returns a new Dir object representing the directory specified by the given integer directory file descriptor" do + dir = Dir.new(DirSpecs.mock_dir) + dir_new = Dir.for_fd(dir.fileno) + + dir_new.should.instance_of?(Dir) + dir_new.children.should == dir.children + dir_new.fileno.should == dir.fileno + ensure + dir.close + end + + it "returns a new Dir object without associated path" do + dir = Dir.new(DirSpecs.mock_dir) + dir_new = Dir.for_fd(dir.fileno) + + dir_new.path.should == nil + ensure + dir.close + end + + it "calls #to_int to convert a value to an Integer" do + dir = Dir.new(DirSpecs.mock_dir) + obj = mock("fd") + obj.should_receive(:to_int).and_return(dir.fileno) + + dir_new = Dir.for_fd(obj) + dir_new.fileno.should == dir.fileno + ensure + dir.close + end + + it "raises TypeError when value cannot be converted to Integer" do + -> { + Dir.for_fd(nil) + }.should raise_error(TypeError, "no implicit conversion from nil to integer") + end + + it "raises a SystemCallError if the file descriptor given is not valid" do + -> { Dir.for_fd(-1) }.should raise_error(SystemCallError, "Bad file descriptor - fdopendir") + end + + it "raises a SystemCallError if the file descriptor given is not for a directory" do + -> { Dir.for_fd $stdout.fileno }.should raise_error(SystemCallError, "Not a directory - fdopendir") + end + end + end + + platform_is :windows do + describe "Dir.for_fd" do + it "raises NotImplementedError" do + -> { Dir.for_fd 1 }.should raise_error(NotImplementedError) + end + end + end +end +end diff --git a/spec/ruby/core/dir/foreach_spec.rb b/spec/ruby/core/dir/foreach_spec.rb new file mode 100644 index 0000000000..9cf34b1d71 --- /dev/null +++ b/spec/ruby/core/dir/foreach_spec.rb @@ -0,0 +1,68 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' + +describe "Dir.foreach" do + before :all do + DirSpecs.create_mock_dirs + end + + after :all do + DirSpecs.delete_mock_dirs + end + + it "yields all names in an existing directory to the provided block" do + a, b = [], [] + + Dir.foreach(DirSpecs.mock_dir) {|f| a << f} + Dir.foreach("#{DirSpecs.mock_dir}/deeply/nested") {|f| b << f} + + a.sort.should == DirSpecs.expected_paths + b.sort.should == %w|. .. .dotfile.ext directory| + end + + it "returns nil when successful" do + Dir.foreach(DirSpecs.mock_dir) {|f| f}.should == nil + end + + it "calls #to_path on non-String arguments" do + p = mock('path') + p.should_receive(:to_path).and_return(DirSpecs.mock_dir) + Dir.foreach(p).to_a + end + + it "raises a SystemCallError if passed a nonexistent directory" do + -> { Dir.foreach(DirSpecs.nonexistent) {} }.should raise_error(SystemCallError) + end + + it "returns an Enumerator if no block given" do + Dir.foreach(DirSpecs.mock_dir).should be_an_instance_of(Enumerator) + Dir.foreach(DirSpecs.mock_dir).to_a.sort.should == DirSpecs.expected_paths + end + + it "accepts an encoding keyword for the encoding of the entries" do + dirs = Dir.foreach("#{DirSpecs.mock_dir}/deeply/nested", encoding: "utf-8").to_a.sort + dirs.each { |dir| dir.encoding.should == Encoding::UTF_8 } + + dirs = Dir.foreach("#{DirSpecs.mock_dir}/deeply/nested", encoding: Encoding::ISO_8859_1).to_a.sort + dirs.each { |dir| dir.encoding.should == Encoding::ISO_8859_1 } + + Dir.foreach("#{DirSpecs.mock_dir}/deeply/nested", encoding: Encoding::ISO_8859_1) do |f| + f.encoding.should == Encoding::ISO_8859_1 + end + end + + describe "when no block is given" do + it "returns an Enumerator" do + Dir.foreach(DirSpecs.mock_dir).should be_an_instance_of(Enumerator) + Dir.foreach(DirSpecs.mock_dir).to_a.sort.should == DirSpecs.expected_paths + end + + describe "returned Enumerator" do + describe "size" do + it "should return nil" do + Dir.foreach(DirSpecs.mock_dir).size.should == nil + end + end + end + end +end diff --git a/spec/ruby/core/dir/getwd_spec.rb b/spec/ruby/core/dir/getwd_spec.rb new file mode 100644 index 0000000000..132634347c --- /dev/null +++ b/spec/ruby/core/dir/getwd_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' +require_relative 'shared/pwd' + +describe "Dir.getwd" do + before :all do + DirSpecs.create_mock_dirs + end + + after :all do + DirSpecs.delete_mock_dirs + end + + it_behaves_like :dir_pwd, :getwd +end diff --git a/spec/ruby/core/dir/glob_spec.rb b/spec/ruby/core/dir/glob_spec.rb new file mode 100644 index 0000000000..a60b233bc0 --- /dev/null +++ b/spec/ruby/core/dir/glob_spec.rb @@ -0,0 +1,362 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' +require_relative 'shared/glob' + +describe "Dir.glob" do + it_behaves_like :dir_glob, :glob +end + +describe "Dir.glob" do + it_behaves_like :dir_glob_recursive, :glob +end + +describe "Dir.glob" do + before :each do + DirSpecs.create_mock_dirs + + @cwd = Dir.pwd + Dir.chdir DirSpecs.mock_dir + end + + after :each do + Dir.chdir @cwd + + DirSpecs.delete_mock_dirs + end + + it "can take an array of patterns" do + Dir.glob(["file_o*", "file_t*"]).should == + %w!file_one.ext file_two.ext! + end + + it 'returns matching file paths when supplied :base keyword argument' do + dir = tmp('dir_glob_base') + file_1 = "#{dir}/lib/bloop.rb" + file_2 = "#{dir}/lib/soup.rb" + file_3 = "#{dir}/lib/mismatched_file_type.txt" + file_4 = "#{dir}/mismatched_directory.rb" + + touch file_1 + touch file_2 + touch file_3 + touch file_4 + + Dir.glob('**/*.rb', base: "#{dir}/lib").sort.should == ["bloop.rb", "soup.rb"].sort + ensure + rm_r dir + end + + it "calls #to_path to convert multiple patterns" do + pat1 = mock('file_one.ext') + pat1.should_receive(:to_path).and_return('file_one.ext') + pat2 = mock('file_two.ext') + pat2.should_receive(:to_path).and_return('file_two.ext') + + Dir.glob([pat1, pat2]).should == %w[file_one.ext file_two.ext] + end + + it "matches both dot and non-dotfiles with '*' and option File::FNM_DOTMATCH" do + Dir.glob('*', File::FNM_DOTMATCH).sort.should == DirSpecs.expected_glob_paths + end + + it "matches files with any beginning with '*<non-special characters>' and option File::FNM_DOTMATCH" do + Dir.glob('*file', File::FNM_DOTMATCH).sort.should == %w|.dotfile nondotfile|.sort + end + + it "matches any files in the current directory with '**' and option File::FNM_DOTMATCH" do + Dir.glob('**', File::FNM_DOTMATCH).sort.should == DirSpecs.expected_glob_paths + end + + it "recursively matches any subdirectories except './' or '../' with '**/' from the current directory and option File::FNM_DOTMATCH" do + expected = %w[ + .dotsubdir/ + brace/ + deeply/ + deeply/nested/ + deeply/nested/directory/ + deeply/nested/directory/structure/ + dir/ + nested/ + nested/.dotsubir/ + special/ + special/test\ +()[]{}/ + special/test{1}/ + special/{}/ + subdir_one/ + subdir_two/ + ] + + Dir.glob('**/', File::FNM_DOTMATCH).sort.should == expected + end + + it "recursively matches files and directories in nested dot subdirectory except . with 'nested/**/*' from the current directory and option File::FNM_DOTMATCH" do + expected = %w[ + nested/. + nested/.dotsubir + nested/.dotsubir/.dotfile + nested/.dotsubir/nondotfile + ] + + Dir.glob('nested/**/*', File::FNM_DOTMATCH).sort.should == expected.sort + end + + # This is a separate case to check **/ coming after a constant + # directory as well. + it "recursively matches any subdirectories except './' or '../' with '**/' and option File::FNM_DOTMATCH" do + expected = %w[ + ./ + ./.dotsubdir/ + ./brace/ + ./deeply/ + ./deeply/nested/ + ./deeply/nested/directory/ + ./deeply/nested/directory/structure/ + ./dir/ + ./nested/ + ./nested/.dotsubir/ + ./special/ + ./special/test\ +()[]{}/ + ./special/test{1}/ + ./special/{}/ + ./subdir_one/ + ./subdir_two/ + ] + + Dir.glob('./**/', File::FNM_DOTMATCH).sort.should == expected + end + + it "matches a list of paths by concatenating their individual results" do + expected = %w[ + deeply/ + deeply/nested/ + deeply/nested/directory/ + deeply/nested/directory/structure/ + subdir_two/nondotfile + subdir_two/nondotfile.ext + ] + + Dir.glob('{deeply/**/,subdir_two/*}').sort.should == expected + end + + it "preserves multiple /s before a **" do + expected = %w[ + deeply//nested/directory/structure + ] + + Dir.glob('{deeply//**/structure}').sort.should == expected + end + + it "accepts a block and yields it with each elements" do + ary = [] + ret = Dir.glob(["file_o*", "file_t*"]) { |t| ary << t } + ret.should be_nil + ary.should == %w!file_one.ext file_two.ext! + end + + it "ignores non-dirs when traversing recursively" do + touch "spec" + Dir.glob("spec/**/*.rb").should == [] + end + + it "matches nothing when given an empty list of paths" do + Dir.glob('{}').should == [] + end + + it "handles infinite directory wildcards" do + Dir.glob('**/**/**').should_not.empty? + end + + it "handles **/** with base keyword argument" do + Dir.glob('**/**', base: "dir").should == ["filename_ordering"] + + expected = %w[ + nested + nested/directory + nested/directory/structure + nested/directory/structure/bar + nested/directory/structure/baz + nested/directory/structure/file_one + nested/directory/structure/file_one.ext + nested/directory/structure/foo + nondotfile + ].sort + + Dir.glob('**/**', base: "deeply").sort.should == expected + end + + it "handles **/ with base keyword argument" do + expected = %w[ + / + directory/ + directory/structure/ + ] + Dir.glob('**/', base: "deeply/nested").sort.should == expected + end + + it "handles **/nondotfile with base keyword argument" do + expected = %w[ + deeply/nondotfile + nondotfile + subdir_one/nondotfile + subdir_two/nondotfile + ] + Dir.glob('**/nondotfile', base: ".").sort.should == expected + end + + it "handles **/nondotfile with base keyword argument and FNM_DOTMATCH" do + expected = %w[ + .dotsubdir/nondotfile + deeply/nondotfile + nested/.dotsubir/nondotfile + nondotfile + subdir_one/nondotfile + subdir_two/nondotfile + ] + Dir.glob('**/nondotfile', File::FNM_DOTMATCH, base: ".").sort.should == expected + end + + it "handles **/.dotfile with base keyword argument" do + expected = %w[ + .dotfile + deeply/.dotfile + subdir_one/.dotfile + ] + Dir.glob('**/.dotfile', base: ".").sort.should == expected + end + + it "handles **/.dotfile with base keyword argument and FNM_DOTMATCH" do + expected = %w[ + .dotfile + .dotsubdir/.dotfile + deeply/.dotfile + nested/.dotsubir/.dotfile + subdir_one/.dotfile + ] + Dir.glob('**/.dotfile', File::FNM_DOTMATCH, base: ".").sort.should == expected + end + + it "handles **/.* with base keyword argument" do + expected = %w[ + .dotfile.ext + directory/structure/.ext + ].sort + + Dir.glob('**/.*', base: "deeply/nested").sort.should == expected + end + + it "handles **/.* with base keyword argument and FNM_DOTMATCH" do + expected = %w[ + . + .dotfile.ext + directory/structure/.ext + ].sort + + Dir.glob('**/.*', File::FNM_DOTMATCH, base: "deeply/nested").sort.should == expected + end + + it "handles **/** with base keyword argument and FNM_DOTMATCH" do + expected = %w[ + . + .dotfile.ext + directory + directory/structure + directory/structure/.ext + directory/structure/bar + directory/structure/baz + directory/structure/file_one + directory/structure/file_one.ext + directory/structure/foo + ].sort + + Dir.glob('**/**', File::FNM_DOTMATCH, base: "deeply/nested").sort.should == expected + end + + it "handles **/*pattern* with base keyword argument and FNM_DOTMATCH" do + expected = %w[ + .dotfile.ext + directory/structure/file_one + directory/structure/file_one.ext + ] + + Dir.glob('**/*file*', File::FNM_DOTMATCH, base: "deeply/nested").sort.should == expected + end + + it "handles **/glob with base keyword argument and FNM_EXTGLOB" do + expected = %w[ + directory/structure/bar + directory/structure/file_one + directory/structure/file_one.ext + ] + + Dir.glob('**/*{file,bar}*', File::FNM_EXTGLOB, base: "deeply/nested").sort.should == expected + end + + it "handles simple filename patterns" do + Dir.glob('.dotfile').should == ['.dotfile'] + end + + it "handles simple directory patterns" do + Dir.glob('.dotsubdir/').should == ['.dotsubdir/'] + end + + it "handles simple directory patterns applied to non-directories" do + Dir.glob('nondotfile/').should == [] + end + + platform_is_not(:windows) do + it "matches the literal character '\\' with option File::FNM_NOESCAPE" do + Dir.mkdir 'foo?bar' + + begin + Dir.glob('foo?bar', File::FNM_NOESCAPE).should == %w|foo?bar| + Dir.glob('foo\?bar', File::FNM_NOESCAPE).should == [] + ensure + Dir.rmdir 'foo?bar' + end + + Dir.mkdir 'foo\?bar' + + begin + Dir.glob('foo\?bar', File::FNM_NOESCAPE).should == %w|foo\\?bar| + ensure + Dir.rmdir 'foo\?bar' + end + end + + it "returns nil for directories current user has no permission to read" do + Dir.mkdir('no_permission') + File.chmod(0, 'no_permission') + + begin + Dir.glob('no_permission/*').should == [] + ensure + Dir.rmdir('no_permission') + end + end + + it "will follow symlinks when processing a `*/` pattern." do + expected = ['special/ln/nondotfile'] + Dir.glob('special/*/nondotfile').should == expected + end + + it "will not follow symlinks when recursively traversing directories" do + expected = %w[ + deeply/nondotfile + nondotfile + subdir_one/nondotfile + subdir_two/nondotfile + ] + Dir.glob('**/nondotfile').sort.should == expected + end + + it "will follow symlinks when testing directory after recursive directory in pattern" do + expected = %w[ + deeply/nondotfile + special/ln/nondotfile + subdir_one/nondotfile + subdir_two/nondotfile + ] + Dir.glob('**/*/nondotfile').sort.should == expected + end + end +end diff --git a/spec/ruby/core/dir/home_spec.rb b/spec/ruby/core/dir/home_spec.rb new file mode 100644 index 0000000000..966ac38af3 --- /dev/null +++ b/spec/ruby/core/dir/home_spec.rb @@ -0,0 +1,85 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' + +describe "Dir.home" do + before :each do + @home = ENV['HOME'] + ENV['HOME'] = "/rubyspec_home" + end + + after :each do + ENV['HOME'] = @home + end + + describe "when called without arguments" do + it "returns the current user's home directory, reading $HOME first" do + Dir.home.should == "/rubyspec_home" + end + + it "returns a non-frozen string" do + Dir.home.should_not.frozen? + end + + it "returns a string with the filesystem encoding" do + Dir.home.encoding.should == Encoding.find("filesystem") + end + + platform_is_not :windows do + it "works even if HOME is unset" do + ENV.delete('HOME') + Dir.home.should.start_with?('/') + Dir.home.encoding.should == Encoding.find("filesystem") + end + end + + platform_is :windows do + it "returns the home directory with forward slashs and as UTF-8" do + ENV['HOME'] = "C:\\rubyspäc\\home" + home = Dir.home + home.should == "C:/rubyspäc/home" + home.encoding.should == Encoding::UTF_8 + end + + it "retrieves the directory from HOME, USERPROFILE, HOMEDRIVE/HOMEPATH and the WinAPI in that order" do + old_dirs = [ENV.delete('HOME'), ENV.delete('USERPROFILE'), ENV.delete('HOMEDRIVE'), ENV.delete('HOMEPATH')] + + Dir.home.should == old_dirs[1].gsub("\\", "/") + ENV['HOMEDRIVE'] = "C:" + ENV['HOMEPATH'] = "\\rubyspec\\home1" + Dir.home.should == "C:/rubyspec/home1" + ENV['USERPROFILE'] = "C:\\rubyspec\\home2" + Dir.home.should == "C:/rubyspec/home2" + ENV['HOME'] = "C:\\rubyspec\\home3" + Dir.home.should == "C:/rubyspec/home3" + ensure + ENV['HOME'], ENV['USERPROFILE'], ENV['HOMEDRIVE'], ENV['HOMEPATH'] = *old_dirs + end + end + end + + describe "when called with the current user name" do + platform_is_not :windows, :android, :wasi do + it "returns the named user's home directory, from the user database" do + Dir.home(ENV['USER']).should == `echo ~#{ENV['USER']}`.chomp + end + end + + it "returns a non-frozen string" do + Dir.home(ENV['USER']).should_not.frozen? + end + + it "returns a string with the filesystem encoding" do + Dir.home(ENV['USER']).encoding.should == Encoding.find("filesystem") + end + end + + it "raises an ArgumentError if the named user doesn't exist" do + -> { Dir.home('geuw2n288dh2k') }.should raise_error(ArgumentError) + end + + describe "when called with a nil user name" do + it "returns the current user's home directory, reading $HOME first" do + Dir.home(nil).should == "/rubyspec_home" + end + end +end diff --git a/spec/ruby/core/dir/initialize_spec.rb b/spec/ruby/core/dir/initialize_spec.rb new file mode 100644 index 0000000000..547b7dc18e --- /dev/null +++ b/spec/ruby/core/dir/initialize_spec.rb @@ -0,0 +1,23 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' + +describe "Dir#initialize" do + before :each do + DirSpecs.create_mock_dirs + end + + after :each do + DirSpecs.delete_mock_dirs + end + + it "calls #to_path on non-String arguments" do + p = mock('path') + p.stub!(:to_path).and_return(DirSpecs.mock_dir) + dir = Dir.new(p) + begin + dir.path.should == DirSpecs.mock_dir + ensure + dir.close + end + end +end diff --git a/spec/ruby/core/dir/inspect_spec.rb b/spec/ruby/core/dir/inspect_spec.rb new file mode 100644 index 0000000000..37338a97d4 --- /dev/null +++ b/spec/ruby/core/dir/inspect_spec.rb @@ -0,0 +1,24 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' + +describe "Dir#inspect" do + before :each do + @dir = Dir.new(Dir.getwd) + end + + after :each do + @dir.close + end + + it "returns a String" do + @dir.inspect.should be_an_instance_of(String) + end + + it "includes the class name" do + @dir.inspect.should =~ /Dir/ + end + + it "includes the directory name" do + @dir.inspect.should include(Dir.getwd) + end +end diff --git a/spec/ruby/core/dir/mkdir_spec.rb b/spec/ruby/core/dir/mkdir_spec.rb new file mode 100644 index 0000000000..076ec19dd9 --- /dev/null +++ b/spec/ruby/core/dir/mkdir_spec.rb @@ -0,0 +1,107 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' + +describe "Dir.mkdir" do + before :all do + DirSpecs.create_mock_dirs + end + + after :all do + DirSpecs.delete_mock_dirs + end + + it "creates the named directory with the given permissions" do + DirSpecs.clear_dirs + + nonexisting = DirSpecs.mock_dir('nonexisting') + default_perms = DirSpecs.mock_dir('default_perms') + reduced = DirSpecs.mock_dir('reduced') + begin + File.should_not.exist?(nonexisting) + Dir.mkdir nonexisting + File.should.exist?(nonexisting) + platform_is_not :windows do + Dir.mkdir default_perms + a = File.stat(default_perms).mode + Dir.mkdir reduced, (a - 1) + File.stat(reduced).mode.should_not == a + end + platform_is :windows do + Dir.mkdir default_perms, 0666 + a = File.stat(default_perms).mode + Dir.mkdir reduced, 0444 + File.stat(reduced).mode.should_not == a + end + + always_returns_0 = DirSpecs.mock_dir('always_returns_0') + Dir.mkdir(always_returns_0).should == 0 + platform_is_not(:windows) do + File.chmod(0777, nonexisting, default_perms, reduced, always_returns_0) + end + platform_is_not(:windows) do + File.chmod(0644, nonexisting, default_perms, reduced, always_returns_0) + end + ensure + DirSpecs.clear_dirs + end + end + + it "calls #to_path on non-String path arguments" do + DirSpecs.clear_dirs + p = mock('path') + p.should_receive(:to_path).and_return(DirSpecs.mock_dir('nonexisting')) + Dir.mkdir(p) + DirSpecs.clear_dirs + end + + it "calls #to_int on non-Integer permissions argument" do + DirSpecs.clear_dirs + path = DirSpecs.mock_dir('nonexisting') + permissions = mock('permissions') + permissions.should_receive(:to_int).and_return(0666) + Dir.mkdir(path, permissions) + DirSpecs.clear_dirs + end + + it "raises TypeError if non-Integer permissions argument does not have #to_int method" do + path = DirSpecs.mock_dir('nonexisting') + permissions = Object.new + + -> { Dir.mkdir(path, permissions) }.should raise_error(TypeError, 'no implicit conversion of Object into Integer') + end + + it "raises a SystemCallError if any of the directories in the path before the last does not exist" do + -> { Dir.mkdir "#{DirSpecs.nonexistent}/subdir" }.should raise_error(SystemCallError) + end + + it "raises Errno::EEXIST if the specified directory already exists" do + -> { Dir.mkdir("#{DirSpecs.mock_dir}/dir") }.should raise_error(Errno::EEXIST) + end + + it "raises Errno::EEXIST if the argument points to the existing file" do + -> { Dir.mkdir("#{DirSpecs.mock_dir}/file_one.ext") }.should raise_error(Errno::EEXIST) + end +end + +# The permissions flag are not supported on Windows as stated in documentation: +# The permissions may be modified by the value of File.umask, and are ignored on NT. +platform_is_not :windows do + as_user do + describe "Dir.mkdir" do + before :each do + @dir = tmp "noperms" + end + + after :each do + File.chmod 0777, @dir + rm_r @dir + end + + it "raises a SystemCallError when lacking adequate permissions in the parent dir" do + Dir.mkdir @dir, 0000 + + -> { Dir.mkdir "#{@dir}/subdir" }.should raise_error(SystemCallError) + end + end + end +end diff --git a/spec/ruby/core/dir/open_spec.rb b/spec/ruby/core/dir/open_spec.rb new file mode 100644 index 0000000000..27f362320b --- /dev/null +++ b/spec/ruby/core/dir/open_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' +require_relative 'shared/open' + +describe "Dir.open" do + before :all do + DirSpecs.create_mock_dirs + end + + after :all do + DirSpecs.delete_mock_dirs + end + + it_behaves_like :dir_open, :open +end diff --git a/spec/ruby/core/dir/path_spec.rb b/spec/ruby/core/dir/path_spec.rb new file mode 100644 index 0000000000..b1c24c406b --- /dev/null +++ b/spec/ruby/core/dir/path_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' +require_relative 'shared/path' + +describe "Dir#path" do + before :all do + DirSpecs.create_mock_dirs + end + + after :all do + DirSpecs.delete_mock_dirs + end + + it_behaves_like :dir_path, :path +end diff --git a/spec/ruby/core/dir/pos_spec.rb b/spec/ruby/core/dir/pos_spec.rb new file mode 100644 index 0000000000..b382bff81f --- /dev/null +++ b/spec/ruby/core/dir/pos_spec.rb @@ -0,0 +1,40 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' +require_relative 'shared/closed' +require_relative 'shared/pos' + +describe "Dir#pos" do + before :all do + DirSpecs.create_mock_dirs + end + + after :all do + DirSpecs.delete_mock_dirs + end + + it_behaves_like :dir_pos, :pos +end + +describe "Dir#pos" do + before :all do + DirSpecs.create_mock_dirs + end + + after :all do + DirSpecs.delete_mock_dirs + end + + it_behaves_like :dir_closed, :pos +end + +describe "Dir#pos=" do + before :all do + DirSpecs.create_mock_dirs + end + + after :all do + DirSpecs.delete_mock_dirs + end + + it_behaves_like :dir_pos_set, :pos= +end diff --git a/spec/ruby/core/dir/pwd_spec.rb b/spec/ruby/core/dir/pwd_spec.rb new file mode 100644 index 0000000000..ad01286c90 --- /dev/null +++ b/spec/ruby/core/dir/pwd_spec.rb @@ -0,0 +1,39 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/common' +require_relative 'shared/pwd' + +describe "Dir.pwd" do + before :all do + DirSpecs.create_mock_dirs + end + + after :all do + DirSpecs.delete_mock_dirs + end + + it_behaves_like :dir_pwd, :pwd +end + +describe "Dir.pwd" do + before :each do + @name = tmp("ã‚").force_encoding('binary') + @fs_encoding = Encoding.find('filesystem') + end + + after :each do + rm_r @name + end + + platform_is_not :windows do + it "correctly handles dirs with unicode characters in them" do + Dir.mkdir @name + Dir.chdir @name do + if @fs_encoding == Encoding::UTF_8 + Dir.pwd.encoding.should == Encoding::UTF_8 + end + Dir.pwd.force_encoding('binary').should == @name + end + end + end +end diff --git a/spec/ruby/core/dir/read_spec.rb b/spec/ruby/core/dir/read_spec.rb new file mode 100644 index 0000000000..276930c6b7 --- /dev/null +++ b/spec/ruby/core/dir/read_spec.rb @@ -0,0 +1,76 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' +require_relative 'shared/closed' + +describe "Dir#read" do + before :all do + DirSpecs.create_mock_dirs + end + + after :all do + DirSpecs.delete_mock_dirs + end + + it "returns the file name in the current seek position" do + # an FS does not necessarily impose order + ls = Dir.entries DirSpecs.mock_dir + dir = Dir.open DirSpecs.mock_dir + ls.should include(dir.read) + dir.close + end + + it "returns nil when there are no more entries" do + dir = Dir.open DirSpecs.mock_dir + DirSpecs.expected_paths.size.times do + dir.read.should_not == nil + end + dir.read.should == nil + dir.close + end + + it "returns each entry successively" do + dir = Dir.open DirSpecs.mock_dir + entries = [] + while entry = dir.read + entries << entry + end + dir.close + + entries.sort.should == DirSpecs.expected_paths + end + + platform_is_not :windows do + it "returns all directory entries even when encoding conversion will fail" do + dir = Dir.open(File.join(DirSpecs.mock_dir, 'special')) + utf8_entries = [] + begin + while entry = dir.read + utf8_entries << entry + end + ensure + dir.close + end + old_internal_encoding = Encoding::default_internal + old_external_encoding = Encoding::default_external + Encoding.default_internal = Encoding::UTF_8 + Encoding.default_external = Encoding::SHIFT_JIS + shift_jis_entries = [] + begin + Dir.open(File.join(DirSpecs.mock_dir, 'special')) do |d| + -> { + while entry = d.read + shift_jis_entries << entry + end + }.should_not raise_error + end + ensure + Encoding.default_internal = old_internal_encoding + Encoding.default_external = old_external_encoding + end + shift_jis_entries.size.should == utf8_entries.size + shift_jis_entries.filter { |f| f.encoding == Encoding::SHIFT_JIS }.size.should == 1 + end + end + + it_behaves_like :dir_closed, :read +end diff --git a/spec/ruby/core/dir/rewind_spec.rb b/spec/ruby/core/dir/rewind_spec.rb new file mode 100644 index 0000000000..220d7f5372 --- /dev/null +++ b/spec/ruby/core/dir/rewind_spec.rb @@ -0,0 +1,36 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' +require_relative 'shared/closed' + +describe "Dir#rewind" do + before :all do + DirSpecs.create_mock_dirs + end + + after :all do + DirSpecs.delete_mock_dirs + end + + before :each do + @dir = Dir.open DirSpecs.mock_dir + end + + after :each do + @dir.close + end + + it "resets the next read to start from the first entry" do + a = @dir.read + b = @dir.read + a.should_not == b + @dir.rewind + c = @dir.read + c.should == a + end + + it "returns the Dir instance" do + @dir.rewind.should == @dir + end + + it_behaves_like :dir_closed, :rewind +end diff --git a/spec/ruby/core/dir/rmdir_spec.rb b/spec/ruby/core/dir/rmdir_spec.rb new file mode 100644 index 0000000000..08cd1a5bc6 --- /dev/null +++ b/spec/ruby/core/dir/rmdir_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' +require_relative 'shared/delete' + +describe "Dir.rmdir" do + before :all do + DirSpecs.create_mock_dirs + end + + after :all do + DirSpecs.delete_mock_dirs + end + + it_behaves_like :dir_delete, :rmdir +end diff --git a/spec/ruby/core/dir/seek_spec.rb b/spec/ruby/core/dir/seek_spec.rb new file mode 100644 index 0000000000..ed409897cd --- /dev/null +++ b/spec/ruby/core/dir/seek_spec.rb @@ -0,0 +1,19 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' +require_relative 'shared/pos' + +describe "Dir#seek" do + before :all do + DirSpecs.create_mock_dirs + end + + after :all do + DirSpecs.delete_mock_dirs + end + + it "returns the Dir instance" do + @dir.seek(@dir.pos).should == @dir + end + + it_behaves_like :dir_pos_set, :seek +end diff --git a/spec/ruby/core/dir/shared/chroot.rb b/spec/ruby/core/dir/shared/chroot.rb new file mode 100644 index 0000000000..a8f7c10a19 --- /dev/null +++ b/spec/ruby/core/dir/shared/chroot.rb @@ -0,0 +1,44 @@ +describe :dir_chroot_as_root, shared: true do + before :all do + DirSpecs.create_mock_dirs + + @real_root = "../" * (__dir__.count('/') - 1) + @ref_dir = File.join("/", File.basename(Dir["/*"].first)) + end + + after :all do + until File.exist?(@ref_dir) + Dir.send(@method, "../") or break + end + + DirSpecs.delete_mock_dirs + end + + # Pending until https://github.com/ruby/ruby/runs/8075149420 is fixed + compilations_ci = ENV["GITHUB_WORKFLOW"] == "Compilations" + + it "can be used to change the process' root directory" do + -> { Dir.send(@method, __dir__) }.should_not raise_error + File.should.exist?("/#{File.basename(__FILE__)}") + end unless compilations_ci + + it "returns 0 if successful" do + Dir.send(@method, '/').should == 0 + end + + it "raises an Errno::ENOENT exception if the directory doesn't exist" do + -> { Dir.send(@method, 'xgwhwhsjai2222jg') }.should raise_error(Errno::ENOENT) + end + + it "can be escaped from with ../" do + Dir.send(@method, @real_root) + File.should.exist?(@ref_dir) + File.should_not.exist?("/#{File.basename(__FILE__)}") + end unless compilations_ci + + it "calls #to_path on non-String argument" do + p = mock('path') + p.should_receive(:to_path).and_return(@real_root) + Dir.send(@method, p) + end +end diff --git a/spec/ruby/core/dir/shared/closed.rb b/spec/ruby/core/dir/shared/closed.rb new file mode 100644 index 0000000000..17d8332c2a --- /dev/null +++ b/spec/ruby/core/dir/shared/closed.rb @@ -0,0 +1,9 @@ +describe :dir_closed, shared: true do + it "raises an IOError when called on a closed Dir instance" do + -> { + dir = Dir.open DirSpecs.mock_dir + dir.close + dir.send(@method) {} + }.should raise_error(IOError) + end +end diff --git a/spec/ruby/core/dir/shared/delete.rb b/spec/ruby/core/dir/shared/delete.rb new file mode 100644 index 0000000000..a81b059759 --- /dev/null +++ b/spec/ruby/core/dir/shared/delete.rb @@ -0,0 +1,53 @@ +describe :dir_delete, shared: true do + before :each do + DirSpecs.rmdir_dirs true + end + + after :each do + DirSpecs.rmdir_dirs false + end + + it "removes empty directories" do + Dir.send(@method, DirSpecs.mock_rmdir("empty")).should == 0 + end + + it "calls #to_path on non-String arguments" do + p = mock('path') + p.should_receive(:to_path).and_return(DirSpecs.mock_rmdir("empty")) + Dir.send(@method, p) + end + + it "raises an Errno::ENOTEMPTY when trying to remove a nonempty directory" do + -> do + Dir.send @method, DirSpecs.mock_rmdir("nonempty") + end.should raise_error(Errno::ENOTEMPTY) + end + + it "raises an Errno::ENOENT when trying to remove a non-existing directory" do + -> do + Dir.send @method, DirSpecs.nonexistent + end.should raise_error(Errno::ENOENT) + end + + it "raises an Errno::ENOTDIR when trying to remove a non-directory" do + file = DirSpecs.mock_rmdir("nonempty/regular") + touch(file) + -> do + Dir.send @method, file + end.should raise_error(Errno::ENOTDIR) + end + + # this won't work on Windows, since chmod(0000) does not remove all permissions + platform_is_not :windows do + as_user do + it "raises an Errno::EACCES if lacking adequate permissions to remove the directory" do + parent = DirSpecs.mock_rmdir("noperm") + child = DirSpecs.mock_rmdir("noperm", "child") + File.chmod(0000, parent) + -> do + Dir.send @method, child + end.should raise_error(Errno::EACCES) + end + end + end +end diff --git a/spec/ruby/core/dir/shared/exist.rb b/spec/ruby/core/dir/shared/exist.rb new file mode 100644 index 0000000000..3097f57715 --- /dev/null +++ b/spec/ruby/core/dir/shared/exist.rb @@ -0,0 +1,57 @@ +describe :dir_exist, shared: true do + it "returns true if the given directory exists" do + Dir.send(@method, __dir__).should be_true + end + + it "returns true for '.'" do + Dir.send(@method, '.').should be_true + end + + it "returns true for '..'" do + Dir.send(@method, '..').should be_true + end + + it "understands non-ASCII paths" do + subdir = File.join(tmp("\u{9876}\u{665}")) + Dir.send(@method, subdir).should be_false + Dir.mkdir(subdir) + Dir.send(@method, subdir).should be_true + Dir.rmdir(subdir) + end + + it "understands relative paths" do + Dir.send(@method, __dir__ + '/../').should be_true + end + + it "returns false if the given directory doesn't exist" do + Dir.send(@method, 'y26dg27n2nwjs8a/').should be_false + end + + it "doesn't require the name to have a trailing slash" do + dir = __dir__ + dir.sub!(/\/$/,'') + Dir.send(@method, dir).should be_true + end + + it "doesn't expand paths" do + skip "$HOME not valid directory" unless ENV['HOME'] && File.directory?(ENV['HOME']) + Dir.send(@method, File.expand_path('~')).should be_true + Dir.send(@method, '~').should be_false + end + + it "returns false if the argument exists but is a file" do + File.should.exist?(__FILE__) + Dir.send(@method, __FILE__).should be_false + end + + it "doesn't set $! when file doesn't exist" do + Dir.send(@method, "/path/to/non/existent/dir") + $!.should be_nil + end + + it "calls #to_path on non String arguments" do + p = mock('path') + p.should_receive(:to_path).and_return(__dir__) + Dir.send(@method, p) + end +end diff --git a/spec/ruby/core/dir/shared/glob.rb b/spec/ruby/core/dir/shared/glob.rb new file mode 100644 index 0000000000..b1fc924f08 --- /dev/null +++ b/spec/ruby/core/dir/shared/glob.rb @@ -0,0 +1,441 @@ +# -*- encoding: utf-8 -*- +describe :dir_glob, shared: true do + before :all do + DirSpecs.create_mock_dirs + @cwd = Dir.pwd + Dir.chdir DirSpecs.mock_dir + end + + after :all do + Dir.chdir @cwd + DirSpecs.delete_mock_dirs + end + + it "raises an Encoding::CompatibilityError if the argument encoding is not compatible with US-ASCII" do + pattern = "files*".dup.force_encoding Encoding::UTF_16BE + -> { Dir.send(@method, pattern) }.should raise_error(Encoding::CompatibilityError) + end + + it "calls #to_path to convert a pattern" do + obj = mock('file_one.ext') + obj.should_receive(:to_path).and_return('file_one.ext') + + Dir.send(@method, obj).should == %w[file_one.ext] + end + + it "raises an ArgumentError if the string contains \\0" do + -> {Dir.send(@method, "file_o*\0file_t*")}.should raise_error ArgumentError, /nul-separated/ + end + + it "result is sorted by default" do + result = Dir.send(@method, '*') + result.should == result.sort + end + + it "result is sorted with sort: true" do + result = Dir.send(@method, '*', sort: true) + result.should == result.sort + end + + it "sort: false returns same files" do + result = Dir.send(@method,'*', sort: false) + result.sort.should == Dir.send(@method, '*').sort + end + + it "raises an ArgumentError if sort: is not true or false" do + -> { Dir.send(@method, '*', sort: 0) }.should raise_error ArgumentError, /expected true or false/ + -> { Dir.send(@method, '*', sort: nil) }.should raise_error ArgumentError, /expected true or false/ + -> { Dir.send(@method, '*', sort: 'false') }.should raise_error ArgumentError, /expected true or false/ + end + + it "matches non-dotfiles with '*'" do + expected = %w[ + brace + deeply + dir + dir_filename_ordering + file_one.ext + file_two.ext + nested + nondotfile + special + subdir_one + subdir_two + ] + + Dir.send(@method,'*').sort.should == expected + end + + it "returns empty array when empty pattern provided" do + Dir.send(@method, '').should == [] + end + + it "matches regexp special +" do + Dir.send(@method, 'special/+').should == ['special/+'] + end + + it "matches directories with special characters when escaped" do + Dir.send(@method, 'special/\{}/special').should == ["special/{}/special"] + end + + platform_is_not :windows do + it "matches regexp special *" do + Dir.send(@method, 'special/\*').should == ['special/*'] + end + + it "matches regexp special ?" do + Dir.send(@method, 'special/\?').should == ['special/?'] + end + + it "matches regexp special |" do + Dir.send(@method, 'special/|').should == ['special/|'] + end + + it "matches files with backslashes in their name" do + Dir.glob('special/\\\\{a,b}').should == ['special/\a'] + end + + it "matches directory with special characters in their name in complex patterns" do + Dir.glob("special/test +()\\[\\]\\{\\}/hello_world{.{en},}{.{html},}{+{phone},}{.{erb},}").should == ['special/test +()[]{}/hello_world.erb'] + end + end + + it "matches regexp special ^" do + Dir.send(@method, 'special/^').should == ['special/^'] + end + + it "matches regexp special $" do + Dir.send(@method, 'special/$').should == ['special/$'] + end + + it "matches regexp special (" do + Dir.send(@method, 'special/(').should == ['special/('] + end + + it "matches regexp special )" do + Dir.send(@method, 'special/)').should == ['special/)'] + end + + it "matches regexp special [" do + Dir.send(@method, 'special/\[').should == ['special/['] + end + + it "matches regexp special ]" do + Dir.send(@method, 'special/]').should == ['special/]'] + end + + it "matches regexp special {" do + Dir.send(@method, 'special/\{').should == ['special/{'] + end + + it "matches regexp special }" do + Dir.send(@method, 'special/\}').should == ['special/}'] + end + + it "matches paths with glob patterns" do + Dir.send(@method, 'special/test\{1\}/*').should == ['special/test{1}/file[1]'] + end + + it "matches dotfiles except .. with '.*'" do + Dir.send(@method, '.*').sort.should == %w|. .dotfile .dotsubdir|.sort + end + + it "matches non-dotfiles with '*<non-special characters>'" do + Dir.send(@method, '*file').sort.should == %w|nondotfile|.sort + end + + it "matches dotfiles with '.*<non-special characters>'" do + Dir.send(@method, '.*file').sort.should == %w|.dotfile|.sort + end + + it "matches files with any ending with '<non-special characters>*'" do + Dir.send(@method, 'file*').sort.should == %w|file_one.ext file_two.ext|.sort + end + + it "matches files with any middle with '<non-special characters>*<non-special characters>'" do + Dir.send(@method, 'sub*_one').sort.should == %w|subdir_one|.sort + end + + it "handles directories with globs" do + Dir.send(@method, 'sub*/*').sort.should == %w!subdir_one/nondotfile subdir_two/nondotfile subdir_two/nondotfile.ext! + end + + it "matches files with multiple '*' special characters" do + Dir.send(@method, '*fi*e*').sort.should == %w|dir_filename_ordering nondotfile file_one.ext file_two.ext|.sort + end + + it "matches non-dotfiles in the current directory with '**'" do + expected = %w[ + brace + deeply + dir + dir_filename_ordering + file_one.ext + file_two.ext + nested + nondotfile + special + subdir_one + subdir_two + ] + + Dir.send(@method, '**').sort.should == expected + end + + it "matches dotfiles in the current directory except .. with '.**'" do + Dir.send(@method, '.**').sort.should == %w|. .dotsubdir .dotfile|.sort + end + + it "recursively matches any nondot subdirectories with '**/'" do + expected = %w[ + brace/ + deeply/ + deeply/nested/ + deeply/nested/directory/ + deeply/nested/directory/structure/ + dir/ + nested/ + special/ + special/test\ +()[]{}/ + special/test{1}/ + special/{}/ + subdir_one/ + subdir_two/ + ] + + Dir.send(@method, '**/').sort.should == expected + end + + it "recursively matches any subdirectories except './' or '../' with '**/' from the base directory if that is specified" do + expected = %w[ + nested/directory + ] + + Dir.send(@method, '**/*ory', base: 'deeply').sort.should == expected + end + + it "recursively matches any subdirectories including ./ with '.**/'" do + Dir.chdir("#{DirSpecs.mock_dir}/subdir_one") do + Dir.send(@method, '.**/').should == ['./'] + end + end + + it "matches a single character except leading '.' with '?'" do + Dir.send(@method, '?ubdir_one').sort.should == %w|subdir_one|.sort + end + + it "accepts multiple '?' characters in a pattern" do + Dir.send(@method, 'subdir_???').sort.should == %w|subdir_one subdir_two|.sort + end + + it "matches any characters in a set with '[<characters>]'" do + Dir.send(@method, '[stfu]ubdir_one').sort.should == %w|subdir_one|.sort + end + + it "matches any characters in a range with '[<character>-<character>]'" do + Dir.send(@method, '[a-zA-Z]ubdir_one').sort.should == %w|subdir_one|.sort + end + + it "matches any characters except those in a set with '[^<characters>]'" do + Dir.send(@method, '[^wtf]ubdir_one').sort.should == %w|subdir_one|.sort + end + + it "matches any characters except those in a range with '[^<character>-<character]'" do + Dir.send(@method, '[^0-9]ubdir_one').sort.should == %w|subdir_one|.sort + end + + it "matches any one of the strings in a set with '{<string>,<other>,...}'" do + Dir.send(@method, 'subdir_{one,two,three}').sort.should == %w|subdir_one subdir_two|.sort + end + + it "matches a set '{<string>,<other>,...}' which also uses a glob" do + Dir.send(@method, 'sub*_{one,two,three}').sort.should == %w|subdir_one subdir_two|.sort + end + + it "accepts string sets with empty strings with {<string>,,<other>}" do + a = Dir.send(@method, 'deeply/nested/directory/structure/file_one{.ext,}').sort + a.should == %w|deeply/nested/directory/structure/file_one.ext + deeply/nested/directory/structure/file_one|.sort + end + + it "matches dot or non-dotfiles with '{,.}*'" do + Dir.send(@method, '{,.}*').sort.should == DirSpecs.expected_glob_paths + end + + it "respects the order of {} expressions, expanding left most first" do + files = Dir.send(@method, "brace/a{.js,.html}{.erb,.rjs}") + files.should == %w!brace/a.js.rjs brace/a.html.erb! + end + + it "respects the optional nested {} expressions" do + files = Dir.send(@method, "brace/a{.{js,html},}{.{erb,rjs},}") + files.should == %w!brace/a.js.rjs brace/a.js brace/a.html.erb brace/a.erb brace/a! + end + + it "matches special characters by escaping with a backslash with '\\<character>'" do + Dir.mkdir 'foo^bar' + + begin + Dir.send(@method, 'foo?bar').should == %w|foo^bar| + Dir.send(@method, 'foo\?bar').should == [] + Dir.send(@method, 'nond\otfile').should == %w|nondotfile| + ensure + Dir.rmdir 'foo^bar' + end + end + + it "recursively matches directories with '**/<characters>'" do + Dir.send(@method, '**/*fil?{,.}*').uniq.sort.should == + %w[deeply/nested/directory/structure/file_one + deeply/nested/directory/structure/file_one.ext + deeply/nondotfile + + dir/filename_ordering + dir_filename_ordering + + file_one.ext + file_two.ext + + nondotfile + + special/test{1}/file[1] + + subdir_one/nondotfile + subdir_two/nondotfile + subdir_two/nondotfile.ext] + end + + it "ignores matching through directories that doesn't exist" do + Dir.send(@method, "deeply/notthere/blah*/whatever").should == [] + end + + it "ignores matching only directories under an nonexistent path" do + Dir.send(@method, "deeply/notthere/blah/").should == [] + end + + platform_is_not :windows do + it "matches UTF-8 paths" do + Dir.send(@method, "special/ã“ã‚“ã«ã¡ã¯{,.txt}").should == ["special/ã“ã‚“ã«ã¡ã¯.txt"] + end + end + + context ":base option passed" do + before :each do + @mock_dir = File.expand_path tmp('dir_glob_mock') + + %w[ + a/b/x + a/b/c/y + a/b/c/d/z + ].each do |path| + file = File.join @mock_dir, path + mkdir_p File.dirname(file) + touch file + end + end + + after :each do + rm_r @mock_dir + end + + it "matches entries only from within the specified directory" do + path = File.join(@mock_dir, "a/b/c") + Dir.send(@method, "*", base: path).sort.should == %w( d y ) + end + + it "accepts both relative and absolute paths" do + require 'pathname' + + path_abs = File.join(@mock_dir, "a/b/c") + path_rel = Pathname.new(path_abs).relative_path_from(Pathname.new(Dir.pwd)) + + result_abs = Dir.send(@method, "*", base: path_abs).sort + result_rel = Dir.send(@method, "*", base: path_rel).sort + + result_abs.should == %w( d y ) + result_rel.should == %w( d y ) + end + + it "returns [] if specified path does not exist" do + path = File.join(@mock_dir, "fake-name") + File.should_not.exist?(path) + + Dir.send(@method, "*", base: path).should == [] + end + + it "returns [] if specified path is a file" do + path = File.join(@mock_dir, "a/b/x") + File.should.exist?(path) + + Dir.send(@method, "*", base: path).should == [] + end + + it "raises TypeError when cannot convert value to string" do + -> { + Dir.send(@method, "*", base: []) + }.should raise_error(TypeError) + end + + it "handles '' as current directory path" do + Dir.chdir @mock_dir do + Dir.send(@method, "*", base: "").should == %w( a ) + end + end + + it "handles nil as current directory path" do + Dir.chdir @mock_dir do + Dir.send(@method, "*", base: nil).should == %w( a ) + end + end + end +end + +describe :dir_glob_recursive, shared: true do + before :each do + @cwd = Dir.pwd + @mock_dir = File.expand_path tmp('dir_glob_mock') + + %w[ + a/x/b/y/e + a/x/b/y/b/z/e + ].each do |path| + file = File.join @mock_dir, path + mkdir_p File.dirname(file) + touch file + end + + Dir.chdir @mock_dir + end + + after :each do + Dir.chdir @cwd + rm_r @mock_dir + end + + it "matches multiple recursives" do + expected = %w[ + a/x/b/y/b/z/e + a/x/b/y/e + ] + + Dir.send(@method, 'a/**/b/**/e').uniq.sort.should == expected + end + + platform_is_not :windows do + it "ignores symlinks" do + file = File.join @mock_dir, 'b/z/e' + link = File.join @mock_dir, 'a/y' + + mkdir_p File.dirname(file) + touch file + File.symlink(File.dirname(file), link) + + expected = %w[ + a/x/b/y/b/z/e + a/x/b/y/e + ] + + Dir.send(@method, 'a/**/e').uniq.sort.should == expected + end + end +end diff --git a/spec/ruby/core/dir/shared/open.rb b/spec/ruby/core/dir/shared/open.rb new file mode 100644 index 0000000000..920845cba1 --- /dev/null +++ b/spec/ruby/core/dir/shared/open.rb @@ -0,0 +1,73 @@ +describe :dir_open, shared: true do + it "returns a Dir instance representing the specified directory" do + dir = Dir.send(@method, DirSpecs.mock_dir) + dir.should be_kind_of(Dir) + dir.close + end + + it "raises a SystemCallError if the directory does not exist" do + -> do + Dir.send @method, DirSpecs.nonexistent + end.should raise_error(SystemCallError) + end + + it "may take a block which is yielded to with the Dir instance" do + Dir.send(@method, DirSpecs.mock_dir) {|dir| dir.should be_kind_of(Dir)} + end + + it "returns the value of the block if a block is given" do + Dir.send(@method, DirSpecs.mock_dir) {|dir| :value }.should == :value + end + + it "closes the Dir instance when the block exits if given a block" do + closed_dir = Dir.send(@method, DirSpecs.mock_dir) { |dir| dir } + -> { closed_dir.read }.should raise_error(IOError) + end + + it "closes the Dir instance when the block exits the block even due to an exception" do + closed_dir = nil + + -> do + Dir.send(@method, DirSpecs.mock_dir) do |dir| + closed_dir = dir + raise "dir specs" + end + end.should raise_error(RuntimeError, "dir specs") + + -> { closed_dir.read }.should raise_error(IOError) + end + + it "calls #to_path on non-String arguments" do + p = mock('path') + p.should_receive(:to_path).and_return(DirSpecs.mock_dir) + Dir.send(@method, p) { true } + end + + it "accepts an options Hash" do + dir = Dir.send(@method, DirSpecs.mock_dir, encoding: "utf-8") {|d| d } + dir.should be_kind_of(Dir) + end + + it "calls #to_hash to convert the options object" do + options = mock("dir_open") + options.should_receive(:to_hash).and_return({ encoding: Encoding::UTF_8 }) + + dir = Dir.send(@method, DirSpecs.mock_dir, **options) {|d| d } + dir.should be_kind_of(Dir) + end + + it "ignores the :encoding option if it is nil" do + dir = Dir.send(@method, DirSpecs.mock_dir, encoding: nil) {|d| d } + dir.should be_kind_of(Dir) + end + + platform_is_not :windows do + it 'sets the close-on-exec flag for the directory file descriptor' do + Dir.send(@method, DirSpecs.mock_dir) do |dir| + io = IO.for_fd(dir.fileno) + io.autoclose = false + io.should.close_on_exec? + end + end + end +end diff --git a/spec/ruby/core/dir/shared/path.rb b/spec/ruby/core/dir/shared/path.rb new file mode 100644 index 0000000000..494dcca775 --- /dev/null +++ b/spec/ruby/core/dir/shared/path.rb @@ -0,0 +1,30 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/common' +require_relative 'closed' + +describe :dir_path, shared: true do + it "returns the path that was supplied to .new or .open" do + dir = Dir.open DirSpecs.mock_dir + begin + dir.send(@method).should == DirSpecs.mock_dir + ensure + dir.close rescue nil + end + end + + it "returns the path even when called on a closed Dir instance" do + dir = Dir.open DirSpecs.mock_dir + dir.close + dir.send(@method).should == DirSpecs.mock_dir + end + + it "returns a String with the same encoding as the argument to .open" do + path = DirSpecs.mock_dir.force_encoding Encoding::IBM866 + dir = Dir.open path + begin + dir.send(@method).encoding.should equal(Encoding::IBM866) + ensure + dir.close + end + end +end diff --git a/spec/ruby/core/dir/shared/pos.rb b/spec/ruby/core/dir/shared/pos.rb new file mode 100644 index 0000000000..2165932d99 --- /dev/null +++ b/spec/ruby/core/dir/shared/pos.rb @@ -0,0 +1,51 @@ +describe :dir_pos, shared: true do + before :each do + @dir = Dir.open DirSpecs.mock_dir + end + + after :each do + @dir.close rescue nil + end + + it "returns an Integer representing the current position in the directory" do + @dir.send(@method).should be_kind_of(Integer) + @dir.send(@method).should be_kind_of(Integer) + @dir.send(@method).should be_kind_of(Integer) + end + + it "returns a different Integer if moved from previous position" do + a = @dir.send(@method) + @dir.read + b = @dir.send(@method) + + a.should be_kind_of(Integer) + b.should be_kind_of(Integer) + + a.should_not == b + end +end + +describe :dir_pos_set, shared: true do + before :each do + @dir = Dir.open DirSpecs.mock_dir + end + + after :each do + @dir.close + end + + # NOTE: #seek/#pos= to a position not returned by #tell/#pos is undefined + # and should not be spec'd. + + it "moves the read position to a previously obtained position" do + pos = @dir.pos + a = @dir.read + b = @dir.read + @dir.send @method, pos + c = @dir.read + + a.should_not == b + b.should_not == c + c.should == a + end +end diff --git a/spec/ruby/core/dir/shared/pwd.rb b/spec/ruby/core/dir/shared/pwd.rb new file mode 100644 index 0000000000..2a8d7fe790 --- /dev/null +++ b/spec/ruby/core/dir/shared/pwd.rb @@ -0,0 +1,45 @@ +describe :dir_pwd, shared: true do + before :each do + @fs_encoding = Encoding.find('filesystem') + end + + it "returns the current working directory" do + pwd = Dir.send(@method) + + File.directory?(pwd).should == true + + # On ubuntu gutsy, for example, /bin/pwd does not + # understand -P. With just `pwd -P`, /bin/pwd is run. + + # The following uses inode rather than file names to account for + # case insensitive file systems like default OS/X file systems + platform_is_not :windows do + File.stat(pwd).ino.should == File.stat(`/bin/sh -c "pwd -P"`.chomp).ino + end + platform_is :windows do + File.stat(pwd).ino.should == File.stat(File.expand_path(`cd`.chomp)).ino + end + end + + it "returns an absolute path" do + pwd = Dir.send(@method) + pwd.should == File.expand_path(pwd) + end + + it "returns an absolute path even when chdir to a relative path" do + Dir.chdir(".") do + pwd = Dir.send(@method) + File.directory?(pwd).should == true + pwd.should == File.expand_path(pwd) + end + end + + it "returns a String with the filesystem encoding" do + enc = Dir.send(@method).encoding + if @fs_encoding == Encoding::US_ASCII + [Encoding::US_ASCII, Encoding::BINARY].should include(enc) + else + enc.should equal(@fs_encoding) + end + end +end diff --git a/spec/ruby/core/dir/tell_spec.rb b/spec/ruby/core/dir/tell_spec.rb new file mode 100644 index 0000000000..af86dc1598 --- /dev/null +++ b/spec/ruby/core/dir/tell_spec.rb @@ -0,0 +1,18 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' +require_relative 'shared/closed' +require_relative 'shared/pos' + +describe "Dir#tell" do + before :all do + DirSpecs.create_mock_dirs + end + + after :all do + DirSpecs.delete_mock_dirs + end + + it_behaves_like :dir_pos, :tell + + it_behaves_like :dir_closed, :tell +end diff --git a/spec/ruby/core/dir/to_path_spec.rb b/spec/ruby/core/dir/to_path_spec.rb new file mode 100644 index 0000000000..77404a3dc8 --- /dev/null +++ b/spec/ruby/core/dir/to_path_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' +require_relative 'shared/path' + +describe "Dir#to_path" do + before :all do + DirSpecs.create_mock_dirs + end + + after :all do + DirSpecs.delete_mock_dirs + end + + it_behaves_like :dir_path, :to_path +end diff --git a/spec/ruby/core/dir/unlink_spec.rb b/spec/ruby/core/dir/unlink_spec.rb new file mode 100644 index 0000000000..79027e020c --- /dev/null +++ b/spec/ruby/core/dir/unlink_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' +require_relative 'shared/delete' + +describe "Dir.unlink" do + before :all do + DirSpecs.create_mock_dirs + end + + after :all do + DirSpecs.delete_mock_dirs + end + + it_behaves_like :dir_delete, :unlink +end diff --git a/spec/ruby/core/encoding/_dump_spec.rb b/spec/ruby/core/encoding/_dump_spec.rb new file mode 100644 index 0000000000..623fe88ec9 --- /dev/null +++ b/spec/ruby/core/encoding/_dump_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../spec_helper' + +describe "Encoding#_dump" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/encoding/_load_spec.rb b/spec/ruby/core/encoding/_load_spec.rb new file mode 100644 index 0000000000..608098d34b --- /dev/null +++ b/spec/ruby/core/encoding/_load_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../spec_helper' + +describe "Encoding._load" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/encoding/aliases_spec.rb b/spec/ruby/core/encoding/aliases_spec.rb new file mode 100644 index 0000000000..786157981a --- /dev/null +++ b/spec/ruby/core/encoding/aliases_spec.rb @@ -0,0 +1,43 @@ +require_relative '../../spec_helper' + +describe "Encoding.aliases" do + it "returns a Hash" do + Encoding.aliases.should be_an_instance_of(Hash) + end + + it "has Strings as keys" do + Encoding.aliases.keys.each do |key| + key.should be_an_instance_of(String) + end + end + + it "has Strings as values" do + Encoding.aliases.values.each do |value| + value.should be_an_instance_of(String) + end + end + + it "has alias names as its keys" do + Encoding.aliases.key?('BINARY').should be_true + Encoding.aliases.key?('ASCII').should be_true + end + + it "has the names of the aliased encoding as its values" do + Encoding.aliases['BINARY'].should == 'ASCII-8BIT' + Encoding.aliases['ASCII'].should == 'US-ASCII' + end + + it "has an 'external' key with the external default encoding as its value" do + Encoding.aliases['external'].should == Encoding.default_external.name + end + + it "has a 'locale' key and its value equals the name of the encoding found by the locale charmap" do + Encoding.aliases['locale'].should == Encoding.find(Encoding.locale_charmap).name + end + + it "only contains valid aliased encodings" do + Encoding.aliases.each do |aliased, original| + Encoding.find(aliased).should == Encoding.find(original) + end + end +end diff --git a/spec/ruby/core/encoding/ascii_compatible_spec.rb b/spec/ruby/core/encoding/ascii_compatible_spec.rb new file mode 100644 index 0000000000..4804300e85 --- /dev/null +++ b/spec/ruby/core/encoding/ascii_compatible_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' + +describe "Encoding#ascii_compatible?" do + it "returns true if self represents an ASCII-compatible encoding" do + Encoding::UTF_8.ascii_compatible?.should be_true + end + + it "returns false if self does not represent an ASCII-compatible encoding" do + Encoding::UTF_16LE.ascii_compatible?.should be_false + end +end diff --git a/spec/ruby/core/encoding/compatible_spec.rb b/spec/ruby/core/encoding/compatible_spec.rb new file mode 100644 index 0000000000..31376a3b75 --- /dev/null +++ b/spec/ruby/core/encoding/compatible_spec.rb @@ -0,0 +1,758 @@ +# encoding: binary + +require_relative '../../spec_helper' + +# TODO: add IO + +describe "Encoding.compatible? String, String" do + describe "when the first's Encoding is valid US-ASCII" do + before :each do + @str = "abc".dup.force_encoding Encoding::US_ASCII + end + + it "returns US-ASCII when the second's is US-ASCII" do + Encoding.compatible?(@str, "def".encode("us-ascii")).should == Encoding::US_ASCII + end + + it "returns US-ASCII if the second String is BINARY and ASCII only" do + Encoding.compatible?(@str, "\x7f").should == Encoding::US_ASCII + end + + it "returns BINARY if the second String is BINARY but not ASCII only" do + Encoding.compatible?(@str, "\xff").should == Encoding::BINARY + end + + it "returns US-ASCII if the second String is UTF-8 and ASCII only" do + Encoding.compatible?(@str, "\x7f".encode("utf-8")).should == Encoding::US_ASCII + end + + it "returns UTF-8 if the second String is UTF-8 but not ASCII only" do + Encoding.compatible?(@str, "\u3042".encode("utf-8")).should == Encoding::UTF_8 + end + end + + describe "when the first's Encoding is ASCII compatible and ASCII only" do + it "returns the first's Encoding if the second is ASCII compatible and ASCII only" do + [ [Encoding, "abc".dup.force_encoding("UTF-8"), "123".dup.force_encoding("Shift_JIS"), Encoding::UTF_8], + [Encoding, "123".dup.force_encoding("Shift_JIS"), "abc".dup.force_encoding("UTF-8"), Encoding::Shift_JIS] + ].should be_computed_by(:compatible?) + end + + it "returns the first's Encoding if the second is ASCII compatible and ASCII only" do + [ [Encoding, "abc".dup.force_encoding("BINARY"), "123".dup.force_encoding("US-ASCII"), Encoding::BINARY], + [Encoding, "123".dup.force_encoding("US-ASCII"), "abc".dup.force_encoding("BINARY"), Encoding::US_ASCII] + ].should be_computed_by(:compatible?) + end + + it "returns the second's Encoding if the second is ASCII compatible but not ASCII only" do + [ [Encoding, "abc".dup.force_encoding("UTF-8"), "\xff".dup.force_encoding("Shift_JIS"), Encoding::Shift_JIS], + [Encoding, "123".dup.force_encoding("Shift_JIS"), "\xff".dup.force_encoding("UTF-8"), Encoding::UTF_8], + [Encoding, "abc".dup.force_encoding("BINARY"), "\xff".dup.force_encoding("US-ASCII"), Encoding::US_ASCII], + [Encoding, "123".dup.force_encoding("US-ASCII"), "\xff".dup.force_encoding("BINARY"), Encoding::BINARY], + ].should be_computed_by(:compatible?) + end + + it "returns nil if the second's Encoding is not ASCII compatible" do + a = "abc".dup.force_encoding("UTF-8") + b = "1234".dup.force_encoding("UTF-16LE") + Encoding.compatible?(a, b).should be_nil + end + end + + describe "when the first's Encoding is ASCII compatible but not ASCII only" do + it "returns the first's Encoding if the second's is valid US-ASCII" do + Encoding.compatible?("\xff", "def".encode("us-ascii")).should == Encoding::BINARY + end + + it "returns the first's Encoding if the second's is UTF-8 and ASCII only" do + Encoding.compatible?("\xff", "\u{7f}".encode("utf-8")).should == Encoding::BINARY + end + + it "returns nil if the second encoding is ASCII compatible but neither String's encoding is ASCII only" do + Encoding.compatible?("\xff", "\u3042".encode("utf-8")).should be_nil + end + end + + describe "when the first's Encoding is not ASCII compatible" do + before :each do + @str = "abc".dup.force_encoding Encoding::UTF_7 + end + + it "returns nil when the second String is US-ASCII" do + Encoding.compatible?(@str, "def".encode("us-ascii")).should be_nil + end + + it "returns nil when the second String is BINARY and ASCII only" do + Encoding.compatible?(@str, "\x7f").should be_nil + end + + it "returns nil when the second String is BINARY but not ASCII only" do + Encoding.compatible?(@str, "\xff").should be_nil + end + + it "returns the Encoding when the second's Encoding is not ASCII compatible but the same as the first's Encoding" do + encoding = Encoding.compatible?(@str, "def".dup.force_encoding("utf-7")) + encoding.should == Encoding::UTF_7 + end + end + + describe "when the first's Encoding is invalid" do + before :each do + @str = "\xff".dup.force_encoding Encoding::UTF_8 + end + + it "returns the first's Encoding when the second's Encoding is US-ASCII" do + Encoding.compatible?(@str, "def".encode("us-ascii")).should == Encoding::UTF_8 + end + + it "returns the first's Encoding when the second String is ASCII only" do + Encoding.compatible?(@str, "\x7f").should == Encoding::UTF_8 + end + + it "returns nil when the second's Encoding is BINARY but not ASCII only" do + Encoding.compatible?(@str, "\xff").should be_nil + end + + it "returns nil when the second's Encoding is invalid and ASCII only" do + Encoding.compatible?(@str, "\x7f\x7f".dup.force_encoding("utf-16be")).should be_nil + end + + it "returns nil when the second's Encoding is invalid and not ASCII only" do + Encoding.compatible?(@str, "\xff\xff".dup.force_encoding("utf-16be")).should be_nil + end + + it "returns the Encoding when the second's Encoding is invalid but the same as the first" do + Encoding.compatible?(@str, @str).should == Encoding::UTF_8 + end + end + + describe "when the first String is empty and the second is not" do + describe "and the first's Encoding is ASCII compatible" do + before :each do + @str = "".dup.force_encoding("utf-8") + end + + it "returns the first's encoding when the second String is ASCII only" do + Encoding.compatible?(@str, "def".encode("us-ascii")).should == Encoding::UTF_8 + end + + it "returns the second's encoding when the second String is not ASCII only" do + Encoding.compatible?(@str, "def".encode("utf-32le")).should == Encoding::UTF_32LE + end + end + + describe "when the first's Encoding is not ASCII compatible" do + before :each do + @str = "".dup.force_encoding Encoding::UTF_7 + end + + it "returns the second string's encoding" do + Encoding.compatible?(@str, "def".encode("us-ascii")).should == Encoding::US_ASCII + end + end + end + + describe "when the second String is empty" do + before :each do + @str = "abc".dup.force_encoding("utf-7") + end + + it "returns the first Encoding" do + Encoding.compatible?(@str, "").should == Encoding::UTF_7 + end + end + + # Encoding negotiation depends on whether encodings are ASCII-compatible, empty + # and contain only ASCII characters (that take 7 bits). Check US-ASCII, UTF-8 and + # BINARY encodings (as most common) as well as an ASCII-compatible, a non-ASCII-compatible and a dummy + # encodings in all possible combinations. + describe "compatibility matrix" do + +# Use the following script to regenerate the matrix: +# +# ``` +# # encoding: binary +# +# ENCODINGS = [ +# "US-ASCII", +# "UTF-8", +# "ASCII-8BIT", +# "ISO-8859-1", # ASCII-compatible +# "UTF-16BE", # non-ASCII-compatible +# "ISO-2022-JP" # dummy +# ] +# +# TYPES = [:empty, :"7bits", :non7bits] +# +# VALUES = { +# empty: "", +# :"7bits" => "\x01\x01", +# non7bits: "\x01\x81" +# } +# +# ENCODINGS.product(TYPES, ENCODINGS, TYPES).each do |encoding1, type1, encoding2, type2| +# value1 = VALUES[type1].dup.force_encoding(encoding1) +# value2 = VALUES[type2].dup.force_encoding(encoding2) +# +# result_encoding = Encoding.compatible?(value1, value2) +# +# puts "[#{encoding1.inspect}, #{value1.inspect}, #{encoding2.inspect}, #{value2.inspect}, #{result_encoding&.name.inspect}]," +# end +# ``` + + matrix = [ + ["US-ASCII", "", "US-ASCII", "", "US-ASCII"], + ["US-ASCII", "", "US-ASCII", "\x01\x01", "US-ASCII"], + ["US-ASCII", "", "US-ASCII", "\x01\x81", "US-ASCII"], + ["US-ASCII", "", "UTF-8", "", "US-ASCII"], + ["US-ASCII", "", "UTF-8", "\u0001\u0001", "US-ASCII"], + ["US-ASCII", "", "UTF-8", "\u0001\x81", "UTF-8"], + ["US-ASCII", "", "ASCII-8BIT", "", "US-ASCII"], + ["US-ASCII", "", "ASCII-8BIT", "\x01\x01", "US-ASCII"], + ["US-ASCII", "", "ASCII-8BIT", "\x01\x81", "ASCII-8BIT"], + ["US-ASCII", "", "ISO-8859-1", "", "US-ASCII"], + ["US-ASCII", "", "ISO-8859-1", "\x01\x01", "US-ASCII"], + ["US-ASCII", "", "ISO-8859-1", "\x01\x81", "ISO-8859-1"], + ["US-ASCII", "", "UTF-16BE", "", "US-ASCII"], + ["US-ASCII", "", "UTF-16BE", "\u0101", "UTF-16BE"], + ["US-ASCII", "", "UTF-16BE", "\u0181", "UTF-16BE"], + ["US-ASCII", "", "ISO-2022-JP", "", "US-ASCII"], + ["US-ASCII", "", "ISO-2022-JP", "\x01\x01", "ISO-2022-JP"], + ["US-ASCII", "", "ISO-2022-JP", "\x01\x81", "ISO-2022-JP"], + ["US-ASCII", "\x01\x01", "US-ASCII", "", "US-ASCII"], + ["US-ASCII", "\x01\x01", "US-ASCII", "\x01\x01", "US-ASCII"], + ["US-ASCII", "\x01\x01", "US-ASCII", "\x01\x81", "US-ASCII"], + ["US-ASCII", "\x01\x01", "UTF-8", "", "US-ASCII"], + ["US-ASCII", "\x01\x01", "UTF-8", "\u0001\u0001", "US-ASCII"], + ["US-ASCII", "\x01\x01", "UTF-8", "\u0001\x81", "UTF-8"], + ["US-ASCII", "\x01\x01", "ASCII-8BIT", "", "US-ASCII"], + ["US-ASCII", "\x01\x01", "ASCII-8BIT", "\x01\x01", "US-ASCII"], + ["US-ASCII", "\x01\x01", "ASCII-8BIT", "\x01\x81", "ASCII-8BIT"], + ["US-ASCII", "\x01\x01", "ISO-8859-1", "", "US-ASCII"], + ["US-ASCII", "\x01\x01", "ISO-8859-1", "\x01\x01", "US-ASCII"], + ["US-ASCII", "\x01\x01", "ISO-8859-1", "\x01\x81", "ISO-8859-1"], + ["US-ASCII", "\x01\x01", "UTF-16BE", "", "US-ASCII"], + ["US-ASCII", "\x01\x01", "UTF-16BE", "\u0101", nil], + ["US-ASCII", "\x01\x01", "UTF-16BE", "\u0181", nil], + ["US-ASCII", "\x01\x01", "ISO-2022-JP", "", "US-ASCII"], + ["US-ASCII", "\x01\x01", "ISO-2022-JP", "\x01\x01", nil], + ["US-ASCII", "\x01\x01", "ISO-2022-JP", "\x01\x81", nil], + ["US-ASCII", "\x01\x81", "US-ASCII", "", "US-ASCII"], + ["US-ASCII", "\x01\x81", "US-ASCII", "\x01\x01", "US-ASCII"], + ["US-ASCII", "\x01\x81", "US-ASCII", "\x01\x81", "US-ASCII"], + ["US-ASCII", "\x01\x81", "UTF-8", "", "US-ASCII"], + ["US-ASCII", "\x01\x81", "UTF-8", "\u0001\u0001", "US-ASCII"], + ["US-ASCII", "\x01\x81", "UTF-8", "\u0001\x81", nil], + ["US-ASCII", "\x01\x81", "ASCII-8BIT", "", "US-ASCII"], + ["US-ASCII", "\x01\x81", "ASCII-8BIT", "\x01\x01", "US-ASCII"], + ["US-ASCII", "\x01\x81", "ASCII-8BIT", "\x01\x81", nil], + ["US-ASCII", "\x01\x81", "ISO-8859-1", "", "US-ASCII"], + ["US-ASCII", "\x01\x81", "ISO-8859-1", "\x01\x01", "US-ASCII"], + ["US-ASCII", "\x01\x81", "ISO-8859-1", "\x01\x81", nil], + ["US-ASCII", "\x01\x81", "UTF-16BE", "", "US-ASCII"], + ["US-ASCII", "\x01\x81", "UTF-16BE", "\u0101", nil], + ["US-ASCII", "\x01\x81", "UTF-16BE", "\u0181", nil], + ["US-ASCII", "\x01\x81", "ISO-2022-JP", "", "US-ASCII"], + ["US-ASCII", "\x01\x81", "ISO-2022-JP", "\x01\x01", nil], + ["US-ASCII", "\x01\x81", "ISO-2022-JP", "\x01\x81", nil], + ["UTF-8", "", "US-ASCII", "", "UTF-8"], + ["UTF-8", "", "US-ASCII", "\x01\x01", "UTF-8"], + ["UTF-8", "", "US-ASCII", "\x01\x81", "US-ASCII"], + ["UTF-8", "", "UTF-8", "", "UTF-8"], + ["UTF-8", "", "UTF-8", "\u0001\u0001", "UTF-8"], + ["UTF-8", "", "UTF-8", "\u0001\x81", "UTF-8"], + ["UTF-8", "", "ASCII-8BIT", "", "UTF-8"], + ["UTF-8", "", "ASCII-8BIT", "\x01\x01", "UTF-8"], + ["UTF-8", "", "ASCII-8BIT", "\x01\x81", "ASCII-8BIT"], + ["UTF-8", "", "ISO-8859-1", "", "UTF-8"], + ["UTF-8", "", "ISO-8859-1", "\x01\x01", "UTF-8"], + ["UTF-8", "", "ISO-8859-1", "\x01\x81", "ISO-8859-1"], + ["UTF-8", "", "UTF-16BE", "", "UTF-8"], + ["UTF-8", "", "UTF-16BE", "\u0101", "UTF-16BE"], + ["UTF-8", "", "UTF-16BE", "\u0181", "UTF-16BE"], + ["UTF-8", "", "ISO-2022-JP", "", "UTF-8"], + ["UTF-8", "", "ISO-2022-JP", "\x01\x01", "ISO-2022-JP"], + ["UTF-8", "", "ISO-2022-JP", "\x01\x81", "ISO-2022-JP"], + ["UTF-8", "\u0001\u0001", "US-ASCII", "", "UTF-8"], + ["UTF-8", "\u0001\u0001", "US-ASCII", "\x01\x01", "UTF-8"], + ["UTF-8", "\u0001\u0001", "US-ASCII", "\x01\x81", "US-ASCII"], + ["UTF-8", "\u0001\u0001", "UTF-8", "", "UTF-8"], + ["UTF-8", "\u0001\u0001", "UTF-8", "\u0001\u0001", "UTF-8"], + ["UTF-8", "\u0001\u0001", "UTF-8", "\u0001\x81", "UTF-8"], + ["UTF-8", "\u0001\u0001", "ASCII-8BIT", "", "UTF-8"], + ["UTF-8", "\u0001\u0001", "ASCII-8BIT", "\x01\x01", "UTF-8"], + ["UTF-8", "\u0001\u0001", "ASCII-8BIT", "\x01\x81", "ASCII-8BIT"], + ["UTF-8", "\u0001\u0001", "ISO-8859-1", "", "UTF-8"], + ["UTF-8", "\u0001\u0001", "ISO-8859-1", "\x01\x01", "UTF-8"], + ["UTF-8", "\u0001\u0001", "ISO-8859-1", "\x01\x81", "ISO-8859-1"], + ["UTF-8", "\u0001\u0001", "UTF-16BE", "", "UTF-8"], + ["UTF-8", "\u0001\u0001", "UTF-16BE", "\u0101", nil], + ["UTF-8", "\u0001\u0001", "UTF-16BE", "\u0181", nil], + ["UTF-8", "\u0001\u0001", "ISO-2022-JP", "", "UTF-8"], + ["UTF-8", "\u0001\u0001", "ISO-2022-JP", "\x01\x01", nil], + ["UTF-8", "\u0001\u0001", "ISO-2022-JP", "\x01\x81", nil], + ["UTF-8", "\u0001\x81", "US-ASCII", "", "UTF-8"], + ["UTF-8", "\u0001\x81", "US-ASCII", "\x01\x01", "UTF-8"], + ["UTF-8", "\u0001\x81", "US-ASCII", "\x01\x81", nil], + ["UTF-8", "\u0001\x81", "UTF-8", "", "UTF-8"], + ["UTF-8", "\u0001\x81", "UTF-8", "\u0001\u0001", "UTF-8"], + ["UTF-8", "\u0001\x81", "UTF-8", "\u0001\x81", "UTF-8"], + ["UTF-8", "\u0001\x81", "ASCII-8BIT", "", "UTF-8"], + ["UTF-8", "\u0001\x81", "ASCII-8BIT", "\x01\x01", "UTF-8"], + ["UTF-8", "\u0001\x81", "ASCII-8BIT", "\x01\x81", nil], + ["UTF-8", "\u0001\x81", "ISO-8859-1", "", "UTF-8"], + ["UTF-8", "\u0001\x81", "ISO-8859-1", "\x01\x01", "UTF-8"], + ["UTF-8", "\u0001\x81", "ISO-8859-1", "\x01\x81", nil], + ["UTF-8", "\u0001\x81", "UTF-16BE", "", "UTF-8"], + ["UTF-8", "\u0001\x81", "UTF-16BE", "\u0101", nil], + ["UTF-8", "\u0001\x81", "UTF-16BE", "\u0181", nil], + ["UTF-8", "\u0001\x81", "ISO-2022-JP", "", "UTF-8"], + ["UTF-8", "\u0001\x81", "ISO-2022-JP", "\x01\x01", nil], + ["UTF-8", "\u0001\x81", "ISO-2022-JP", "\x01\x81", nil], + ["ASCII-8BIT", "", "US-ASCII", "", "ASCII-8BIT"], + ["ASCII-8BIT", "", "US-ASCII", "\x01\x01", "ASCII-8BIT"], + ["ASCII-8BIT", "", "US-ASCII", "\x01\x81", "US-ASCII"], + ["ASCII-8BIT", "", "UTF-8", "", "ASCII-8BIT"], + ["ASCII-8BIT", "", "UTF-8", "\u0001\u0001", "ASCII-8BIT"], + ["ASCII-8BIT", "", "UTF-8", "\u0001\x81", "UTF-8"], + ["ASCII-8BIT", "", "ASCII-8BIT", "", "ASCII-8BIT"], + ["ASCII-8BIT", "", "ASCII-8BIT", "\x01\x01", "ASCII-8BIT"], + ["ASCII-8BIT", "", "ASCII-8BIT", "\x01\x81", "ASCII-8BIT"], + ["ASCII-8BIT", "", "ISO-8859-1", "", "ASCII-8BIT"], + ["ASCII-8BIT", "", "ISO-8859-1", "\x01\x01", "ASCII-8BIT"], + ["ASCII-8BIT", "", "ISO-8859-1", "\x01\x81", "ISO-8859-1"], + ["ASCII-8BIT", "", "UTF-16BE", "", "ASCII-8BIT"], + ["ASCII-8BIT", "", "UTF-16BE", "\u0101", "UTF-16BE"], + ["ASCII-8BIT", "", "UTF-16BE", "\u0181", "UTF-16BE"], + ["ASCII-8BIT", "", "ISO-2022-JP", "", "ASCII-8BIT"], + ["ASCII-8BIT", "", "ISO-2022-JP", "\x01\x01", "ISO-2022-JP"], + ["ASCII-8BIT", "", "ISO-2022-JP", "\x01\x81", "ISO-2022-JP"], + ["ASCII-8BIT", "\x01\x01", "US-ASCII", "", "ASCII-8BIT"], + ["ASCII-8BIT", "\x01\x01", "US-ASCII", "\x01\x01", "ASCII-8BIT"], + ["ASCII-8BIT", "\x01\x01", "US-ASCII", "\x01\x81", "US-ASCII"], + ["ASCII-8BIT", "\x01\x01", "UTF-8", "", "ASCII-8BIT"], + ["ASCII-8BIT", "\x01\x01", "UTF-8", "\u0001\u0001", "ASCII-8BIT"], + ["ASCII-8BIT", "\x01\x01", "UTF-8", "\u0001\x81", "UTF-8"], + ["ASCII-8BIT", "\x01\x01", "ASCII-8BIT", "", "ASCII-8BIT"], + ["ASCII-8BIT", "\x01\x01", "ASCII-8BIT", "\x01\x01", "ASCII-8BIT"], + ["ASCII-8BIT", "\x01\x01", "ASCII-8BIT", "\x01\x81", "ASCII-8BIT"], + ["ASCII-8BIT", "\x01\x01", "ISO-8859-1", "", "ASCII-8BIT"], + ["ASCII-8BIT", "\x01\x01", "ISO-8859-1", "\x01\x01", "ASCII-8BIT"], + ["ASCII-8BIT", "\x01\x01", "ISO-8859-1", "\x01\x81", "ISO-8859-1"], + ["ASCII-8BIT", "\x01\x01", "UTF-16BE", "", "ASCII-8BIT"], + ["ASCII-8BIT", "\x01\x01", "UTF-16BE", "\u0101", nil], + ["ASCII-8BIT", "\x01\x01", "UTF-16BE", "\u0181", nil], + ["ASCII-8BIT", "\x01\x01", "ISO-2022-JP", "", "ASCII-8BIT"], + ["ASCII-8BIT", "\x01\x01", "ISO-2022-JP", "\x01\x01", nil], + ["ASCII-8BIT", "\x01\x01", "ISO-2022-JP", "\x01\x81", nil], + ["ASCII-8BIT", "\x01\x81", "US-ASCII", "", "ASCII-8BIT"], + ["ASCII-8BIT", "\x01\x81", "US-ASCII", "\x01\x01", "ASCII-8BIT"], + ["ASCII-8BIT", "\x01\x81", "US-ASCII", "\x01\x81", nil], + ["ASCII-8BIT", "\x01\x81", "UTF-8", "", "ASCII-8BIT"], + ["ASCII-8BIT", "\x01\x81", "UTF-8", "\u0001\u0001", "ASCII-8BIT"], + ["ASCII-8BIT", "\x01\x81", "UTF-8", "\u0001\x81", nil], + ["ASCII-8BIT", "\x01\x81", "ASCII-8BIT", "", "ASCII-8BIT"], + ["ASCII-8BIT", "\x01\x81", "ASCII-8BIT", "\x01\x01", "ASCII-8BIT"], + ["ASCII-8BIT", "\x01\x81", "ASCII-8BIT", "\x01\x81", "ASCII-8BIT"], + ["ASCII-8BIT", "\x01\x81", "ISO-8859-1", "", "ASCII-8BIT"], + ["ASCII-8BIT", "\x01\x81", "ISO-8859-1", "\x01\x01", "ASCII-8BIT"], + ["ASCII-8BIT", "\x01\x81", "ISO-8859-1", "\x01\x81", nil], + ["ASCII-8BIT", "\x01\x81", "UTF-16BE", "", "ASCII-8BIT"], + ["ASCII-8BIT", "\x01\x81", "UTF-16BE", "\u0101", nil], + ["ASCII-8BIT", "\x01\x81", "UTF-16BE", "\u0181", nil], + ["ASCII-8BIT", "\x01\x81", "ISO-2022-JP", "", "ASCII-8BIT"], + ["ASCII-8BIT", "\x01\x81", "ISO-2022-JP", "\x01\x01", nil], + ["ASCII-8BIT", "\x01\x81", "ISO-2022-JP", "\x01\x81", nil], + ["ISO-8859-1", "", "US-ASCII", "", "ISO-8859-1"], + ["ISO-8859-1", "", "US-ASCII", "\x01\x01", "ISO-8859-1"], + ["ISO-8859-1", "", "US-ASCII", "\x01\x81", "US-ASCII"], + ["ISO-8859-1", "", "UTF-8", "", "ISO-8859-1"], + ["ISO-8859-1", "", "UTF-8", "\u0001\u0001", "ISO-8859-1"], + ["ISO-8859-1", "", "UTF-8", "\u0001\x81", "UTF-8"], + ["ISO-8859-1", "", "ASCII-8BIT", "", "ISO-8859-1"], + ["ISO-8859-1", "", "ASCII-8BIT", "\x01\x01", "ISO-8859-1"], + ["ISO-8859-1", "", "ASCII-8BIT", "\x01\x81", "ASCII-8BIT"], + ["ISO-8859-1", "", "ISO-8859-1", "", "ISO-8859-1"], + ["ISO-8859-1", "", "ISO-8859-1", "\x01\x01", "ISO-8859-1"], + ["ISO-8859-1", "", "ISO-8859-1", "\x01\x81", "ISO-8859-1"], + ["ISO-8859-1", "", "UTF-16BE", "", "ISO-8859-1"], + ["ISO-8859-1", "", "UTF-16BE", "\u0101", "UTF-16BE"], + ["ISO-8859-1", "", "UTF-16BE", "\u0181", "UTF-16BE"], + ["ISO-8859-1", "", "ISO-2022-JP", "", "ISO-8859-1"], + ["ISO-8859-1", "", "ISO-2022-JP", "\x01\x01", "ISO-2022-JP"], + ["ISO-8859-1", "", "ISO-2022-JP", "\x01\x81", "ISO-2022-JP"], + ["ISO-8859-1", "\x01\x01", "US-ASCII", "", "ISO-8859-1"], + ["ISO-8859-1", "\x01\x01", "US-ASCII", "\x01\x01", "ISO-8859-1"], + ["ISO-8859-1", "\x01\x01", "US-ASCII", "\x01\x81", "US-ASCII"], + ["ISO-8859-1", "\x01\x01", "UTF-8", "", "ISO-8859-1"], + ["ISO-8859-1", "\x01\x01", "UTF-8", "\u0001\u0001", "ISO-8859-1"], + ["ISO-8859-1", "\x01\x01", "UTF-8", "\u0001\x81", "UTF-8"], + ["ISO-8859-1", "\x01\x01", "ASCII-8BIT", "", "ISO-8859-1"], + ["ISO-8859-1", "\x01\x01", "ASCII-8BIT", "\x01\x01", "ISO-8859-1"], + ["ISO-8859-1", "\x01\x01", "ASCII-8BIT", "\x01\x81", "ASCII-8BIT"], + ["ISO-8859-1", "\x01\x01", "ISO-8859-1", "", "ISO-8859-1"], + ["ISO-8859-1", "\x01\x01", "ISO-8859-1", "\x01\x01", "ISO-8859-1"], + ["ISO-8859-1", "\x01\x01", "ISO-8859-1", "\x01\x81", "ISO-8859-1"], + ["ISO-8859-1", "\x01\x01", "UTF-16BE", "", "ISO-8859-1"], + ["ISO-8859-1", "\x01\x01", "UTF-16BE", "\u0101", nil], + ["ISO-8859-1", "\x01\x01", "UTF-16BE", "\u0181", nil], + ["ISO-8859-1", "\x01\x01", "ISO-2022-JP", "", "ISO-8859-1"], + ["ISO-8859-1", "\x01\x01", "ISO-2022-JP", "\x01\x01", nil], + ["ISO-8859-1", "\x01\x01", "ISO-2022-JP", "\x01\x81", nil], + ["ISO-8859-1", "\x01\x81", "US-ASCII", "", "ISO-8859-1"], + ["ISO-8859-1", "\x01\x81", "US-ASCII", "\x01\x01", "ISO-8859-1"], + ["ISO-8859-1", "\x01\x81", "US-ASCII", "\x01\x81", nil], + ["ISO-8859-1", "\x01\x81", "UTF-8", "", "ISO-8859-1"], + ["ISO-8859-1", "\x01\x81", "UTF-8", "\u0001\u0001", "ISO-8859-1"], + ["ISO-8859-1", "\x01\x81", "UTF-8", "\u0001\x81", nil], + ["ISO-8859-1", "\x01\x81", "ASCII-8BIT", "", "ISO-8859-1"], + ["ISO-8859-1", "\x01\x81", "ASCII-8BIT", "\x01\x01", "ISO-8859-1"], + ["ISO-8859-1", "\x01\x81", "ASCII-8BIT", "\x01\x81", nil], + ["ISO-8859-1", "\x01\x81", "ISO-8859-1", "", "ISO-8859-1"], + ["ISO-8859-1", "\x01\x81", "ISO-8859-1", "\x01\x01", "ISO-8859-1"], + ["ISO-8859-1", "\x01\x81", "ISO-8859-1", "\x01\x81", "ISO-8859-1"], + ["ISO-8859-1", "\x01\x81", "UTF-16BE", "", "ISO-8859-1"], + ["ISO-8859-1", "\x01\x81", "UTF-16BE", "\u0101", nil], + ["ISO-8859-1", "\x01\x81", "UTF-16BE", "\u0181", nil], + ["ISO-8859-1", "\x01\x81", "ISO-2022-JP", "", "ISO-8859-1"], + ["ISO-8859-1", "\x01\x81", "ISO-2022-JP", "\x01\x01", nil], + ["ISO-8859-1", "\x01\x81", "ISO-2022-JP", "\x01\x81", nil], + ["UTF-16BE", "", "US-ASCII", "", "UTF-16BE"], + ["UTF-16BE", "", "US-ASCII", "\x01\x01", "US-ASCII"], + ["UTF-16BE", "", "US-ASCII", "\x01\x81", "US-ASCII"], + ["UTF-16BE", "", "UTF-8", "", "UTF-16BE"], + ["UTF-16BE", "", "UTF-8", "\u0001\u0001", "UTF-8"], + ["UTF-16BE", "", "UTF-8", "\u0001\x81", "UTF-8"], + ["UTF-16BE", "", "ASCII-8BIT", "", "UTF-16BE"], + ["UTF-16BE", "", "ASCII-8BIT", "\x01\x01", "ASCII-8BIT"], + ["UTF-16BE", "", "ASCII-8BIT", "\x01\x81", "ASCII-8BIT"], + ["UTF-16BE", "", "ISO-8859-1", "", "UTF-16BE"], + ["UTF-16BE", "", "ISO-8859-1", "\x01\x01", "ISO-8859-1"], + ["UTF-16BE", "", "ISO-8859-1", "\x01\x81", "ISO-8859-1"], + ["UTF-16BE", "", "UTF-16BE", "", "UTF-16BE"], + ["UTF-16BE", "", "UTF-16BE", "\u0101", "UTF-16BE"], + ["UTF-16BE", "", "UTF-16BE", "\u0181", "UTF-16BE"], + ["UTF-16BE", "", "ISO-2022-JP", "", "UTF-16BE"], + ["UTF-16BE", "", "ISO-2022-JP", "\x01\x01", "ISO-2022-JP"], + ["UTF-16BE", "", "ISO-2022-JP", "\x01\x81", "ISO-2022-JP"], + ["UTF-16BE", "\u0101", "US-ASCII", "", "UTF-16BE"], + ["UTF-16BE", "\u0101", "US-ASCII", "\x01\x01", nil], + ["UTF-16BE", "\u0101", "US-ASCII", "\x01\x81", nil], + ["UTF-16BE", "\u0101", "UTF-8", "", "UTF-16BE"], + ["UTF-16BE", "\u0101", "UTF-8", "\u0001\u0001", nil], + ["UTF-16BE", "\u0101", "UTF-8", "\u0001\x81", nil], + ["UTF-16BE", "\u0101", "ASCII-8BIT", "", "UTF-16BE"], + ["UTF-16BE", "\u0101", "ASCII-8BIT", "\x01\x01", nil], + ["UTF-16BE", "\u0101", "ASCII-8BIT", "\x01\x81", nil], + ["UTF-16BE", "\u0101", "ISO-8859-1", "", "UTF-16BE"], + ["UTF-16BE", "\u0101", "ISO-8859-1", "\x01\x01", nil], + ["UTF-16BE", "\u0101", "ISO-8859-1", "\x01\x81", nil], + ["UTF-16BE", "\u0101", "UTF-16BE", "", "UTF-16BE"], + ["UTF-16BE", "\u0101", "UTF-16BE", "\u0101", "UTF-16BE"], + ["UTF-16BE", "\u0101", "UTF-16BE", "\u0181", "UTF-16BE"], + ["UTF-16BE", "\u0101", "ISO-2022-JP", "", "UTF-16BE"], + ["UTF-16BE", "\u0101", "ISO-2022-JP", "\x01\x01", nil], + ["UTF-16BE", "\u0101", "ISO-2022-JP", "\x01\x81", nil], + ["UTF-16BE", "\u0181", "US-ASCII", "", "UTF-16BE"], + ["UTF-16BE", "\u0181", "US-ASCII", "\x01\x01", nil], + ["UTF-16BE", "\u0181", "US-ASCII", "\x01\x81", nil], + ["UTF-16BE", "\u0181", "UTF-8", "", "UTF-16BE"], + ["UTF-16BE", "\u0181", "UTF-8", "\u0001\u0001", nil], + ["UTF-16BE", "\u0181", "UTF-8", "\u0001\x81", nil], + ["UTF-16BE", "\u0181", "ASCII-8BIT", "", "UTF-16BE"], + ["UTF-16BE", "\u0181", "ASCII-8BIT", "\x01\x01", nil], + ["UTF-16BE", "\u0181", "ASCII-8BIT", "\x01\x81", nil], + ["UTF-16BE", "\u0181", "ISO-8859-1", "", "UTF-16BE"], + ["UTF-16BE", "\u0181", "ISO-8859-1", "\x01\x01", nil], + ["UTF-16BE", "\u0181", "ISO-8859-1", "\x01\x81", nil], + ["UTF-16BE", "\u0181", "UTF-16BE", "", "UTF-16BE"], + ["UTF-16BE", "\u0181", "UTF-16BE", "\u0101", "UTF-16BE"], + ["UTF-16BE", "\u0181", "UTF-16BE", "\u0181", "UTF-16BE"], + ["UTF-16BE", "\u0181", "ISO-2022-JP", "", "UTF-16BE"], + ["UTF-16BE", "\u0181", "ISO-2022-JP", "\x01\x01", nil], + ["UTF-16BE", "\u0181", "ISO-2022-JP", "\x01\x81", nil], + ["ISO-2022-JP", "", "US-ASCII", "", "ISO-2022-JP"], + ["ISO-2022-JP", "", "US-ASCII", "\x01\x01", "US-ASCII"], + ["ISO-2022-JP", "", "US-ASCII", "\x01\x81", "US-ASCII"], + ["ISO-2022-JP", "", "UTF-8", "", "ISO-2022-JP"], + ["ISO-2022-JP", "", "UTF-8", "\u0001\u0001", "UTF-8"], + ["ISO-2022-JP", "", "UTF-8", "\u0001\x81", "UTF-8"], + ["ISO-2022-JP", "", "ASCII-8BIT", "", "ISO-2022-JP"], + ["ISO-2022-JP", "", "ASCII-8BIT", "\x01\x01", "ASCII-8BIT"], + ["ISO-2022-JP", "", "ASCII-8BIT", "\x01\x81", "ASCII-8BIT"], + ["ISO-2022-JP", "", "ISO-8859-1", "", "ISO-2022-JP"], + ["ISO-2022-JP", "", "ISO-8859-1", "\x01\x01", "ISO-8859-1"], + ["ISO-2022-JP", "", "ISO-8859-1", "\x01\x81", "ISO-8859-1"], + ["ISO-2022-JP", "", "UTF-16BE", "", "ISO-2022-JP"], + ["ISO-2022-JP", "", "UTF-16BE", "\u0101", "UTF-16BE"], + ["ISO-2022-JP", "", "UTF-16BE", "\u0181", "UTF-16BE"], + ["ISO-2022-JP", "", "ISO-2022-JP", "", "ISO-2022-JP"], + ["ISO-2022-JP", "", "ISO-2022-JP", "\x01\x01", "ISO-2022-JP"], + ["ISO-2022-JP", "", "ISO-2022-JP", "\x01\x81", "ISO-2022-JP"], + ["ISO-2022-JP", "\x01\x01", "US-ASCII", "", "ISO-2022-JP"], + ["ISO-2022-JP", "\x01\x01", "US-ASCII", "\x01\x01", nil], + ["ISO-2022-JP", "\x01\x01", "US-ASCII", "\x01\x81", nil], + ["ISO-2022-JP", "\x01\x01", "UTF-8", "", "ISO-2022-JP"], + ["ISO-2022-JP", "\x01\x01", "UTF-8", "\u0001\u0001", nil], + ["ISO-2022-JP", "\x01\x01", "UTF-8", "\u0001\x81", nil], + ["ISO-2022-JP", "\x01\x01", "ASCII-8BIT", "", "ISO-2022-JP"], + ["ISO-2022-JP", "\x01\x01", "ASCII-8BIT", "\x01\x01", nil], + ["ISO-2022-JP", "\x01\x01", "ASCII-8BIT", "\x01\x81", nil], + ["ISO-2022-JP", "\x01\x01", "ISO-8859-1", "", "ISO-2022-JP"], + ["ISO-2022-JP", "\x01\x01", "ISO-8859-1", "\x01\x01", nil], + ["ISO-2022-JP", "\x01\x01", "ISO-8859-1", "\x01\x81", nil], + ["ISO-2022-JP", "\x01\x01", "UTF-16BE", "", "ISO-2022-JP"], + ["ISO-2022-JP", "\x01\x01", "UTF-16BE", "\u0101", nil], + ["ISO-2022-JP", "\x01\x01", "UTF-16BE", "\u0181", nil], + ["ISO-2022-JP", "\x01\x01", "ISO-2022-JP", "", "ISO-2022-JP"], + ["ISO-2022-JP", "\x01\x01", "ISO-2022-JP", "\x01\x01", "ISO-2022-JP"], + ["ISO-2022-JP", "\x01\x01", "ISO-2022-JP", "\x01\x81", "ISO-2022-JP"], + ["ISO-2022-JP", "\x01\x81", "US-ASCII", "", "ISO-2022-JP"], + ["ISO-2022-JP", "\x01\x81", "US-ASCII", "\x01\x01", nil], + ["ISO-2022-JP", "\x01\x81", "US-ASCII", "\x01\x81", nil], + ["ISO-2022-JP", "\x01\x81", "UTF-8", "", "ISO-2022-JP"], + ["ISO-2022-JP", "\x01\x81", "UTF-8", "\u0001\u0001", nil], + ["ISO-2022-JP", "\x01\x81", "UTF-8", "\u0001\x81", nil], + ["ISO-2022-JP", "\x01\x81", "ASCII-8BIT", "", "ISO-2022-JP"], + ["ISO-2022-JP", "\x01\x81", "ASCII-8BIT", "\x01\x01", nil], + ["ISO-2022-JP", "\x01\x81", "ASCII-8BIT", "\x01\x81", nil], + ["ISO-2022-JP", "\x01\x81", "ISO-8859-1", "", "ISO-2022-JP"], + ["ISO-2022-JP", "\x01\x81", "ISO-8859-1", "\x01\x01", nil], + ["ISO-2022-JP", "\x01\x81", "ISO-8859-1", "\x01\x81", nil], + ["ISO-2022-JP", "\x01\x81", "UTF-16BE", "", "ISO-2022-JP"], + ["ISO-2022-JP", "\x01\x81", "UTF-16BE", "\u0101", nil], + ["ISO-2022-JP", "\x01\x81", "UTF-16BE", "\u0181", nil], + ["ISO-2022-JP", "\x01\x81", "ISO-2022-JP", "", "ISO-2022-JP"], + ["ISO-2022-JP", "\x01\x81", "ISO-2022-JP", "\x01\x01", "ISO-2022-JP"], + ["ISO-2022-JP", "\x01\x81", "ISO-2022-JP", "\x01\x81", "ISO-2022-JP"], + ] + + matrix.each do |encoding1, value1, encoding2, value2, compatible_encoding| + it "returns #{compatible_encoding} for #{value1.inspect} in #{encoding1} and #{value2.inspect} in #{encoding2}" do + actual_encoding = Encoding.compatible?(value1.dup.force_encoding(encoding1), value2.dup.force_encoding(encoding2)) + actual_encoding&.name.should == compatible_encoding + end + end + end +end + +describe "Encoding.compatible? String, Regexp" do + it "returns US-ASCII if both are US-ASCII" do + str = "abc".dup.force_encoding("us-ascii") + Encoding.compatible?(str, /abc/).should == Encoding::US_ASCII + end + + it "returns the String's Encoding if it is not US-ASCII but both are ASCII only" do + [ [Encoding, "abc", Encoding::BINARY], + [Encoding, "abc".encode("utf-8"), Encoding::UTF_8], + [Encoding, "abc".encode("euc-jp"), Encoding::EUC_JP], + [Encoding, "abc".encode("shift_jis"), Encoding::Shift_JIS], + ].should be_computed_by(:compatible?, /abc/) + end + + it "returns the String's Encoding if the String is not ASCII only" do + [ [Encoding, "\xff", Encoding::BINARY], + [Encoding, "\u3042".encode("utf-8"), Encoding::UTF_8], + [Encoding, "\xa4\xa2".dup.force_encoding("euc-jp"), Encoding::EUC_JP], + [Encoding, "\x82\xa0".dup.force_encoding("shift_jis"), Encoding::Shift_JIS], + ].should be_computed_by(:compatible?, /abc/) + end +end + +describe "Encoding.compatible? String, Symbol" do + it "returns US-ASCII if both are ASCII only" do + str = "abc".dup.force_encoding("us-ascii") + Encoding.compatible?(str, :abc).should == Encoding::US_ASCII + end + + it "returns the String's Encoding if it is not US-ASCII but both are ASCII only" do + [ [Encoding, "abc", Encoding::BINARY], + [Encoding, "abc".encode("utf-8"), Encoding::UTF_8], + [Encoding, "abc".encode("euc-jp"), Encoding::EUC_JP], + [Encoding, "abc".encode("shift_jis"), Encoding::Shift_JIS], + ].should be_computed_by(:compatible?, :abc) + end + + it "returns the String's Encoding if the String is not ASCII only" do + [ [Encoding, "\xff", Encoding::BINARY], + [Encoding, "\u3042".encode("utf-8"), Encoding::UTF_8], + [Encoding, "\xa4\xa2".dup.force_encoding("euc-jp"), Encoding::EUC_JP], + [Encoding, "\x82\xa0".dup.force_encoding("shift_jis"), Encoding::Shift_JIS], + ].should be_computed_by(:compatible?, :abc) + end +end + +describe "Encoding.compatible? String, Encoding" do + it "returns nil if the String's encoding is not ASCII compatible" do + Encoding.compatible?("abc".encode("utf-32le"), Encoding::US_ASCII).should be_nil + end + + it "returns nil if the Encoding is not ASCII compatible" do + Encoding.compatible?("abc".encode("us-ascii"), Encoding::UTF_32LE).should be_nil + end + + it "returns the String's encoding if the Encoding is US-ASCII" do + [ [Encoding, "\xff", Encoding::BINARY], + [Encoding, "\u3042".encode("utf-8"), Encoding::UTF_8], + [Encoding, "\xa4\xa2".dup.force_encoding("euc-jp"), Encoding::EUC_JP], + [Encoding, "\x82\xa0".dup.force_encoding("shift_jis"), Encoding::Shift_JIS], + ].should be_computed_by(:compatible?, Encoding::US_ASCII) + end + + it "returns the Encoding if the String's encoding is ASCII compatible and the String is ASCII only" do + str = "abc".encode("utf-8") + + Encoding.compatible?(str, Encoding::BINARY).should == Encoding::BINARY + Encoding.compatible?(str, Encoding::UTF_8).should == Encoding::UTF_8 + Encoding.compatible?(str, Encoding::EUC_JP).should == Encoding::EUC_JP + Encoding.compatible?(str, Encoding::Shift_JIS).should == Encoding::Shift_JIS + end + + it "returns nil if the String's encoding is ASCII compatible but the string is not ASCII only" do + Encoding.compatible?("\u3042".encode("utf-8"), Encoding::BINARY).should be_nil + end +end + +describe "Encoding.compatible? Regexp, String" do + it "returns US-ASCII if both are US-ASCII" do + str = "abc".dup.force_encoding("us-ascii") + Encoding.compatible?(/abc/, str).should == Encoding::US_ASCII + end + +end + +describe "Encoding.compatible? Regexp, Regexp" do + it "returns US-ASCII if both are US-ASCII" do + Encoding.compatible?(/abc/, /def/).should == Encoding::US_ASCII + end + + it "returns the first's Encoding if it is not US-ASCII and not ASCII only" do + [ [Encoding, Regexp.new("\xff"), Encoding::BINARY], + [Encoding, Regexp.new("\u3042".encode("utf-8")), Encoding::UTF_8], + [Encoding, Regexp.new("\xa4\xa2".dup.force_encoding("euc-jp")), Encoding::EUC_JP], + [Encoding, Regexp.new("\x82\xa0".dup.force_encoding("shift_jis")), Encoding::Shift_JIS], + ].should be_computed_by(:compatible?, /abc/) + end +end + +describe "Encoding.compatible? Regexp, Symbol" do + it "returns US-ASCII if both are US-ASCII" do + Encoding.compatible?(/abc/, :def).should == Encoding::US_ASCII + end + + it "returns the first's Encoding if it is not US-ASCII and not ASCII only" do + [ [Encoding, Regexp.new("\xff"), Encoding::BINARY], + [Encoding, Regexp.new("\u3042".encode("utf-8")), Encoding::UTF_8], + [Encoding, Regexp.new("\xa4\xa2".dup.force_encoding("euc-jp")), Encoding::EUC_JP], + [Encoding, Regexp.new("\x82\xa0".dup.force_encoding("shift_jis")), Encoding::Shift_JIS], + ].should be_computed_by(:compatible?, /abc/) + end +end + +describe "Encoding.compatible? Symbol, String" do + it "returns US-ASCII if both are ASCII only" do + str = "abc".dup.force_encoding("us-ascii") + Encoding.compatible?(str, :abc).should == Encoding::US_ASCII + end +end + +describe "Encoding.compatible? Symbol, Regexp" do + it "returns US-ASCII if both are US-ASCII" do + Encoding.compatible?(:abc, /def/).should == Encoding::US_ASCII + end + + it "returns the Regexp's Encoding if it is not US-ASCII and not ASCII only" do + a = Regexp.new("\xff") + b = Regexp.new("\u3042".encode("utf-8")) + c = Regexp.new("\xa4\xa2".dup.force_encoding("euc-jp")) + d = Regexp.new("\x82\xa0".dup.force_encoding("shift_jis")) + + [ [Encoding, :abc, a, Encoding::BINARY], + [Encoding, :abc, b, Encoding::UTF_8], + [Encoding, :abc, c, Encoding::EUC_JP], + [Encoding, :abc, d, Encoding::Shift_JIS], + ].should be_computed_by(:compatible?) + end +end + +describe "Encoding.compatible? Symbol, Symbol" do + it "returns US-ASCII if both are US-ASCII" do + Encoding.compatible?(:abc, :def).should == Encoding::US_ASCII + end + + it "returns the first's Encoding if it is not ASCII only" do + [ [Encoding, "\xff".to_sym, Encoding::BINARY], + [Encoding, "\u3042".encode("utf-8").to_sym, Encoding::UTF_8], + [Encoding, "\xa4\xa2".dup.force_encoding("euc-jp").to_sym, Encoding::EUC_JP], + [Encoding, "\x82\xa0".dup.force_encoding("shift_jis").to_sym, Encoding::Shift_JIS], + ].should be_computed_by(:compatible?, :abc) + end +end + +describe "Encoding.compatible? Encoding, Encoding" do + it "returns nil if one of the encodings is a dummy encoding" do + [ [Encoding, Encoding::UTF_7, Encoding::US_ASCII, nil], + [Encoding, Encoding::US_ASCII, Encoding::UTF_7, nil], + [Encoding, Encoding::EUC_JP, Encoding::UTF_7, nil], + [Encoding, Encoding::UTF_7, Encoding::EUC_JP, nil], + [Encoding, Encoding::UTF_7, Encoding::BINARY, nil], + [Encoding, Encoding::BINARY, Encoding::UTF_7, nil], + ].should be_computed_by(:compatible?) + end + + it "returns nil if one of the encodings is not US-ASCII" do + [ [Encoding, Encoding::UTF_8, Encoding::BINARY, nil], + [Encoding, Encoding::BINARY, Encoding::UTF_8, nil], + [Encoding, Encoding::BINARY, Encoding::EUC_JP, nil], + [Encoding, Encoding::Shift_JIS, Encoding::EUC_JP, nil], + ].should be_computed_by(:compatible?) + end + + it "returns the first if the second is US-ASCII" do + [ [Encoding, Encoding::UTF_8, Encoding::US_ASCII, Encoding::UTF_8], + [Encoding, Encoding::EUC_JP, Encoding::US_ASCII, Encoding::EUC_JP], + [Encoding, Encoding::Shift_JIS, Encoding::US_ASCII, Encoding::Shift_JIS], + [Encoding, Encoding::BINARY, Encoding::US_ASCII, Encoding::BINARY], + ].should be_computed_by(:compatible?) + end + + it "returns the Encoding if both are the same" do + [ [Encoding, Encoding::UTF_8, Encoding::UTF_8, Encoding::UTF_8], + [Encoding, Encoding::US_ASCII, Encoding::US_ASCII, Encoding::US_ASCII], + [Encoding, Encoding::BINARY, Encoding::BINARY, Encoding::BINARY], + [Encoding, Encoding::UTF_7, Encoding::UTF_7, Encoding::UTF_7], + ].should be_computed_by(:compatible?) + end +end + +describe "Encoding.compatible? Object, Object" do + it "returns nil for Object, String" do + Encoding.compatible?(Object.new, "abc").should be_nil + end + + it "returns nil for Object, Regexp" do + Encoding.compatible?(Object.new, /./).should be_nil + end + + it "returns nil for Object, Symbol" do + Encoding.compatible?(Object.new, :sym).should be_nil + end + + it "returns nil for String, Object" do + Encoding.compatible?("abc", Object.new).should be_nil + end + + it "returns nil for Regexp, Object" do + Encoding.compatible?(/./, Object.new).should be_nil + end + + it "returns nil for Symbol, Object" do + Encoding.compatible?(:sym, Object.new).should be_nil + end +end + +describe "Encoding.compatible? nil, nil" do + it "returns nil" do + Encoding.compatible?(nil, nil).should be_nil + end +end diff --git a/spec/ruby/core/encoding/converter/asciicompat_encoding_spec.rb b/spec/ruby/core/encoding/converter/asciicompat_encoding_spec.rb new file mode 100644 index 0000000000..1beb40af3f --- /dev/null +++ b/spec/ruby/core/encoding/converter/asciicompat_encoding_spec.rb @@ -0,0 +1,37 @@ +require_relative '../../../spec_helper' + +describe "Encoding::Converter.asciicompat_encoding" do + it "accepts an encoding name as a String argument" do + -> { Encoding::Converter.asciicompat_encoding('UTF-8') }. + should_not raise_error + end + + it "coerces non-String/Encoding objects with #to_str" do + str = mock('string') + str.should_receive(:to_str).at_least(1).times.and_return('string') + Encoding::Converter.asciicompat_encoding(str) + end + + it "accepts an Encoding object as an argument" do + Encoding::Converter. + asciicompat_encoding(Encoding.find("ISO-2022-JP")). + should == Encoding::Converter.asciicompat_encoding("ISO-2022-JP") + end + + it "returns a corresponding ASCII compatible encoding for ASCII-incompatible encodings" do + Encoding::Converter.asciicompat_encoding('UTF-16BE').should == Encoding::UTF_8 + Encoding::Converter.asciicompat_encoding("ISO-2022-JP").should == Encoding.find("stateless-ISO-2022-JP") + end + + it "returns nil when the given encoding is ASCII compatible" do + Encoding::Converter.asciicompat_encoding('ASCII').should be_nil + Encoding::Converter.asciicompat_encoding('UTF-8').should be_nil + end + + it "handles encoding names who resolve to nil encodings" do + internal = Encoding.default_internal + Encoding.default_internal = nil + Encoding::Converter.asciicompat_encoding('internal').should be_nil + Encoding.default_internal = internal + end +end diff --git a/spec/ruby/core/encoding/converter/constants_spec.rb b/spec/ruby/core/encoding/converter/constants_spec.rb new file mode 100644 index 0000000000..7d29bdb278 --- /dev/null +++ b/spec/ruby/core/encoding/converter/constants_spec.rb @@ -0,0 +1,131 @@ +require_relative '../../../spec_helper' + +describe "Encoding::Converter::INVALID_MASK" do + it "exists" do + Encoding::Converter.should have_constant(:INVALID_MASK) + end + + it "has an Integer value" do + Encoding::Converter::INVALID_MASK.should be_an_instance_of(Integer) + end +end + +describe "Encoding::Converter::INVALID_REPLACE" do + it "exists" do + Encoding::Converter.should have_constant(:INVALID_REPLACE) + end + + it "has an Integer value" do + Encoding::Converter::INVALID_REPLACE.should be_an_instance_of(Integer) + end +end + +describe "Encoding::Converter::UNDEF_MASK" do + it "exists" do + Encoding::Converter.should have_constant(:UNDEF_MASK) + end + + it "has an Integer value" do + Encoding::Converter::UNDEF_MASK.should be_an_instance_of(Integer) + end +end + +describe "Encoding::Converter::UNDEF_REPLACE" do + it "exists" do + Encoding::Converter.should have_constant(:UNDEF_REPLACE) + end + + it "has an Integer value" do + Encoding::Converter::UNDEF_REPLACE.should be_an_instance_of(Integer) + end +end + +describe "Encoding::Converter::UNDEF_HEX_CHARREF" do + it "exists" do + Encoding::Converter.should have_constant(:UNDEF_HEX_CHARREF) + end + + it "has an Integer value" do + Encoding::Converter::UNDEF_HEX_CHARREF.should be_an_instance_of(Integer) + end +end + +describe "Encoding::Converter::PARTIAL_INPUT" do + it "exists" do + Encoding::Converter.should have_constant(:PARTIAL_INPUT) + end + + it "has an Integer value" do + Encoding::Converter::PARTIAL_INPUT.should be_an_instance_of(Integer) + end +end + +describe "Encoding::Converter::AFTER_OUTPUT" do + it "exists" do + Encoding::Converter.should have_constant(:AFTER_OUTPUT) + end + + it "has an Integer value" do + Encoding::Converter::AFTER_OUTPUT.should be_an_instance_of(Integer) + end +end + +describe "Encoding::Converter::UNIVERSAL_NEWLINE_DECORATOR" do + it "exists" do + Encoding::Converter.should have_constant(:UNIVERSAL_NEWLINE_DECORATOR) + end + + it "has an Integer value" do + Encoding::Converter::UNIVERSAL_NEWLINE_DECORATOR.should be_an_instance_of(Integer) + end +end + +describe "Encoding::Converter::CRLF_NEWLINE_DECORATOR" do + it "exists" do + Encoding::Converter.should have_constant(:CRLF_NEWLINE_DECORATOR) + end + + it "has an Integer value" do + Encoding::Converter::CRLF_NEWLINE_DECORATOR.should be_an_instance_of(Integer) + end +end + +describe "Encoding::Converter::CR_NEWLINE_DECORATOR" do + it "exists" do + Encoding::Converter.should have_constant(:CR_NEWLINE_DECORATOR) + end + + it "has an Integer value" do + Encoding::Converter::CR_NEWLINE_DECORATOR.should be_an_instance_of(Integer) + end +end + +describe "Encoding::Converter::XML_TEXT_DECORATOR" do + it "exists" do + Encoding::Converter.should have_constant(:XML_TEXT_DECORATOR) + end + + it "has an Integer value" do + Encoding::Converter::XML_TEXT_DECORATOR.should be_an_instance_of(Integer) + end +end + +describe "Encoding::Converter::XML_ATTR_CONTENT_DECORATOR" do + it "exists" do + Encoding::Converter.should have_constant(:XML_ATTR_CONTENT_DECORATOR) + end + + it "has an Integer value" do + Encoding::Converter::XML_ATTR_CONTENT_DECORATOR.should be_an_instance_of(Integer) + end +end + +describe "Encoding::Converter::XML_ATTR_QUOTE_DECORATOR" do + it "exists" do + Encoding::Converter.should have_constant(:XML_ATTR_QUOTE_DECORATOR) + end + + it "has an Integer value" do + Encoding::Converter::XML_ATTR_QUOTE_DECORATOR.should be_an_instance_of(Integer) + end +end diff --git a/spec/ruby/core/encoding/converter/convert_spec.rb b/spec/ruby/core/encoding/converter/convert_spec.rb new file mode 100644 index 0000000000..8533af4565 --- /dev/null +++ b/spec/ruby/core/encoding/converter/convert_spec.rb @@ -0,0 +1,46 @@ +# encoding: binary +# frozen_string_literal: true +require_relative '../../../spec_helper' + +describe "Encoding::Converter#convert" do + it "returns a String" do + ec = Encoding::Converter.new('ascii', 'utf-8') + ec.convert('glark').should be_an_instance_of(String) + end + + it "sets the encoding of the result to the target encoding" do + ec = Encoding::Converter.new('ascii', 'utf-8') + str = 'glark'.dup.force_encoding('ascii') + ec.convert(str).encoding.should == Encoding::UTF_8 + end + + it "transcodes the given String to the target encoding" do + ec = Encoding::Converter.new("utf-8", "euc-jp") + ec.convert("\u3042".dup.force_encoding('UTF-8')).should == \ + "\xA4\xA2".dup.force_encoding('EUC-JP') + end + + it "allows Strings of different encodings to the source encoding" do + ec = Encoding::Converter.new('ascii', 'utf-8') + str = 'glark'.dup.force_encoding('SJIS') + ec.convert(str).encoding.should == Encoding::UTF_8 + end + + it "reuses the given encoding pair if called multiple times" do + ec = Encoding::Converter.new('ascii', 'SJIS') + ec.convert('a'.dup.force_encoding('ASCII')).should == 'a'.dup.force_encoding('SJIS') + ec.convert('b'.dup.force_encoding('ASCII')).should == 'b'.dup.force_encoding('SJIS') + end + + it "raises UndefinedConversionError if the String contains characters invalid for the target encoding" do + ec = Encoding::Converter.new('UTF-8', Encoding.find('macCyrillic')) + -> { ec.convert("\u{6543}".dup.force_encoding('UTF-8')) }.should \ + raise_error(Encoding::UndefinedConversionError) + end + + it "raises an ArgumentError if called on a finished stream" do + ec = Encoding::Converter.new('UTF-8', Encoding.find('macCyrillic')) + ec.finish + -> { ec.convert("\u{65}") }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/encoding/converter/convpath_spec.rb b/spec/ruby/core/encoding/converter/convpath_spec.rb new file mode 100644 index 0000000000..23f1e5dc33 --- /dev/null +++ b/spec/ruby/core/encoding/converter/convpath_spec.rb @@ -0,0 +1,24 @@ +require_relative '../../../spec_helper' + +describe "Encoding::Converter#convpath" do + it "returns an Array with a single element if there is a direct converter" do + cp = Encoding::Converter.new('ASCII', 'UTF-8').convpath + cp.should == [[Encoding::US_ASCII, Encoding::UTF_8]] + end + + it "returns multiple encoding pairs when direct conversion is impossible" do + cp = Encoding::Converter.new('ascii','Big5').convpath + cp.should == [ + [Encoding::US_ASCII, Encoding::UTF_8], + [Encoding::UTF_8, Encoding::Big5] + ] + end + + it "indicates if crlf_newline conversion would occur" do + ec = Encoding::Converter.new("ISo-8859-1", "EUC-JP", crlf_newline: true) + ec.convpath.last.should == "crlf_newline" + + ec = Encoding::Converter.new("ASCII", "UTF-8", crlf_newline: false) + ec.convpath.last.should_not == "crlf_newline" + end +end diff --git a/spec/ruby/core/encoding/converter/destination_encoding_spec.rb b/spec/ruby/core/encoding/converter/destination_encoding_spec.rb new file mode 100644 index 0000000000..481a857909 --- /dev/null +++ b/spec/ruby/core/encoding/converter/destination_encoding_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../../spec_helper' + +describe "Encoding::Converter#destination_encoding" do + it "returns the destination encoding as an Encoding object" do + ec = Encoding::Converter.new('ASCII','Big5') + ec.destination_encoding.should == Encoding::BIG5 + + ec = Encoding::Converter.new('SJIS','EUC-JP') + ec.destination_encoding.should == Encoding::EUC_JP + end +end diff --git a/spec/ruby/core/encoding/converter/finish_spec.rb b/spec/ruby/core/encoding/converter/finish_spec.rb new file mode 100644 index 0000000000..22e66df38c --- /dev/null +++ b/spec/ruby/core/encoding/converter/finish_spec.rb @@ -0,0 +1,36 @@ +require_relative '../../../spec_helper' + +describe "Encoding::Converter#finish" do + before :each do + @ec = Encoding::Converter.new("utf-8", "iso-2022-jp") + end + + it "returns a String" do + @ec.convert('foo') + @ec.finish.should be_an_instance_of(String) + end + + it "returns an empty String if there is nothing more to convert" do + @ec.convert("glark") + @ec.finish.should == "" + end + + it "returns the last part of the converted String if it hasn't already" do + @ec.convert("\u{9999}").should == "\e$B9a".dup.force_encoding('iso-2022-jp') + @ec.finish.should == "\e(B".dup.force_encoding('iso-2022-jp') + end + + it "returns a String in the destination encoding" do + @ec.convert("glark") + @ec.finish.encoding.should == Encoding::ISO2022_JP + end + + it "returns an empty String if self was not given anything to convert" do + @ec.finish.should == "" + end + + it "returns an empty String on subsequent invocations" do + @ec.finish.should == "" + @ec.finish.should == "" + end +end diff --git a/spec/ruby/core/encoding/converter/insert_output_spec.rb b/spec/ruby/core/encoding/converter/insert_output_spec.rb new file mode 100644 index 0000000000..1346adde1e --- /dev/null +++ b/spec/ruby/core/encoding/converter/insert_output_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../../spec_helper' + +describe "Encoding::Converter#insert_output" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/encoding/converter/inspect_spec.rb b/spec/ruby/core/encoding/converter/inspect_spec.rb new file mode 100644 index 0000000000..3170ee451f --- /dev/null +++ b/spec/ruby/core/encoding/converter/inspect_spec.rb @@ -0,0 +1,13 @@ +require_relative '../../../spec_helper' + +describe "Encoding::Converter#inspect" do + it "includes the source and destination encodings in the return value" do + source = Encoding::UTF_8 + destination = Encoding::UTF_16LE + + output = "#<Encoding::Converter: #{source.name} to #{destination.name}>" + + x = Encoding::Converter.new(source, destination) + x.inspect.should == output + end +end diff --git a/spec/ruby/core/encoding/converter/last_error_spec.rb b/spec/ruby/core/encoding/converter/last_error_spec.rb new file mode 100644 index 0000000000..ff2a2b4cbe --- /dev/null +++ b/spec/ruby/core/encoding/converter/last_error_spec.rb @@ -0,0 +1,91 @@ +# encoding: binary +require_relative '../../../spec_helper' + +describe "Encoding::Converter#last_error" do + it "returns nil when the no conversion has been attempted" do + ec = Encoding::Converter.new('ascii','utf-8') + ec.last_error.should be_nil + end + + it "returns nil when the last conversion did not produce an error" do + ec = Encoding::Converter.new('ascii','utf-8') + ec.convert('a'.dup.force_encoding('ascii')) + ec.last_error.should be_nil + end + + it "returns nil when #primitive_convert last returned :destination_buffer_full" do + ec = Encoding::Converter.new("utf-8", "iso-2022-jp") + ec.primitive_convert(+"\u{9999}", +"", 0, 0, partial_input: false) \ + .should == :destination_buffer_full + ec.last_error.should be_nil + end + + it "returns nil when #primitive_convert last returned :finished" do + ec = Encoding::Converter.new("utf-8", "iso-8859-1") + ec.primitive_convert("glark".dup.force_encoding('utf-8'), +"").should == :finished + ec.last_error.should be_nil + end + + it "returns nil if the last conversion succeeded but the penultimate failed" do + ec = Encoding::Converter.new("utf-8", "iso-8859-1") + ec.primitive_convert(+"\xf1abcd", +"").should == :invalid_byte_sequence + ec.primitive_convert("glark".dup.force_encoding('utf-8'), +"").should == :finished + ec.last_error.should be_nil + end + + it "returns an Encoding::InvalidByteSequenceError when #primitive_convert last returned :invalid_byte_sequence" do + ec = Encoding::Converter.new("utf-8", "iso-8859-1") + ec.primitive_convert(+"\xf1abcd", +"").should == :invalid_byte_sequence + ec.last_error.should be_an_instance_of(Encoding::InvalidByteSequenceError) + end + + it "returns an Encoding::UndefinedConversionError when #primitive_convert last returned :undefined_conversion" do + ec = Encoding::Converter.new("utf-8", "iso-8859-1") + ec.primitive_convert(+"\u{9876}", +"").should == :undefined_conversion + ec.last_error.should be_an_instance_of(Encoding::UndefinedConversionError) + end + + it "returns an Encoding::InvalidByteSequenceError when #primitive_convert last returned :incomplete_input" do + ec = Encoding::Converter.new("EUC-JP", "ISO-8859-1") + ec.primitive_convert(+"\xa4", +"", nil, 10).should == :incomplete_input + ec.last_error.should be_an_instance_of(Encoding::InvalidByteSequenceError) + end + + it "returns an Encoding::InvalidByteSequenceError when the last call to #convert produced one" do + ec = Encoding::Converter.new("utf-8", "iso-8859-1") + exception = nil + -> { + ec.convert("\xf1abcd") + }.should raise_error(Encoding::InvalidByteSequenceError) { |e| + exception = e + } + ec.last_error.should be_an_instance_of(Encoding::InvalidByteSequenceError) + ec.last_error.message.should == exception.message + end + + it "returns an Encoding::UndefinedConversionError when the last call to #convert produced one" do + ec = Encoding::Converter.new("utf-8", "iso-8859-1") + exception = nil + -> { + ec.convert("\u{9899}") + }.should raise_error(Encoding::UndefinedConversionError) { |e| + exception = e + } + ec.last_error.should be_an_instance_of(Encoding::UndefinedConversionError) + ec.last_error.message.should == exception.message + ec.last_error.message.should include "from UTF-8 to ISO-8859-1" + end + + it "returns the last error of #convert with a message showing the transcoding path" do + ec = Encoding::Converter.new("iso-8859-1", "Big5") + exception = nil + -> { + ec.convert("\xE9") # é in ISO-8859-1 + }.should raise_error(Encoding::UndefinedConversionError) { |e| + exception = e + } + ec.last_error.should be_an_instance_of(Encoding::UndefinedConversionError) + ec.last_error.message.should == exception.message + ec.last_error.message.should include "from ISO-8859-1 to UTF-8 to Big5" + end +end diff --git a/spec/ruby/core/encoding/converter/new_spec.rb b/spec/ruby/core/encoding/converter/new_spec.rb new file mode 100644 index 0000000000..a7bef53809 --- /dev/null +++ b/spec/ruby/core/encoding/converter/new_spec.rb @@ -0,0 +1,119 @@ +# encoding: binary +require_relative '../../../spec_helper' + +describe "Encoding::Converter.new" do + it "accepts a String for the source encoding" do + conv = Encoding::Converter.new("us-ascii", "utf-8") + conv.source_encoding.should == Encoding::US_ASCII + end + + it "accepts a String for the destination encoding" do + conv = Encoding::Converter.new("us-ascii", "utf-8") + conv.destination_encoding.should == Encoding::UTF_8 + end + + it "accepts an Encoding object for the source encoding" do + conv = Encoding::Converter.new(Encoding::US_ASCII, "utf-8") + conv.source_encoding.should == Encoding::US_ASCII + end + + it "accepts an Encoding object for the destination encoding" do + conv = Encoding::Converter.new("us-ascii", Encoding::UTF_8) + conv.destination_encoding.should == Encoding::UTF_8 + end + + it "raises an Encoding::ConverterNotFoundError if both encodings are the same" do + -> do + Encoding::Converter.new "utf-8", "utf-8" + end.should raise_error(Encoding::ConverterNotFoundError) + end + + it "calls #to_str to convert the source encoding argument to an encoding name" do + enc = mock("us-ascii") + enc.should_receive(:to_str).and_return("us-ascii") + conv = Encoding::Converter.new(enc, "utf-8") + conv.source_encoding.should == Encoding::US_ASCII + end + + it "calls #to_str to convert the destination encoding argument to an encoding name" do + enc = mock("utf-8") + enc.should_receive(:to_str).and_return("utf-8") + conv = Encoding::Converter.new("us-ascii", enc) + conv.destination_encoding.should == Encoding::UTF_8 + end + + it "sets replacement from the options Hash" do + conv = Encoding::Converter.new("us-ascii", "utf-8", replace: "fubar") + conv.replacement.should == "fubar" + end + + it "calls #to_hash to convert the options argument to a Hash if not an Integer" do + opts = mock("encoding converter options") + opts.should_receive(:to_hash).and_return({ replace: "fubar" }) + conv = Encoding::Converter.new("us-ascii", "utf-8", **opts) + conv.replacement.should == "fubar" + end + + it "calls #to_str to convert the replacement object to a String" do + obj = mock("encoding converter replacement") + obj.should_receive(:to_str).and_return("fubar") + conv = Encoding::Converter.new("us-ascii", "utf-8", replace: obj) + conv.replacement.should == "fubar" + end + + it "raises a TypeError if #to_str does not return a String" do + obj = mock("encoding converter replacement") + obj.should_receive(:to_str).and_return(1) + + -> do + Encoding::Converter.new("us-ascii", "utf-8", replace: obj) + end.should raise_error(TypeError) + end + + it "raises a TypeError if passed true for the replacement object" do + -> do + Encoding::Converter.new("us-ascii", "utf-8", replace: true) + end.should raise_error(TypeError) + end + + it "raises a TypeError if passed false for the replacement object" do + -> do + Encoding::Converter.new("us-ascii", "utf-8", replace: false) + end.should raise_error(TypeError) + end + + it "raises a TypeError if passed an Integer for the replacement object" do + -> do + Encoding::Converter.new("us-ascii", "utf-8", replace: 1) + end.should raise_error(TypeError) + end + + it "accepts an empty String for the replacement object" do + conv = Encoding::Converter.new("us-ascii", "utf-8", replace: "") + conv.replacement.should == "" + end + + describe "when passed nil for the replacement object" do + describe "when the destination encoding is not UTF-8" do + it "sets the replacement String to '?'" do + conv = Encoding::Converter.new("us-ascii", "binary", replace: nil) + conv.replacement.should == "?" + end + + it "sets the replacement String encoding to US-ASCII" do + conv = Encoding::Converter.new("us-ascii", "binary", replace: nil) + conv.replacement.encoding.should == Encoding::US_ASCII + end + + it "sets the replacement String to '\\uFFFD'" do + conv = Encoding::Converter.new("us-ascii", "utf-8", replace: nil) + conv.replacement.should == "\u{fffd}".dup.force_encoding("utf-8") + end + + it "sets the replacement String encoding to UTF-8" do + conv = Encoding::Converter.new("us-ascii", "utf-8", replace: nil) + conv.replacement.encoding.should == Encoding::UTF_8 + end + end + end +end diff --git a/spec/ruby/core/encoding/converter/primitive_convert_spec.rb b/spec/ruby/core/encoding/converter/primitive_convert_spec.rb new file mode 100644 index 0000000000..e4aeed103e --- /dev/null +++ b/spec/ruby/core/encoding/converter/primitive_convert_spec.rb @@ -0,0 +1,216 @@ +# encoding: binary +# frozen_string_literal: false +require_relative '../../../spec_helper' + +describe "Encoding::Converter#primitive_convert" do + before :each do + @ec = Encoding::Converter.new("utf-8", "iso-8859-1") + end + + it "accepts a nil source buffer" do + -> { @ec.primitive_convert(nil,"") }.should_not raise_error + end + + it "accepts a String as the source buffer" do + -> { @ec.primitive_convert("","") }.should_not raise_error + end + + it "raises FrozenError when the destination buffer is a frozen String" do + -> { @ec.primitive_convert("", "".freeze) }.should raise_error(FrozenError) + end + + it "accepts nil for the destination byte offset" do + -> { @ec.primitive_convert("","", nil) }.should_not raise_error + end + + it "accepts an integer for the destination byte offset" do + -> { @ec.primitive_convert("","a", 1) }.should_not raise_error + end + + it "calls #to_int to convert the destination byte offset" do + offset = mock("encoding primitive_convert destination byte offset") + offset.should_receive(:to_int).and_return(2) + @ec.primitive_convert("abc", result = " ", offset).should == :finished + result.should == " abc" + end + + it "raises an ArgumentError if the destination byte offset is greater than the bytesize of the destination buffer" do + -> { @ec.primitive_convert("","am", 0) }.should_not raise_error + -> { @ec.primitive_convert("","am", 1) }.should_not raise_error + -> { @ec.primitive_convert("","am", 2) }.should_not raise_error + -> { @ec.primitive_convert("","am", 3) }.should raise_error(ArgumentError) + end + + it "uses the destination byte offset to determine where to write the result in the destination buffer" do + dest = "aa" + @ec.primitive_convert("b",dest, nil, 0) + dest.should == "aa" + + @ec.primitive_convert("b",dest, nil, 1) + dest.should == "aab" + + @ec.primitive_convert("b",dest, nil, 2) + dest.should == "aabbb" + end + + it "accepts nil for the destination bytesize" do + -> { @ec.primitive_convert("","", nil, nil) }.should_not raise_error + end + + it "accepts an integer for the destination bytesize" do + -> { @ec.primitive_convert("","", nil, 0) }.should_not raise_error + end + + it "allows a destination bytesize value greater than the bytesize of the source buffer" do + -> { @ec.primitive_convert("am","", nil, 3) }.should_not raise_error + end + + it "allows a destination bytesize value less than the bytesize of the source buffer" do + -> { @ec.primitive_convert("am","", nil, 1) }.should_not raise_error + end + + it "calls #to_int to convert the destination byte size" do + size = mock("encoding primitive_convert destination byte size") + size.should_receive(:to_int).and_return(2) + @ec.primitive_convert("abc", result = " ", 0, size).should == :destination_buffer_full + result.should == "ab" + end + + it "uses destination bytesize as the maximum bytesize of the destination buffer" do + dest = "" + @ec.primitive_convert("glark", dest, nil, 1) + dest.bytesize.should == 1 + end + + it "allows a destination buffer of unlimited size if destination bytesize is nil" do + source = "glark".force_encoding('utf-8') + dest = "" + @ec.primitive_convert("glark", dest, nil, nil) + dest.bytesize.should == source.bytesize + end + + it "accepts an options hash" do + @ec.primitive_convert("","",nil,nil, after_output: true).should == :finished + end + + it "sets the destination buffer's encoding to the destination encoding if the conversion succeeded" do + dest = "".force_encoding('utf-8') + dest.encoding.should == Encoding::UTF_8 + @ec.primitive_convert("\u{98}",dest).should == :finished + dest.encoding.should == Encoding::ISO_8859_1 + end + + it "sets the destination buffer's encoding to the destination encoding if the conversion failed" do + dest = "".force_encoding('utf-8') + dest.encoding.should == Encoding::UTF_8 + @ec.primitive_convert("\u{9878}",dest).should == :undefined_conversion + dest.encoding.should == Encoding::ISO_8859_1 + end + + it "removes the undefined part from the source buffer when returning :undefined_conversion" do + dest = "".force_encoding('utf-8') + s = "\u{9878}abcd" + @ec.primitive_convert(s, dest).should == :undefined_conversion + + s.should == "abcd" + end + + it "returns :incomplete_input when source buffer ends unexpectedly and :partial_input isn't specified" do + ec = Encoding::Converter.new("EUC-JP", "ISO-8859-1") + ec.primitive_convert("\xa4", "", nil, nil, partial_input: false).should == :incomplete_input + end + + it "clears the source buffer when returning :incomplete_input" do + ec = Encoding::Converter.new("EUC-JP", "ISO-8859-1") + s = "\xa4" + ec.primitive_convert(s, "").should == :incomplete_input + + s.should == "" + end + + it "returns :source_buffer_empty when source buffer ends unexpectedly and :partial_input is true" do + ec = Encoding::Converter.new("EUC-JP", "ISO-8859-1") + ec.primitive_convert("\xa4", "", nil, nil, partial_input: true).should == :source_buffer_empty + end + + it "clears the source buffer when returning :source_buffer_empty" do + ec = Encoding::Converter.new("EUC-JP", "ISO-8859-1") + s = "\xa4" + ec.primitive_convert(s, "", nil, nil, partial_input: true).should == :source_buffer_empty + + s.should == "" + end + + it "returns :undefined_conversion when a character in the source buffer is not representable in the output encoding" do + @ec.primitive_convert("\u{9876}","").should == :undefined_conversion + end + + it "returns :invalid_byte_sequence when an invalid byte sequence was found in the source buffer" do + @ec.primitive_convert("\xf1abcd","").should == :invalid_byte_sequence + end + + it "removes consumed and erroneous bytes from the source buffer when returning :invalid_byte_sequence" do + ec = Encoding::Converter.new(Encoding::UTF_8, Encoding::UTF_8_MAC) + s = "\xC3\xA1\x80\x80\xC3\xA1".force_encoding("utf-8") + dest = "".force_encoding("utf-8") + ec.primitive_convert(s, dest) + + s.should == "\x80\xC3\xA1".force_encoding("utf-8") + end + + it "returns :finished when the conversion succeeded" do + @ec.primitive_convert("glark".force_encoding('utf-8'),"").should == :finished + end + + it "clears the source buffer when returning :finished" do + s = "glark".force_encoding('utf-8') + @ec.primitive_convert(s, "").should == :finished + + s.should == "" + end + + it "returns :destination_buffer_full when the destination buffer is too small" do + ec = Encoding::Converter.new("utf-8", "iso-2022-jp") + source = "\u{9999}" + destination_bytesize = source.bytesize - 1 + ec.primitive_convert(source, "", 0, destination_bytesize) \ + .should == :destination_buffer_full + source.should == "" + end + + it "clears the source buffer when returning :destination_buffer_full" do + ec = Encoding::Converter.new("utf-8", "iso-2022-jp") + s = "\u{9999}" + destination_bytesize = s.bytesize - 1 + ec.primitive_convert(s, "", 0, destination_bytesize).should == :destination_buffer_full + + s.should == "" + end + + it "keeps removing invalid bytes from the source buffer" do + ec = Encoding::Converter.new(Encoding::UTF_8, Encoding::UTF_8_MAC) + s = "\x80\x80\x80" + dest = "".force_encoding(Encoding::UTF_8_MAC) + + ec.primitive_convert(s, dest) + s.should == "\x80\x80" + ec.primitive_convert(s, dest) + s.should == "\x80" + ec.primitive_convert(s, dest) + s.should == "" + end + + it "reuses read-again bytes after the first error" do + s = "\xf1abcd" + dest = "" + + @ec.primitive_convert(s, dest).should == :invalid_byte_sequence + s.should == "bcd" + @ec.primitive_errinfo[4].should == "a" + + @ec.primitive_convert(s, dest).should == :finished + s.should == "" + + dest.should == "abcd" + end +end diff --git a/spec/ruby/core/encoding/converter/primitive_errinfo_spec.rb b/spec/ruby/core/encoding/converter/primitive_errinfo_spec.rb new file mode 100644 index 0000000000..5ee8b1fecd --- /dev/null +++ b/spec/ruby/core/encoding/converter/primitive_errinfo_spec.rb @@ -0,0 +1,69 @@ +# encoding: binary +# frozen_string_literal: false +require_relative '../../../spec_helper' + +describe "Encoding::Converter#primitive_errinfo" do + it "returns [:source_buffer_empty,nil,nil,nil,nil] when no conversion has been attempted" do + ec = Encoding::Converter.new('ascii','utf-8') + ec.primitive_errinfo.should == [:source_buffer_empty, nil, nil, nil, nil] + end + + it "returns [:finished,nil,nil,nil,nil] when #primitive_convert last returned :finished" do + ec = Encoding::Converter.new('ascii','utf-8') + ec.primitive_convert("a","").should == :finished + ec.primitive_errinfo.should == [:finished, nil, nil, nil, nil] + end + + it "returns [:source_buffer_empty,nil,nil,nil, nil] when #convert last succeeded" do + ec = Encoding::Converter.new('ascii','utf-8') + ec.convert("a".force_encoding('ascii')).should == "a".force_encoding('utf-8') + ec.primitive_errinfo.should == [:source_buffer_empty, nil, nil, nil, nil] + end + + it "returns [:destination_buffer_full,nil,nil,nil,nil] when #primitive_convert last returned :destination_buffer_full" do + ec = Encoding::Converter.new("utf-8", "iso-2022-jp") + ec.primitive_convert("\u{9999}", "", 0, 0, partial_input: false).should == :destination_buffer_full + ec.primitive_errinfo.should == [:destination_buffer_full, nil, nil, nil, nil] + end + + it "returns the status of the last primitive conversion, even if it was successful and the previous one wasn't" do + ec = Encoding::Converter.new("utf-8", "iso-8859-1") + ec.primitive_convert("\xf1abcd","").should == :invalid_byte_sequence + ec.primitive_convert("glark".force_encoding('utf-8'),"").should == :finished + ec.primitive_errinfo.should == [:finished, nil, nil, nil, nil] + end + + it "returns the state, source encoding, target encoding, and the erroneous bytes when #primitive_convert last returned :undefined_conversion" do + ec = Encoding::Converter.new("utf-8", "iso-8859-1") + ec.primitive_convert("\u{9876}","").should == :undefined_conversion + ec.primitive_errinfo.should == + [:undefined_conversion, "UTF-8", "ISO-8859-1", "\xE9\xA1\xB6", ""] + end + + it "returns the state, source encoding, target encoding, and erroneous bytes when #primitive_convert last returned :incomplete_input" do + ec = Encoding::Converter.new("EUC-JP", "ISO-8859-1") + ec.primitive_convert("\xa4", "", nil, 10).should == :incomplete_input + ec.primitive_errinfo.should == [:incomplete_input, "EUC-JP", "UTF-8", "\xA4", ""] + end + + it "returns the state, source encoding, target encoding, erroneous bytes, and the read-again bytes when #primitive_convert last returned :invalid_byte_sequence" do + ec = Encoding::Converter.new("utf-8", "iso-8859-1") + ec.primitive_convert("\xf1abcd","").should == :invalid_byte_sequence + ec.primitive_errinfo.should == + [:invalid_byte_sequence, "UTF-8", "ISO-8859-1", "\xF1", "a"] + end + + it "returns the state, source encoding, target encoding, erroneous bytes, and the read-again bytes when #convert last raised InvalidByteSequenceError" do + ec = Encoding::Converter.new("utf-8", "iso-8859-1") + -> { ec.convert("\xf1abcd") }.should raise_error(Encoding::InvalidByteSequenceError) + ec.primitive_errinfo.should == + [:invalid_byte_sequence, "UTF-8", "ISO-8859-1", "\xF1", "a"] + end + + it "returns the state, source encoding, target encoding, erroneous bytes, and the read-again bytes when #finish last raised InvalidByteSequenceError" do + ec = Encoding::Converter.new("EUC-JP", "ISO-8859-1") + ec.convert("\xa4") + -> { ec.finish }.should raise_error(Encoding::InvalidByteSequenceError) + ec.primitive_errinfo.should == [:incomplete_input, "EUC-JP", "UTF-8", "\xA4", ""] + end +end diff --git a/spec/ruby/core/encoding/converter/putback_spec.rb b/spec/ruby/core/encoding/converter/putback_spec.rb new file mode 100644 index 0000000000..04bb565655 --- /dev/null +++ b/spec/ruby/core/encoding/converter/putback_spec.rb @@ -0,0 +1,56 @@ +# encoding: binary +require_relative '../../../spec_helper' + +describe "Encoding::Converter#putback" do + before :each do + @ec = Encoding::Converter.new("EUC-JP", "ISO-8859-1") + @ret = @ec.primitive_convert(@src=+"abc\xa1def", @dst=+"", nil, 10) + end + + it "returns a String" do + @ec.putback.should be_an_instance_of(String) + end + + it "returns a String in the source encoding" do + @ec.putback.encoding.should == Encoding::EUC_JP + end + + it "returns the bytes buffered due to an :invalid_byte_sequence error" do + @ret.should == :invalid_byte_sequence + @ec.putback.should == 'd' + @ec.primitive_errinfo.last.should == 'd' + end + + it "allows conversion to be resumed after an :invalid_byte_sequence" do + @src = @ec.putback + @src + @ret = @ec.primitive_convert(@src, @dst, nil, 10) + @ret.should == :finished + @dst.should == "abcdef" + @src.should == "" + end + + it "returns an empty String when there are no more bytes to put back" do + @ec.putback + @ec.putback.should == "" + end + + it "returns the problematic bytes for UTF-16LE" do + ec = Encoding::Converter.new("utf-16le", "iso-8859-1") + src = +"\x00\xd8\x61\x00" + dst = +"" + ec.primitive_convert(src, dst).should == :invalid_byte_sequence + ec.primitive_errinfo.should == [:invalid_byte_sequence, "UTF-16LE", "UTF-8", "\x00\xD8", "a\x00"] + ec.putback.should == "a\x00".dup.force_encoding("utf-16le") + ec.putback.should == "" + end + + it "accepts an integer argument corresponding to the number of bytes to be put back" do + ec = Encoding::Converter.new("utf-16le", "iso-8859-1") + src = +"\x00\xd8\x61\x00" + dst = +"" + ec.primitive_convert(src, dst).should == :invalid_byte_sequence + ec.primitive_errinfo.should == [:invalid_byte_sequence, "UTF-16LE", "UTF-8", "\x00\xD8", "a\x00"] + ec.putback(2).should == "a\x00".dup.force_encoding("utf-16le") + ec.putback.should == "" + end +end diff --git a/spec/ruby/core/encoding/converter/replacement_spec.rb b/spec/ruby/core/encoding/converter/replacement_spec.rb new file mode 100644 index 0000000000..ea514ca8dd --- /dev/null +++ b/spec/ruby/core/encoding/converter/replacement_spec.rb @@ -0,0 +1,72 @@ +require_relative '../../../spec_helper' + +describe "Encoding::Converter#replacement" do + it "returns '?' in US-ASCII when the destination encoding is not UTF-8" do + ec = Encoding::Converter.new("utf-8", "us-ascii") + ec.replacement.should == "?" + ec.replacement.encoding.should == Encoding::US_ASCII + + ec = Encoding::Converter.new("utf-8", "sjis") + ec.replacement.should == "?" + ec.replacement.encoding.should == Encoding::US_ASCII + end + + it "returns \\uFFFD when the destination encoding is UTF-8" do + ec = Encoding::Converter.new("us-ascii", "utf-8") + ec.replacement.should == "\u{fffd}".dup.force_encoding('utf-8') + ec.replacement.encoding.should == Encoding::UTF_8 + end +end + +describe "Encoding::Converter#replacement=" do + it "accepts a String argument" do + ec = Encoding::Converter.new("utf-8", "us-ascii") + ec.replacement = "!" + ec.replacement.should == "!" + end + + it "accepts a String argument of arbitrary length" do + ec = Encoding::Converter.new("utf-8", "us-ascii") + ec.replacement = "?!?" * 9999 + ec.replacement.should == "?!?" * 9999 + end + + it "raises a TypeError if assigned a non-String argument" do + ec = Encoding::Converter.new("utf-8", "us-ascii") + -> { ec.replacement = nil }.should raise_error(TypeError) + end + + it "sets #replacement" do + ec = Encoding::Converter.new("us-ascii", "utf-8") + ec.replacement.should == "\u{fffd}".dup.force_encoding('utf-8') + ec.replacement = '?'.encode('utf-8') + ec.replacement.should == '?'.dup.force_encoding('utf-8') + end + + it "raises an UndefinedConversionError is the argument cannot be converted into the destination encoding" do + ec = Encoding::Converter.new("sjis", "ascii") + utf8_q = "\u{986}".dup.force_encoding('utf-8') + ec.primitive_convert(utf8_q.dup, +"").should == :undefined_conversion + -> { ec.replacement = utf8_q }.should \ + raise_error(Encoding::UndefinedConversionError) + end + + it "does not change the replacement character if the argument cannot be converted into the destination encoding" do + ec = Encoding::Converter.new("sjis", "ascii") + utf8_q = "\u{986}".dup.force_encoding('utf-8') + ec.primitive_convert(utf8_q.dup, +"").should == :undefined_conversion + -> { ec.replacement = utf8_q }.should \ + raise_error(Encoding::UndefinedConversionError) + ec.replacement.should == "?".dup.force_encoding('us-ascii') + end + + it "uses the replacement character" do + ec = Encoding::Converter.new("utf-8", "us-ascii", :invalid => :replace, :undef => :replace) + ec.replacement = "!" + dest = +"" + status = ec.primitive_convert(+"䏿–‡123", dest) + + status.should == :finished + dest.should == "!!123" + end +end diff --git a/spec/ruby/core/encoding/converter/search_convpath_spec.rb b/spec/ruby/core/encoding/converter/search_convpath_spec.rb new file mode 100644 index 0000000000..59fe4520c0 --- /dev/null +++ b/spec/ruby/core/encoding/converter/search_convpath_spec.rb @@ -0,0 +1,30 @@ +require_relative '../../../spec_helper' + +describe "Encoding::Converter.search_convpath" do + it "returns an Array with a single element if there is a direct converter" do + cp = Encoding::Converter.search_convpath('ASCII', 'UTF-8') + cp.should == [[Encoding::US_ASCII, Encoding::UTF_8]] + end + + it "returns multiple encoding pairs when direct conversion is impossible" do + cp = Encoding::Converter.search_convpath('ascii','Big5') + cp.should == [ + [Encoding::US_ASCII, Encoding::UTF_8], + [Encoding::UTF_8, Encoding::Big5] + ] + end + + it "indicates if crlf_newline conversion would occur" do + cp = Encoding::Converter.search_convpath("ISO-8859-1", "EUC-JP", crlf_newline: true) + cp.last.should == "crlf_newline" + + cp = Encoding::Converter.search_convpath("ASCII", "UTF-8", crlf_newline: false) + cp.last.should_not == "crlf_newline" + end + + it "raises an Encoding::ConverterNotFoundError if no conversion path exists" do + -> do + Encoding::Converter.search_convpath(Encoding::BINARY, Encoding::Emacs_Mule) + end.should raise_error(Encoding::ConverterNotFoundError) + end +end diff --git a/spec/ruby/core/encoding/converter/source_encoding_spec.rb b/spec/ruby/core/encoding/converter/source_encoding_spec.rb new file mode 100644 index 0000000000..6196f717bd --- /dev/null +++ b/spec/ruby/core/encoding/converter/source_encoding_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../../spec_helper' + +describe "Encoding::Converter#source_encoding" do + it "returns the source encoding as an Encoding object" do + ec = Encoding::Converter.new('ASCII','Big5') + ec.source_encoding.should == Encoding::US_ASCII + + ec = Encoding::Converter.new('Shift_JIS','EUC-JP') + ec.source_encoding.should == Encoding::SHIFT_JIS + end +end diff --git a/spec/ruby/core/encoding/default_external_spec.rb b/spec/ruby/core/encoding/default_external_spec.rb new file mode 100644 index 0000000000..9aae4976e0 --- /dev/null +++ b/spec/ruby/core/encoding/default_external_spec.rb @@ -0,0 +1,69 @@ +require_relative '../../spec_helper' + +describe "Encoding.default_external" do + before :each do + @original_encoding = Encoding.default_external + end + + after :each do + Encoding.default_external = @original_encoding + end + + it "returns an Encoding object" do + Encoding.default_external.should be_an_instance_of(Encoding) + end + + it "returns the default external encoding" do + Encoding.default_external = Encoding::SHIFT_JIS + Encoding.default_external.should == Encoding::SHIFT_JIS + end + + platform_is :windows do + it 'is UTF-8 by default on Windows' do + Encoding.default_external.should == Encoding::UTF_8 + end + end +end + +describe "Encoding.default_external=" do + before :each do + @original_encoding = Encoding.default_external + end + + after :each do + Encoding.default_external = @original_encoding + end + + it "sets the default external encoding" do + Encoding.default_external = Encoding::SHIFT_JIS + Encoding.default_external.should == Encoding::SHIFT_JIS + Encoding.find('external').should == Encoding::SHIFT_JIS + end + + platform_is_not :windows do + it "also sets the filesystem encoding" do + Encoding.default_external = Encoding::SHIFT_JIS + Encoding.find('filesystem').should == Encoding::SHIFT_JIS + end + end + + it "can accept a name of an encoding as a String" do + Encoding.default_external = 'Shift_JIS' + Encoding.default_external.should == Encoding::SHIFT_JIS + end + + it "calls #to_s on arguments that are neither Strings nor Encodings" do + string = mock('string') + string.should_receive(:to_str).at_least(1).and_return('US-ASCII') + Encoding.default_external = string + Encoding.default_external.should == Encoding::ASCII + end + + it "raises a TypeError unless the argument is an Encoding or convertible to a String" do + -> { Encoding.default_external = [] }.should raise_error(TypeError) + end + + it "raises an ArgumentError if the argument is nil" do + -> { Encoding.default_external = nil }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/encoding/default_internal_spec.rb b/spec/ruby/core/encoding/default_internal_spec.rb new file mode 100644 index 0000000000..855f4e9f32 --- /dev/null +++ b/spec/ruby/core/encoding/default_internal_spec.rb @@ -0,0 +1,74 @@ +require_relative '../../spec_helper' + +describe "Encoding.default_internal" do + before :each do + @original_encoding = Encoding.default_internal + end + + after :each do + Encoding.default_internal = @original_encoding + end + + it "is nil by default" do + Encoding.default_internal.should be_nil + end + + it "returns an Encoding object if a default internal encoding is set" do + Encoding.default_internal = Encoding::ASCII + Encoding.default_internal.should be_an_instance_of(Encoding) + end + + it "returns nil if no default internal encoding is set" do + Encoding.default_internal = nil + Encoding.default_internal.should be_nil + end + + it "returns the default internal encoding" do + Encoding.default_internal = Encoding::BINARY + Encoding.default_internal.should == Encoding::BINARY + end +end + +describe "Encoding.default_internal=" do + before :each do + @original_encoding = Encoding.default_internal + end + + after :each do + Encoding.default_internal = @original_encoding + end + + it "sets the default internal encoding" do + Encoding.default_internal = Encoding::SHIFT_JIS + Encoding.default_internal.should == Encoding::SHIFT_JIS + end + + it "can accept a name of an encoding as a String" do + Encoding.default_internal = 'Shift_JIS' + Encoding.default_internal.should == Encoding::SHIFT_JIS + end + + it "calls #to_str to convert an object to a String" do + obj = mock('string') + obj.should_receive(:to_str).at_least(1).times.and_return('ascii') + + Encoding.default_internal = obj + Encoding.default_internal.should == Encoding::ASCII + end + + it "raises a TypeError if #to_str does not return a String" do + obj = mock('string') + obj.should_receive(:to_str).at_least(1).times.and_return(1) + + -> { Encoding.default_internal = obj }.should raise_error(TypeError) + end + + it "raises a TypeError when passed an object not providing #to_str" do + -> { Encoding.default_internal = mock("encoding") }.should raise_error(TypeError) + end + + it "accepts an argument of nil to unset the default internal encoding" do + Encoding.default_internal = nil + Encoding.default_internal.should be_nil + end +end diff --git a/spec/ruby/core/encoding/dummy_spec.rb b/spec/ruby/core/encoding/dummy_spec.rb new file mode 100644 index 0000000000..75ffcd5a4e --- /dev/null +++ b/spec/ruby/core/encoding/dummy_spec.rb @@ -0,0 +1,14 @@ +require_relative '../../spec_helper' + +describe "Encoding#dummy?" do + it "returns false for proper encodings" do + Encoding::UTF_8.dummy?.should be_false + Encoding::ASCII.dummy?.should be_false + end + + it "returns true for dummy encodings" do + Encoding::ISO_2022_JP.dummy?.should be_true + Encoding::CP50221.dummy?.should be_true + Encoding::UTF_7.dummy?.should be_true + end +end diff --git a/spec/ruby/core/encoding/find_spec.rb b/spec/ruby/core/encoding/find_spec.rb new file mode 100644 index 0000000000..9c34fe0e77 --- /dev/null +++ b/spec/ruby/core/encoding/find_spec.rb @@ -0,0 +1,82 @@ +require_relative '../../spec_helper' + +describe "Encoding.find" do + before :all do + @encodings = Encoding.aliases.to_a.flatten.uniq + end + + it "returns the corresponding Encoding object if given a valid encoding name" do + @encodings.each do |enc| + Encoding.find(enc).should be_an_instance_of(Encoding) + end + end + + it "returns the corresponding Encoding object if given a valid alias name" do + Encoding.aliases.keys.each do |enc_alias| + Encoding.find(enc_alias).should be_an_instance_of(Encoding) + end + end + + it "raises a TypeError if passed a Symbol" do + -> { Encoding.find(:"utf-8") }.should raise_error(TypeError) + end + + it "returns the passed Encoding object" do + Encoding.find(Encoding::UTF_8).should == Encoding::UTF_8 + end + + it "accepts encoding names as Strings" do + Encoding.list.each do |enc| + Encoding.find(enc.name).should == enc + end + end + + it "accepts any object as encoding name, if it responds to #to_str" do + obj = Class.new do + attr_writer :encoding_name + def to_str; @encoding_name; end + end.new + + Encoding.list.each do |enc| + obj.encoding_name = enc.name + Encoding.find(obj).should == enc + end + end + + it "is case insensitive" do + @encodings.each do |enc| + Encoding.find(enc.upcase).should == Encoding.find(enc) + end + end + + it "raises an ArgumentError if the given encoding does not exist" do + -> { Encoding.find('dh2dh278d') }.should raise_error(ArgumentError, 'unknown encoding name - dh2dh278d') + end + + # Not sure how to do a better test, since locale depends on weird platform-specific stuff + it "supports the 'locale' encoding alias" do + enc = Encoding.find('locale') + enc.should_not == nil + end + + it "returns default external encoding for the 'external' encoding alias" do + enc = Encoding.find('external') + enc.should == Encoding.default_external + end + + it "returns default internal encoding for the 'internal' encoding alias" do + enc = Encoding.find('internal') + enc.should == Encoding.default_internal + end + + platform_is_not :windows do + it "uses default external encoding for the 'filesystem' encoding alias" do + enc = Encoding.find('filesystem') + enc.should == Encoding.default_external + end + end + + platform_is :windows do + it "needs to be reviewed for spec completeness" + end +end diff --git a/spec/ruby/core/encoding/fixtures/classes.rb b/spec/ruby/core/encoding/fixtures/classes.rb new file mode 100644 index 0000000000..943865e8d8 --- /dev/null +++ b/spec/ruby/core/encoding/fixtures/classes.rb @@ -0,0 +1,49 @@ +# encoding: binary +module EncodingSpecs + class UndefinedConversionError + def self.exception + ec = Encoding::Converter.new('utf-8','ascii') + begin + ec.convert("\u{8765}") + rescue Encoding::UndefinedConversionError => e + e + end + end + end + + class UndefinedConversionErrorIndirect + def self.exception + ec = Encoding::Converter.new("ISO-8859-1", "EUC-JP") + begin + ec.convert("\xA0") + rescue Encoding::UndefinedConversionError => e + e + end + end + end + + class InvalidByteSequenceError + def self.exception + ec = Encoding::Converter.new("utf-8", "iso-8859-1") + begin + ec.convert("\xf1abcd") + rescue Encoding::InvalidByteSequenceError => e + # Return the exception object and the primitive_errinfo Array + [e, ec.primitive_errinfo] + end + end + end + + class InvalidByteSequenceErrorIndirect + def self.exception + ec = Encoding::Converter.new("EUC-JP", "ISO-8859-1") + begin + ec.convert("abc\xA1\xFFdef") + rescue Encoding::InvalidByteSequenceError => e + # Return the exception object and the discarded bytes reported by + # #primitive_errinfo + [e, ec.primitive_errinfo] + end + end + end +end diff --git a/spec/ruby/core/encoding/inspect_spec.rb b/spec/ruby/core/encoding/inspect_spec.rb new file mode 100644 index 0000000000..df96141db9 --- /dev/null +++ b/spec/ruby/core/encoding/inspect_spec.rb @@ -0,0 +1,33 @@ +require_relative '../../spec_helper' + +describe "Encoding#inspect" do + it "returns a String" do + Encoding::UTF_8.inspect.should be_an_instance_of(String) + end + + ruby_version_is ""..."3.4" do + it "returns #<Encoding:name> for a non-dummy encoding named 'name'" do + Encoding.list.to_a.reject {|e| e.dummy? }.each do |enc| + enc.inspect.should =~ /#<Encoding:#{enc.name}>/ + end + end + end + + ruby_version_is "3.4" do + it "returns #<Encoding:name> for a non-dummy encoding named 'name'" do + Encoding.list.to_a.reject {|e| e.dummy? }.each do |enc| + if enc.name == "ASCII-8BIT" + enc.inspect.should == "#<Encoding:BINARY (ASCII-8BIT)>" + else + enc.inspect.should =~ /#<Encoding:#{enc.name}>/ + end + end + end + end + + it "returns #<Encoding:name (dummy)> for a dummy encoding named 'name'" do + Encoding.list.to_a.select {|e| e.dummy? }.each do |enc| + enc.inspect.should =~ /#<Encoding:#{enc.name} \(dummy\)>/ + end + end +end diff --git a/spec/ruby/core/encoding/invalid_byte_sequence_error/destination_encoding_name_spec.rb b/spec/ruby/core/encoding/invalid_byte_sequence_error/destination_encoding_name_spec.rb new file mode 100644 index 0000000000..2b15fc1a0f --- /dev/null +++ b/spec/ruby/core/encoding/invalid_byte_sequence_error/destination_encoding_name_spec.rb @@ -0,0 +1,19 @@ +require_relative "../../../spec_helper" +require_relative '../fixtures/classes' + +describe "Encoding::InvalidByteSequenceError#destination_encoding_name" do + before :each do + @exception, = EncodingSpecs::InvalidByteSequenceError.exception + @exception2, = EncodingSpecs::InvalidByteSequenceErrorIndirect.exception + end + + it "returns a String" do + @exception.destination_encoding_name.should be_an_instance_of(String) + @exception2.destination_encoding_name.should be_an_instance_of(String) + end + + it "is equal to the destination encoding name of the object that raised it" do + @exception.destination_encoding_name.should == "ISO-8859-1" + @exception2.destination_encoding_name.should == "UTF-8" + end +end diff --git a/spec/ruby/core/encoding/invalid_byte_sequence_error/destination_encoding_spec.rb b/spec/ruby/core/encoding/invalid_byte_sequence_error/destination_encoding_spec.rb new file mode 100644 index 0000000000..c2ed6de1d8 --- /dev/null +++ b/spec/ruby/core/encoding/invalid_byte_sequence_error/destination_encoding_spec.rb @@ -0,0 +1,19 @@ +require_relative "../../../spec_helper" +require_relative '../fixtures/classes' + +describe "Encoding::InvalidByteSequenceError#destination_encoding" do + before :each do + @exception, = EncodingSpecs::InvalidByteSequenceError.exception + @exception2, = EncodingSpecs::InvalidByteSequenceErrorIndirect.exception + end + + it "returns an Encoding object" do + @exception.destination_encoding.should be_an_instance_of(Encoding) + @exception2.destination_encoding.should be_an_instance_of(Encoding) + end + + it "is equal to the destination encoding of the object that raised it" do + @exception.destination_encoding.should == Encoding::ISO_8859_1 + @exception2.destination_encoding.should == Encoding::UTF_8 + end +end diff --git a/spec/ruby/core/encoding/invalid_byte_sequence_error/error_bytes_spec.rb b/spec/ruby/core/encoding/invalid_byte_sequence_error/error_bytes_spec.rb new file mode 100644 index 0000000000..8b7e87960f --- /dev/null +++ b/spec/ruby/core/encoding/invalid_byte_sequence_error/error_bytes_spec.rb @@ -0,0 +1,31 @@ +# encoding: binary +require_relative "../../../spec_helper" +require_relative '../fixtures/classes' + +describe "Encoding::InvalidByteSequenceError#error_bytes" do + before :each do + @exception, @errinfo = EncodingSpecs::InvalidByteSequenceError.exception + @exception2, @errinfo2 = EncodingSpecs::InvalidByteSequenceErrorIndirect.exception + end + + it "returns a String" do + @exception.error_bytes.should be_an_instance_of(String) + @exception2.error_bytes.should be_an_instance_of(String) + end + + it "returns the bytes that caused the exception" do + @exception.error_bytes.size.should == 1 + @exception.error_bytes.should == "\xF1" + @exception.error_bytes.should == @errinfo[-2] + + @exception2.error_bytes.size.should == 1 + @exception2.error_bytes.should == "\xA1" + @exception2.error_bytes.should == @errinfo2[-2] + end + + it "uses BINARY as the encoding" do + @exception.error_bytes.encoding.should == Encoding::BINARY + + @exception2.error_bytes.encoding.should == Encoding::BINARY + end +end diff --git a/spec/ruby/core/encoding/invalid_byte_sequence_error/incomplete_input_spec.rb b/spec/ruby/core/encoding/invalid_byte_sequence_error/incomplete_input_spec.rb new file mode 100644 index 0000000000..83606f77b4 --- /dev/null +++ b/spec/ruby/core/encoding/invalid_byte_sequence_error/incomplete_input_spec.rb @@ -0,0 +1,28 @@ +# encoding: binary +require_relative '../../../spec_helper' + +describe "Encoding::InvalidByteSequenceError#incomplete_input?" do + it "returns nil by default" do + Encoding::InvalidByteSequenceError.new.incomplete_input?.should be_nil + end + + it "returns true if #primitive_convert returned :incomplete_input for the same data" do + ec = Encoding::Converter.new("EUC-JP", "ISO-8859-1") + ec.primitive_convert(+"\xA1", +'').should == :incomplete_input + begin + ec.convert("\xA1") + rescue Encoding::InvalidByteSequenceError => e + e.incomplete_input?.should be_true + end + end + + it "returns false if #primitive_convert returned :invalid_byte_sequence for the same data" do + ec = Encoding::Converter.new("ascii", "utf-8") + ec.primitive_convert(+"\xfffffffff", +'').should == :invalid_byte_sequence + begin + ec.convert("\xfffffffff") + rescue Encoding::InvalidByteSequenceError => e + e.incomplete_input?.should be_false + end + end +end diff --git a/spec/ruby/core/encoding/invalid_byte_sequence_error/readagain_bytes_spec.rb b/spec/ruby/core/encoding/invalid_byte_sequence_error/readagain_bytes_spec.rb new file mode 100644 index 0000000000..e5ad0a61bd --- /dev/null +++ b/spec/ruby/core/encoding/invalid_byte_sequence_error/readagain_bytes_spec.rb @@ -0,0 +1,31 @@ +# encoding: binary +require_relative "../../../spec_helper" +require_relative '../fixtures/classes' + +describe "Encoding::InvalidByteSequenceError#readagain_bytes" do + before :each do + @exception, @errinfo = EncodingSpecs::InvalidByteSequenceError.exception + @exception2, @errinfo2 = EncodingSpecs::InvalidByteSequenceErrorIndirect.exception + end + + it "returns a String" do + @exception.readagain_bytes.should be_an_instance_of(String) + @exception2.readagain_bytes.should be_an_instance_of(String) + end + + it "returns the bytes to be read again" do + @exception.readagain_bytes.size.should == 1 + @exception.readagain_bytes.should == "a".dup.force_encoding('binary') + @exception.readagain_bytes.should == @errinfo[-1] + + @exception2.readagain_bytes.size.should == 1 + @exception2.readagain_bytes.should == "\xFF".dup.force_encoding('binary') + @exception2.readagain_bytes.should == @errinfo2[-1] + end + + it "uses BINARY as the encoding" do + @exception.readagain_bytes.encoding.should == Encoding::BINARY + + @exception2.readagain_bytes.encoding.should == Encoding::BINARY + end +end diff --git a/spec/ruby/core/encoding/invalid_byte_sequence_error/source_encoding_name_spec.rb b/spec/ruby/core/encoding/invalid_byte_sequence_error/source_encoding_name_spec.rb new file mode 100644 index 0000000000..a9464114a8 --- /dev/null +++ b/spec/ruby/core/encoding/invalid_byte_sequence_error/source_encoding_name_spec.rb @@ -0,0 +1,29 @@ +require_relative "../../../spec_helper" +require_relative '../fixtures/classes' + +describe "Encoding::UndefinedConversionError#source_encoding_name" do + before :each do + @exception, = EncodingSpecs::UndefinedConversionError.exception + @exception2, = EncodingSpecs::UndefinedConversionErrorIndirect.exception + end + + it "returns a String" do + @exception.source_encoding_name.should be_an_instance_of(String) + end + + it "is equal to the source encoding name of the object that raised it" do + @exception.source_encoding_name.should == "UTF-8" + end + + # The source encoding specified in the Encoding::Converter constructor may + # differ from the source encoding returned here. What seems to happen is + # that when transcoding along a path with multiple pairs of encodings, the + # last one encountered when the error occurred is returned. So in this + # case, the conversion path is ISO-8859-1 -> UTF-8 -> EUC-JP. The + # conversion from ISO-8859-1 -> UTF-8 succeeded, but the conversion from + # UTF-8 to EUC-JP failed. IOW, it failed when the source encoding was + # UTF-8, so UTF-8 is regarded as the source encoding. + it "is equal to the source encoding at the stage of the conversion path where the error occurred" do + @exception2.source_encoding_name.should == 'UTF-8' + end +end diff --git a/spec/ruby/core/encoding/invalid_byte_sequence_error/source_encoding_spec.rb b/spec/ruby/core/encoding/invalid_byte_sequence_error/source_encoding_spec.rb new file mode 100644 index 0000000000..7fdc0a122b --- /dev/null +++ b/spec/ruby/core/encoding/invalid_byte_sequence_error/source_encoding_spec.rb @@ -0,0 +1,34 @@ +require_relative "../../../spec_helper" +require_relative '../fixtures/classes' + +describe "Encoding::InvalidByteSequenceError#source_encoding" do + before :each do + @exception, = EncodingSpecs::InvalidByteSequenceError.exception + @exception2, = EncodingSpecs::InvalidByteSequenceErrorIndirect.exception + end + + it "returns an Encoding object" do + @exception.source_encoding.should be_an_instance_of(Encoding) + @exception2.source_encoding.should be_an_instance_of(Encoding) + end + + it "is equal to the source encoding of the object that raised it" do + @exception.source_encoding.should == Encoding::UTF_8 + end + + # The source encoding specified in the Encoding::Converter constructor may + # differ from the source encoding returned here. What seems to happen is + # that when transcoding along a path with multiple pairs of encodings, the + # last one encountered when the error occurred is returned. So in this + # case, the conversion path is EUC-JP -> UTF-8 -> ISO-8859-1. The + # conversions failed with the first pair of encodings (i.e. transcoding + # from EUC-JP to UTF-8, so UTF-8 is regarded as the source encoding; if + # the error had occurred when converting from UTF-8 to ISO-8859-1, UTF-8 + # would have been the source encoding. + + # FIXME: Derive example where the failure occurs at the UTF-8 -> + # ISO-8859-1 case so as to better illustrate the issue + it "is equal to the source encoding at the stage of the conversion path where the error occurred" do + @exception2.source_encoding.should == Encoding::EUC_JP + end +end diff --git a/spec/ruby/core/encoding/list_spec.rb b/spec/ruby/core/encoding/list_spec.rb new file mode 100644 index 0000000000..bd3d5b7bc0 --- /dev/null +++ b/spec/ruby/core/encoding/list_spec.rb @@ -0,0 +1,49 @@ +require_relative '../../spec_helper' + +describe "Encoding.list" do + it "returns an Array" do + Encoding.list.should be_an_instance_of(Array) + end + + it "returns an Array of Encoding objects" do + Encoding.list.each do |enc| + enc.should be_an_instance_of(Encoding) + end + end + + it "returns each encoding only once" do + orig = Encoding.list.map { |e| e.name } + orig.should == orig.uniq + end + + it "includes the default external encoding" do + Encoding.list.include?(Encoding.default_external).should be_true + end + + it "does not include any alias names" do + Encoding.aliases.keys.each do |enc_alias| + Encoding.list.include?(enc_alias).should be_false + end + end + + it "includes all aliased encodings" do + Encoding.aliases.values.each do |enc_alias| + Encoding.list.include?(Encoding.find(enc_alias)).should be_true + end + end + + it "includes dummy encodings" do + Encoding.list.select { |e| e.dummy? }.should_not == [] + end + + it 'includes UTF-8 encoding' do + Encoding.list.should.include?(Encoding::UTF_8) + end + + it 'includes CESU-8 encoding' do + Encoding.list.should.include?(Encoding::CESU_8) + end + + # TODO: Find example that illustrates this + it "updates the list when #find is used to load a new encoding" +end diff --git a/spec/ruby/core/encoding/locale_charmap_spec.rb b/spec/ruby/core/encoding/locale_charmap_spec.rb new file mode 100644 index 0000000000..8143b9083a --- /dev/null +++ b/spec/ruby/core/encoding/locale_charmap_spec.rb @@ -0,0 +1,56 @@ +require_relative '../../spec_helper' + +describe "Encoding.locale_charmap" do + it "returns a String" do + Encoding.locale_charmap.should be_an_instance_of(String) + end + + # FIXME: Get this working on Windows + platform_is :linux do + platform_is_not :android do + it "returns a value based on the LC_ALL environment variable" do + old_lc_all = ENV['LC_ALL'] + ENV['LC_ALL'] = 'C' + ruby_exe("print Encoding.locale_charmap").should == 'ANSI_X3.4-1968' + ENV['LC_ALL'] = old_lc_all + end + end + end + + platform_is :freebsd, :openbsd, :darwin do + it "returns a value based on the LC_ALL environment variable" do + old_lc_all = ENV['LC_ALL'] + ENV['LC_ALL'] = 'C' + ruby_exe("print Encoding.locale_charmap").should == 'US-ASCII' + ENV['LC_ALL'] = old_lc_all + end + end + + platform_is :netbsd do + it "returns a value based on the LC_ALL environment variable" do + old_lc_all = ENV['LC_ALL'] + ENV['LC_ALL'] = 'C' + ruby_exe("print Encoding.locale_charmap").should == '646' + ENV['LC_ALL'] = old_lc_all + end + end + + platform_is :android do + it "always returns UTF-8" do + old_lc_all = ENV['LC_ALL'] + ENV['LC_ALL'] = 'C' + ruby_exe("print Encoding.locale_charmap").should == 'UTF-8' + ENV['LC_ALL'] = old_lc_all + end + end + + platform_is :bsd, :darwin, :linux do + it "is unaffected by assigning to ENV['LC_ALL'] in the same process" do + old_charmap = Encoding.locale_charmap + old_lc_all = ENV['LC_ALL'] + ENV['LC_ALL'] = 'C' + Encoding.locale_charmap.should == old_charmap + ENV['LC_ALL'] = old_lc_all + end + end +end diff --git a/spec/ruby/core/encoding/name_list_spec.rb b/spec/ruby/core/encoding/name_list_spec.rb new file mode 100644 index 0000000000..836381c4d8 --- /dev/null +++ b/spec/ruby/core/encoding/name_list_spec.rb @@ -0,0 +1,23 @@ +require_relative '../../spec_helper' + +describe "Encoding.name_list" do + it "returns an Array" do + Encoding.name_list.should be_an_instance_of(Array) + end + + it "returns encoding names as Strings" do + Encoding.name_list.each {|e| e.should be_an_instance_of(String) } + end + + it "includes all aliases" do + Encoding.aliases.keys.each do |enc_alias| + Encoding.name_list.include?(enc_alias).should be_true + end + end + + it "includes all non-dummy encodings" do + Encoding.list.each do |enc| + Encoding.name_list.include?(enc.name).should be_true + end + end +end diff --git a/spec/ruby/core/encoding/name_spec.rb b/spec/ruby/core/encoding/name_spec.rb new file mode 100644 index 0000000000..dce9347978 --- /dev/null +++ b/spec/ruby/core/encoding/name_spec.rb @@ -0,0 +1,6 @@ +require_relative "../../spec_helper" +require_relative 'shared/name' + +describe "Encoding#name" do + it_behaves_like :encoding_name, :name +end diff --git a/spec/ruby/core/encoding/names_spec.rb b/spec/ruby/core/encoding/names_spec.rb new file mode 100644 index 0000000000..9ded043bbb --- /dev/null +++ b/spec/ruby/core/encoding/names_spec.rb @@ -0,0 +1,35 @@ +require_relative '../../spec_helper' + +describe "Encoding#names" do + it "returns an Array" do + Encoding.name_list.each do |name| + e = Encoding.find(name) or next + e.names.should be_an_instance_of(Array) + end + end + + it "returns names as Strings" do + Encoding.name_list.each do |name| + e = Encoding.find(name) or next + e.names.each do |this_name| + this_name.should be_an_instance_of(String) + end + end + end + + it "returns #name as the first value" do + Encoding.name_list.each do |name| + e = Encoding.find(name) or next + e.names.first.should == e.name + end + end + + it "includes any aliases the encoding has" do + Encoding.name_list.each do |name| + e = Encoding.find(name) or next + aliases = Encoding.aliases.select{|a,n| n == name}.keys + names = e.names + aliases.each {|a| names.include?(a).should be_true} + end + end +end diff --git a/spec/ruby/core/encoding/replicate_spec.rb b/spec/ruby/core/encoding/replicate_spec.rb new file mode 100644 index 0000000000..2da998837f --- /dev/null +++ b/spec/ruby/core/encoding/replicate_spec.rb @@ -0,0 +1,88 @@ +# encoding: binary +require_relative '../../spec_helper' + +describe "Encoding#replicate" do + ruby_version_is ""..."3.3" do + before :all do + @i = 0 + end + + before :each do + @i += 1 + @prefix = "RS#{@i}" + end + + it "returns a replica of ASCII" do + name = @prefix + '-ASCII' + e = suppress_warning { Encoding::ASCII.replicate(name) } + e.name.should == name + Encoding.find(name).should == e + + "a".dup.force_encoding(e).valid_encoding?.should be_true + "\x80".dup.force_encoding(e).valid_encoding?.should be_false + end + + it "returns a replica of UTF-8" do + name = @prefix + 'UTF-8' + e = suppress_warning { Encoding::UTF_8.replicate(name) } + e.name.should == name + Encoding.find(name).should == e + + "a".dup.force_encoding(e).valid_encoding?.should be_true + "\u3042".dup.force_encoding(e).valid_encoding?.should be_true + "\x80".dup.force_encoding(e).valid_encoding?.should be_false + end + + it "returns a replica of UTF-16BE" do + name = @prefix + 'UTF-16-BE' + e = suppress_warning { Encoding::UTF_16BE.replicate(name) } + e.name.should == name + Encoding.find(name).should == e + + "a".dup.force_encoding(e).valid_encoding?.should be_false + "\x30\x42".dup.force_encoding(e).valid_encoding?.should be_true + "\x80".dup.force_encoding(e).valid_encoding?.should be_false + end + + it "returns a replica of ISO-2022-JP" do + name = @prefix + 'ISO-2022-JP' + e = suppress_warning { Encoding::ISO_2022_JP.replicate(name) } + Encoding.find(name).should == e + + e.name.should == name + e.dummy?.should be_true + end + + # NOTE: it's unclear of the value of this (for the complexity cost of it), + # but it is the current CRuby behavior. + it "can be associated with a String" do + name = @prefix + '-US-ASCII' + e = suppress_warning { Encoding::US_ASCII.replicate(name) } + e.name.should == name + Encoding.find(name).should == e + + s = "abc".dup.force_encoding(e) + s.encoding.should == e + s.encoding.name.should == name + end + end + + ruby_version_is ""..."3.3" do + it "warns about deprecation" do + -> { + Encoding::US_ASCII.replicate('MY-US-ASCII') + }.should complain(/warning: Encoding#replicate is deprecated and will be removed in Ruby 3.3; use the original encoding instead/) + end + + it "raises EncodingError if too many encodings" do + code = '1_000.times {|i| Encoding::US_ASCII.replicate("R_#{i}") }' + ruby_exe(code, args: "2>&1", exit_status: 1).should.include?('too many encoding (> 256) (EncodingError)') + end + end + + ruby_version_is "3.3" do + it "has been removed" do + Encoding::US_ASCII.should_not.respond_to?(:replicate, true) + end + end +end diff --git a/spec/ruby/core/encoding/shared/name.rb b/spec/ruby/core/encoding/shared/name.rb new file mode 100644 index 0000000000..cd37ea06db --- /dev/null +++ b/spec/ruby/core/encoding/shared/name.rb @@ -0,0 +1,15 @@ +require_relative '../../../spec_helper' + +describe :encoding_name, shared: true do + it "returns a String" do + Encoding.list.each do |e| + e.send(@method).should be_an_instance_of(String) + end + end + + it "uniquely identifies an encoding" do + Encoding.list.each do |e| + e.should == Encoding.find(e.send(@method)) + end + end +end diff --git a/spec/ruby/core/encoding/to_s_spec.rb b/spec/ruby/core/encoding/to_s_spec.rb new file mode 100644 index 0000000000..bab394a888 --- /dev/null +++ b/spec/ruby/core/encoding/to_s_spec.rb @@ -0,0 +1,6 @@ +require_relative "../../spec_helper" +require_relative 'shared/name' + +describe "Encoding#to_s" do + it_behaves_like :encoding_name, :to_s +end diff --git a/spec/ruby/core/encoding/undefined_conversion_error/destination_encoding_name_spec.rb b/spec/ruby/core/encoding/undefined_conversion_error/destination_encoding_name_spec.rb new file mode 100644 index 0000000000..a51a9f46a0 --- /dev/null +++ b/spec/ruby/core/encoding/undefined_conversion_error/destination_encoding_name_spec.rb @@ -0,0 +1,16 @@ +require_relative "../../../spec_helper" +require_relative '../fixtures/classes' + +describe "Encoding::UndefinedConversionError#destination_encoding_name" do + before :each do + @exception = EncodingSpecs::UndefinedConversionError.exception + end + + it "returns a String" do + @exception.destination_encoding_name.should be_an_instance_of(String) + end + + it "is equal to the destination encoding name of the object that raised it" do + @exception.destination_encoding_name.should == "US-ASCII" + end +end diff --git a/spec/ruby/core/encoding/undefined_conversion_error/destination_encoding_spec.rb b/spec/ruby/core/encoding/undefined_conversion_error/destination_encoding_spec.rb new file mode 100644 index 0000000000..905556407c --- /dev/null +++ b/spec/ruby/core/encoding/undefined_conversion_error/destination_encoding_spec.rb @@ -0,0 +1,16 @@ +require_relative "../../../spec_helper" +require_relative '../fixtures/classes' + +describe "Encoding::UndefinedConversionError#destination_encoding" do + before :each do + @exception = EncodingSpecs::UndefinedConversionError.exception + end + + it "returns an Encoding object" do + @exception.destination_encoding.should be_an_instance_of(Encoding) + end + + it "is equal to the destination encoding of the object that raised it" do + @exception.destination_encoding.should == Encoding::US_ASCII + end +end diff --git a/spec/ruby/core/encoding/undefined_conversion_error/error_char_spec.rb b/spec/ruby/core/encoding/undefined_conversion_error/error_char_spec.rb new file mode 100644 index 0000000000..9cb55e6d95 --- /dev/null +++ b/spec/ruby/core/encoding/undefined_conversion_error/error_char_spec.rb @@ -0,0 +1,28 @@ +require_relative "../../../spec_helper" +require_relative '../fixtures/classes' + +describe "Encoding::UndefinedConversionError#error_char" do + before :each do + @exception = EncodingSpecs::UndefinedConversionError.exception + @exception2 = EncodingSpecs::UndefinedConversionErrorIndirect.exception + end + + it "returns a String" do + @exception.error_char.should be_an_instance_of(String) + @exception2.error_char.should be_an_instance_of(String) + end + + it "returns the one-character String that caused the exception" do + @exception.error_char.size.should == 1 + @exception.error_char.should == "\u{8765}" + + @exception2.error_char.size.should == 1 + @exception2.error_char.should == "\u{A0}" + end + + it "uses the source encoding" do + @exception.error_char.encoding.should == @exception.source_encoding + + @exception2.error_char.encoding.should == @exception2.source_encoding + end +end diff --git a/spec/ruby/core/encoding/undefined_conversion_error/source_encoding_name_spec.rb b/spec/ruby/core/encoding/undefined_conversion_error/source_encoding_name_spec.rb new file mode 100644 index 0000000000..d5e60e78db --- /dev/null +++ b/spec/ruby/core/encoding/undefined_conversion_error/source_encoding_name_spec.rb @@ -0,0 +1,29 @@ +require_relative "../../../spec_helper" +require_relative '../fixtures/classes' + +describe "Encoding::UndefinedConversionError#source_encoding_name" do + before :each do + @exception = EncodingSpecs::UndefinedConversionError.exception + @exception2 = EncodingSpecs::UndefinedConversionErrorIndirect.exception + end + + it "returns a String" do + @exception.source_encoding_name.should be_an_instance_of(String) + end + + it "is equal to the source encoding name of the object that raised it" do + @exception.source_encoding_name.should == "UTF-8" + end + + # The source encoding specified in the Encoding::Converter constructor may + # differ from the source encoding returned here. What seems to happen is + # that when transcoding along a path with multiple pairs of encodings, the + # last one encountered when the error occurred is returned. So in this + # case, the conversion path is ISO-8859-1 -> UTF-8 -> EUC-JP. The + # conversion from ISO-8859-1 -> UTF-8 succeeded, but the conversion from + # UTF-8 to EUC-JP failed. IOW, it failed when the source encoding was + # UTF-8, so UTF-8 is regarded as the source encoding. + it "is equal to the source encoding at the stage of the conversion path where the error occurred" do + @exception2.source_encoding_name.should == 'UTF-8' + end +end diff --git a/spec/ruby/core/encoding/undefined_conversion_error/source_encoding_spec.rb b/spec/ruby/core/encoding/undefined_conversion_error/source_encoding_spec.rb new file mode 100644 index 0000000000..de456a4b5a --- /dev/null +++ b/spec/ruby/core/encoding/undefined_conversion_error/source_encoding_spec.rb @@ -0,0 +1,30 @@ +require_relative "../../../spec_helper" +require_relative '../fixtures/classes' + +describe "Encoding::UndefinedConversionError#source_encoding" do + before :each do + @exception = EncodingSpecs::UndefinedConversionError.exception + @exception2 = EncodingSpecs::UndefinedConversionErrorIndirect.exception + end + + it "returns an Encoding object" do + @exception.source_encoding.should be_an_instance_of(Encoding) + @exception2.source_encoding.should be_an_instance_of(Encoding) + end + + it "is equal to the source encoding of the object that raised it" do + @exception.source_encoding.should == Encoding::UTF_8 + end + + # The source encoding specified in the Encoding::Converter constructor may + # differ from the source encoding returned here. What seems to happen is + # that when transcoding along a path with multiple pairs of encodings, the + # last one encountered when the error occurred is returned. So in this + # case, the conversion path is ISO-8859-1 -> UTF-8 -> EUC-JP. The + # conversion from ISO-8859-1 -> UTF-8 succeeded, but the conversion from + # UTF-8 to EUC-JP failed. IOW, it failed when the source encoding was + # UTF-8, so UTF-8 is regarded as the source encoding. + it "is equal to the source encoding at the stage of the conversion path where the error occurred" do + @exception2.source_encoding.should == Encoding::UTF_8 + end +end diff --git a/spec/ruby/core/enumerable/all_spec.rb b/spec/ruby/core/enumerable/all_spec.rb new file mode 100644 index 0000000000..160cd52628 --- /dev/null +++ b/spec/ruby/core/enumerable/all_spec.rb @@ -0,0 +1,187 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Enumerable#all?" do + before :each do + @enum = EnumerableSpecs::Numerous.new + @empty = EnumerableSpecs::Empty.new() + @enum1 = EnumerableSpecs::Numerous.new(0, 1, 2, -1) + @enum2 = EnumerableSpecs::Numerous.new(nil, false, true) + end + + it "always returns true on empty enumeration" do + @empty.should.all? + @empty.all? { nil }.should == true + + [].should.all? + [].all? { false }.should == true + + {}.should.all? + {}.all? { nil }.should == true + end + + it "raises an ArgumentError when more than 1 argument is provided" do + -> { @enum.all?(1, 2, 3) }.should raise_error(ArgumentError) + -> { [].all?(1, 2, 3) }.should raise_error(ArgumentError) + -> { {}.all?(1, 2, 3) }.should raise_error(ArgumentError) + end + + it "does not hide exceptions out of #each" do + -> { + EnumerableSpecs::ThrowingEach.new.all? + }.should raise_error(RuntimeError) + + -> { + EnumerableSpecs::ThrowingEach.new.all? { false } + }.should raise_error(RuntimeError) + end + + describe "with no block" do + it "returns true if no elements are false or nil" do + @enum.should.all? + @enum1.should.all? + @enum2.should_not.all? + + EnumerableSpecs::Numerous.new('a','b','c').should.all? + EnumerableSpecs::Numerous.new(0, "x", true).should.all? + end + + it "returns false if there are false or nil elements" do + EnumerableSpecs::Numerous.new(false).should_not.all? + EnumerableSpecs::Numerous.new(false, false).should_not.all? + + EnumerableSpecs::Numerous.new(nil).should_not.all? + EnumerableSpecs::Numerous.new(nil, nil).should_not.all? + + EnumerableSpecs::Numerous.new(1, nil, 2).should_not.all? + EnumerableSpecs::Numerous.new(0, "x", false, true).should_not.all? + @enum2.should_not.all? + end + + it "gathers whole arrays as elements when each yields multiple" do + multi = EnumerableSpecs::YieldsMultiWithFalse.new + multi.all?.should be_true + end + end + + describe "with block" do + it "returns true if the block never returns false or nil" do + @enum.all? { true }.should == true + @enum1.all?{ |o| o < 5 }.should == true + @enum1.all?{ |o| 5 }.should == true + end + + it "returns false if the block ever returns false or nil" do + @enum.all? { false }.should == false + @enum.all? { nil }.should == false + @enum1.all?{ |o| o > 2 }.should == false + + EnumerableSpecs::Numerous.new.all? { |i| i > 5 }.should == false + EnumerableSpecs::Numerous.new.all? { |i| i == 3 ? nil : true }.should == false + end + + it "stops iterating once the return value is determined" do + yielded = [] + EnumerableSpecs::Numerous.new(:one, :two, :three).all? do |e| + yielded << e + false + end.should == false + yielded.should == [:one] + + yielded = [] + EnumerableSpecs::Numerous.new(true, true, false, true).all? do |e| + yielded << e + e + end.should == false + yielded.should == [true, true, false] + + yielded = [] + EnumerableSpecs::Numerous.new(1, 2, 3, 4, 5).all? do |e| + yielded << e + e + end.should == true + yielded.should == [1, 2, 3, 4, 5] + end + + it "does not hide exceptions out of the block" do + -> { + @enum.all? { raise "from block" } + }.should raise_error(RuntimeError) + end + + it "gathers initial args as elements when each yields multiple" do + multi = EnumerableSpecs::YieldsMulti.new + yielded = [] + multi.all? { |e| yielded << e }.should == true + yielded.should == [1, 3, 6] + end + + it "yields multiple arguments when each yields multiple" do + multi = EnumerableSpecs::YieldsMulti.new + yielded = [] + multi.all? { |*args| yielded << args }.should == true + yielded.should == [[1, 2], [3, 4, 5], [6, 7, 8, 9]] + end + end + + describe 'when given a pattern argument' do + it "calls `===` on the pattern the return value " do + pattern = EnumerableSpecs::Pattern.new { |x| x >= 0 } + @enum1.all?(pattern).should == false + pattern.yielded.should == [[0], [1], [2], [-1]] + end + + it "always returns true on empty enumeration" do + @empty.all?(Integer).should == true + [].all?(Integer).should == true + {}.all?(NilClass).should == true + end + + it "does not hide exceptions out of #each" do + -> { + EnumerableSpecs::ThrowingEach.new.all?(Integer) + }.should raise_error(RuntimeError) + end + + it "returns true if the pattern never returns false or nil" do + pattern = EnumerableSpecs::Pattern.new { |x| 42 } + @enum.all?(pattern).should == true + + [1, 42, 3].all?(pattern).should == true + + pattern = EnumerableSpecs::Pattern.new { |x| Array === x } + {a: 1, b: 2}.all?(pattern).should == true + end + + it "returns false if the pattern ever returns false or nil" do + pattern = EnumerableSpecs::Pattern.new { |x| x >= 0 } + @enum1.all?(pattern).should == false + pattern.yielded.should == [[0], [1], [2], [-1]] + + [1, 2, 3, -1].all?(pattern).should == false + + pattern = EnumerableSpecs::Pattern.new { |x| x[1] >= 0 } + {a: 1, b: -1}.all?(pattern).should == false + end + + it "does not hide exceptions out of pattern#===" do + pattern = EnumerableSpecs::Pattern.new { raise "from pattern" } + -> { + @enum.all?(pattern) + }.should raise_error(RuntimeError) + end + + it "calls the pattern with gathered array when yielded with multiple arguments" do + multi = EnumerableSpecs::YieldsMulti.new + pattern = EnumerableSpecs::Pattern.new { true } + multi.all?(pattern).should == true + pattern.yielded.should == [[[1, 2]], [[3, 4, 5]], [[6, 7, 8, 9]]] + end + + it "ignores the block if there is an argument" do + -> { + EnumerableSpecs::Numerous.new(1, 2, 3, 4, 5).all?(String) { true }.should == false + }.should complain(/given block not used/) + end + end +end diff --git a/spec/ruby/core/enumerable/any_spec.rb b/spec/ruby/core/enumerable/any_spec.rb new file mode 100644 index 0000000000..243f8735d5 --- /dev/null +++ b/spec/ruby/core/enumerable/any_spec.rb @@ -0,0 +1,200 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Enumerable#any?" do + before :each do + @enum = EnumerableSpecs::Numerous.new + @empty = EnumerableSpecs::Empty.new + @enum1 = EnumerableSpecs::Numerous.new(0, 1, 2, -1) + @enum2 = EnumerableSpecs::Numerous.new(nil, false, true) + end + + it "always returns false on empty enumeration" do + @empty.should_not.any? + @empty.any? { nil }.should == false + + [].should_not.any? + [].any? { false }.should == false + + {}.should_not.any? + {}.any? { nil }.should == false + end + + it "raises an ArgumentError when more than 1 argument is provided" do + -> { @enum.any?(1, 2, 3) }.should raise_error(ArgumentError) + -> { [].any?(1, 2, 3) }.should raise_error(ArgumentError) + -> { {}.any?(1, 2, 3) }.should raise_error(ArgumentError) + end + + it "does not hide exceptions out of #each" do + -> { + EnumerableSpecs::ThrowingEach.new.any? + }.should raise_error(RuntimeError) + + -> { + EnumerableSpecs::ThrowingEach.new.any? { false } + }.should raise_error(RuntimeError) + end + + describe "with no block" do + it "returns true if any element is not false or nil" do + @enum.should.any? + @enum1.should.any? + @enum2.should.any? + EnumerableSpecs::Numerous.new(true).should.any? + EnumerableSpecs::Numerous.new('a','b','c').should.any? + EnumerableSpecs::Numerous.new('a','b','c', nil).should.any? + EnumerableSpecs::Numerous.new(1, nil, 2).should.any? + EnumerableSpecs::Numerous.new(1, false).should.any? + EnumerableSpecs::Numerous.new(false, nil, 1, false).should.any? + EnumerableSpecs::Numerous.new(false, 0, nil).should.any? + end + + it "returns false if all elements are false or nil" do + EnumerableSpecs::Numerous.new(false).should_not.any? + EnumerableSpecs::Numerous.new(false, false).should_not.any? + EnumerableSpecs::Numerous.new(nil).should_not.any? + EnumerableSpecs::Numerous.new(nil, nil).should_not.any? + EnumerableSpecs::Numerous.new(nil, false, nil).should_not.any? + end + + it "gathers whole arrays as elements when each yields multiple" do + multi = EnumerableSpecs::YieldsMultiWithFalse.new + multi.any?.should be_true + end + end + + describe "with block" do + it "returns true if the block ever returns other than false or nil" do + @enum.any? { true }.should == true + @enum.any? { 0 }.should == true + @enum.any? { 1 }.should == true + + @enum1.any? { Object.new }.should == true + @enum1.any?{ |o| o < 1 }.should == true + @enum1.any?{ |o| 5 }.should == true + + @enum2.any? { |i| i == nil }.should == true + end + + it "returns false if the block never returns other than false or nil" do + @enum.any? { false }.should == false + @enum.any? { nil }.should == false + + @enum1.any?{ |o| o < -10 }.should == false + @enum1.any?{ |o| nil }.should == false + + @enum2.any? { |i| i == :stuff }.should == false + end + + it "stops iterating once the return value is determined" do + yielded = [] + EnumerableSpecs::Numerous.new(:one, :two, :three).any? do |e| + yielded << e + false + end.should == false + yielded.should == [:one, :two, :three] + + yielded = [] + EnumerableSpecs::Numerous.new(true, true, false, true).any? do |e| + yielded << e + e + end.should == true + yielded.should == [true] + + yielded = [] + EnumerableSpecs::Numerous.new(false, nil, false, true, false).any? do |e| + yielded << e + e + end.should == true + yielded.should == [false, nil, false, true] + + yielded = [] + EnumerableSpecs::Numerous.new(1, 2, 3, 4, 5).any? do |e| + yielded << e + e + end.should == true + yielded.should == [1] + end + + it "does not hide exceptions out of the block" do + -> { + @enum.any? { raise "from block" } + }.should raise_error(RuntimeError) + end + + it "gathers initial args as elements when each yields multiple" do + multi = EnumerableSpecs::YieldsMulti.new + yielded = [] + multi.any? { |e| yielded << e; false }.should == false + yielded.should == [1, 3, 6] + end + + it "yields multiple arguments when each yields multiple" do + multi = EnumerableSpecs::YieldsMulti.new + yielded = [] + multi.any? { |*args| yielded << args; false }.should == false + yielded.should == [[1, 2], [3, 4, 5], [6, 7, 8, 9]] + end + end + + describe 'when given a pattern argument' do + it "calls `===` on the pattern the return value " do + pattern = EnumerableSpecs::Pattern.new { |x| x == 2 } + @enum1.any?(pattern).should == true + pattern.yielded.should == [[0], [1], [2]] + end + + it "always returns false on empty enumeration" do + @empty.any?(Integer).should == false + [].any?(Integer).should == false + {}.any?(NilClass).should == false + end + + it "does not hide exceptions out of #each" do + -> { + EnumerableSpecs::ThrowingEach.new.any?(Integer) + }.should raise_error(RuntimeError) + end + + it "returns true if the pattern ever returns a truthy value" do + @enum2.any?(NilClass).should == true + pattern = EnumerableSpecs::Pattern.new { |x| 42 } + @enum.any?(pattern).should == true + + [1, 42, 3].any?(pattern).should == true + + pattern = EnumerableSpecs::Pattern.new { |x| x == [:b, 2] } + {a: 1, b: 2}.any?(pattern).should == true + end + + it "returns false if the block never returns other than false or nil" do + pattern = EnumerableSpecs::Pattern.new { |x| nil } + @enum1.any?(pattern).should == false + pattern.yielded.should == [[0], [1], [2], [-1]] + + [1, 2, 3].any?(pattern).should == false + {a: 1}.any?(pattern).should == false + end + + it "does not hide exceptions out of pattern#===" do + pattern = EnumerableSpecs::Pattern.new { raise "from pattern" } + -> { + @enum.any?(pattern) + }.should raise_error(RuntimeError) + end + + it "calls the pattern with gathered array when yielded with multiple arguments" do + multi = EnumerableSpecs::YieldsMulti.new + pattern = EnumerableSpecs::Pattern.new { false } + multi.any?(pattern).should == false + pattern.yielded.should == [[[1, 2]], [[3, 4, 5]], [[6, 7, 8, 9]]] + end + + it "ignores the block if there is an argument" do + -> { + EnumerableSpecs::Numerous.new(1, 2, 3, 4, 5).any?(String) { true }.should == false + }.should complain(/given block not used/) + end + end +end diff --git a/spec/ruby/core/enumerable/chain_spec.rb b/spec/ruby/core/enumerable/chain_spec.rb new file mode 100644 index 0000000000..5e2105d294 --- /dev/null +++ b/spec/ruby/core/enumerable/chain_spec.rb @@ -0,0 +1,23 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Enumerable#chain" do + before :each do + ScratchPad.record [] + end + + it "returns a chain of self and provided enumerables" do + one = EnumerableSpecs::Numerous.new(1) + two = EnumerableSpecs::Numerous.new(2, 3) + three = EnumerableSpecs::Numerous.new(4, 5, 6) + + chain = one.chain(two, three) + + chain.each { |item| ScratchPad << item } + ScratchPad.recorded.should == [1, 2, 3, 4, 5, 6] + end + + it "returns an Enumerator::Chain if given a block" do + EnumerableSpecs::Numerous.new.chain.should be_an_instance_of(Enumerator::Chain) + end +end diff --git a/spec/ruby/core/enumerable/chunk_spec.rb b/spec/ruby/core/enumerable/chunk_spec.rb new file mode 100644 index 0000000000..ed6304307f --- /dev/null +++ b/spec/ruby/core/enumerable/chunk_spec.rb @@ -0,0 +1,77 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Enumerable#chunk" do + before do + ScratchPad.record [] + end + + it "returns an Enumerator if called without a block" do + chunk = EnumerableSpecs::Numerous.new(1, 2, 3, 1, 2).chunk + chunk.should be_an_instance_of(Enumerator) + result = chunk.with_index {|elt, i| elt - i }.to_a + result.should == [[1, [1, 2, 3]], [-2, [1, 2]]] + end + + it "returns an Enumerator if given a block" do + EnumerableSpecs::Numerous.new.chunk {}.should be_an_instance_of(Enumerator) + end + + it "yields the current element and the current chunk to the block" do + e = EnumerableSpecs::Numerous.new(1, 2, 3) + e.chunk { |x| ScratchPad << x }.to_a + ScratchPad.recorded.should == [1, 2, 3] + end + + it "returns elements of the Enumerable in an Array of Arrays, [v, ary], where 'ary' contains the consecutive elements for which the block returned the value 'v'" do + e = EnumerableSpecs::Numerous.new(1, 2, 3, 2, 3, 2, 1) + result = e.chunk { |x| x < 3 && 1 || 0 }.to_a + result.should == [[1, [1, 2]], [0, [3]], [1, [2]], [0, [3]], [1, [2, 1]]] + end + + it "returns a partitioned Array of values" do + e = EnumerableSpecs::Numerous.new(1,2,3) + e.chunk { |x| x > 2 }.map(&:last).should == [[1, 2], [3]] + end + + it "returns elements for which the block returns :_alone in separate Arrays" do + e = EnumerableSpecs::Numerous.new(1, 2, 3, 2, 1) + result = e.chunk { |x| x < 2 && :_alone }.to_a + result.should == [[:_alone, [1]], [false, [2, 3, 2]], [:_alone, [1]]] + end + + it "yields Arrays as a single argument to a rest argument" do + e = EnumerableSpecs::Numerous.new([1, 2]) + result = e.chunk { |*x| x.should == [[1,2]] }.to_a + end + + it "does not return elements for which the block returns :_separator" do + e = EnumerableSpecs::Numerous.new(1, 2, 3, 3, 2, 1) + result = e.chunk { |x| x == 2 ? :_separator : 1 }.to_a + result.should == [[1, [1]], [1, [3, 3]], [1, [1]]] + end + + it "does not return elements for which the block returns nil" do + e = EnumerableSpecs::Numerous.new(1, 2, 3, 2, 1) + result = e.chunk { |x| x == 2 ? nil : 1 }.to_a + result.should == [[1, [1]], [1, [3]], [1, [1]]] + end + + it "raises a RuntimeError if the block returns a Symbol starting with an underscore other than :_alone or :_separator" do + e = EnumerableSpecs::Numerous.new(1, 2, 3, 2, 1) + -> { e.chunk { |x| :_arbitrary }.to_a }.should raise_error(RuntimeError) + end + + it "does not accept arguments" do + e = EnumerableSpecs::Numerous.new(1, 2, 3) + -> { + e.chunk(1) {} + }.should raise_error(ArgumentError) + end + + it 'returned Enumerator size returns nil' do + e = EnumerableSpecs::NumerousWithSize.new(1, 2, 3, 2, 1) + enum = e.chunk { |x| true } + enum.size.should == nil + end +end diff --git a/spec/ruby/core/enumerable/chunk_while_spec.rb b/spec/ruby/core/enumerable/chunk_while_spec.rb new file mode 100644 index 0000000000..26bcc983db --- /dev/null +++ b/spec/ruby/core/enumerable/chunk_while_spec.rb @@ -0,0 +1,42 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Enumerable#chunk_while" do + before :each do + ary = [10, 9, 7, 6, 4, 3, 2, 1] + @enum = EnumerableSpecs::Numerous.new(*ary) + @result = @enum.chunk_while { |i, j| i - 1 == j } + @enum_length = ary.length + end + + context "when given a block" do + it "returns an enumerator" do + @result.should be_an_instance_of(Enumerator) + end + + it "splits chunks between adjacent elements i and j where the block returns false" do + @result.to_a.should == [[10, 9], [7, 6], [4, 3, 2, 1]] + end + + it "calls the block for length of the receiver enumerable minus one times" do + times_called = 0 + @enum.chunk_while do |i, j| + times_called += 1 + i - 1 == j + end.to_a + times_called.should == (@enum_length - 1) + end + end + + context "when not given a block" do + it "raises an ArgumentError" do + -> { @enum.chunk_while }.should raise_error(ArgumentError) + end + end + + context "on a single-element array" do + it "ignores the block and returns an enumerator that yields [element]" do + [1].chunk_while {|x| x.even?}.to_a.should == [[1]] + end + end +end diff --git a/spec/ruby/core/enumerable/collect_concat_spec.rb b/spec/ruby/core/enumerable/collect_concat_spec.rb new file mode 100644 index 0000000000..59317cfe34 --- /dev/null +++ b/spec/ruby/core/enumerable/collect_concat_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/collect_concat' + +describe "Enumerable#collect_concat" do + it_behaves_like :enumerable_collect_concat, :collect_concat +end diff --git a/spec/ruby/core/enumerable/collect_spec.rb b/spec/ruby/core/enumerable/collect_spec.rb new file mode 100644 index 0000000000..cfa2895cce --- /dev/null +++ b/spec/ruby/core/enumerable/collect_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/collect' + +describe "Enumerable#collect" do + it_behaves_like :enumerable_collect, :collect +end diff --git a/spec/ruby/core/enumerable/compact_spec.rb b/spec/ruby/core/enumerable/compact_spec.rb new file mode 100644 index 0000000000..1895430c4e --- /dev/null +++ b/spec/ruby/core/enumerable/compact_spec.rb @@ -0,0 +1,9 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Enumerable#compact" do + it 'returns array without nil elements' do + arr = EnumerableSpecs::Numerous.new(nil, 1, 2, nil, true) + arr.compact.should == [1, 2, true] + end +end diff --git a/spec/ruby/core/enumerable/count_spec.rb b/spec/ruby/core/enumerable/count_spec.rb new file mode 100644 index 0000000000..50a1c8e1a4 --- /dev/null +++ b/spec/ruby/core/enumerable/count_spec.rb @@ -0,0 +1,59 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Enumerable#count" do + before :each do + @elements = [1, 2, 4, 2] + @numerous = EnumerableSpecs::Numerous.new(*@elements) + end + + describe "when no argument or a block" do + it "returns size" do + @numerous.count.should == 4 + end + + describe "with a custom size method" do + before :each do + class << @numerous + def size + :any_object + end + end + end + + it "ignores the custom size method" do + @numerous.count.should == 4 + end + end + end + + it "counts nils if given nil as an argument" do + EnumerableSpecs::Numerous.new(nil, nil, nil, false).count(nil).should == 3 + end + + it "accepts an argument for comparison using ==" do + @numerous.count(2).should == 2 + end + + it "uses a block for comparison" do + @numerous.count{|x| x%2==0 }.should == 3 + end + + it "ignores the block when given an argument" do + -> { + @numerous.count(4){|x| x%2==0 }.should == 1 + }.should complain(/given block not used/) + end + + describe "when each yields multiple values" do + it "gathers initial args as elements" do + multi = EnumerableSpecs::YieldsMulti.new + multi.count {|e| e == 1 }.should == 1 + end + + it "accepts an argument for comparison using ==" do + multi = EnumerableSpecs::YieldsMulti.new + multi.count([1, 2]).should == 1 + end + end +end diff --git a/spec/ruby/core/enumerable/cycle_spec.rb b/spec/ruby/core/enumerable/cycle_spec.rb new file mode 100644 index 0000000000..487086cba3 --- /dev/null +++ b/spec/ruby/core/enumerable/cycle_spec.rb @@ -0,0 +1,104 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/enumeratorized' + +describe "Enumerable#cycle" do + describe "passed no argument or nil" do + it "loops indefinitely" do + [[],[nil]].each do |args| + bomb = 10 + EnumerableSpecs::Numerous.new.cycle(*args) do + bomb -= 1 + break 42 if bomb <= 0 + end.should == 42 + bomb.should == 0 + end + end + + it "returns nil if there are no elements" do + out = EnumerableSpecs::Empty.new.cycle { break :nope } + out.should be_nil + end + + it "yields successive elements of the array repeatedly" do + b = [] + EnumerableSpecs::Numerous.new(1,2,3).cycle do |elem| + b << elem + break if b.size == 7 + end + b.should == [1,2,3,1,2,3,1] + end + + it "calls each at most once" do + enum = EnumerableSpecs::EachCounter.new(1, 2) + enum.cycle.first(6).should == [1,2,1,2,1,2] + enum.times_called.should == 1 + end + + it "yields only when necessary" do + enum = EnumerableSpecs::EachCounter.new(10, 20, 30) + enum.cycle { |x| break if x == 20} + enum.times_yielded.should == 2 + end + end + + describe "passed a number n as an argument" do + it "returns nil and does nothing for non positive n" do + EnumerableSpecs::ThrowingEach.new.cycle(0) {}.should be_nil + EnumerableSpecs::NoEach.new.cycle(-22) {}.should be_nil + end + + it "calls each at most once" do + enum = EnumerableSpecs::EachCounter.new(1, 2) + enum.cycle(3).to_a.should == [1,2,1,2,1,2] + enum.times_called.should == 1 + end + + it "yields only when necessary" do + enum = EnumerableSpecs::EachCounter.new(10, 20, 30) + enum.cycle(3) { |x| break if x == 20} + enum.times_yielded.should == 2 + end + + it "tries to convert n to an Integer using #to_int" do + enum = EnumerableSpecs::Numerous.new(3, 2, 1) + enum.cycle(2.3).to_a.should == [3, 2, 1, 3, 2, 1] + + obj = mock('to_int') + obj.should_receive(:to_int).and_return(2) + enum.cycle(obj).to_a.should == [3, 2, 1, 3, 2, 1] + end + + it "raises a TypeError when the passed n cannot be coerced to Integer" do + enum = EnumerableSpecs::Numerous.new + ->{ enum.cycle("cat"){} }.should raise_error(TypeError) + end + + it "raises an ArgumentError if more arguments are passed" do + enum = EnumerableSpecs::Numerous.new + ->{ enum.cycle(1, 2) {} }.should raise_error(ArgumentError) + end + + it "gathers whole arrays as elements when each yields multiple" do + multi = EnumerableSpecs::YieldsMulti.new + multi.cycle(2).to_a.should == [[1, 2], [3, 4, 5], [6, 7, 8, 9], [1, 2], [3, 4, 5], [6, 7, 8, 9]] + end + end + + describe "Enumerable with size" do + before :all do + @object = EnumerableSpecs::NumerousWithSize.new(1, 2, 3, 4) + @empty_object = EnumerableSpecs::EmptyWithSize.new + end + it_should_behave_like :enumeratorized_with_cycle_size + end + + describe "Enumerable with no size" do + before :all do + @object = EnumerableSpecs::Numerous.new(1, 2, 3, 4) + @method = :cycle + end + it_should_behave_like :enumeratorized_with_unknown_size + end + +end diff --git a/spec/ruby/core/enumerable/detect_spec.rb b/spec/ruby/core/enumerable/detect_spec.rb new file mode 100644 index 0000000000..6959aadc44 --- /dev/null +++ b/spec/ruby/core/enumerable/detect_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/find' + +describe "Enumerable#detect" do + it_behaves_like :enumerable_find, :detect +end diff --git a/spec/ruby/core/enumerable/drop_spec.rb b/spec/ruby/core/enumerable/drop_spec.rb new file mode 100644 index 0000000000..423cc0088b --- /dev/null +++ b/spec/ruby/core/enumerable/drop_spec.rb @@ -0,0 +1,43 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Enumerable#drop" do + before :each do + @enum = EnumerableSpecs::Numerous.new(3, 2, 1, :go) + end + + it "requires exactly one argument" do + ->{ @enum.drop{} }.should raise_error(ArgumentError) + ->{ @enum.drop(1, 2){} }.should raise_error(ArgumentError) + end + + describe "passed a number n as an argument" do + it "raises ArgumentError if n < 0" do + ->{ @enum.drop(-1) }.should raise_error(ArgumentError) + end + + it "tries to convert n to an Integer using #to_int" do + @enum.drop(2.3).should == [1, :go] + + obj = mock('to_int') + obj.should_receive(:to_int).and_return(2) + @enum.drop(obj).should == [1, :go] + end + + it "returns [] for empty enumerables" do + EnumerableSpecs::Empty.new.drop(0).should == [] + EnumerableSpecs::Empty.new.drop(2).should == [] + end + + it "returns [] if dropping all" do + @enum.drop(5).should == [] + EnumerableSpecs::Numerous.new(3, 2, 1, :go).drop(4).should == [] + end + + it "raises a TypeError when the passed n cannot be coerced to Integer" do + ->{ @enum.drop("hat") }.should raise_error(TypeError) + ->{ @enum.drop(nil) }.should raise_error(TypeError) + end + + end +end diff --git a/spec/ruby/core/enumerable/drop_while_spec.rb b/spec/ruby/core/enumerable/drop_while_spec.rb new file mode 100644 index 0000000000..636c3d284a --- /dev/null +++ b/spec/ruby/core/enumerable/drop_while_spec.rb @@ -0,0 +1,50 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/enumerable_enumeratorized' + +describe "Enumerable#drop_while" do + before :each do + @enum = EnumerableSpecs::Numerous.new(3, 2, 1, :go) + end + + it "returns an Enumerator if no block given" do + @enum.drop_while.should be_an_instance_of(Enumerator) + end + + it "returns no/all elements for {true/false} block" do + @enum.drop_while{true}.should == [] + @enum.drop_while{false}.should == @enum.to_a + end + + it "accepts returns other than true/false" do + @enum.drop_while{1}.should == [] + @enum.drop_while{nil}.should == @enum.to_a + end + + it "passes elements to the block until the first false" do + a = [] + @enum.drop_while{|obj| (a << obj).size < 3}.should == [1, :go] + a.should == [3, 2, 1] + end + + it "will only go through what's needed" do + enum = EnumerableSpecs::EachCounter.new(1,2,3,4) + enum.drop_while { |x| + break 42 if x == 3 + true + }.should == 42 + enum.times_yielded.should == 3 + end + + it "doesn't return self when it could" do + a = [1,2,3] + a.drop_while{false}.should_not equal(a) + end + + it "gathers whole arrays as elements when each yields multiple" do + multi = EnumerableSpecs::YieldsMulti.new + multi.drop_while {|e| e != [6, 7, 8, 9] }.should == [[6, 7, 8, 9]] + end + + it_behaves_like :enumerable_enumeratorized_with_unknown_size, :drop_while +end diff --git a/spec/ruby/core/enumerable/each_cons_spec.rb b/spec/ruby/core/enumerable/each_cons_spec.rb new file mode 100644 index 0000000000..ed77741862 --- /dev/null +++ b/spec/ruby/core/enumerable/each_cons_spec.rb @@ -0,0 +1,103 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/enumeratorized' + +describe "Enumerable#each_cons" do + before :each do + @enum = EnumerableSpecs::Numerous.new(4,3,2,1) + @in_threes = [[4,3,2],[3,2,1]] + end + + it "passes element groups to the block" do + acc = [] + @enum.each_cons(3){|g| acc << g} + acc.should == @in_threes + end + + it "raises an ArgumentError if there is not a single parameter > 0" do + ->{ @enum.each_cons(0){} }.should raise_error(ArgumentError) + ->{ @enum.each_cons(-2){} }.should raise_error(ArgumentError) + ->{ @enum.each_cons{} }.should raise_error(ArgumentError) + ->{ @enum.each_cons(2,2){} }.should raise_error(ArgumentError) + ->{ @enum.each_cons(0) }.should raise_error(ArgumentError) + ->{ @enum.each_cons(-2) }.should raise_error(ArgumentError) + ->{ @enum.each_cons }.should raise_error(ArgumentError) + ->{ @enum.each_cons(2,2) }.should raise_error(ArgumentError) + end + + it "tries to convert n to an Integer using #to_int" do + acc = [] + @enum.each_cons(3.3){|g| acc << g} + acc.should == @in_threes + + obj = mock('to_int') + obj.should_receive(:to_int).and_return(3) + @enum.each_cons(obj){|g| break g.length}.should == 3 + end + + it "works when n is >= full length" do + full = @enum.to_a + acc = [] + @enum.each_cons(full.length){|g| acc << g} + acc.should == [full] + acc = [] + @enum.each_cons(full.length+1){|g| acc << g} + acc.should == [] + end + + it "yields only as much as needed" do + cnt = EnumerableSpecs::EachCounter.new(1, 2, :stop, "I said stop!", :got_it) + cnt.each_cons(2) {|g| break 42 if g[-1] == :stop }.should == 42 + cnt.times_yielded.should == 3 + end + + it "gathers whole arrays as elements when each yields multiple" do + multi = EnumerableSpecs::YieldsMulti.new + multi.each_cons(2).to_a.should == [[[1, 2], [3, 4, 5]], [[3, 4, 5], [6, 7, 8, 9]]] + end + + it "returns self when a block is given" do + @enum.each_cons(3){}.should == @enum + end + + describe "when no block is given" do + it "returns an enumerator" do + e = @enum.each_cons(3) + e.should be_an_instance_of(Enumerator) + e.to_a.should == @in_threes + end + + describe "Enumerable with size" do + describe "returned Enumerator" do + describe "size" do + it "returns enum size - each_cons argument + 1" do + enum = EnumerableSpecs::NumerousWithSize.new(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) + enum.each_cons(10).size.should == 1 + enum.each_cons(9).size.should == 2 + enum.each_cons(3).size.should == 8 + enum.each_cons(2).size.should == 9 + enum.each_cons(1).size.should == 10 + end + + it "returns 0 when the argument is larger than self" do + enum = EnumerableSpecs::NumerousWithSize.new(1, 2, 3) + enum.each_cons(20).size.should == 0 + end + + it "returns 0 when the enum is empty" do + enum = EnumerableSpecs::EmptyWithSize.new + enum.each_cons(10).size.should == 0 + end + end + end + end + + describe "Enumerable with no size" do + before :all do + @object = EnumerableSpecs::Numerous.new(1, 2, 3, 4) + @method = [:each_cons, 8] + end + it_should_behave_like :enumeratorized_with_unknown_size + end + end +end diff --git a/spec/ruby/core/enumerable/each_entry_spec.rb b/spec/ruby/core/enumerable/each_entry_spec.rb new file mode 100644 index 0000000000..edf00f3137 --- /dev/null +++ b/spec/ruby/core/enumerable/each_entry_spec.rb @@ -0,0 +1,41 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/enumerable_enumeratorized' + +describe "Enumerable#each_entry" do + before :each do + ScratchPad.record [] + @enum = EnumerableSpecs::YieldsMixed.new + @entries = [1, [2], [3,4], [5,6,7], [8,9], nil, []] + end + + it "yields multiple arguments as an array" do + acc = [] + @enum.each_entry {|g| acc << g}.should equal(@enum) + acc.should == @entries + end + + it "returns an enumerator if no block" do + e = @enum.each_entry + e.should be_an_instance_of(Enumerator) + e.to_a.should == @entries + end + + it "passes through the values yielded by #each_with_index" do + [:a, :b].each_with_index.each_entry { |x, i| ScratchPad << [x, i] } + ScratchPad.recorded.should == [[:a, 0], [:b, 1]] + end + + it "raises an ArgumentError when extra arguments" do + -> { @enum.each_entry("one").to_a }.should raise_error(ArgumentError) + -> { @enum.each_entry("one"){}.to_a }.should raise_error(ArgumentError) + end + + it "passes extra arguments to #each" do + enum = EnumerableSpecs::EachCounter.new(1, 2) + enum.each_entry(:foo, "bar").to_a.should == [1,2] + enum.arguments_passed.should == [:foo, "bar"] + end + + it_behaves_like :enumerable_enumeratorized_with_origin_size, :each_entry +end diff --git a/spec/ruby/core/enumerable/each_slice_spec.rb b/spec/ruby/core/enumerable/each_slice_spec.rb new file mode 100644 index 0000000000..47b8c9ba33 --- /dev/null +++ b/spec/ruby/core/enumerable/each_slice_spec.rb @@ -0,0 +1,105 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/enumeratorized' + +describe "Enumerable#each_slice" do + before :each do + @enum = EnumerableSpecs::Numerous.new(7,6,5,4,3,2,1) + @sliced = [[7,6,5],[4,3,2],[1]] + end + + it "passes element groups to the block" do + acc = [] + @enum.each_slice(3){|g| acc << g} + acc.should == @sliced + end + + it "raises an ArgumentError if there is not a single parameter > 0" do + ->{ @enum.each_slice(0){} }.should raise_error(ArgumentError) + ->{ @enum.each_slice(-2){} }.should raise_error(ArgumentError) + ->{ @enum.each_slice{} }.should raise_error(ArgumentError) + ->{ @enum.each_slice(2,2){} }.should raise_error(ArgumentError) + ->{ @enum.each_slice(0) }.should raise_error(ArgumentError) + ->{ @enum.each_slice(-2) }.should raise_error(ArgumentError) + ->{ @enum.each_slice }.should raise_error(ArgumentError) + ->{ @enum.each_slice(2,2) }.should raise_error(ArgumentError) + end + + it "tries to convert n to an Integer using #to_int" do + acc = [] + @enum.each_slice(3.3){|g| acc << g} + acc.should == @sliced + + obj = mock('to_int') + obj.should_receive(:to_int).and_return(3) + @enum.each_slice(obj){|g| break g.length}.should == 3 + end + + it "works when n is >= full length" do + full = @enum.to_a + acc = [] + @enum.each_slice(full.length){|g| acc << g} + acc.should == [full] + acc = [] + @enum.each_slice(full.length+1){|g| acc << g} + acc.should == [full] + end + + it "yields only as much as needed" do + cnt = EnumerableSpecs::EachCounter.new(1, 2, :stop, "I said stop!", :got_it) + cnt.each_slice(2) {|g| break 42 if g[0] == :stop }.should == 42 + cnt.times_yielded.should == 4 + end + + it "returns an enumerator if no block" do + e = @enum.each_slice(3) + e.should be_an_instance_of(Enumerator) + e.to_a.should == @sliced + end + + it "returns self when a block is given" do + @enum.each_slice(3){}.should == @enum + end + + it "gathers whole arrays as elements when each yields multiple" do + multi = EnumerableSpecs::YieldsMulti.new + multi.each_slice(2).to_a.should == [[[1, 2], [3, 4, 5]], [[6, 7, 8, 9]]] + end + + describe "when no block is given" do + it "returns an enumerator" do + e = @enum.each_slice(3) + e.should be_an_instance_of(Enumerator) + e.to_a.should == @sliced + end + + describe "Enumerable with size" do + describe "returned Enumerator" do + describe "size" do + it "returns the ceil of Enumerable size divided by the argument value" do + enum = EnumerableSpecs::NumerousWithSize.new(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) + enum.each_slice(10).size.should == 1 + enum.each_slice(9).size.should == 2 + enum.each_slice(3).size.should == 4 + enum.each_slice(2).size.should == 5 + enum.each_slice(1).size.should == 10 + end + + it "returns 0 when the Enumerable is empty" do + enum = EnumerableSpecs::EmptyWithSize.new + enum.each_slice(10).size.should == 0 + end + end + end + end + + describe "Enumerable with no size" do + before :all do + @object = EnumerableSpecs::Numerous.new(1, 2, 3, 4) + @method = [:each_slice, 8] + end + it_should_behave_like :enumeratorized_with_unknown_size + end + end + +end diff --git a/spec/ruby/core/enumerable/each_with_index_spec.rb b/spec/ruby/core/enumerable/each_with_index_spec.rb new file mode 100644 index 0000000000..122e88eab7 --- /dev/null +++ b/spec/ruby/core/enumerable/each_with_index_spec.rb @@ -0,0 +1,53 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/enumerable_enumeratorized' + +describe "Enumerable#each_with_index" do + + before :each do + @b = EnumerableSpecs::Numerous.new(2, 5, 3, 6, 1, 4) + end + + it "passes each element and its index to block" do + @a = [] + @b.each_with_index { |o, i| @a << [o, i] } + @a.should == [[2, 0], [5, 1], [3, 2], [6, 3], [1, 4], [4, 5]] + end + + it "provides each element to the block" do + acc = [] + obj = EnumerableSpecs::EachDefiner.new() + res = obj.each_with_index {|a,i| acc << [a,i]} + acc.should == [] + obj.should == res + end + + it "provides each element to the block and its index" do + acc = [] + res = @b.each_with_index {|a,i| acc << [a,i]} + [[2, 0], [5, 1], [3, 2], [6, 3], [1, 4], [4, 5]].should == acc + res.should eql(@b) + end + + it "binds splat arguments properly" do + acc = [] + res = @b.each_with_index { |*b| c,d = b; acc << c; acc << d } + [2, 0, 5, 1, 3, 2, 6, 3, 1, 4, 4, 5].should == acc + res.should eql(@b) + end + + it "returns an enumerator if no block" do + e = @b.each_with_index + e.should be_an_instance_of(Enumerator) + e.to_a.should == [[2, 0], [5, 1], [3, 2], [6, 3], [1, 4], [4, 5]] + end + + it "passes extra parameters to each" do + count = EnumerableSpecs::EachCounter.new(:apple) + e = count.each_with_index(:foo, :bar) + e.to_a.should == [[:apple, 0]] + count.arguments_passed.should == [:foo, :bar] + end + + it_behaves_like :enumerable_enumeratorized_with_origin_size, :each_with_index +end diff --git a/spec/ruby/core/enumerable/each_with_object_spec.rb b/spec/ruby/core/enumerable/each_with_object_spec.rb new file mode 100644 index 0000000000..35665e7019 --- /dev/null +++ b/spec/ruby/core/enumerable/each_with_object_spec.rb @@ -0,0 +1,41 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/enumerable_enumeratorized' + +describe "Enumerable#each_with_object" do + before :each do + @values = [2, 5, 3, 6, 1, 4] + @enum = EnumerableSpecs::Numerous.new(*@values) + @initial = "memo" + end + + it "passes each element and its argument to the block" do + acc = [] + @enum.each_with_object(@initial) do |elem, obj| + obj.should equal(@initial) + obj = 42 + acc << elem + end.should equal(@initial) + acc.should == @values + end + + it "returns an enumerator if no block" do + acc = [] + e = @enum.each_with_object(@initial) + e.each do |elem, obj| + obj.should equal(@initial) + obj = 42 + acc << elem + end.should equal(@initial) + acc.should == @values + end + + it "gathers whole arrays as elements when each yields multiple" do + multi = EnumerableSpecs::YieldsMulti.new + array = [] + multi.each_with_object(array) { |elem, obj| obj << elem } + array.should == [[1, 2], [3, 4, 5], [6, 7, 8, 9]] + end + + it_behaves_like :enumerable_enumeratorized_with_origin_size, [:each_with_object, []] +end diff --git a/spec/ruby/core/enumerable/entries_spec.rb b/spec/ruby/core/enumerable/entries_spec.rb new file mode 100644 index 0000000000..2de4fc756a --- /dev/null +++ b/spec/ruby/core/enumerable/entries_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/entries' + +describe "Enumerable#entries" do + it_behaves_like :enumerable_entries, :entries +end diff --git a/spec/ruby/core/enumerable/filter_map_spec.rb b/spec/ruby/core/enumerable/filter_map_spec.rb new file mode 100644 index 0000000000..aa4894230b --- /dev/null +++ b/spec/ruby/core/enumerable/filter_map_spec.rb @@ -0,0 +1,24 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe 'Enumerable#filter_map' do + before :each do + @numerous = EnumerableSpecs::Numerous.new(*(1..8).to_a) + end + + it 'returns an empty array if there are no elements' do + EnumerableSpecs::Empty.new.filter_map { true }.should == [] + end + + it 'returns an array with truthy results of passing each element to block' do + @numerous.filter_map { |i| i * 2 if i.even? }.should == [4, 8, 12, 16] + @numerous.filter_map { |i| i * 2 }.should == [2, 4, 6, 8, 10, 12, 14, 16] + @numerous.filter_map { 0 }.should == [0, 0, 0, 0, 0, 0, 0, 0] + @numerous.filter_map { false }.should == [] + @numerous.filter_map { nil }.should == [] + end + + it 'returns an enumerator when no block given' do + @numerous.filter_map.should be_an_instance_of(Enumerator) + end +end diff --git a/spec/ruby/core/enumerable/filter_spec.rb b/spec/ruby/core/enumerable/filter_spec.rb new file mode 100644 index 0000000000..1c3a7e9ff5 --- /dev/null +++ b/spec/ruby/core/enumerable/filter_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/find_all' + +describe "Enumerable#filter" do + it_behaves_like :enumerable_find_all, :filter +end diff --git a/spec/ruby/core/enumerable/find_all_spec.rb b/spec/ruby/core/enumerable/find_all_spec.rb new file mode 100644 index 0000000000..9cd635f247 --- /dev/null +++ b/spec/ruby/core/enumerable/find_all_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/find_all' + +describe "Enumerable#find_all" do + it_behaves_like :enumerable_find_all, :find_all +end diff --git a/spec/ruby/core/enumerable/find_index_spec.rb b/spec/ruby/core/enumerable/find_index_spec.rb new file mode 100644 index 0000000000..542660fe04 --- /dev/null +++ b/spec/ruby/core/enumerable/find_index_spec.rb @@ -0,0 +1,89 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/enumerable_enumeratorized' + +describe "Enumerable#find_index" do + before :each do + @elements = [2, 4, 6, 8, 10] + @numerous = EnumerableSpecs::Numerous.new(*@elements) + @yieldsmixed = EnumerableSpecs::YieldsMixed2.new + end + + it "passes each entry in enum to block while block when block is false" do + visited_elements = [] + @numerous.find_index do |element| + visited_elements << element + false + end + visited_elements.should == @elements + end + + it "returns nil when the block is false" do + @numerous.find_index {|e| false }.should == nil + end + + it "returns the first index for which the block is not false" do + @elements.each_with_index do |element, index| + @numerous.find_index {|e| e > element - 1 }.should == index + end + end + + it "returns the first index found" do + repeated = [10, 11, 11, 13, 11, 13, 10, 10, 13, 11] + numerous_repeat = EnumerableSpecs::Numerous.new(*repeated) + repeated.each do |element| + numerous_repeat.find_index(element).should == element - 10 + end + end + + it "returns nil when the element not found" do + @numerous.find_index(-1).should == nil + end + + it "ignores the block if an argument is given" do + -> { + @numerous.find_index(-1) {|e| true }.should == nil + }.should complain(/given block not used/) + end + + it "returns an Enumerator if no block given" do + @numerous.find_index.should be_an_instance_of(Enumerator) + end + + it "uses #== for testing equality" do + [2].to_enum.find_index(2.0).should == 0 + [2.0].to_enum.find_index(2).should == 0 + end + + describe "without block" do + it "gathers whole arrays as elements when each yields multiple" do + @yieldsmixed.find_index([0, 1, 2]).should == 3 + end + end + + describe "with block" do + before :each do + ScratchPad.record [] + end + + after :each do + ScratchPad.clear + end + + describe "given a single yield parameter" do + it "passes first element to the parameter" do + @yieldsmixed.find_index {|a| ScratchPad << a; false } + ScratchPad.recorded.should == EnumerableSpecs::YieldsMixed2.first_yields + end + end + + describe "given a greedy yield parameter" do + it "passes a gathered array to the parameter" do + @yieldsmixed.find_index {|*args| ScratchPad << args; false } + ScratchPad.recorded.should == EnumerableSpecs::YieldsMixed2.greedy_yields + end + end + end + + it_behaves_like :enumerable_enumeratorized_with_unknown_size, :find_index +end diff --git a/spec/ruby/core/enumerable/find_spec.rb b/spec/ruby/core/enumerable/find_spec.rb new file mode 100644 index 0000000000..5ddebc05f8 --- /dev/null +++ b/spec/ruby/core/enumerable/find_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/find' + +describe "Enumerable#find" do + it_behaves_like :enumerable_find, :find +end diff --git a/spec/ruby/core/enumerable/first_spec.rb b/spec/ruby/core/enumerable/first_spec.rb new file mode 100644 index 0000000000..ed1ba599b4 --- /dev/null +++ b/spec/ruby/core/enumerable/first_spec.rb @@ -0,0 +1,28 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/take' + +describe "Enumerable#first" do + it "returns the first element" do + EnumerableSpecs::Numerous.new.first.should == 2 + EnumerableSpecs::Empty.new.first.should == nil + end + + it "returns nil if self is empty" do + EnumerableSpecs::Empty.new.first.should == nil + end + + it 'returns a gathered array from yield parameters' do + EnumerableSpecs::YieldsMulti.new.to_enum.first.should == [1, 2] + EnumerableSpecs::YieldsMixed2.new.to_enum.first.should == nil + end + + it "raises a RangeError when passed a Bignum" do + enum = EnumerableSpecs::Empty.new + -> { enum.first(bignum_value) }.should raise_error(RangeError) + end + + describe "when passed an argument" do + it_behaves_like :enumerable_take, :first + end +end diff --git a/spec/ruby/core/enumerable/fixtures/classes.rb b/spec/ruby/core/enumerable/fixtures/classes.rb new file mode 100644 index 0000000000..b5feafcfb7 --- /dev/null +++ b/spec/ruby/core/enumerable/fixtures/classes.rb @@ -0,0 +1,350 @@ +module EnumerableSpecs + + class Numerous + include Enumerable + def initialize(*list) + @list = list.empty? ? [2, 5, 3, 6, 1, 4] : list + end + + def each + @list.each { |i| yield i } + end + end + + class NumerousWithSize < Numerous + def size + @list.size + end + end + + class EachCounter < Numerous + attr_reader :times_called, :times_yielded, :arguments_passed + def initialize(*list) + super(*list) + @times_yielded = @times_called = 0 + end + + def each(*arg) + @times_called += 1 + @times_yielded = 0 + @arguments_passed = arg + @list.each do |i| + @times_yielded +=1 + yield i + end + end + end + + class Empty + include Enumerable + def each + self + end + end + + class EmptyWithSize + include Enumerable + def each + self + end + def size + 0 + end + end + + class ThrowingEach + include Enumerable + def each + raise "from each" + end + end + + class NoEach + include Enumerable + end + + # (Legacy form rubycon) + class EachDefiner + + include Enumerable + + attr_reader :arr + + def initialize(*arr) + @arr = arr + end + + def each + i = 0 + loop do + break if i == @arr.size + yield @arr[i] + i += 1 + end + end + + end + + class SortByDummy + def initialize(s) + @s = s + end + + def s + @s + end + end + + class ComparesByVowelCount + + attr_accessor :value, :vowels + + def self.wrap(*args) + args.map {|element| ComparesByVowelCount.new(element)} + end + + def initialize(string) + self.value = string + self.vowels = string.gsub(/[^aeiou]/, '').size + end + + def <=>(other) + self.vowels <=> other.vowels + end + + end + + class InvalidComparable + def <=>(other) + "Not Valid" + end + end + + class ArrayConvertible + attr_accessor :called + def initialize(*values) + @values = values + end + + def to_a + self.called = :to_a + @values + end + + def to_ary + self.called = :to_ary + @values + end + end + + class EnumConvertible + attr_accessor :called + attr_accessor :sym + def initialize(delegate) + @delegate = delegate + end + + def to_enum(sym) + self.called = :to_enum + self.sym = sym + @delegate.to_enum(sym) + end + + def respond_to_missing?(*args) + @delegate.respond_to?(*args) + end + end + + class Equals + def initialize(obj) + @obj = obj + end + def ==(other) + @obj == other + end + end + + class YieldsMulti + include Enumerable + def each + yield 1,2 + yield 3,4,5 + yield 6,7,8,9 + end + end + + class YieldsMultiWithFalse + include Enumerable + def each + yield false,2 + yield false,4,5 + yield false,7,8,9 + end + end + + class YieldsMultiWithSingleTrue + include Enumerable + def each + yield false,2 + yield true,4,5 + yield false,7,8,9 + end + end + + class YieldsMixed + include Enumerable + def each + yield 1 + yield [2] + yield 3,4 + yield 5,6,7 + yield [8,9] + yield nil + yield [] + end + end + + class YieldsMixed2 + include Enumerable + + def self.first_yields + [nil, 0, 0, 0, 0, nil, :default_arg, [], [], [0], [0, 1], [0, 1, 2]] + end + + def self.gathered_yields + [nil, 0, [0, 1], [0, 1, 2], [0, 1, 2], nil, :default_arg, [], [], [0], [0, 1], [0, 1, 2]] + end + + def self.gathered_yields_with_args(arg, *args) + [nil, 0, [0, 1], [0, 1, 2], [0, 1, 2], nil, arg, args, [], [0], [0, 1], [0, 1, 2]] + end + + def self.greedy_yields + [[], [0], [0, 1], [0, 1, 2], [0, 1, 2], [nil], [:default_arg], [[]], [[]], [[0]], [[0, 1]], [[0, 1, 2]]] + end + + def each(arg=:default_arg, *args) + yield + yield 0 + yield 0, 1 + yield 0, 1, 2 + yield(*[0, 1, 2]) + yield nil + yield arg + yield args + yield [] + yield [0] + yield [0, 1] + yield [0, 1, 2] + end + end + + class ReverseComparable + include Comparable + def initialize(num) + @num = num + end + + attr_accessor :num + + # Reverse comparison + def <=>(other) + other.num <=> @num + end + end + + class ComparableWithInteger + include Comparable + def initialize(num) + @num = num + end + + def <=>(fixnum) + @num <=> fixnum + end + end + + class Uncomparable + def <=>(obj) + nil + end + end + + class Undupable + attr_reader :initialize_called, :initialize_dup_called + def dup + raise "Can't, sorry" + end + + def clone + raise "Can't, either, sorry" + end + + def initialize + @initialize_dup = true + end + + def initialize_dup(arg) + @initialize_dup_called = true + end + end + + class Freezy + include Enumerable + + def each + yield 1 + yield 2 + end + + def to_a + super.freeze + end + end + + class MapReturnsEnumerable + include Enumerable + + class EnumerableMapping + include Enumerable + + def initialize(items, block) + @items = items + @block = block + end + + def each + @items.each do |i| + yield @block.call(i) + end + end + end + + def each + yield 1 + yield 2 + yield 3 + end + + def map(&block) + EnumerableMapping.new(self, block) + end + end + + class Pattern + attr_reader :yielded + + def initialize(&block) + @block = block + @yielded = [] + end + + def ===(*args) + @yielded << args + @block.call(*args) + end + end + + class SetSubclass < Set + end +end # EnumerableSpecs utility classes diff --git a/spec/ruby/core/enumerable/flat_map_spec.rb b/spec/ruby/core/enumerable/flat_map_spec.rb new file mode 100644 index 0000000000..bd07eab6c5 --- /dev/null +++ b/spec/ruby/core/enumerable/flat_map_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/collect_concat' + +describe "Enumerable#flat_map" do + it_behaves_like :enumerable_collect_concat, :flat_map +end diff --git a/spec/ruby/core/enumerable/grep_spec.rb b/spec/ruby/core/enumerable/grep_spec.rb new file mode 100644 index 0000000000..989358f01b --- /dev/null +++ b/spec/ruby/core/enumerable/grep_spec.rb @@ -0,0 +1,87 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Enumerable#grep" do + before :each do + @a = EnumerableSpecs::EachDefiner.new( 2, 4, 6, 8, 10) + end + + it "grep without a block should return an array of all elements === pattern" do + class EnumerableSpecGrep; def ===(obj); obj == '2'; end; end + + EnumerableSpecs::Numerous.new('2', 'a', 'nil', '3', false).grep(EnumerableSpecGrep.new).should == ['2'] + end + + it "grep with a block should return an array of elements === pattern passed through block" do + class EnumerableSpecGrep2; def ===(obj); /^ca/ =~ obj; end; end + + EnumerableSpecs::Numerous.new("cat", "coat", "car", "cadr", "cost").grep(EnumerableSpecGrep2.new) { |i| i.upcase }.should == ["CAT", "CAR", "CADR"] + end + + it "grep the enumerable (rubycon legacy)" do + EnumerableSpecs::EachDefiner.new().grep(1).should == [] + @a.grep(3..7).should == [4,6] + @a.grep(3..7) {|a| a+1}.should == [5,7] + end + + it "can use $~ in the block when used with a Regexp" do + ary = ["aba", "aba"] + ary.grep(/a(b)a/) { $1 }.should == ["b", "b"] + end + + it "sets $~ in the block" do + "z" =~ /z/ # Reset $~ + ["abc", "def"].grep(/b/) { |e| + e.should == "abc" + $&.should == "b" + } + + # Set by the failed match of "def" + $~.should == nil + end + + it "does not set $~ when given no block" do + "z" =~ /z/ # Reset $~ + ["abc", "def"].grep(/b/).should == ["abc"] + $&.should == "z" + end + + it "does not modify Regexp.last_match without block" do + "z" =~ /z/ # Reset last match + ["abc", "def"].grep(/b/).should == ["abc"] + Regexp.last_match[0].should == "z" + end + + it "correctly handles non-string elements" do + 'set last match' =~ /set last (.*)/ + [:a, 'b', 'z', :c, 42, nil].grep(/[a-d]/).should == [:a, 'b', :c] + $1.should == 'match' + + o = Object.new + def o.to_str + 'hello' + end + [o].grep(/ll/).first.should.equal?(o) + end + + describe "with a block" do + before :each do + @numerous = EnumerableSpecs::Numerous.new(*(0..9).to_a) + def (@odd_matcher = BasicObject.new).===(obj) + obj.odd? + end + end + + it "returns an Array of matched elements that mapped by the block" do + @numerous.grep(@odd_matcher) { |n| n * 2 }.should == [2, 6, 10, 14, 18] + end + + it "calls the block with gathered array when yielded with multiple arguments" do + EnumerableSpecs::YieldsMixed2.new.grep(Object){ |e| e }.should == EnumerableSpecs::YieldsMixed2.gathered_yields + end + + it "raises an ArgumentError when not given a pattern" do + -> { @numerous.grep { |e| e } }.should raise_error(ArgumentError) + end + end +end diff --git a/spec/ruby/core/enumerable/grep_v_spec.rb b/spec/ruby/core/enumerable/grep_v_spec.rb new file mode 100644 index 0000000000..ba19216968 --- /dev/null +++ b/spec/ruby/core/enumerable/grep_v_spec.rb @@ -0,0 +1,76 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Enumerable#grep_v" do + before :each do + @numerous = EnumerableSpecs::Numerous.new(*(0..9).to_a) + def (@odd_matcher = BasicObject.new).===(obj) + obj.odd? + end + end + + it "sets $~ in the block" do + "z" =~ /z/ # Reset $~ + ["abc", "def"].grep_v(/e/) { |e| + e.should == "abc" + $~.should == nil + } + + # Set by the match of "def" + $&.should == "e" + end + + it "does not set $~ when given no block" do + "z" =~ /z/ # Reset $~ + ["abc", "def"].grep_v(/e/).should == ["abc"] + $&.should == "z" + end + + it "does not modify Regexp.last_match without block" do + "z" =~ /z/ # Reset last match + ["abc", "def"].grep_v(/e/).should == ["abc"] + Regexp.last_match[0].should == "z" + end + + it "correctly handles non-string elements" do + 'set last match' =~ /set last (.*)/ + [:a, 'b', 'z', :c, 42, nil].grep_v(/[a-d]/).should == ['z', 42, nil] + $1.should == 'match' + + o = Object.new + def o.to_str + 'hello' + end + [o].grep_v(/mm/).first.should.equal?(o) + end + + describe "without block" do + it "returns an Array of matched elements" do + @numerous.grep_v(@odd_matcher).should == [0, 2, 4, 6, 8] + end + + it "compares pattern with gathered array when yielded with multiple arguments" do + (unmatcher = Object.new).stub!(:===).and_return(false) + EnumerableSpecs::YieldsMixed2.new.grep_v(unmatcher).should == EnumerableSpecs::YieldsMixed2.gathered_yields + end + + it "raises an ArgumentError when not given a pattern" do + -> { @numerous.grep_v }.should raise_error(ArgumentError) + end + end + + describe "with block" do + it "returns an Array of matched elements that mapped by the block" do + @numerous.grep_v(@odd_matcher) { |n| n * 2 }.should == [0, 4, 8, 12, 16] + end + + it "calls the block with gathered array when yielded with multiple arguments" do + (unmatcher = Object.new).stub!(:===).and_return(false) + EnumerableSpecs::YieldsMixed2.new.grep_v(unmatcher){ |e| e }.should == EnumerableSpecs::YieldsMixed2.gathered_yields + end + + it "raises an ArgumentError when not given a pattern" do + -> { @numerous.grep_v { |e| e } }.should raise_error(ArgumentError) + end + end +end diff --git a/spec/ruby/core/enumerable/group_by_spec.rb b/spec/ruby/core/enumerable/group_by_spec.rb new file mode 100644 index 0000000000..4fd1603819 --- /dev/null +++ b/spec/ruby/core/enumerable/group_by_spec.rb @@ -0,0 +1,37 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/enumerable_enumeratorized' + +describe "Enumerable#group_by" do + it "returns a hash with values grouped according to the block" do + e = EnumerableSpecs::Numerous.new("foo", "bar", "baz") + h = e.group_by { |word| word[0..0].to_sym } + h.should == { f: ["foo"], b: ["bar", "baz"]} + end + + it "returns an empty hash for empty enumerables" do + EnumerableSpecs::Empty.new.group_by { |x| x}.should == {} + end + + it "returns a hash without default_proc" do + e = EnumerableSpecs::Numerous.new("foo", "bar", "baz") + h = e.group_by { |word| word[0..0].to_sym } + h[:some].should be_nil + h.default_proc.should be_nil + h.default.should be_nil + end + + it "returns an Enumerator if called without a block" do + EnumerableSpecs::Numerous.new.group_by.should be_an_instance_of(Enumerator) + end + + it "gathers whole arrays as elements when each yields multiple" do + e = EnumerableSpecs::YieldsMulti.new + h = e.group_by { |i| i } + h.should == { [1, 2] => [[1, 2]], + [6, 7, 8, 9] => [[6, 7, 8, 9]], + [3, 4, 5] => [[3, 4, 5]] } + end + + it_behaves_like :enumerable_enumeratorized_with_origin_size, :group_by +end diff --git a/spec/ruby/core/enumerable/include_spec.rb b/spec/ruby/core/enumerable/include_spec.rb new file mode 100644 index 0000000000..dab1b04451 --- /dev/null +++ b/spec/ruby/core/enumerable/include_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/include' + +describe "Enumerable#include?" do + it_behaves_like :enumerable_include, :include? +end diff --git a/spec/ruby/core/enumerable/inject_spec.rb b/spec/ruby/core/enumerable/inject_spec.rb new file mode 100644 index 0000000000..e1fe216144 --- /dev/null +++ b/spec/ruby/core/enumerable/inject_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/inject' + +describe "Enumerable#inject" do + it_behaves_like :enumerable_inject, :inject +end diff --git a/spec/ruby/core/enumerable/lazy_spec.rb b/spec/ruby/core/enumerable/lazy_spec.rb new file mode 100644 index 0000000000..9a9ead81a0 --- /dev/null +++ b/spec/ruby/core/enumerable/lazy_spec.rb @@ -0,0 +1,10 @@ +# -*- encoding: us-ascii -*- + +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Enumerable#lazy" do + it "returns an instance of Enumerator::Lazy" do + EnumerableSpecs::Numerous.new.lazy.should be_an_instance_of(Enumerator::Lazy) + end +end diff --git a/spec/ruby/core/enumerable/map_spec.rb b/spec/ruby/core/enumerable/map_spec.rb new file mode 100644 index 0000000000..98a70781cf --- /dev/null +++ b/spec/ruby/core/enumerable/map_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/collect' + +describe "Enumerable#map" do + it_behaves_like :enumerable_collect, :map +end diff --git a/spec/ruby/core/enumerable/max_by_spec.rb b/spec/ruby/core/enumerable/max_by_spec.rb new file mode 100644 index 0000000000..ec1738ea3b --- /dev/null +++ b/spec/ruby/core/enumerable/max_by_spec.rb @@ -0,0 +1,81 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/enumerable_enumeratorized' + +describe "Enumerable#max_by" do + it "returns an enumerator if no block" do + EnumerableSpecs::Numerous.new(42).max_by.should be_an_instance_of(Enumerator) + end + + it "returns nil if #each yields no objects" do + EnumerableSpecs::Empty.new.max_by {|o| o.nonesuch }.should == nil + end + + it "returns the object for whom the value returned by block is the largest" do + EnumerableSpecs::Numerous.new(*%w[1 2 3]).max_by {|obj| obj.to_i }.should == '3' + EnumerableSpecs::Numerous.new(*%w[three five]).max_by {|obj| obj.length }.should == 'three' + end + + it "returns the object that appears first in #each in case of a tie" do + a, b, c = '1', '2', '2' + EnumerableSpecs::Numerous.new(a, b, c).max_by {|obj| obj.to_i }.should equal(b) + end + + it "uses max.<=>(current) to determine order" do + a, b, c = (1..3).map{|n| EnumerableSpecs::ReverseComparable.new(n)} + + # Just using self here to avoid additional complexity + EnumerableSpecs::Numerous.new(a, b, c).max_by {|obj| obj }.should == a + end + + it "is able to return the maximum for enums that contain nils" do + enum = EnumerableSpecs::Numerous.new(nil, nil, true) + enum.max_by {|o| o.nil? ? 0 : 1 }.should == true + enum.max_by {|o| o.nil? ? 1 : 0 }.should == nil + end + + it "gathers whole arrays as elements when each yields multiple" do + multi = EnumerableSpecs::YieldsMulti.new + multi.max_by {|e| e.size}.should == [6, 7, 8, 9] + end + + it_behaves_like :enumerable_enumeratorized_with_origin_size, :max_by + + context "when called with an argument n" do + before :each do + @enum = EnumerableSpecs::Numerous.new(101, 55, 1, 20, 33, 500, 60) + end + + context "without a block" do + it "returns an enumerator" do + @enum.max_by(2).should be_an_instance_of(Enumerator) + end + end + + context "with a block" do + it "returns an array containing the maximum n elements based on the block's value" do + result = @enum.max_by(3) { |i| i.to_s } + result.should == [60, 55, 500] + end + + context "on a enumerable of length x where x < n" do + it "returns an array containing the maximum n elements of length n" do + result = @enum.max_by(500) { |i| i.to_s } + result.length.should == 7 + end + end + + context "when n is negative" do + it "raises an ArgumentError" do + -> { @enum.max_by(-1) { |i| i.to_s } }.should raise_error(ArgumentError) + end + end + end + + context "when n is nil" do + it "returns the maximum element" do + @enum.max_by(nil) { |i| i.to_s }.should == 60 + end + end + end +end diff --git a/spec/ruby/core/enumerable/max_spec.rb b/spec/ruby/core/enumerable/max_spec.rb new file mode 100644 index 0000000000..0c11ca0969 --- /dev/null +++ b/spec/ruby/core/enumerable/max_spec.rb @@ -0,0 +1,119 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Enumerable#max" do + before :each do + @e_strs = EnumerableSpecs::EachDefiner.new("333", "22", "666666", "1", "55555", "1010101010") + @e_ints = EnumerableSpecs::EachDefiner.new( 333, 22, 666666, 55555, 1010101010) + end + + it "returns the maximum element" do + EnumerableSpecs::Numerous.new.max.should == 6 + end + + it "returns the maximum element (basics cases)" do + EnumerableSpecs::EachDefiner.new(55).max.should == 55 + + EnumerableSpecs::EachDefiner.new(11,99).max.should == 99 + EnumerableSpecs::EachDefiner.new(99,11).max.should == 99 + EnumerableSpecs::EachDefiner.new(2, 33, 4, 11).max.should == 33 + + EnumerableSpecs::EachDefiner.new(1,2,3,4,5).max.should == 5 + EnumerableSpecs::EachDefiner.new(5,4,3,2,1).max.should == 5 + EnumerableSpecs::EachDefiner.new(1,4,3,5,2).max.should == 5 + EnumerableSpecs::EachDefiner.new(5,5,5,5,5).max.should == 5 + + EnumerableSpecs::EachDefiner.new("aa","tt").max.should == "tt" + EnumerableSpecs::EachDefiner.new("tt","aa").max.should == "tt" + EnumerableSpecs::EachDefiner.new("2","33","4","11").max.should == "4" + + @e_strs.max.should == "666666" + @e_ints.max.should == 1010101010 + end + + it "returns nil for an empty Enumerable" do + EnumerableSpecs::EachDefiner.new.max.should == nil + end + + it "raises a NoMethodError for elements without #<=>" do + -> do + EnumerableSpecs::EachDefiner.new(BasicObject.new, BasicObject.new).max + end.should raise_error(NoMethodError) + end + + it "raises an ArgumentError for incomparable elements" do + -> do + EnumerableSpecs::EachDefiner.new(11,"22").max + end.should raise_error(ArgumentError) + -> do + EnumerableSpecs::EachDefiner.new(11,12,22,33).max{|a, b| nil} + end.should raise_error(ArgumentError) + end + + context "when passed a block" do + it "returns the maximum element" do + EnumerableSpecs::EachDefiner.new("2","33","4","11").max {|a,b| a <=> b }.should == "4" + EnumerableSpecs::EachDefiner.new( 2 , 33 , 4 , 11 ).max {|a,b| a <=> b }.should == 33 + + EnumerableSpecs::EachDefiner.new("2","33","4","11").max {|a,b| b <=> a }.should == "11" + EnumerableSpecs::EachDefiner.new( 2 , 33 , 4 , 11 ).max {|a,b| b <=> a }.should == 2 + + @e_strs.max {|a,b| a.length <=> b.length }.should == "1010101010" + + @e_strs.max {|a,b| a <=> b }.should == "666666" + @e_strs.max {|a,b| a.to_i <=> b.to_i }.should == "1010101010" + + @e_ints.max {|a,b| a <=> b }.should == 1010101010 + @e_ints.max {|a,b| a.to_s <=> b.to_s }.should == 666666 + end + end + + it "returns the maximum for enumerables that contain nils" do + arr = EnumerableSpecs::Numerous.new(nil, nil, true) + arr.max { |a, b| + x = a.nil? ? 1 : a ? 0 : -1 + y = b.nil? ? 1 : b ? 0 : -1 + x <=> y + }.should == nil + end + + it "gathers whole arrays as elements when each yields multiple" do + multi = EnumerableSpecs::YieldsMulti.new + multi.max.should == [6, 7, 8, 9] + end + + context "when called with an argument n" do + context "without a block" do + it "returns an array containing the maximum n elements" do + result = @e_ints.max(2) + result.should == [1010101010, 666666] + end + end + + context "with a block" do + it "returns an array containing the maximum n elements" do + result = @e_ints.max(2) { |a, b| a * 2 <=> b * 2 } + result.should == [1010101010, 666666] + end + end + + context "on a enumerable of length x where x < n" do + it "returns an array containing the maximum n elements of length x" do + result = @e_ints.max(500) + result.length.should == 5 + end + end + + context "that is negative" do + it "raises an ArgumentError" do + -> { @e_ints.max(-1) }.should raise_error(ArgumentError) + end + end + end + + context "that is nil" do + it "returns the maximum element" do + @e_ints.max(nil).should == 1010101010 + end + end +end diff --git a/spec/ruby/core/enumerable/member_spec.rb b/spec/ruby/core/enumerable/member_spec.rb new file mode 100644 index 0000000000..1fe3cebd28 --- /dev/null +++ b/spec/ruby/core/enumerable/member_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/include' + +describe "Enumerable#member?" do + it_behaves_like :enumerable_include, :member? +end diff --git a/spec/ruby/core/enumerable/min_by_spec.rb b/spec/ruby/core/enumerable/min_by_spec.rb new file mode 100644 index 0000000000..3ff87e49d8 --- /dev/null +++ b/spec/ruby/core/enumerable/min_by_spec.rb @@ -0,0 +1,81 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/enumerable_enumeratorized' + +describe "Enumerable#min_by" do + it "returns an enumerator if no block" do + EnumerableSpecs::Numerous.new(42).min_by.should be_an_instance_of(Enumerator) + end + + it "returns nil if #each yields no objects" do + EnumerableSpecs::Empty.new.min_by {|o| o.nonesuch }.should == nil + end + + it "returns the object for whom the value returned by block is the smallest" do + EnumerableSpecs::Numerous.new(*%w[3 2 1]).min_by {|obj| obj.to_i }.should == '1' + EnumerableSpecs::Numerous.new(*%w[five three]).min_by {|obj| obj.length }.should == 'five' + end + + it "returns the object that appears first in #each in case of a tie" do + a, b, c = '2', '1', '1' + EnumerableSpecs::Numerous.new(a, b, c).min_by {|obj| obj.to_i }.should equal(b) + end + + it "uses min.<=>(current) to determine order" do + a, b, c = (1..3).map{|n| EnumerableSpecs::ReverseComparable.new(n)} + + # Just using self here to avoid additional complexity + EnumerableSpecs::Numerous.new(a, b, c).min_by {|obj| obj }.should == c + end + + it "is able to return the minimum for enums that contain nils" do + enum = EnumerableSpecs::Numerous.new(nil, nil, true) + enum.min_by {|o| o.nil? ? 0 : 1 }.should == nil + enum.min_by {|o| o.nil? ? 1 : 0 }.should == true + end + + it "gathers whole arrays as elements when each yields multiple" do + multi = EnumerableSpecs::YieldsMulti.new + multi.min_by {|e| e.size}.should == [1, 2] + end + + it_behaves_like :enumerable_enumeratorized_with_origin_size, :min_by + + context "when called with an argument n" do + before :each do + @enum = EnumerableSpecs::Numerous.new(101, 55, 1, 20, 33, 500, 60) + end + + context "without a block" do + it "returns an enumerator" do + @enum.min_by(2).should be_an_instance_of(Enumerator) + end + end + + context "with a block" do + it "returns an array containing the minimum n elements based on the block's value" do + result = @enum.min_by(3) { |i| i.to_s } + result.should == [1, 101, 20] + end + + context "on a enumerable of length x where x < n" do + it "returns an array containing the minimum n elements of length n" do + result = @enum.min_by(500) { |i| i.to_s } + result.length.should == 7 + end + end + + context "when n is negative" do + it "raises an ArgumentError" do + -> { @enum.min_by(-1) { |i| i.to_s } }.should raise_error(ArgumentError) + end + end + end + + context "when n is nil" do + it "returns the minimum element" do + @enum.min_by(nil) { |i| i.to_s }.should == 1 + end + end + end +end diff --git a/spec/ruby/core/enumerable/min_spec.rb b/spec/ruby/core/enumerable/min_spec.rb new file mode 100644 index 0000000000..4b6ae248fa --- /dev/null +++ b/spec/ruby/core/enumerable/min_spec.rb @@ -0,0 +1,123 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Enumerable#min" do + before :each do + @e_strs = EnumerableSpecs::EachDefiner.new("333", "22", "666666", "1", "55555", "1010101010") + @e_ints = EnumerableSpecs::EachDefiner.new( 333, 22, 666666, 55555, 1010101010) + end + + it "min should return the minimum element" do + EnumerableSpecs::Numerous.new.min.should == 1 + end + + it "returns the minimum (basic cases)" do + EnumerableSpecs::EachDefiner.new(55).min.should == 55 + + EnumerableSpecs::EachDefiner.new(11,99).min.should == 11 + EnumerableSpecs::EachDefiner.new(99,11).min.should == 11 + EnumerableSpecs::EachDefiner.new(2, 33, 4, 11).min.should == 2 + + EnumerableSpecs::EachDefiner.new(1,2,3,4,5).min.should == 1 + EnumerableSpecs::EachDefiner.new(5,4,3,2,1).min.should == 1 + EnumerableSpecs::EachDefiner.new(4,1,3,5,2).min.should == 1 + EnumerableSpecs::EachDefiner.new(5,5,5,5,5).min.should == 5 + + EnumerableSpecs::EachDefiner.new("aa","tt").min.should == "aa" + EnumerableSpecs::EachDefiner.new("tt","aa").min.should == "aa" + EnumerableSpecs::EachDefiner.new("2","33","4","11").min.should == "11" + + @e_strs.min.should == "1" + @e_ints.min.should == 22 + end + + it "returns nil for an empty Enumerable" do + EnumerableSpecs::EachDefiner.new.min.should be_nil + end + + it "raises a NoMethodError for elements without #<=>" do + -> do + EnumerableSpecs::EachDefiner.new(BasicObject.new, BasicObject.new).min + end.should raise_error(NoMethodError) + end + + it "raises an ArgumentError for incomparable elements" do + -> do + EnumerableSpecs::EachDefiner.new(11,"22").min + end.should raise_error(ArgumentError) + -> do + EnumerableSpecs::EachDefiner.new(11,12,22,33).min{|a, b| nil} + end.should raise_error(ArgumentError) + end + + it "returns the minimum when using a block rule" do + EnumerableSpecs::EachDefiner.new("2","33","4","11").min {|a,b| a <=> b }.should == "11" + EnumerableSpecs::EachDefiner.new( 2 , 33 , 4 , 11 ).min {|a,b| a <=> b }.should == 2 + + EnumerableSpecs::EachDefiner.new("2","33","4","11").min {|a,b| b <=> a }.should == "4" + EnumerableSpecs::EachDefiner.new( 2 , 33 , 4 , 11 ).min {|a,b| b <=> a }.should == 33 + + EnumerableSpecs::EachDefiner.new( 1, 2, 3, 4 ).min {|a,b| 15 }.should == 1 + + EnumerableSpecs::EachDefiner.new(11,12,22,33).min{|a, b| 2 }.should == 11 + @i = -2 + EnumerableSpecs::EachDefiner.new(11,12,22,33).min{|a, b| @i += 1 }.should == 12 + + @e_strs.min {|a,b| a.length <=> b.length }.should == "1" + + @e_strs.min {|a,b| a <=> b }.should == "1" + @e_strs.min {|a,b| a.to_i <=> b.to_i }.should == "1" + + @e_ints.min {|a,b| a <=> b }.should == 22 + @e_ints.min {|a,b| a.to_s <=> b.to_s }.should == 1010101010 + end + + it "returns the minimum for enumerables that contain nils" do + arr = EnumerableSpecs::Numerous.new(nil, nil, true) + arr.min { |a, b| + x = a.nil? ? -1 : a ? 0 : 1 + y = b.nil? ? -1 : b ? 0 : 1 + x <=> y + }.should == nil + end + + it "gathers whole arrays as elements when each yields multiple" do + multi = EnumerableSpecs::YieldsMulti.new + multi.min.should == [1, 2] + end + + context "when called with an argument n" do + context "without a block" do + it "returns an array containing the minimum n elements" do + result = @e_ints.min(2) + result.should == [22, 333] + end + end + + context "with a block" do + it "returns an array containing the minimum n elements" do + result = @e_ints.min(2) { |a, b| a * 2 <=> b * 2 } + result.should == [22, 333] + end + end + + context "on a enumerable of length x where x < n" do + it "returns an array containing the minimum n elements of length x" do + result = @e_ints.min(500) + result.length.should == 5 + end + end + + context "that is negative" do + it "raises an ArgumentError" do + -> { @e_ints.min(-1) }.should raise_error(ArgumentError) + end + end + end + + context "that is nil" do + it "returns the minimum element" do + @e_ints.min(nil).should == 22 + end + end +end diff --git a/spec/ruby/core/enumerable/minmax_by_spec.rb b/spec/ruby/core/enumerable/minmax_by_spec.rb new file mode 100644 index 0000000000..a6a9249270 --- /dev/null +++ b/spec/ruby/core/enumerable/minmax_by_spec.rb @@ -0,0 +1,44 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/enumerable_enumeratorized' + +describe "Enumerable#minmax_by" do + it "returns an enumerator if no block" do + EnumerableSpecs::Numerous.new(42).minmax_by.should be_an_instance_of(Enumerator) + end + + it "returns nil if #each yields no objects" do + EnumerableSpecs::Empty.new.minmax_by {|o| o.nonesuch }.should == [nil, nil] + end + + it "returns the object for whom the value returned by block is the largest" do + EnumerableSpecs::Numerous.new(*%w[1 2 3]).minmax_by {|obj| obj.to_i }.should == ['1', '3'] + EnumerableSpecs::Numerous.new(*%w[three five]).minmax_by {|obj| obj.length }.should == ['five', 'three'] + end + + it "returns the object that appears first in #each in case of a tie" do + a, b, c, d = '1', '1', '2', '2' + mm = EnumerableSpecs::Numerous.new(a, b, c, d).minmax_by {|obj| obj.to_i } + mm[0].should equal(a) + mm[1].should equal(c) + end + + it "uses min/max.<=>(current) to determine order" do + a, b, c = (1..3).map{|n| EnumerableSpecs::ReverseComparable.new(n)} + + # Just using self here to avoid additional complexity + EnumerableSpecs::Numerous.new(a, b, c).minmax_by {|obj| obj }.should == [c, a] + end + + it "is able to return the maximum for enums that contain nils" do + enum = EnumerableSpecs::Numerous.new(nil, nil, true) + enum.minmax_by {|o| o.nil? ? 0 : 1 }.should == [nil, true] + end + + it "gathers whole arrays as elements when each yields multiple" do + multi = EnumerableSpecs::YieldsMulti.new + multi.minmax_by {|e| e.size}.should == [[1, 2], [6, 7, 8, 9]] + end + + it_behaves_like :enumerable_enumeratorized_with_origin_size, :minmax_by +end diff --git a/spec/ruby/core/enumerable/minmax_spec.rb b/spec/ruby/core/enumerable/minmax_spec.rb new file mode 100644 index 0000000000..f5f17ef079 --- /dev/null +++ b/spec/ruby/core/enumerable/minmax_spec.rb @@ -0,0 +1,20 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative '../../shared/enumerable/minmax' + +describe "Enumerable#minmax" do + before :each do + @enum = EnumerableSpecs::Numerous.new(6, 4, 5, 10, 8) + @empty_enum = EnumerableSpecs::Empty.new + @incomparable_enum = EnumerableSpecs::Numerous.new(BasicObject.new, BasicObject.new) + @incompatible_enum = EnumerableSpecs::Numerous.new(11,"22") + @strs = EnumerableSpecs::Numerous.new("333", "2", "60", "55555", "1010", "111") + end + + it_behaves_like :enumerable_minmax, :minmax + + it "gathers whole arrays as elements when each yields multiple" do + multi = EnumerableSpecs::YieldsMulti.new + multi.minmax.should == [[1, 2], [6, 7, 8, 9]] + end +end diff --git a/spec/ruby/core/enumerable/none_spec.rb b/spec/ruby/core/enumerable/none_spec.rb new file mode 100644 index 0000000000..fb42f13386 --- /dev/null +++ b/spec/ruby/core/enumerable/none_spec.rb @@ -0,0 +1,153 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Enumerable#none?" do + before :each do + @empty = EnumerableSpecs::Empty.new + @enum = EnumerableSpecs::Numerous.new + @enum1 = EnumerableSpecs::Numerous.new(0, 1, 2, -1) + @enum2 = EnumerableSpecs::Numerous.new(nil, false, true) + end + + it "always returns true on empty enumeration" do + @empty.should.none? + @empty.none? { true }.should == true + end + + it "raises an ArgumentError when more than 1 argument is provided" do + -> { @enum.none?(1, 2, 3) }.should raise_error(ArgumentError) + -> { [].none?(1, 2, 3) }.should raise_error(ArgumentError) + -> { {}.none?(1, 2, 3) }.should raise_error(ArgumentError) + end + + it "does not hide exceptions out of #each" do + -> { + EnumerableSpecs::ThrowingEach.new.none? + }.should raise_error(RuntimeError) + + -> { + EnumerableSpecs::ThrowingEach.new.none? { false } + }.should raise_error(RuntimeError) + end + + describe "with no block" do + it "returns true if none of the elements in self are true" do + e = EnumerableSpecs::Numerous.new(false, nil, false) + e.none?.should be_true + end + + it "returns false if at least one of the elements in self are true" do + e = EnumerableSpecs::Numerous.new(false, nil, true, false) + e.none?.should be_false + end + + it "gathers whole arrays as elements when each yields multiple" do + multi = EnumerableSpecs::YieldsMultiWithFalse.new + multi.none?.should be_false + end + end + + describe "with a block" do + before :each do + @e = EnumerableSpecs::Numerous.new(1,1,2,3,4) + end + + it "passes each element to the block in turn until it returns true" do + acc = [] + @e.none? {|e| acc << e; false } + acc.should == [1,1,2,3,4] + end + + it "stops passing elements to the block when it returns true" do + acc = [] + @e.none? {|e| acc << e; e == 3 ? true : false } + acc.should == [1,1,2,3] + end + + it "returns true if the block never returns true" do + @e.none? {|e| false }.should be_true + end + + it "returns false if the block ever returns true" do + @e.none? {|e| e == 3 ? true : false }.should be_false + end + + it "does not hide exceptions out of the block" do + -> { + @enum.none? { raise "from block" } + }.should raise_error(RuntimeError) + end + + it "gathers initial args as elements when each yields multiple" do + multi = EnumerableSpecs::YieldsMulti.new + yielded = [] + multi.none? { |e| yielded << e; false } + yielded.should == [1, 3, 6] + end + + it "yields multiple arguments when each yields multiple" do + multi = EnumerableSpecs::YieldsMulti.new + yielded = [] + multi.none? { |*args| yielded << args; false } + yielded.should == [[1, 2], [3, 4, 5], [6, 7, 8, 9]] + end + end + + describe 'when given a pattern argument' do + it "calls `===` on the pattern the return value " do + pattern = EnumerableSpecs::Pattern.new { |x| x == 3 } + @enum1.none?(pattern).should == true + pattern.yielded.should == [[0], [1], [2], [-1]] + end + + it "always returns true on empty enumeration" do + @empty.none?(Integer).should == true + [].none?(Integer).should == true + {}.none?(NilClass).should == true + end + + it "does not hide exceptions out of #each" do + -> { + EnumerableSpecs::ThrowingEach.new.none?(Integer) + }.should raise_error(RuntimeError) + end + + it "returns true if the pattern never returns a truthy value" do + @enum2.none?(Integer).should == true + pattern = EnumerableSpecs::Pattern.new { |x| nil } + @enum.none?(pattern).should == true + + [1, 42, 3].none?(pattern).should == true + {a: 1, b: 2}.none?(pattern).should == true + end + + it "returns false if the pattern ever returns other than false or nil" do + pattern = EnumerableSpecs::Pattern.new { |x| x < 0 } + @enum1.none?(pattern).should == false + pattern.yielded.should == [[0], [1], [2], [-1]] + + [1, 2, 3, -1].none?(pattern).should == false + {a: 1}.none?(Array).should == false + end + + it "does not hide exceptions out of pattern#===" do + pattern = EnumerableSpecs::Pattern.new { raise "from pattern" } + -> { + @enum.none?(pattern) + }.should raise_error(RuntimeError) + end + + it "calls the pattern with gathered array when yielded with multiple arguments" do + multi = EnumerableSpecs::YieldsMulti.new + pattern = EnumerableSpecs::Pattern.new { false } + multi.none?(pattern).should == true + pattern.yielded.should == [[[1, 2]], [[3, 4, 5]], [[6, 7, 8, 9]]] + end + + it "ignores the block if there is an argument" do + -> { + EnumerableSpecs::Numerous.new(1, 2, 3, 4, 5).none?(String) { true }.should == true + }.should complain(/given block not used/) + end + end +end diff --git a/spec/ruby/core/enumerable/one_spec.rb b/spec/ruby/core/enumerable/one_spec.rb new file mode 100644 index 0000000000..4bf8623d2e --- /dev/null +++ b/spec/ruby/core/enumerable/one_spec.rb @@ -0,0 +1,154 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Enumerable#one?" do + before :each do + @empty = EnumerableSpecs::Empty.new + @enum = EnumerableSpecs::Numerous.new + @enum1 = EnumerableSpecs::Numerous.new(0, 1, 2, -1) + @enum2 = EnumerableSpecs::Numerous.new(nil, false, true) + end + + it "always returns false on empty enumeration" do + @empty.should_not.one? + @empty.one? { true }.should == false + end + + it "raises an ArgumentError when more than 1 argument is provided" do + -> { @enum.one?(1, 2, 3) }.should raise_error(ArgumentError) + -> { [].one?(1, 2, 3) }.should raise_error(ArgumentError) + -> { {}.one?(1, 2, 3) }.should raise_error(ArgumentError) + end + + it "does not hide exceptions out of #each" do + -> { + EnumerableSpecs::ThrowingEach.new.one? + }.should raise_error(RuntimeError) + + -> { + EnumerableSpecs::ThrowingEach.new.one? { false } + }.should raise_error(RuntimeError) + end + + describe "with no block" do + it "returns true if only one element evaluates to true" do + [false, nil, true].one?.should be_true + end + + it "returns false if two elements evaluate to true" do + [false, :value, nil, true].one?.should be_false + end + + it "returns false if all elements evaluate to false" do + [false, nil, false].one?.should be_false + end + + it "gathers whole arrays as elements when each yields multiple" do + multi = EnumerableSpecs::YieldsMultiWithSingleTrue.new + multi.one?.should be_false + end + end + + describe "with a block" do + it "returns true if block returns true once" do + [:a, :b, :c].one? { |s| s == :a }.should be_true + end + + it "returns false if the block returns true more than once" do + [:a, :b, :c].one? { |s| s == :a || s == :b }.should be_false + end + + it "returns false if the block only returns false" do + [:a, :b, :c].one? { |s| s == :d }.should be_false + end + + it "does not hide exceptions out of the block" do + -> { + @enum.one? { raise "from block" } + }.should raise_error(RuntimeError) + end + + it "gathers initial args as elements when each yields multiple" do + multi = EnumerableSpecs::YieldsMulti.new + yielded = [] + multi.one? { |e| yielded << e; false }.should == false + yielded.should == [1, 3, 6] + end + + it "yields multiple arguments when each yields multiple" do + multi = EnumerableSpecs::YieldsMulti.new + yielded = [] + multi.one? { |*args| yielded << args; false }.should == false + yielded.should == [[1, 2], [3, 4, 5], [6, 7, 8, 9]] + end + end + + describe 'when given a pattern argument' do + it "calls `===` on the pattern the return value " do + pattern = EnumerableSpecs::Pattern.new { |x| x == 1 } + @enum1.one?(pattern).should == true + pattern.yielded.should == [[0], [1], [2], [-1]] + end + + it "always returns false on empty enumeration" do + @empty.one?(Integer).should == false + [].one?(Integer).should == false + {}.one?(NilClass).should == false + end + + it "does not hide exceptions out of #each" do + -> { + EnumerableSpecs::ThrowingEach.new.one?(Integer) + }.should raise_error(RuntimeError) + end + + it "returns true if the pattern returns a truthy value only once" do + @enum2.one?(NilClass).should == true + pattern = EnumerableSpecs::Pattern.new { |x| x == 2 } + @enum1.one?(pattern).should == true + + [1, 2, 42, 3].one?(pattern).should == true + + pattern = EnumerableSpecs::Pattern.new { |x| x == [:b, 2] } + {a: 1, b: 2}.one?(pattern).should == true + end + + it "returns false if the pattern returns a truthy value more than once" do + pattern = EnumerableSpecs::Pattern.new { |x| !x } + @enum2.one?(pattern).should == false + pattern.yielded.should == [[nil], [false]] + + [1, 2, 3].one?(Integer).should == false + {a: 1, b: 2}.one?(Array).should == false + end + + it "returns false if the pattern never returns a truthy value" do + pattern = EnumerableSpecs::Pattern.new { |x| nil } + @enum1.one?(pattern).should == false + pattern.yielded.should == [[0], [1], [2], [-1]] + + [1, 2, 3].one?(pattern).should == false + {a: 1}.one?(pattern).should == false + end + + it "does not hide exceptions out of pattern#===" do + pattern = EnumerableSpecs::Pattern.new { raise "from pattern" } + -> { + @enum.one?(pattern) + }.should raise_error(RuntimeError) + end + + it "calls the pattern with gathered array when yielded with multiple arguments" do + multi = EnumerableSpecs::YieldsMulti.new + pattern = EnumerableSpecs::Pattern.new { false } + multi.one?(pattern).should == false + pattern.yielded.should == [[[1, 2]], [[3, 4, 5]], [[6, 7, 8, 9]]] + end + + it "ignores the block if there is an argument" do + -> { + EnumerableSpecs::Numerous.new(1, 2, 3, 4, "5").one?(String) { false }.should == true + }.should complain(/given block not used/) + end + end +end diff --git a/spec/ruby/core/enumerable/partition_spec.rb b/spec/ruby/core/enumerable/partition_spec.rb new file mode 100644 index 0000000000..d3d220b4b4 --- /dev/null +++ b/spec/ruby/core/enumerable/partition_spec.rb @@ -0,0 +1,20 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/enumerable_enumeratorized' + +describe "Enumerable#partition" do + it "returns two arrays, the first containing elements for which the block is true, the second containing the rest" do + EnumerableSpecs::Numerous.new.partition { |i| i % 2 == 0 }.should == [[2, 6, 4], [5, 3, 1]] + end + + it "returns an Enumerator if called without a block" do + EnumerableSpecs::Numerous.new.partition.should be_an_instance_of(Enumerator) + end + + it "gathers whole arrays as elements when each yields multiple" do + multi = EnumerableSpecs::YieldsMulti.new + multi.partition {|e| e == [3, 4, 5] }.should == [[[3, 4, 5]], [[1, 2], [6, 7, 8, 9]]] + end + + it_behaves_like :enumerable_enumeratorized_with_origin_size, :partition +end diff --git a/spec/ruby/core/enumerable/reduce_spec.rb b/spec/ruby/core/enumerable/reduce_spec.rb new file mode 100644 index 0000000000..bc8691c1b0 --- /dev/null +++ b/spec/ruby/core/enumerable/reduce_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/inject' + +describe "Enumerable#reduce" do + it_behaves_like :enumerable_inject, :reduce +end diff --git a/spec/ruby/core/enumerable/reject_spec.rb b/spec/ruby/core/enumerable/reject_spec.rb new file mode 100644 index 0000000000..0d86b49ea2 --- /dev/null +++ b/spec/ruby/core/enumerable/reject_spec.rb @@ -0,0 +1,25 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/enumerable_enumeratorized' + +describe "Enumerable#reject" do + it "returns an array of the elements for which block is false" do + EnumerableSpecs::Numerous.new.reject { |i| i > 3 }.should == [2, 3, 1] + entries = (1..10).to_a + numerous = EnumerableSpecs::Numerous.new(*entries) + numerous.reject {|i| i % 2 == 0 }.should == [1,3,5,7,9] + numerous.reject {|i| true }.should == [] + numerous.reject {|i| false }.should == entries + end + + it "returns an Enumerator if called without a block" do + EnumerableSpecs::Numerous.new.reject.should be_an_instance_of(Enumerator) + end + + it "gathers whole arrays as elements when each yields multiple" do + multi = EnumerableSpecs::YieldsMulti.new + multi.reject {|e| e == [3, 4, 5] }.should == [[1, 2], [6, 7, 8, 9]] + end + + it_behaves_like :enumerable_enumeratorized_with_origin_size, :reject +end diff --git a/spec/ruby/core/enumerable/reverse_each_spec.rb b/spec/ruby/core/enumerable/reverse_each_spec.rb new file mode 100644 index 0000000000..2b1c233488 --- /dev/null +++ b/spec/ruby/core/enumerable/reverse_each_spec.rb @@ -0,0 +1,26 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/enumerable_enumeratorized' + +describe "Enumerable#reverse_each" do + it "traverses enum in reverse order and pass each element to block" do + a=[] + EnumerableSpecs::Numerous.new.reverse_each { |i| a << i } + a.should == [4, 1, 6, 3, 5, 2] + end + + it "returns an Enumerator if no block given" do + enum = EnumerableSpecs::Numerous.new.reverse_each + enum.should be_an_instance_of(Enumerator) + enum.to_a.should == [4, 1, 6, 3, 5, 2] + end + + it "gathers whole arrays as elements when each yields multiple" do + multi = EnumerableSpecs::YieldsMulti.new + yielded = [] + multi.reverse_each {|e| yielded << e } + yielded.should == [[6, 7, 8, 9], [3, 4, 5], [1, 2]] + end + + it_behaves_like :enumerable_enumeratorized_with_origin_size, :reverse_each +end diff --git a/spec/ruby/core/enumerable/select_spec.rb b/spec/ruby/core/enumerable/select_spec.rb new file mode 100644 index 0000000000..7fc61926f9 --- /dev/null +++ b/spec/ruby/core/enumerable/select_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/find_all' + +describe "Enumerable#select" do + it_behaves_like :enumerable_find_all, :select +end diff --git a/spec/ruby/core/enumerable/shared/collect.rb b/spec/ruby/core/enumerable/shared/collect.rb new file mode 100644 index 0000000000..6df1a616eb --- /dev/null +++ b/spec/ruby/core/enumerable/shared/collect.rb @@ -0,0 +1,107 @@ +require_relative 'enumerable_enumeratorized' + +describe :enumerable_collect, shared: true do + before :each do + ScratchPad.record [] + end + + it "returns a new array with the results of passing each element to block" do + entries = [0, 1, 3, 4, 5, 6] + numerous = EnumerableSpecs::Numerous.new(*entries) + numerous.send(@method) { |i| i % 2 }.should == [0, 1, 1, 0, 1, 0] + numerous.send(@method) { |i| i }.should == entries + end + + it "passes through the values yielded by #each_with_index" do + [:a, :b].each_with_index.send(@method) { |x, i| ScratchPad << [x, i]; nil } + ScratchPad.recorded.should == [[:a, 0], [:b, 1]] + end + + it "gathers initial args as elements when each yields multiple" do + multi = EnumerableSpecs::YieldsMulti.new + multi.send(@method) {|e| e}.should == [1,3,6] + end + + it "only yields increasing values for a Range" do + (1..0).send(@method) { |x| x }.should == [] + (1..1).send(@method) { |x| x }.should == [1] + (1..2).send(@method) { |x| x }.should == [1, 2] + end + + it "returns an enumerator when no block given" do + enum = EnumerableSpecs::Numerous.new.send(@method) + enum.should be_an_instance_of(Enumerator) + enum.each { |i| -i }.should == [-2, -5, -3, -6, -1, -4] + end + + it "reports the same arity as the given block" do + entries = [0, 1, 3, 4, 5, 6] + numerous = EnumerableSpecs::Numerous.new(*entries) + + def numerous.each(&block) + ScratchPad << block.arity + super + end + + numerous.send(@method) { |a, b| a % 2 }.should == [0, 1, 1, 0, 1, 0] + ScratchPad.recorded.should == [2] + ScratchPad.clear + ScratchPad.record [] + numerous.send(@method) { |i| i }.should == entries + ScratchPad.recorded.should == [1] + end + + it "yields an Array of 2 elements for a Hash when block arity is 1" do + c = Class.new do + def register(a) + ScratchPad << a + end + end + m = c.new.method(:register) + + ScratchPad.record [] + { 1 => 'a', 2 => 'b' }.map(&m) + ScratchPad.recorded.should == [[1, 'a'], [2, 'b']] + end + + it "yields 2 arguments for a Hash when block arity is 2" do + c = Class.new do + def register(a, b) + ScratchPad << [a, b] + end + end + m = c.new.method(:register) + + ScratchPad.record [] + { 1 => 'a', 2 => 'b' }.map(&m) + ScratchPad.recorded.should == [[1, 'a'], [2, 'b']] + end + + it "raises an error for a Hash when an arity enforcing block of arity >2 is passed in" do + c = Class.new do + def register(a, b, c) + end + end + m = c.new.method(:register) + + -> do + { 1 => 'a', 2 => 'b' }.map(&m) + end.should raise_error(ArgumentError) + end + + it "calls the each method on sub-classes" do + c = Class.new(Hash) do + def each + ScratchPad << 'in each' + super + end + end + h = c.new + h[1] = 'a' + ScratchPad.record [] + h.send(@method) { |k,v| v } + ScratchPad.recorded.should == ['in each'] + end + + it_should_behave_like :enumerable_enumeratorized_with_origin_size +end diff --git a/spec/ruby/core/enumerable/shared/collect_concat.rb b/spec/ruby/core/enumerable/shared/collect_concat.rb new file mode 100644 index 0000000000..ddd431baeb --- /dev/null +++ b/spec/ruby/core/enumerable/shared/collect_concat.rb @@ -0,0 +1,54 @@ +require_relative 'enumerable_enumeratorized' + +describe :enumerable_collect_concat, shared: true do + it "yields elements to the block and flattens one level" do + numerous = EnumerableSpecs::Numerous.new(1, [2, 3], [4, [5, 6]], {foo: :bar}) + numerous.send(@method) { |i| i }.should == [1, 2, 3, 4, [5, 6], {foo: :bar}] + end + + it "appends non-Array elements that do not define #to_ary" do + obj = mock("to_ary undefined") + + numerous = EnumerableSpecs::Numerous.new(1, obj, 2) + numerous.send(@method) { |i| i }.should == [1, obj, 2] + end + + it "concatenates the result of calling #to_ary if it returns an Array" do + obj = mock("to_ary defined") + obj.should_receive(:to_ary).and_return([:a, :b]) + + numerous = EnumerableSpecs::Numerous.new(1, obj, 2) + numerous.send(@method) { |i| i }.should == [1, :a, :b, 2] + end + + it "does not call #to_a" do + obj = mock("to_ary undefined") + obj.should_not_receive(:to_a) + + numerous = EnumerableSpecs::Numerous.new(1, obj, 2) + numerous.send(@method) { |i| i }.should == [1, obj, 2] + end + + it "appends an element that defines #to_ary that returns nil" do + obj = mock("to_ary defined") + obj.should_receive(:to_ary).and_return(nil) + + numerous = EnumerableSpecs::Numerous.new(1, obj, 2) + numerous.send(@method) { |i| i }.should == [1, obj, 2] + end + + it "raises a TypeError if an element defining #to_ary does not return an Array or nil" do + obj = mock("to_ary defined") + obj.should_receive(:to_ary).and_return("array") + + -> { [1, obj, 3].send(@method) { |i| i } }.should raise_error(TypeError) + end + + it "returns an enumerator when no block given" do + enum = EnumerableSpecs::Numerous.new(1, 2).send(@method) + enum.should be_an_instance_of(Enumerator) + enum.each{ |i| [i] * i }.should == [1, 2, 2] + end + + it_should_behave_like :enumerable_enumeratorized_with_origin_size +end diff --git a/spec/ruby/core/enumerable/shared/entries.rb b/spec/ruby/core/enumerable/shared/entries.rb new file mode 100644 index 0000000000..e32eb23d2a --- /dev/null +++ b/spec/ruby/core/enumerable/shared/entries.rb @@ -0,0 +1,16 @@ +describe :enumerable_entries, shared: true do + it "returns an array containing the elements" do + numerous = EnumerableSpecs::Numerous.new(1, nil, 'a', 2, false, true) + numerous.send(@method).should == [1, nil, "a", 2, false, true] + end + + it "passes through the values yielded by #each_with_index" do + [:a, :b].each_with_index.send(@method).should == [[:a, 0], [:b, 1]] + end + + it "passes arguments to each" do + count = EnumerableSpecs::EachCounter.new(1, 2, 3) + count.send(@method, :hello, "world").should == [1, 2, 3] + count.arguments_passed.should == [:hello, "world"] + end +end diff --git a/spec/ruby/core/enumerable/shared/enumerable_enumeratorized.rb b/spec/ruby/core/enumerable/shared/enumerable_enumeratorized.rb new file mode 100644 index 0000000000..e2bbe18eda --- /dev/null +++ b/spec/ruby/core/enumerable/shared/enumerable_enumeratorized.rb @@ -0,0 +1,33 @@ +require_relative 'enumeratorized' + +describe :enumerable_enumeratorized_with_unknown_size, shared: true do + describe "Enumerable with size" do + before :all do + @object = EnumerableSpecs::NumerousWithSize.new(1, 2, 3, 4) + end + it_should_behave_like :enumeratorized_with_unknown_size + end + + describe "Enumerable with no size" do + before :all do + @object = EnumerableSpecs::Numerous.new(1, 2, 3, 4) + end + it_should_behave_like :enumeratorized_with_unknown_size + end +end + +describe :enumerable_enumeratorized_with_origin_size, shared: true do + describe "Enumerable with size" do + before :all do + @object = EnumerableSpecs::NumerousWithSize.new(1, 2, 3, 4) + end + it_should_behave_like :enumeratorized_with_origin_size + end + + describe "Enumerable with no size" do + before :all do + @object = EnumerableSpecs::Numerous.new(1, 2, 3, 4) + end + it_should_behave_like :enumeratorized_with_unknown_size + end +end diff --git a/spec/ruby/core/enumerable/shared/enumeratorized.rb b/spec/ruby/core/enumerable/shared/enumeratorized.rb new file mode 100644 index 0000000000..05d27b5783 --- /dev/null +++ b/spec/ruby/core/enumerable/shared/enumeratorized.rb @@ -0,0 +1,42 @@ +describe :enumeratorized_with_unknown_size, shared: true do + describe "when no block is given" do + describe "returned Enumerator" do + it "size returns nil" do + @object.send(*@method).size.should == nil + end + end + end +end + +describe :enumeratorized_with_origin_size, shared: true do + describe "when no block is given" do + describe "returned Enumerator" do + it "size returns the enumerable size" do + @object.send(*@method).size.should == @object.size + end + end + end +end + +describe :enumeratorized_with_cycle_size, shared: true do + describe "when no block is given" do + describe "returned Enumerator" do + describe "size" do + it "should be the result of multiplying the enumerable size by the argument passed" do + @object.cycle(2).size.should == @object.size * 2 + @object.cycle(7).size.should == @object.size * 7 + @object.cycle(0).size.should == 0 + @empty_object.cycle(2).size.should == 0 + end + + it "should be zero when the argument passed is 0 or less" do + @object.cycle(-1).size.should == 0 + end + + it "should be Float::INFINITY when no argument is passed" do + @object.cycle.size.should == Float::INFINITY + end + end + end + end +end diff --git a/spec/ruby/core/enumerable/shared/find.rb b/spec/ruby/core/enumerable/shared/find.rb new file mode 100644 index 0000000000..61d63ba3d5 --- /dev/null +++ b/spec/ruby/core/enumerable/shared/find.rb @@ -0,0 +1,77 @@ +require_relative 'enumerable_enumeratorized' + +describe :enumerable_find, shared: true do + # #detect and #find are aliases, so we only need one function + before :each do + ScratchPad.record [] + @elements = [2, 4, 6, 8, 10] + @numerous = EnumerableSpecs::Numerous.new(*@elements) + @empty = [] + end + + it "passes each entry in enum to block while block when block is false" do + visited_elements = [] + @numerous.send(@method) do |element| + visited_elements << element + false + end + visited_elements.should == @elements + end + + it "returns nil when the block is false and there is no ifnone proc given" do + @numerous.send(@method) {|e| false }.should == nil + end + + it "returns the first element for which the block is not false" do + @elements.each do |element| + @numerous.send(@method) {|e| e > element - 1 }.should == element + end + end + + it "returns the value of the ifnone proc if the block is false" do + fail_proc = -> { "cheeseburgers" } + @numerous.send(@method, fail_proc) {|e| false }.should == "cheeseburgers" + end + + it "doesn't call the ifnone proc if an element is found" do + fail_proc = -> { raise "This shouldn't have been called" } + @numerous.send(@method, fail_proc) {|e| e == @elements.first }.should == 2 + end + + it "calls the ifnone proc only once when the block is false" do + times = 0 + fail_proc = -> { times += 1; raise if times > 1; "cheeseburgers" } + @numerous.send(@method, fail_proc) {|e| false }.should == "cheeseburgers" + end + + it "calls the ifnone proc when there are no elements" do + fail_proc = -> { "yay" } + @empty.send(@method, fail_proc) {|e| true}.should == "yay" + end + + it "ignores the ifnone argument when nil" do + @numerous.send(@method, nil) {|e| false }.should == nil + end + + it "passes through the values yielded by #each_with_index" do + [:a, :b].each_with_index.send(@method) { |x, i| ScratchPad << [x, i]; nil } + ScratchPad.recorded.should == [[:a, 0], [:b, 1]] + end + + it "returns an enumerator when no block given" do + @numerous.send(@method).should be_an_instance_of(Enumerator) + end + + it "passes the ifnone proc to the enumerator" do + times = 0 + fail_proc = -> { times += 1; raise if times > 1; "cheeseburgers" } + @numerous.send(@method, fail_proc).each {|e| false }.should == "cheeseburgers" + end + + it "gathers whole arrays as elements when each yields multiple" do + multi = EnumerableSpecs::YieldsMulti.new + multi.send(@method) {|e| e == [1, 2] }.should == [1, 2] + end + + it_should_behave_like :enumerable_enumeratorized_with_unknown_size +end diff --git a/spec/ruby/core/enumerable/shared/find_all.rb b/spec/ruby/core/enumerable/shared/find_all.rb new file mode 100644 index 0000000000..1bbe71f372 --- /dev/null +++ b/spec/ruby/core/enumerable/shared/find_all.rb @@ -0,0 +1,31 @@ +require_relative 'enumerable_enumeratorized' + +describe :enumerable_find_all, shared: true do + before :each do + ScratchPad.record [] + @elements = (1..10).to_a + @numerous = EnumerableSpecs::Numerous.new(*@elements) + end + + it "returns all elements for which the block is not false" do + @numerous.send(@method) {|i| i % 3 == 0 }.should == [3, 6, 9] + @numerous.send(@method) {|i| true }.should == @elements + @numerous.send(@method) {|i| false }.should == [] + end + + it "returns an enumerator when no block given" do + @numerous.send(@method).should be_an_instance_of(Enumerator) + end + + it "passes through the values yielded by #each_with_index" do + [:a, :b].each_with_index.send(@method) { |x, i| ScratchPad << [x, i] } + ScratchPad.recorded.should == [[:a, 0], [:b, 1]] + end + + it "gathers whole arrays as elements when each yields multiple" do + multi = EnumerableSpecs::YieldsMulti.new + multi.send(@method) {|e| e == [3, 4, 5] }.should == [[3, 4, 5]] + end + + it_should_behave_like :enumerable_enumeratorized_with_origin_size +end diff --git a/spec/ruby/core/enumerable/shared/include.rb b/spec/ruby/core/enumerable/shared/include.rb new file mode 100644 index 0000000000..569f350fd5 --- /dev/null +++ b/spec/ruby/core/enumerable/shared/include.rb @@ -0,0 +1,34 @@ +describe :enumerable_include, shared: true do + it "returns true if any element == argument for numbers" do + class EnumerableSpecIncludeP; def ==(obj) obj == 5; end; end + + elements = (0..5).to_a + EnumerableSpecs::Numerous.new(*elements).send(@method,5).should == true + EnumerableSpecs::Numerous.new(*elements).send(@method,10).should == false + EnumerableSpecs::Numerous.new(*elements).send(@method,EnumerableSpecIncludeP.new).should == true + end + + it "returns true if any element == argument for other objects" do + class EnumerableSpecIncludeP11; def ==(obj); obj == '11'; end; end + + elements = ('0'..'5').to_a + [EnumerableSpecIncludeP11.new] + EnumerableSpecs::Numerous.new(*elements).send(@method,'5').should == true + EnumerableSpecs::Numerous.new(*elements).send(@method,'10').should == false + EnumerableSpecs::Numerous.new(*elements).send(@method,EnumerableSpecIncludeP11.new).should == true + EnumerableSpecs::Numerous.new(*elements).send(@method,'11').should == true + end + + + it "returns true if any member of enum equals obj when == compare different classes (legacy rubycon)" do + # equality is tested with == + EnumerableSpecs::Numerous.new(2,4,6,8,10).send(@method, 2.0).should == true + EnumerableSpecs::Numerous.new(2,4,[6,8],10).send(@method, [6, 8]).should == true + EnumerableSpecs::Numerous.new(2,4,[6,8],10).send(@method, [6.0, 8.0]).should == true + end + + it "gathers whole arrays as elements when each yields multiple" do + multi = EnumerableSpecs::YieldsMulti.new + multi.send(@method, [1,2]).should be_true + end + +end diff --git a/spec/ruby/core/enumerable/shared/inject.rb b/spec/ruby/core/enumerable/shared/inject.rb new file mode 100644 index 0000000000..8fb7e98c2b --- /dev/null +++ b/spec/ruby/core/enumerable/shared/inject.rb @@ -0,0 +1,142 @@ +require_relative '../../array/shared/iterable_and_tolerating_size_increasing' + +describe :enumerable_inject, shared: true do + it "with argument takes a block with an accumulator (with argument as initial value) and the current element. Value of block becomes new accumulator" do + a = [] + EnumerableSpecs::Numerous.new.send(@method, 0) { |memo, i| a << [memo, i]; i } + a.should == [[0, 2], [2, 5], [5, 3], [3, 6], [6, 1], [1, 4]] + EnumerableSpecs::EachDefiner.new(true, true, true).send(@method, nil) {|result, i| i && result}.should == nil + end + + it "produces an array of the accumulator and the argument when given a block with a *arg" do + a = [] + [1,2].send(@method, 0) {|*args| a << args; args[0] + args[1]} + a.should == [[0, 1], [1, 2]] + end + + it "can take two argument" do + EnumerableSpecs::Numerous.new(1, 2, 3).send(@method, 10, :-).should == 4 + EnumerableSpecs::Numerous.new(1, 2, 3).send(@method, 10, "-").should == 4 + + [1, 2, 3].send(@method, 10, :-).should == 4 + [1, 2, 3].send(@method, 10, "-").should == 4 + end + + it "converts non-Symbol method name argument to String with #to_str if two arguments" do + name = Object.new + def name.to_str; "-"; end + + EnumerableSpecs::Numerous.new(1, 2, 3).send(@method, 10, name).should == 4 + [1, 2, 3].send(@method, 10, name).should == 4 + end + + it "raises TypeError when the second argument is not Symbol or String and it cannot be converted to String if two arguments" do + -> { EnumerableSpecs::Numerous.new(1, 2, 3).send(@method, 10, Object.new) }.should raise_error(TypeError, /is not a symbol nor a string/) + -> { [1, 2, 3].send(@method, 10, Object.new) }.should raise_error(TypeError, /is not a symbol nor a string/) + end + + it "ignores the block if two arguments" do + -> { + EnumerableSpecs::Numerous.new(1, 2, 3).send(@method, 10, :-) { raise "we never get here"}.should == 4 + }.should complain(/#{__FILE__}:#{__LINE__-1}: warning: given block not used/, verbose: true) + + -> { + [1, 2, 3].send(@method, 10, :-) { raise "we never get here"}.should == 4 + }.should complain(/#{__FILE__}:#{__LINE__-1}: warning: given block not used/, verbose: true) + end + + it "does not warn when given a Symbol with $VERBOSE true" do + -> { + [1, 2].send(@method, 0, :+) + [1, 2].send(@method, :+) + EnumerableSpecs::Numerous.new(1, 2).send(@method, 0, :+) + EnumerableSpecs::Numerous.new(1, 2).send(@method, :+) + }.should_not complain(verbose: true) + end + + it "can take a symbol argument" do + EnumerableSpecs::Numerous.new(10, 1, 2, 3).send(@method, :-).should == 4 + [10, 1, 2, 3].send(@method, :-).should == 4 + end + + it "can take a String argument" do + EnumerableSpecs::Numerous.new(10, 1, 2, 3).send(@method, "-").should == 4 + [10, 1, 2, 3].send(@method, "-").should == 4 + end + + it "converts non-Symbol method name argument to String with #to_str" do + name = Object.new + def name.to_str; "-"; end + + EnumerableSpecs::Numerous.new(10, 1, 2, 3).send(@method, name).should == 4 + [10, 1, 2, 3].send(@method, name).should == 4 + end + + it "raises TypeError when passed not Symbol or String method name argument and it cannot be converted to String" do + -> { EnumerableSpecs::Numerous.new(10, 1, 2, 3).send(@method, Object.new) }.should raise_error(TypeError, /is not a symbol nor a string/) + -> { [10, 1, 2, 3].send(@method, Object.new) }.should raise_error(TypeError, /is not a symbol nor a string/) + end + + it "without argument takes a block with an accumulator (with first element as initial value) and the current element. Value of block becomes new accumulator" do + a = [] + EnumerableSpecs::Numerous.new.send(@method) { |memo, i| a << [memo, i]; i } + a.should == [[2, 5], [5, 3], [3, 6], [6, 1], [1, 4]] + end + + it "gathers whole arrays as elements when each yields multiple" do + multi = EnumerableSpecs::YieldsMulti.new + multi.send(@method, []) {|acc, e| acc << e }.should == [[1, 2], [3, 4, 5], [6, 7, 8, 9]] + end + + it "with inject arguments(legacy rubycon)" do + # with inject argument + EnumerableSpecs::EachDefiner.new().send(@method, 1) {|acc,x| 999 }.should == 1 + EnumerableSpecs::EachDefiner.new(2).send(@method, 1) {|acc,x| 999 }.should == 999 + EnumerableSpecs::EachDefiner.new(2).send(@method, 1) {|acc,x| acc }.should == 1 + EnumerableSpecs::EachDefiner.new(2).send(@method, 1) {|acc,x| x }.should == 2 + + EnumerableSpecs::EachDefiner.new(1,2,3,4).send(@method, 100) {|acc,x| acc + x }.should == 110 + EnumerableSpecs::EachDefiner.new(1,2,3,4).send(@method, 100) {|acc,x| acc * x }.should == 2400 + + EnumerableSpecs::EachDefiner.new('a','b','c').send(@method, "z") {|result, i| i+result}.should == "cbaz" + end + + it "without inject arguments(legacy rubycon)" do + # no inject argument + EnumerableSpecs::EachDefiner.new(2).send(@method) {|acc,x| 999 }.should == 2 + EnumerableSpecs::EachDefiner.new(2).send(@method) {|acc,x| acc }.should == 2 + EnumerableSpecs::EachDefiner.new(2).send(@method) {|acc,x| x }.should == 2 + + EnumerableSpecs::EachDefiner.new(1,2,3,4).send(@method) {|acc,x| acc + x }.should == 10 + EnumerableSpecs::EachDefiner.new(1,2,3,4).send(@method) {|acc,x| acc * x }.should == 24 + + EnumerableSpecs::EachDefiner.new('a','b','c').send(@method) {|result, i| i+result}.should == "cba" + EnumerableSpecs::EachDefiner.new(3, 4, 5).send(@method) {|result, i| result*i}.should == 60 + EnumerableSpecs::EachDefiner.new([1], 2, 'a','b').send(@method){|r,i| r<<i}.should == [1, 2, 'a', 'b'] + end + + it "returns nil when fails(legacy rubycon)" do + EnumerableSpecs::EachDefiner.new().send(@method) {|acc,x| 999 }.should == nil + end + + it "tolerates increasing a collection size during iterating Array" do + array = [:a, :b, :c] + ScratchPad.record [] + i = 0 + + array.send(@method, nil) do |_, e| + ScratchPad << e + array << i if i < 100 + i += 1 + end + + actual = ScratchPad.recorded + expected = [:a, :b, :c] + (0..99).to_a + actual.sort_by(&:to_s).should == expected.sort_by(&:to_s) + end + + it "raises an ArgumentError when no parameters or block is given" do + -> { [1,2].send(@method) }.should raise_error(ArgumentError) + -> { {one: 1, two: 2}.send(@method) }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/enumerable/shared/take.rb b/spec/ruby/core/enumerable/shared/take.rb new file mode 100644 index 0000000000..ce2ace20fa --- /dev/null +++ b/spec/ruby/core/enumerable/shared/take.rb @@ -0,0 +1,63 @@ +describe :enumerable_take, shared: true do + before :each do + @values = [4,3,2,1,0,-1] + @enum = EnumerableSpecs::Numerous.new(*@values) + end + + it "returns the first count elements if given a count" do + @enum.send(@method, 2).should == [4, 3] + @enum.send(@method, 4).should == [4, 3, 2, 1] # See redmine #1686 ! + end + + it "returns an empty array when passed count on an empty array" do + empty = EnumerableSpecs::Empty.new + empty.send(@method, 0).should == [] + empty.send(@method, 1).should == [] + empty.send(@method, 2).should == [] + end + + it "returns an empty array when passed count == 0" do + @enum.send(@method, 0).should == [] + end + + it "returns an array containing the first element when passed count == 1" do + @enum.send(@method, 1).should == [4] + end + + it "raises an ArgumentError when count is negative" do + -> { @enum.send(@method, -1) }.should raise_error(ArgumentError) + end + + it "returns the entire array when count > length" do + @enum.send(@method, 100).should == @values + @enum.send(@method, 8).should == @values # See redmine #1686 ! + end + + it "tries to convert the passed argument to an Integer using #to_int" do + obj = mock('to_int') + obj.should_receive(:to_int).and_return(3).at_most(:twice) # called twice, no apparent reason. See redmine #1554 + @enum.send(@method, obj).should == [4, 3, 2] + end + + it "raises a TypeError if the passed argument is not numeric" do + -> { @enum.send(@method, nil) }.should raise_error(TypeError) + -> { @enum.send(@method, "a") }.should raise_error(TypeError) + + obj = mock("nonnumeric") + -> { @enum.send(@method, obj) }.should raise_error(TypeError) + end + + it "gathers whole arrays as elements when each yields multiple" do + multi = EnumerableSpecs::YieldsMulti.new + multi.send(@method, 1).should == [[1, 2]] + end + + it "consumes only what is needed" do + thrower = EnumerableSpecs::ThrowingEach.new + thrower.send(@method, 0).should == [] + counter = EnumerableSpecs::EachCounter.new(1,2,3,4) + counter.send(@method, 2).should == [1,2] + counter.times_called.should == 1 + counter.times_yielded.should == 2 + end +end diff --git a/spec/ruby/core/enumerable/slice_after_spec.rb b/spec/ruby/core/enumerable/slice_after_spec.rb new file mode 100644 index 0000000000..0e46688db1 --- /dev/null +++ b/spec/ruby/core/enumerable/slice_after_spec.rb @@ -0,0 +1,61 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Enumerable#slice_after" do + before :each do + @enum = EnumerableSpecs::Numerous.new(7, 6, 5, 4, 3, 2, 1) + end + + describe "when given an argument and no block" do + it "calls === on the argument to determine when to yield" do + arg = mock("filter") + arg.should_receive(:===).and_return(false, true, false, false, false, true, false) + e = @enum.slice_after(arg) + e.should be_an_instance_of(Enumerator) + e.to_a.should == [[7, 6], [5, 4, 3, 2], [1]] + end + + it "doesn't yield an empty array if the filter matches the first entry or the last entry" do + arg = mock("filter") + arg.should_receive(:===).and_return(true).exactly(7) + e = @enum.slice_after(arg) + e.to_a.should == [[7], [6], [5], [4], [3], [2], [1]] + end + + it "uses standard boolean as a test" do + arg = mock("filter") + arg.should_receive(:===).and_return(false, :foo, nil, false, false, 42, false) + e = @enum.slice_after(arg) + e.to_a.should == [[7, 6], [5, 4, 3, 2], [1]] + end + end + + describe "when given a block" do + describe "and no argument" do + it "calls the block to determine when to yield" do + e = @enum.slice_after{ |i| i == 6 || i == 2 } + e.should be_an_instance_of(Enumerator) + e.to_a.should == [[7, 6], [5, 4, 3, 2], [1]] + end + end + + describe "and an argument" do + it "raises an ArgumentError" do + -> { @enum.slice_after(42) { |i| i == 6 } }.should raise_error(ArgumentError) + end + end + end + + it "raises an ArgumentError when given an incorrect number of arguments" do + -> { @enum.slice_after("one", "two") }.should raise_error(ArgumentError) + -> { @enum.slice_after }.should raise_error(ArgumentError) + end +end + +describe "when an iterator method yields more than one value" do + it "processes all yielded values" do + enum = EnumerableSpecs::YieldsMulti.new + result = enum.slice_after { |i| i == [3, 4, 5] }.to_a + result.should == [[[1, 2], [3, 4, 5]], [[6, 7, 8, 9]]] + end +end diff --git a/spec/ruby/core/enumerable/slice_before_spec.rb b/spec/ruby/core/enumerable/slice_before_spec.rb new file mode 100644 index 0000000000..f9b33f7b28 --- /dev/null +++ b/spec/ruby/core/enumerable/slice_before_spec.rb @@ -0,0 +1,64 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/enumerable_enumeratorized' + +describe "Enumerable#slice_before" do + before :each do + @enum = EnumerableSpecs::Numerous.new(7,6,5,4,3,2,1) + end + + describe "when given an argument and no block" do + it "calls === on the argument to determine when to yield" do + arg = mock "filter" + arg.should_receive(:===).and_return(false, true, false, false, false, true, false) + e = @enum.slice_before(arg) + e.should be_an_instance_of(Enumerator) + e.to_a.should == [[7], [6, 5, 4, 3], [2, 1]] + end + + it "doesn't yield an empty array if the filter matches the first entry or the last entry" do + arg = mock "filter" + arg.should_receive(:===).and_return(true).exactly(7) + e = @enum.slice_before(arg) + e.to_a.should == [[7], [6], [5], [4], [3], [2], [1]] + end + + it "uses standard boolean as a test" do + arg = mock "filter" + arg.should_receive(:===).and_return(false, :foo, nil, false, false, 42, false) + e = @enum.slice_before(arg) + e.to_a.should == [[7], [6, 5, 4, 3], [2, 1]] + end + end + + describe "when given a block" do + describe "and no argument" do + it "calls the block to determine when to yield" do + e = @enum.slice_before{|i| i == 6 || i == 2} + e.should be_an_instance_of(Enumerator) + e.to_a.should == [[7], [6, 5, 4, 3], [2, 1]] + end + end + + it "does not accept arguments" do + -> { + @enum.slice_before(1) {} + }.should raise_error(ArgumentError) + end + end + + it "raises an ArgumentError when given an incorrect number of arguments" do + -> { @enum.slice_before("one", "two") }.should raise_error(ArgumentError) + -> { @enum.slice_before }.should raise_error(ArgumentError) + end + + describe "when an iterator method yields more than one value" do + it "processes all yielded values" do + enum = EnumerableSpecs::YieldsMulti.new + result = enum.slice_before { |i| i == [3, 4, 5] }.to_a + result.should == [[[1, 2]], [[3, 4, 5], [6, 7, 8, 9]]] + end + end + + it_behaves_like :enumerable_enumeratorized_with_unknown_size, [:slice_before, 3] +end diff --git a/spec/ruby/core/enumerable/slice_when_spec.rb b/spec/ruby/core/enumerable/slice_when_spec.rb new file mode 100644 index 0000000000..6b8ea0923e --- /dev/null +++ b/spec/ruby/core/enumerable/slice_when_spec.rb @@ -0,0 +1,54 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Enumerable#slice_when" do + before :each do + ary = [10, 9, 7, 6, 4, 3, 2, 1] + @enum = EnumerableSpecs::Numerous.new(*ary) + @result = @enum.slice_when { |i, j| i - 1 != j } + @enum_length = ary.length + end + + context "when given a block" do + it "returns an enumerator" do + @result.should be_an_instance_of(Enumerator) + end + + it "splits chunks between adjacent elements i and j where the block returns true" do + @result.to_a.should == [[10, 9], [7, 6], [4, 3, 2, 1]] + end + + it "calls the block for length of the receiver enumerable minus one times" do + times_called = 0 + @enum.slice_when do |i, j| + times_called += 1 + i - 1 != j + end.to_a + times_called.should == (@enum_length - 1) + end + + it "doesn't yield an empty array if the block matches the first or the last time" do + @enum.slice_when { true }.to_a.should == [[10], [9], [7], [6], [4], [3], [2], [1]] + end + + it "doesn't yield an empty array on a small enumerable" do + EnumerableSpecs::Empty.new.slice_when { raise }.to_a.should == [] + EnumerableSpecs::Numerous.new(42).slice_when { raise }.to_a.should == [[42]] + end + end + + context "when not given a block" do + it "raises an ArgumentError" do + -> { @enum.slice_when }.should raise_error(ArgumentError) + end + end + + describe "when an iterator method yields more than one value" do + it "processes all yielded values" do + def foo + yield 1, 2 + end + to_enum(:foo).slice_when { true }.to_a.should == [[[1, 2]]] + end + end +end diff --git a/spec/ruby/core/enumerable/sort_by_spec.rb b/spec/ruby/core/enumerable/sort_by_spec.rb new file mode 100644 index 0000000000..8fdd923fb4 --- /dev/null +++ b/spec/ruby/core/enumerable/sort_by_spec.rb @@ -0,0 +1,43 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/enumerable_enumeratorized' + +describe "Enumerable#sort_by" do + it "returns an array of elements ordered by the result of block" do + a = EnumerableSpecs::Numerous.new("once", "upon", "a", "time") + a.sort_by { |i| i[0] }.should == ["a", "once", "time", "upon"] + end + + it "sorts the object by the given attribute" do + a = EnumerableSpecs::SortByDummy.new("fooo") + b = EnumerableSpecs::SortByDummy.new("bar") + + ar = [a, b].sort_by { |d| d.s } + ar.should == [b, a] + end + + it "returns an Enumerator when a block is not supplied" do + a = EnumerableSpecs::Numerous.new("a","b") + a.sort_by.should be_an_instance_of(Enumerator) + a.to_a.should == ["a", "b"] + end + + it "gathers whole arrays as elements when each yields multiple" do + multi = EnumerableSpecs::YieldsMulti.new + multi.sort_by {|e| e.size}.should == [[1, 2], [3, 4, 5], [6, 7, 8, 9]] + end + + it "returns an array of elements when a block is supplied and #map returns an enumerable" do + b = EnumerableSpecs::MapReturnsEnumerable.new + b.sort_by{ |x| -x }.should == [3, 2, 1] + end + + it "calls #each to iterate over the elements to be sorted" do + b = EnumerableSpecs::Numerous.new( 1, 2, 3 ) + b.should_receive(:each).once.and_yield(1).and_yield(2).and_yield(3) + b.should_not_receive :map + b.sort_by { |x| -x }.should == [3, 2, 1] + end + + it_behaves_like :enumerable_enumeratorized_with_origin_size, :sort_by +end diff --git a/spec/ruby/core/enumerable/sort_spec.rb b/spec/ruby/core/enumerable/sort_spec.rb new file mode 100644 index 0000000000..6fc64f325e --- /dev/null +++ b/spec/ruby/core/enumerable/sort_spec.rb @@ -0,0 +1,54 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Enumerable#sort" do + it "sorts by the natural order as defined by <=>" do + EnumerableSpecs::Numerous.new.sort.should == [1, 2, 3, 4, 5, 6] + sorted = EnumerableSpecs::ComparesByVowelCount.wrap("a" * 1, "a" * 2, "a"*3, "a"*4, "a"*5) + EnumerableSpecs::Numerous.new(sorted[2],sorted[0],sorted[1],sorted[3],sorted[4]).sort.should == sorted + end + + it "yields elements to the provided block" do + EnumerableSpecs::Numerous.new.sort { |a, b| b <=> a }.should == [6, 5, 4, 3, 2, 1] + EnumerableSpecs::Numerous.new(2,0,1,3,4).sort { |n, m| -(n <=> m) }.should == [4,3,2,1,0] + end + + it "raises a NoMethodError if elements do not define <=>" do + -> do + EnumerableSpecs::Numerous.new(BasicObject.new, BasicObject.new, BasicObject.new).sort + end.should raise_error(NoMethodError) + end + + it "sorts enumerables that contain nils" do + arr = EnumerableSpecs::Numerous.new(nil, true, nil, false, nil, true, nil, false, nil) + arr.sort { |a, b| + x = a ? -1 : a.nil? ? 0 : 1 + y = b ? -1 : b.nil? ? 0 : 1 + x <=> y + }.should == [true, true, nil, nil, nil, nil, nil, false, false] + end + + it "compare values returned by block with 0" do + EnumerableSpecs::Numerous.new.sort { |n, m| -(n+m) * (n <=> m) }.should == [6, 5, 4, 3, 2, 1] + EnumerableSpecs::Numerous.new.sort { |n, m| + EnumerableSpecs::ComparableWithInteger.new(-(n+m) * (n <=> m)) + }.should == [6, 5, 4, 3, 2, 1] + -> { + EnumerableSpecs::Numerous.new.sort { |n, m| (n <=> m).to_s } + }.should raise_error(ArgumentError) + end + + it "raises an error if objects can't be compared" do + a=EnumerableSpecs::Numerous.new(EnumerableSpecs::Uncomparable.new, EnumerableSpecs::Uncomparable.new) + -> {a.sort}.should raise_error(ArgumentError) + end + + it "gathers whole arrays as elements when each yields multiple" do + multi = EnumerableSpecs::YieldsMulti.new + multi.sort {|a, b| a.first <=> b.first}.should == [[1, 2], [3, 4, 5], [6, 7, 8, 9]] + end + + it "doesn't raise an error if #to_a returns a frozen Array" do + EnumerableSpecs::Freezy.new.sort.should == [1,2] + end +end diff --git a/spec/ruby/core/enumerable/sum_spec.rb b/spec/ruby/core/enumerable/sum_spec.rb new file mode 100644 index 0000000000..2eb74db6ac --- /dev/null +++ b/spec/ruby/core/enumerable/sum_spec.rb @@ -0,0 +1,50 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe 'Enumerable#sum' do + before :each do + @enum = Object.new.to_enum + class << @enum + def each + yield 0 + yield(-1) + yield 2 + yield 2/3r + end + end + end + + it 'returns amount of the elements with taking an argument as the initial value' do + @enum.sum(10).should == 35/3r + end + + it 'gives 0 as a default argument' do + @enum.sum.should == 5/3r + end + + context 'with a block' do + it 'transforms the elements' do + @enum.sum { |element| element * 2 }.should == 10/3r + end + + it 'does not destructure array elements' do + class << @enum + def each + yield [1,2] + yield [3] + end + end + + @enum.sum(&:last).should == 5 + end + end + + # https://bugs.ruby-lang.org/issues/12217 + # https://github.com/ruby/ruby/blob/master/doc/ChangeLog/ChangeLog-2.4.0#L6208-L6214 + it "uses Kahan's compensated summation algorithm for precise sum of float numbers" do + floats = [2.7800000000000002, 5.0, 2.5, 4.44, 3.89, 3.89, 4.44, 7.78, 5.0, 2.7800000000000002, 5.0, 2.5].to_enum + naive_sum = floats.reduce { |sum, e| sum + e } + naive_sum.should == 50.00000000000001 + floats.sum.should == 50.0 + end +end diff --git a/spec/ruby/core/enumerable/take_spec.rb b/spec/ruby/core/enumerable/take_spec.rb new file mode 100644 index 0000000000..41a7438330 --- /dev/null +++ b/spec/ruby/core/enumerable/take_spec.rb @@ -0,0 +1,13 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/take' + +describe "Enumerable#take" do + it "requires an argument" do + ->{ EnumerableSpecs::Numerous.new.take}.should raise_error(ArgumentError) + end + + describe "when passed an argument" do + it_behaves_like :enumerable_take, :take + end +end diff --git a/spec/ruby/core/enumerable/take_while_spec.rb b/spec/ruby/core/enumerable/take_while_spec.rb new file mode 100644 index 0000000000..26db39ac4b --- /dev/null +++ b/spec/ruby/core/enumerable/take_while_spec.rb @@ -0,0 +1,51 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/enumerable_enumeratorized' + +describe "Enumerable#take_while" do + before :each do + @enum = EnumerableSpecs::Numerous.new(3, 2, 1, :go) + end + + it "returns an Enumerator if no block given" do + @enum.take_while.should be_an_instance_of(Enumerator) + end + + it "returns no/all elements for {true/false} block" do + @enum.take_while{true}.should == @enum.to_a + @enum.take_while{false}.should == [] + end + + it "accepts returns other than true/false" do + @enum.take_while{1}.should == @enum.to_a + @enum.take_while{nil}.should == [] + end + + it "passes elements to the block until the first false" do + a = [] + @enum.take_while{|obj| (a << obj).size < 3}.should == [3, 2] + a.should == [3, 2, 1] + end + + it "will only go through what's needed" do + enum = EnumerableSpecs::EachCounter.new(4, 3, 2, 1, :stop) + enum.take_while { |x| + break 42 if x == 3 + true + }.should == 42 + enum.times_yielded.should == 2 + end + + it "doesn't return self when it could" do + a = [1,2,3] + a.take_while{true}.should_not equal(a) + end + + it "calls the block with initial args when yielded with multiple arguments" do + yields = [] + EnumerableSpecs::YieldsMixed.new.take_while{ |v| yields << v } + yields.should == [1, [2], 3, 5, [8, 9], nil, []] + end + + it_behaves_like :enumerable_enumeratorized_with_unknown_size, :take_while +end diff --git a/spec/ruby/core/enumerable/tally_spec.rb b/spec/ruby/core/enumerable/tally_spec.rb new file mode 100644 index 0000000000..95c64c1294 --- /dev/null +++ b/spec/ruby/core/enumerable/tally_spec.rb @@ -0,0 +1,91 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Enumerable#tally" do + before :each do + ScratchPad.record [] + end + + it "returns a hash with counts according to the value" do + enum = EnumerableSpecs::Numerous.new('foo', 'bar', 'foo', 'baz') + enum.tally.should == { 'foo' => 2, 'bar' => 1, 'baz' => 1} + end + + it "returns a hash without default" do + hash = EnumerableSpecs::Numerous.new('foo', 'bar', 'foo', 'baz').tally + hash.default_proc.should be_nil + hash.default.should be_nil + end + + it "returns an empty hash for empty enumerables" do + EnumerableSpecs::Empty.new.tally.should == {} + end + + it "counts values as gathered array when yielded with multiple arguments" do + EnumerableSpecs::YieldsMixed2.new.tally.should == EnumerableSpecs::YieldsMixed2.gathered_yields.group_by(&:itself).transform_values(&:size) + end + + it "does not call given block" do + enum = EnumerableSpecs::Numerous.new('foo', 'bar', 'foo', 'baz') + enum.tally { |v| ScratchPad << v } + ScratchPad.recorded.should == [] + end +end + +describe "Enumerable#tally with a hash" do + before :each do + ScratchPad.record [] + end + + it "returns a hash with counts according to the value" do + enum = EnumerableSpecs::Numerous.new('foo', 'bar', 'foo', 'baz') + enum.tally({ 'foo' => 1 }).should == { 'foo' => 3, 'bar' => 1, 'baz' => 1} + end + + it "returns the given hash" do + enum = EnumerableSpecs::Numerous.new('foo', 'bar', 'foo', 'baz') + hash = { 'foo' => 1 } + enum.tally(hash).should equal(hash) + end + + it "calls #to_hash to convert argument to Hash implicitly if passed not a Hash" do + enum = EnumerableSpecs::Numerous.new('foo', 'bar', 'foo', 'baz') + object = Object.new + def object.to_hash; { 'foo' => 1 }; end + enum.tally(object).should == { 'foo' => 3, 'bar' => 1, 'baz' => 1} + end + + it "raises a FrozenError and does not update the given hash when the hash is frozen" do + enum = EnumerableSpecs::Numerous.new('foo', 'bar', 'foo', 'baz') + hash = { 'foo' => 1 }.freeze + -> { enum.tally(hash) }.should raise_error(FrozenError) + hash.should == { 'foo' => 1 } + end + + it "raises a FrozenError even if enumerable is empty" do + enum = EnumerableSpecs::Numerous.new() + hash = { 'foo' => 1 }.freeze + -> { enum.tally(hash) }.should raise_error(FrozenError) + end + + it "does not call given block" do + enum = EnumerableSpecs::Numerous.new('foo', 'bar', 'foo', 'baz') + enum.tally({ 'foo' => 1 }) { |v| ScratchPad << v } + ScratchPad.recorded.should == [] + end + + it "ignores the default value" do + enum = EnumerableSpecs::Numerous.new('foo', 'bar', 'foo', 'baz') + enum.tally(Hash.new(100)).should == { 'foo' => 2, 'bar' => 1, 'baz' => 1} + end + + it "ignores the default proc" do + enum = EnumerableSpecs::Numerous.new('foo', 'bar', 'foo', 'baz') + enum.tally(Hash.new {100}).should == { 'foo' => 2, 'bar' => 1, 'baz' => 1} + end + + it "needs the values counting each elements to be an integer" do + enum = EnumerableSpecs::Numerous.new('foo') + -> { enum.tally({ 'foo' => 'bar' }) }.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/enumerable/to_a_spec.rb b/spec/ruby/core/enumerable/to_a_spec.rb new file mode 100644 index 0000000000..723f922574 --- /dev/null +++ b/spec/ruby/core/enumerable/to_a_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/entries' + +describe "Enumerable#to_a" do + it_behaves_like :enumerable_entries, :to_a +end diff --git a/spec/ruby/core/enumerable/to_h_spec.rb b/spec/ruby/core/enumerable/to_h_spec.rb new file mode 100644 index 0000000000..11a4933c10 --- /dev/null +++ b/spec/ruby/core/enumerable/to_h_spec.rb @@ -0,0 +1,96 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Enumerable#to_h" do + it "converts empty enumerable to empty hash" do + enum = EnumerableSpecs::EachDefiner.new + enum.to_h.should == {} + end + + it "converts yielded [key, value] pairs to a hash" do + enum = EnumerableSpecs::EachDefiner.new([:a, 1], [:b, 2]) + enum.to_h.should == { a: 1, b: 2 } + end + + it "uses the last value of a duplicated key" do + enum = EnumerableSpecs::EachDefiner.new([:a, 1], [:b, 2], [:a, 3]) + enum.to_h.should == { a: 3, b: 2 } + end + + it "calls #to_ary on contents" do + pair = mock('to_ary') + pair.should_receive(:to_ary).and_return([:b, 2]) + enum = EnumerableSpecs::EachDefiner.new([:a, 1], pair) + enum.to_h.should == { a: 1, b: 2 } + end + + it "forwards arguments to #each" do + enum = Object.new + def enum.each(*args) + yield(*args) + yield([:b, 2]) + end + enum.extend Enumerable + enum.to_h(:a, 1).should == { a: 1, b: 2 } + end + + it "raises TypeError if an element is not an array" do + enum = EnumerableSpecs::EachDefiner.new(:x) + -> { enum.to_h }.should raise_error(TypeError) + end + + it "raises ArgumentError if an element is not a [key, value] pair" do + enum = EnumerableSpecs::EachDefiner.new([:x]) + -> { enum.to_h }.should raise_error(ArgumentError) + end + + context "with block" do + before do + @enum = EnumerableSpecs::EachDefiner.new(:a, :b) + end + + it "converts [key, value] pairs returned by the block to a hash" do + @enum.to_h { |k| [k, k.to_s] }.should == { a: 'a', b: 'b' } + end + + it "passes to a block each element as a single argument" do + enum_of_arrays = EnumerableSpecs::EachDefiner.new([:a, 1], [:b, 2]) + + ScratchPad.record [] + enum_of_arrays.to_h { |*args| ScratchPad << args; [args[0], args[1]] } + ScratchPad.recorded.sort.should == [[[:a, 1]], [[:b, 2]]] + end + + it "raises ArgumentError if block returns longer or shorter array" do + -> do + @enum.to_h { |k| [k, k.to_s, 1] } + end.should raise_error(ArgumentError, /element has wrong array length/) + + -> do + @enum.to_h { |k| [k] } + end.should raise_error(ArgumentError, /element has wrong array length/) + end + + it "raises TypeError if block returns something other than Array" do + -> do + @enum.to_h { |k| "not-array" } + end.should raise_error(TypeError, /wrong element type String/) + end + + it "coerces returned pair to Array with #to_ary" do + x = mock('x') + x.stub!(:to_ary).and_return([:b, 'b']) + + @enum.to_h { |k| x }.should == { :b => 'b' } + end + + it "does not coerce returned pair to Array with #to_a" do + x = mock('x') + x.stub!(:to_a).and_return([:b, 'b']) + + -> do + @enum.to_h { |k| x } + end.should raise_error(TypeError, /wrong element type MockObject/) + end + end +end diff --git a/spec/ruby/core/enumerable/to_set_spec.rb b/spec/ruby/core/enumerable/to_set_spec.rb new file mode 100644 index 0000000000..c02ead11fa --- /dev/null +++ b/spec/ruby/core/enumerable/to_set_spec.rb @@ -0,0 +1,30 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Enumerable#to_set" do + it "returns a new Set created from self" do + [1, 2, 3].to_set.should == Set[1, 2, 3] + {a: 1, b: 2}.to_set.should == Set[[:b, 2], [:a, 1]] + end + + it "passes down passed blocks" do + [1, 2, 3].to_set { |x| x * x }.should == Set[1, 4, 9] + end + + ruby_version_is "4.0"..."4.1" do + it "instantiates an object of provided as the first argument set class" do + set = nil + proc{set = [1, 2, 3].to_set(EnumerableSpecs::SetSubclass)}.should complain(/Enumerable#to_set/) + set.should be_kind_of(EnumerableSpecs::SetSubclass) + set.to_a.sort.should == [1, 2, 3] + end + end + + ruby_version_is ""..."4.0" do + it "instantiates an object of provided as the first argument set class" do + set = [1, 2, 3].to_set(EnumerableSpecs::SetSubclass) + set.should be_kind_of(EnumerableSpecs::SetSubclass) + set.to_a.sort.should == [1, 2, 3] + end + end +end diff --git a/spec/ruby/core/enumerable/uniq_spec.rb b/spec/ruby/core/enumerable/uniq_spec.rb new file mode 100644 index 0000000000..a1ed44796f --- /dev/null +++ b/spec/ruby/core/enumerable/uniq_spec.rb @@ -0,0 +1,78 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe 'Enumerable#uniq' do + it 'returns an array that contains only unique elements' do + [0, 1, 2, 3].to_enum.uniq { |n| n.even? }.should == [0, 1] + end + + it "uses eql? semantics" do + [1.0, 1].to_enum.uniq.should == [1.0, 1] + end + + it "compares elements first with hash" do + x = mock('0') + x.should_receive(:hash).at_least(1).and_return(0) + y = mock('0') + y.should_receive(:hash).at_least(1).and_return(0) + + [x, y].to_enum.uniq.should == [x, y] + end + + it "does not compare elements with different hash codes via eql?" do + x = mock('0') + x.should_not_receive(:eql?) + y = mock('1') + y.should_not_receive(:eql?) + + x.should_receive(:hash).at_least(1).and_return(0) + y.should_receive(:hash).at_least(1).and_return(1) + + [x, y].to_enum.uniq.should == [x, y] + end + + it "compares elements with matching hash codes with #eql?" do + a = Array.new(2) do + obj = mock('0') + obj.should_receive(:hash).at_least(1).and_return(0) + + def obj.eql?(o) + false + end + + obj + end + + a.uniq.should == a + + a = Array.new(2) do + obj = mock('0') + obj.should_receive(:hash).at_least(1).and_return(0) + + def obj.eql?(o) + true + end + + obj + end + + a.to_enum.uniq.size.should == 1 + end + + context 'when yielded with multiple arguments' do + before :each do + @enum = Object.new.to_enum + class << @enum + def each + yield 0, 'foo' + yield 1, 'FOO' + yield 2, 'bar' + end + end + end + + it 'returns all yield arguments as an array' do + @enum.uniq { |_, label| label.downcase }.should == [[0, 'foo'], [2, 'bar']] + end + end +end diff --git a/spec/ruby/core/enumerable/zip_spec.rb b/spec/ruby/core/enumerable/zip_spec.rb new file mode 100644 index 0000000000..ab148f2a6e --- /dev/null +++ b/spec/ruby/core/enumerable/zip_spec.rb @@ -0,0 +1,46 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Enumerable#zip" do + + it "combines each element of the receiver with the element of the same index in arrays given as arguments" do + EnumerableSpecs::Numerous.new(1,2,3).zip([4,5,6],[7,8,9]).should == [[1,4,7],[2,5,8],[3,6,9]] + EnumerableSpecs::Numerous.new(1,2,3).zip.should == [[1],[2],[3]] + end + + it "passes each element of the result array to a block and return nil if a block is given" do + expected = [[1,4,7],[2,5,8],[3,6,9]] + EnumerableSpecs::Numerous.new(1,2,3).zip([4,5,6],[7,8,9]) do |result_component| + result_component.should == expected.shift + end.should == nil + expected.size.should == 0 + end + + it "fills resulting array with nils if an argument array is too short" do + EnumerableSpecs::Numerous.new(1,2,3).zip([4,5,6], [7,8]).should == [[1,4,7],[2,5,8],[3,6,nil]] + end + + it "converts arguments to arrays using #to_ary" do + convertible = EnumerableSpecs::ArrayConvertible.new(4,5,6) + EnumerableSpecs::Numerous.new(1,2,3).zip(convertible).should == [[1,4],[2,5],[3,6]] + convertible.called.should == :to_ary + end + + it "converts arguments to enums using #to_enum" do + convertible = EnumerableSpecs::EnumConvertible.new(4..6) + EnumerableSpecs::Numerous.new(1,2,3).zip(convertible).should == [[1,4],[2,5],[3,6]] + convertible.called.should == :to_enum + convertible.sym.should == :each + end + + it "gathers whole arrays as elements when each yields multiple" do + multi = EnumerableSpecs::YieldsMulti.new + multi.zip(multi).should == [[[1, 2], [1, 2]], [[3, 4, 5], [3, 4, 5]], [[6, 7, 8, 9], [6, 7, 8, 9]]] + end + + it "raises TypeError when some argument isn't Array and doesn't respond to #to_ary and #to_enum" do + -> { EnumerableSpecs::Numerous.new(1,2,3).zip(Object.new) }.should raise_error(TypeError, "wrong argument type Object (must respond to :each)") + -> { EnumerableSpecs::Numerous.new(1,2,3).zip(1) }.should raise_error(TypeError, "wrong argument type Integer (must respond to :each)") + -> { EnumerableSpecs::Numerous.new(1,2,3).zip(true) }.should raise_error(TypeError, "wrong argument type TrueClass (must respond to :each)") + end +end diff --git a/spec/ruby/core/enumerator/arithmetic_sequence/begin_spec.rb b/spec/ruby/core/enumerator/arithmetic_sequence/begin_spec.rb new file mode 100644 index 0000000000..bd243fa0b5 --- /dev/null +++ b/spec/ruby/core/enumerator/arithmetic_sequence/begin_spec.rb @@ -0,0 +1,16 @@ +require_relative '../../../spec_helper' + +describe "Enumerator::ArithmeticSequence#begin" do + it "returns the begin of the sequence" do + 1.step(10).begin.should == 1 + (1..10).step.begin.should == 1 + (1...10).step.begin.should == 1 + end + + context "with beginless" do + it "returns nil as begin of the sequence" do + (..10).step(1).begin.should == nil + (...10).step(1).begin.should == nil + end + end +end diff --git a/spec/ruby/core/enumerator/arithmetic_sequence/each_spec.rb b/spec/ruby/core/enumerator/arithmetic_sequence/each_spec.rb new file mode 100644 index 0000000000..d4fff3e01f --- /dev/null +++ b/spec/ruby/core/enumerator/arithmetic_sequence/each_spec.rb @@ -0,0 +1,17 @@ +require_relative '../../../spec_helper' + +describe "Enumerator::ArithmeticSequence#each" do + before :each do + ScratchPad.record [] + @seq = 1.step(10, 4) + end + + it "calls given block on each item of the sequence" do + @seq.each { |item| ScratchPad << item } + ScratchPad.recorded.should == [1, 5, 9] + end + + it "returns self" do + @seq.each { |item| }.should equal(@seq) + end +end diff --git a/spec/ruby/core/enumerator/arithmetic_sequence/end_spec.rb b/spec/ruby/core/enumerator/arithmetic_sequence/end_spec.rb new file mode 100644 index 0000000000..05429cac3e --- /dev/null +++ b/spec/ruby/core/enumerator/arithmetic_sequence/end_spec.rb @@ -0,0 +1,16 @@ +require_relative '../../../spec_helper' + +describe "Enumerator::ArithmeticSequence#end" do + it "returns the end of the sequence" do + 1.step(10).end.should == 10 + (1..10).step.end.should == 10 + (1...10).step(17).end.should == 10 + end + + context "with endless" do + it "returns nil as end of the sequence" do + (1..).step(1).end.should == nil + (1...).step(1).end.should == nil + end + end +end diff --git a/spec/ruby/core/enumerator/arithmetic_sequence/eq_spec.rb b/spec/ruby/core/enumerator/arithmetic_sequence/eq_spec.rb new file mode 100644 index 0000000000..77eed02d8b --- /dev/null +++ b/spec/ruby/core/enumerator/arithmetic_sequence/eq_spec.rb @@ -0,0 +1,18 @@ +require_relative '../../../spec_helper' + +describe "Enumerator::ArithmeticSequence#==" do + it "returns true if begin, end, step and exclude_end? are equal" do + 1.step(10).should == 1.step(10) + 1.step(10, 5).should == 1.step(10, 5) + + (1..10).step.should == (1..10).step + (1...10).step(8).should == (1...10).step(8) + + # both have exclude_end? == false + (1..10).step(100).should == 1.step(10, 100) + + ((1..10).step == (1..11).step).should == false + ((1..10).step == (1...10).step).should == false + ((1..10).step == (1..10).step(2)).should == false + end +end diff --git a/spec/ruby/core/enumerator/arithmetic_sequence/exclude_end_spec.rb b/spec/ruby/core/enumerator/arithmetic_sequence/exclude_end_spec.rb new file mode 100644 index 0000000000..021fe7d90f --- /dev/null +++ b/spec/ruby/core/enumerator/arithmetic_sequence/exclude_end_spec.rb @@ -0,0 +1,17 @@ +require_relative '../../../spec_helper' + +describe "Enumerator::ArithmeticSequence#exclude_end?" do + context "when created using Numeric#step" do + it "always returns false" do + 1.step(10).should_not.exclude_end? + 10.step(1).should_not.exclude_end? + end + end + + context "when created using Range#step" do + it "mirrors range.exclude_end?" do + (1...10).step.should.exclude_end? + (1..10).step.should_not.exclude_end? + end + end +end diff --git a/spec/ruby/core/enumerator/arithmetic_sequence/first_spec.rb b/spec/ruby/core/enumerator/arithmetic_sequence/first_spec.rb new file mode 100644 index 0000000000..ccd02be020 --- /dev/null +++ b/spec/ruby/core/enumerator/arithmetic_sequence/first_spec.rb @@ -0,0 +1,9 @@ +require_relative '../../../spec_helper' + +describe "Enumerator::ArithmeticSequence#first" do + it "returns the first element of the sequence" do + 1.step(10).first.should == 1 + (1..10).step.first.should == 1 + (1...10).step.first.should == 1 + end +end diff --git a/spec/ruby/core/enumerator/arithmetic_sequence/hash_spec.rb b/spec/ruby/core/enumerator/arithmetic_sequence/hash_spec.rb new file mode 100644 index 0000000000..bdb308074b --- /dev/null +++ b/spec/ruby/core/enumerator/arithmetic_sequence/hash_spec.rb @@ -0,0 +1,20 @@ +require_relative '../../../spec_helper' + +describe "Enumerator::ArithmeticSequence#hash" do + it "is based on begin, end, step and exclude_end?" do + 1.step(10).hash.should be_an_instance_of(Integer) + + 1.step(10).hash.should == 1.step(10).hash + 1.step(10, 5).hash.should == 1.step(10, 5).hash + + (1..10).step.hash.should == (1..10).step.hash + (1...10).step(8).hash.should == (1...10).step(8).hash + + # both have exclude_end? == false + (1..10).step(100).hash.should == 1.step(10, 100).hash + + ((1..10).step.hash == (1..11).step.hash).should == false + ((1..10).step.hash == (1...10).step.hash).should == false + ((1..10).step.hash == (1..10).step(2).hash).should == false + end +end diff --git a/spec/ruby/core/enumerator/arithmetic_sequence/inspect_spec.rb b/spec/ruby/core/enumerator/arithmetic_sequence/inspect_spec.rb new file mode 100644 index 0000000000..b73b49d272 --- /dev/null +++ b/spec/ruby/core/enumerator/arithmetic_sequence/inspect_spec.rb @@ -0,0 +1,20 @@ +require_relative '../../../spec_helper' + +describe "Enumerator::ArithmeticSequence#inspect" do + context 'when Numeric#step is used' do + it "returns '(begin.step(end{, step}))'" do + 1.step(10).inspect.should == "(1.step(10))" + 1.step(10, 3).inspect.should == "(1.step(10, 3))" + end + end + + context 'when Range#step is used' do + it "returns '((range).step{(step)})'" do + (1..10).step.inspect.should == "((1..10).step)" + (1..10).step(3).inspect.should == "((1..10).step(3))" + + (1...10).step.inspect.should == "((1...10).step)" + (1...10).step(3).inspect.should == "((1...10).step(3))" + end + end +end diff --git a/spec/ruby/core/enumerator/arithmetic_sequence/last_spec.rb b/spec/ruby/core/enumerator/arithmetic_sequence/last_spec.rb new file mode 100644 index 0000000000..31f982b7c4 --- /dev/null +++ b/spec/ruby/core/enumerator/arithmetic_sequence/last_spec.rb @@ -0,0 +1,9 @@ +require_relative '../../../spec_helper' + +describe "Enumerator::ArithmeticSequence#last" do + it "returns the last element of the sequence" do + 1.step(10).last.should == 10 + (1..10).step.last.should == 10 + (1...10).step(4).last.should == 9 + end +end diff --git a/spec/ruby/core/enumerator/arithmetic_sequence/new_spec.rb b/spec/ruby/core/enumerator/arithmetic_sequence/new_spec.rb new file mode 100644 index 0000000000..2015983826 --- /dev/null +++ b/spec/ruby/core/enumerator/arithmetic_sequence/new_spec.rb @@ -0,0 +1,17 @@ +require_relative '../../../spec_helper' + +describe "Enumerator::ArithmeticSequence.new" do + it "is not defined" do + -> { + Enumerator::ArithmeticSequence.new + }.should raise_error(NoMethodError) + end +end + +describe "Enumerator::ArithmeticSequence.allocate" do + it "is not defined" do + -> { + Enumerator::ArithmeticSequence.allocate + }.should raise_error(TypeError, 'allocator undefined for Enumerator::ArithmeticSequence') + end +end diff --git a/spec/ruby/core/enumerator/arithmetic_sequence/size_spec.rb b/spec/ruby/core/enumerator/arithmetic_sequence/size_spec.rb new file mode 100644 index 0000000000..7e03edd961 --- /dev/null +++ b/spec/ruby/core/enumerator/arithmetic_sequence/size_spec.rb @@ -0,0 +1,17 @@ +require_relative '../../../spec_helper' + +describe "Enumerator::ArithmeticSequence#size" do + context "for finite sequence" do + it "returns the number of elements in this arithmetic sequence" do + 1.step(10).size.should == 10 + (1...10).step.size.should == 9 + end + end + + context "for infinite sequence" do + it "returns Infinity" do + 1.step(Float::INFINITY).size.should == Float::INFINITY + (1..Float::INFINITY).step.size.should == Float::INFINITY + end + end +end diff --git a/spec/ruby/core/enumerator/arithmetic_sequence/step_spec.rb b/spec/ruby/core/enumerator/arithmetic_sequence/step_spec.rb new file mode 100644 index 0000000000..c1f2d9173f --- /dev/null +++ b/spec/ruby/core/enumerator/arithmetic_sequence/step_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../../spec_helper' + +describe "Enumerator::ArithmeticSequence#step" do + it "returns the original value given to step method" do + (1..10).step.step.should == 1 + (1..10).step(3).step.should == 3 + + 1.step(10).step.should == 1 + 1.step(10, 3).step.should == 3 + end +end diff --git a/spec/ruby/core/enumerator/chain/each_spec.rb b/spec/ruby/core/enumerator/chain/each_spec.rb new file mode 100644 index 0000000000..cc93cbac60 --- /dev/null +++ b/spec/ruby/core/enumerator/chain/each_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../../spec_helper' +require_relative '../../enumerable/fixtures/classes' + +describe "Enumerator::Chain#each" do + it "calls each on its constituents as needed" do + a = EnumerableSpecs::EachCounter.new(:a, :b) + b = EnumerableSpecs::EachCounter.new(:c, :d) + + ScratchPad.record [] + Enumerator::Chain.new(a, b).each do |elem| + ScratchPad << elem << b.times_yielded + end + ScratchPad.recorded.should == [:a, 0, :b, 0, :c, 1, :d, 2] + end +end diff --git a/spec/ruby/core/enumerator/chain/initialize_spec.rb b/spec/ruby/core/enumerator/chain/initialize_spec.rb new file mode 100644 index 0000000000..daa30351d7 --- /dev/null +++ b/spec/ruby/core/enumerator/chain/initialize_spec.rb @@ -0,0 +1,31 @@ +require_relative '../../../spec_helper' + +describe "Enumerator::Chain#initialize" do + before :each do + @uninitialized = Enumerator::Chain.allocate + end + + it "is a private method" do + Enumerator::Chain.should have_private_instance_method(:initialize, false) + end + + it "returns self" do + @uninitialized.send(:initialize).should equal(@uninitialized) + end + + it "accepts many arguments" do + @uninitialized.send(:initialize, 0..1, 2..3, 4..5).should equal(@uninitialized) + end + + it "accepts arguments that are not Enumerable nor responding to :each" do + @uninitialized.send(:initialize, Object.new).should equal(@uninitialized) + end + + describe "on frozen instance" do + it "raises a FrozenError" do + -> { + @uninitialized.freeze.send(:initialize) + }.should raise_error(FrozenError) + end + end +end diff --git a/spec/ruby/core/enumerator/chain/inspect_spec.rb b/spec/ruby/core/enumerator/chain/inspect_spec.rb new file mode 100644 index 0000000000..9b5a442b75 --- /dev/null +++ b/spec/ruby/core/enumerator/chain/inspect_spec.rb @@ -0,0 +1,18 @@ +require_relative '../../../spec_helper' + +describe "Enumerator::Chain#inspect" do + it "shows a representation of the Enumerator" do + Enumerator::Chain.new.inspect.should == "#<Enumerator::Chain: []>" + Enumerator::Chain.new(1..2, 3..4).inspect.should == "#<Enumerator::Chain: [1..2, 3..4]>" + end + + it "calls inspect on its chain elements" do + obj = mock('inspect') + obj.should_receive(:inspect).and_return('some desc') + Enumerator::Chain.new(obj).inspect.should == "#<Enumerator::Chain: [some desc]>" + end + + it "returns a not initialized representation if #initialized is not called yet" do + Enumerator::Chain.allocate.inspect.should == "#<Enumerator::Chain: uninitialized>" + end +end diff --git a/spec/ruby/core/enumerator/chain/rewind_spec.rb b/spec/ruby/core/enumerator/chain/rewind_spec.rb new file mode 100644 index 0000000000..5f51ce2cf1 --- /dev/null +++ b/spec/ruby/core/enumerator/chain/rewind_spec.rb @@ -0,0 +1,51 @@ +require_relative '../../../spec_helper' + +describe "Enumerator::Chain#rewind" do + before(:each) do + @obj = mock('obj') + @obj.should_receive(:each).any_number_of_times.and_yield + @second = mock('obj') + @second.should_receive(:each).any_number_of_times.and_yield + @enum = Enumerator::Chain.new(@obj, @second) + end + + it "returns self" do + @enum.rewind.should equal @enum + end + + it "does nothing if receiver has not been iterated" do + @obj.should_not_receive(:rewind) + @obj.respond_to?(:rewind).should == true # sanity check + @enum.rewind + end + + it "does nothing on objects that don't respond_to rewind" do + @obj.respond_to?(:rewind).should == false # sanity check + @enum.each {} + @enum.rewind + end + + it "calls_rewind its objects" do + @obj.should_receive(:rewind) + @enum.each {} + @enum.rewind + end + + it "calls_rewind in reverse order" do + @obj.should_not_receive(:rewind) + @second.should_receive(:rewind).and_raise(RuntimeError) + @enum.each {} + -> { @enum.rewind }.should raise_error(RuntimeError) + end + + it "calls rewind only for objects that have actually been iterated on" do + @obj = mock('obj') + @obj.should_receive(:each).any_number_of_times.and_raise(RuntimeError) + @enum = Enumerator::Chain.new(@obj, @second) + + @obj.should_receive(:rewind) + @second.should_not_receive(:rewind) + -> { @enum.each {} }.should raise_error(RuntimeError) + @enum.rewind + end +end diff --git a/spec/ruby/core/enumerator/chain/size_spec.rb b/spec/ruby/core/enumerator/chain/size_spec.rb new file mode 100644 index 0000000000..d85b88ee8b --- /dev/null +++ b/spec/ruby/core/enumerator/chain/size_spec.rb @@ -0,0 +1,22 @@ +require_relative '../../../spec_helper' +require_relative '../../enumerable/fixtures/classes' + +describe "Enumerator::Chain#size" do + it "returns the sum of the sizes of the elements" do + a = mock('size') + a.should_receive(:size).and_return(40) + Enumerator::Chain.new(a, [:a, :b]).size.should == 42 + end + + it "returns nil or Infinity for the first element of such a size" do + [nil, Float::INFINITY].each do |special| + a = mock('size') + a.should_receive(:size).and_return(40) + b = mock('special') + b.should_receive(:size).and_return(special) + c = mock('not called') + c.should_not_receive(:size) + Enumerator::Chain.new(a, b, c).size.should == special + end + end +end diff --git a/spec/ruby/core/enumerator/each_spec.rb b/spec/ruby/core/enumerator/each_spec.rb new file mode 100644 index 0000000000..3af16e5587 --- /dev/null +++ b/spec/ruby/core/enumerator/each_spec.rb @@ -0,0 +1,89 @@ +require_relative '../../spec_helper' + +describe "Enumerator#each" do + before :each do + object_each_with_arguments = Object.new + def object_each_with_arguments.each_with_arguments(arg, *args) + yield arg, *args + :method_returned + end + + @enum_with_arguments = object_each_with_arguments.to_enum(:each_with_arguments, :arg0, :arg1, :arg2) + + @enum_with_yielder = Enumerator.new { |y| y.yield :ok } + end + + it "yields each element of self to the given block" do + acc = [] + [1, 2, 3].to_enum.each { |e| acc << e } + acc.should == [1, 2, 3] + end + + it "calls #each on the object given in the constructor by default" do + each = mock('each') + each.should_receive(:each) + each.to_enum.each { |e| e } + end + + it "calls #each on the underlying object until it's exhausted" do + each = mock('each') + each.should_receive(:each).and_yield(1).and_yield(2).and_yield(3) + acc = [] + each.to_enum.each { |e| acc << e } + acc.should == [1, 2, 3] + end + + it "calls the method given in the constructor instead of #each" do + each = mock('peach') + each.should_receive(:peach) + each.to_enum(:peach).each { |e| e } + end + + it "calls the method given in the constructor until it's exhausted" do + each = mock('peach') + each.should_receive(:peach).and_yield(1).and_yield(2).and_yield(3) + acc = [] + each.to_enum(:peach).each { |e| acc << e } + acc.should == [1, 2, 3] + end + + it "raises a NoMethodError if the object doesn't respond to #each" do + enum = Object.new.to_enum + -> do + enum.each { |e| e } + end.should raise_error(NoMethodError) + end + + it "returns self if not given arguments and not given a block" do + @enum_with_arguments.each.should equal(@enum_with_arguments) + + @enum_with_yielder.each.should equal(@enum_with_yielder) + end + + it "returns the same value from receiver.each if block is given" do + @enum_with_arguments.each {}.should equal(:method_returned) + end + + it "passes given arguments at initialized to receiver.each" do + @enum_with_arguments.each.to_a.should == [[:arg0, :arg1, :arg2]] + end + + it "requires multiple arguments" do + Enumerator.instance_method(:each).arity.should < 0 + end + + it "appends given arguments to receiver.each" do + @enum_with_arguments.each(:each0, :each1).to_a.should == [[:arg0, :arg1, :arg2, :each0, :each1]] + @enum_with_arguments.each(:each2, :each3).to_a.should == [[:arg0, :arg1, :arg2, :each2, :each3]] + end + + it "returns the same value from receiver.each if block and arguments are given" do + @enum_with_arguments.each(:each1, :each2) {}.should equal(:method_returned) + end + + it "returns new Enumerator if given arguments but not given a block" do + ret = @enum_with_arguments.each 1 + ret.should be_an_instance_of(Enumerator) + ret.should_not equal(@enum_with_arguments) + end +end diff --git a/spec/ruby/core/enumerator/each_with_index_spec.rb b/spec/ruby/core/enumerator/each_with_index_spec.rb new file mode 100644 index 0000000000..4898e86fa9 --- /dev/null +++ b/spec/ruby/core/enumerator/each_with_index_spec.rb @@ -0,0 +1,36 @@ +require_relative '../../spec_helper' +require_relative 'shared/with_index' +require_relative '../enumerable/shared/enumeratorized' + +describe "Enumerator#each_with_index" do + it_behaves_like :enum_with_index, :each_with_index + it_behaves_like :enumeratorized_with_origin_size, :each_with_index, [1,2,3].select + + it "returns a new Enumerator when no block is given" do + enum1 = [1,2,3].select + enum2 = enum1.each_with_index + enum2.should be_an_instance_of(Enumerator) + enum1.should_not == enum2 + end + + it "raises an ArgumentError if passed extra arguments" do + -> do + [1].to_enum.each_with_index(:glark) + end.should raise_error(ArgumentError) + end + + it "passes on the given block's return value" do + arr = [1,2,3] + arr.delete_if.each_with_index { |a,b| false } + arr.should == [1,2,3] + end + + it "returns the iterator's return value" do + [1,2,3].select.each_with_index { |a,b| false }.should == [] + [1,2,3].select.each_with_index { |a,b| true }.should == [1,2,3] + end + + it "returns the correct value if chained with itself" do + [:a].each_with_index.each_with_index.to_a.should == [[[:a,0],0]] + end +end diff --git a/spec/ruby/core/enumerator/each_with_object_spec.rb b/spec/ruby/core/enumerator/each_with_object_spec.rb new file mode 100644 index 0000000000..84a45ae89d --- /dev/null +++ b/spec/ruby/core/enumerator/each_with_object_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/with_object' + +describe "Enumerator#each_with_object" do + it_behaves_like :enum_with_object, :each_with_object +end diff --git a/spec/ruby/core/enumerator/enum_for_spec.rb b/spec/ruby/core/enumerator/enum_for_spec.rb new file mode 100644 index 0000000000..fbdf98545a --- /dev/null +++ b/spec/ruby/core/enumerator/enum_for_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/enum_for' + +describe "Enumerator#enum_for" do + it_behaves_like :enum_for, :enum_for +end diff --git a/spec/ruby/core/enumerator/enumerator_spec.rb b/spec/ruby/core/enumerator/enumerator_spec.rb new file mode 100644 index 0000000000..7a263336cb --- /dev/null +++ b/spec/ruby/core/enumerator/enumerator_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' + +describe "Enumerator" do + it "includes Enumerable" do + Enumerator.include?(Enumerable).should == true + end +end diff --git a/spec/ruby/core/enumerator/feed_spec.rb b/spec/ruby/core/enumerator/feed_spec.rb new file mode 100644 index 0000000000..e387c6cd39 --- /dev/null +++ b/spec/ruby/core/enumerator/feed_spec.rb @@ -0,0 +1,52 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' + +describe "Enumerator#feed" do + before :each do + ScratchPad.record [] + @enum = EnumeratorSpecs::Feed.new.to_enum(:each) + end + + it "sets the future return value of yield if called before advancing the iterator" do + @enum.feed :a + @enum.next + @enum.next + @enum.next + ScratchPad.recorded.should == [:a, nil] + end + + it "causes yield to return the value if called during iteration" do + @enum.next + @enum.feed :a + @enum.next + @enum.next + ScratchPad.recorded.should == [:a, nil] + end + + it "can be called for each iteration" do + @enum.next + @enum.feed :a + @enum.next + @enum.feed :b + @enum.next + ScratchPad.recorded.should == [:a, :b] + end + + it "returns nil" do + @enum.feed(:a).should be_nil + end + + it "raises a TypeError if called more than once without advancing the enumerator" do + @enum.feed :a + @enum.next + -> { @enum.feed :b }.should raise_error(TypeError) + end + + it "sets the return value of Yielder#yield" do + enum = Enumerator.new { |y| ScratchPad << y.yield } + enum.next + enum.feed :a + -> { enum.next }.should raise_error(StopIteration) + ScratchPad.recorded.should == [:a] + end +end diff --git a/spec/ruby/core/enumerator/first_spec.rb b/spec/ruby/core/enumerator/first_spec.rb new file mode 100644 index 0000000000..458080bb31 --- /dev/null +++ b/spec/ruby/core/enumerator/first_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' + +describe "Enumerator#first" do + it "returns arrays correctly when calling #first (2376)" do + Enumerator.new {|y| y << [42] }.first.should == [42] + end +end diff --git a/spec/ruby/core/enumerator/fixtures/classes.rb b/spec/ruby/core/enumerator/fixtures/classes.rb new file mode 100644 index 0000000000..6f285b8efa --- /dev/null +++ b/spec/ruby/core/enumerator/fixtures/classes.rb @@ -0,0 +1,15 @@ +module EnumSpecs + class Numerous + + include Enumerable + + def initialize(*list) + @list = list.empty? ? [2, 5, 3, 6, 1, 4] : list + end + + def each + @list.each { |i| yield i } + end + end + +end diff --git a/spec/ruby/core/enumerator/fixtures/common.rb b/spec/ruby/core/enumerator/fixtures/common.rb new file mode 100644 index 0000000000..e332e3195b --- /dev/null +++ b/spec/ruby/core/enumerator/fixtures/common.rb @@ -0,0 +1,9 @@ +module EnumeratorSpecs + class Feed + def each + ScratchPad << yield + ScratchPad << yield + ScratchPad << yield + end + end +end diff --git a/spec/ruby/core/enumerator/generator/each_spec.rb b/spec/ruby/core/enumerator/generator/each_spec.rb new file mode 100644 index 0000000000..a43805dd16 --- /dev/null +++ b/spec/ruby/core/enumerator/generator/each_spec.rb @@ -0,0 +1,40 @@ +require_relative '../../../spec_helper' + +describe "Enumerator::Generator#each" do + before :each do + @generator = Enumerator::Generator.new do |y, *args| + y << 3 << 2 << 1 + y << args unless args.empty? + :block_returned + end + end + + it "is an enumerable" do + @generator.should be_kind_of(Enumerable) + end + + it "supports enumeration with a block" do + r = [] + @generator.each { |v| r << v } + + r.should == [3, 2, 1] + end + + it "raises a LocalJumpError if no block given" do + -> { @generator.each }.should raise_error(LocalJumpError) + end + + it "returns the block returned value" do + @generator.each {}.should equal(:block_returned) + end + + it "requires multiple arguments" do + Enumerator::Generator.instance_method(:each).arity.should < 0 + end + + it "appends given arguments to receiver.each" do + yields = [] + @generator.each(:each0, :each1) { |yielded| yields << yielded } + yields.should == [3, 2, 1, [:each0, :each1]] + end +end diff --git a/spec/ruby/core/enumerator/generator/initialize_spec.rb b/spec/ruby/core/enumerator/generator/initialize_spec.rb new file mode 100644 index 0000000000..acc1174253 --- /dev/null +++ b/spec/ruby/core/enumerator/generator/initialize_spec.rb @@ -0,0 +1,26 @@ +# -*- encoding: us-ascii -*- + +require_relative '../../../spec_helper' + +describe "Enumerator::Generator#initialize" do + before :each do + @class = Enumerator::Generator + @uninitialized = @class.allocate + end + + it "is a private method" do + @class.should have_private_instance_method(:initialize, false) + end + + it "returns self when given a block" do + @uninitialized.send(:initialize) {}.should equal(@uninitialized) + end + + describe "on frozen instance" do + it "raises a FrozenError" do + -> { + @uninitialized.freeze.send(:initialize) {} + }.should raise_error(FrozenError) + end + end +end diff --git a/spec/ruby/core/enumerator/initialize_spec.rb b/spec/ruby/core/enumerator/initialize_spec.rb new file mode 100644 index 0000000000..5e0256ca46 --- /dev/null +++ b/spec/ruby/core/enumerator/initialize_spec.rb @@ -0,0 +1,57 @@ +# -*- encoding: us-ascii -*- + +require_relative '../../spec_helper' + +describe "Enumerator#initialize" do + before :each do + @uninitialized = Enumerator.allocate + end + + it "is a private method" do + Enumerator.should have_private_instance_method(:initialize, false) + end + + it "returns self when given a block" do + @uninitialized.send(:initialize) {}.should equal(@uninitialized) + end + + # Maybe spec should be broken up? + it "accepts a block" do + @uninitialized.send(:initialize) do |yielder| + r = yielder.yield 3 + yielder << r << 2 << 1 + end + @uninitialized.should be_an_instance_of(Enumerator) + r = [] + @uninitialized.each{|x| r << x; x * 2} + r.should == [3, 6, 2, 1] + end + + it "sets size to nil if size is not given" do + @uninitialized.send(:initialize) {}.size.should be_nil + end + + it "sets size to nil if the given size is nil" do + @uninitialized.send(:initialize, nil) {}.size.should be_nil + end + + it "sets size to the given size if the given size is Float::INFINITY" do + @uninitialized.send(:initialize, Float::INFINITY) {}.size.should equal(Float::INFINITY) + end + + it "sets size to the given size if the given size is an Integer" do + @uninitialized.send(:initialize, 100) {}.size.should == 100 + end + + it "sets size to the given size if the given size is a Proc" do + @uninitialized.send(:initialize, -> { 200 }) {}.size.should == 200 + end + + describe "on frozen instance" do + it "raises a FrozenError" do + -> { + @uninitialized.freeze.send(:initialize) {} + }.should raise_error(FrozenError) + end + end +end diff --git a/spec/ruby/core/enumerator/inspect_spec.rb b/spec/ruby/core/enumerator/inspect_spec.rb new file mode 100644 index 0000000000..7e97864246 --- /dev/null +++ b/spec/ruby/core/enumerator/inspect_spec.rb @@ -0,0 +1,22 @@ +require_relative '../../spec_helper' + +describe "Enumerator#inspect" do + describe "shows a representation of the Enumerator" do + it "including receiver and method" do + (1..3).each.inspect.should == "#<Enumerator: 1..3:each>" + end + + it "including receiver and method and arguments" do + (1..3).each_slice(2).inspect.should == "#<Enumerator: 1..3:each_slice(2)>" + end + + it "including the nested Enumerator" do + (1..3).each.each_slice(2).inspect.should == "#<Enumerator: #<Enumerator: 1..3:each>:each_slice(2)>" + end + end + + it "returns a not initialized representation if #initialized is not called yet" do + Enumerator.allocate.inspect.should == "#<Enumerator: uninitialized>" + Enumerator::Lazy.allocate.inspect.should == "#<Enumerator::Lazy: uninitialized>" + end +end diff --git a/spec/ruby/core/enumerator/lazy/chunk_spec.rb b/spec/ruby/core/enumerator/lazy/chunk_spec.rb new file mode 100644 index 0000000000..87d2b0c206 --- /dev/null +++ b/spec/ruby/core/enumerator/lazy/chunk_spec.rb @@ -0,0 +1,67 @@ +# -*- encoding: us-ascii -*- + +require_relative '../../../spec_helper' +require_relative 'fixtures/classes' + +describe "Enumerator::Lazy#chunk" do + + before :each do + @yieldsmixed = EnumeratorLazySpecs::YieldsMixed.new.to_enum.lazy + @eventsmixed = EnumeratorLazySpecs::EventsMixed.new.to_enum.lazy + ScratchPad.record [] + end + + after :each do + ScratchPad.clear + end + + it "returns a new instance of Enumerator::Lazy" do + ret = @yieldsmixed.chunk {} + ret.should be_an_instance_of(Enumerator::Lazy) + ret.should_not equal(@yieldsmixed) + end + + it "sets #size to nil" do + Enumerator::Lazy.new(Object.new, 100) {}.chunk { |v| v }.size.should == nil + end + + it "returns an Enumerator if called without a block" do + chunk = @yieldsmixed.chunk + chunk.should be_an_instance_of(Enumerator::Lazy) + + res = chunk.each { |v| true }.force + res.should == [[true, EnumeratorLazySpecs::YieldsMixed.gathered_yields]] + end + + describe "when the returned lazy enumerator is evaluated by Enumerable#first" do + it "stops after specified times" do + first_two = (0..Float::INFINITY).lazy.chunk { |n| n.even? }.first(2) + first_two.should == [[true, [0]], [false, [1]]] + end + end + + it "calls the block with gathered values when yield with multiple arguments" do + yields = [] + @yieldsmixed.chunk { |v| yields << v; true }.force + yields.should == EnumeratorLazySpecs::YieldsMixed.gathered_yields + end + + describe "on a nested Lazy" do + it "sets #size to nil" do + Enumerator::Lazy.new(Object.new, 100) {}.take(20).chunk { |v| v }.size.should == nil + end + + describe "when the returned lazy enumerator is evaluated by Enumerable#first" do + it "stops after specified times" do + remains_lazy = (0..Float::INFINITY).lazy.chunk { |n| n } + remains_lazy.chunk { |n| n }.first(2).size.should == 2 + end + end + end + + it "works with an infinite enumerable" do + s = 0..Float::INFINITY + s.lazy.chunk { |n| n.even? }.first(100).should == + s.first(100).chunk { |n| n.even? }.to_a + end +end diff --git a/spec/ruby/core/enumerator/lazy/chunk_while_spec.rb b/spec/ruby/core/enumerator/lazy/chunk_while_spec.rb new file mode 100644 index 0000000000..772bd42de9 --- /dev/null +++ b/spec/ruby/core/enumerator/lazy/chunk_while_spec.rb @@ -0,0 +1,14 @@ +require_relative '../../../spec_helper' + +describe "Enumerator::Lazy#chunk_while" do + it "works with an infinite enumerable" do + s = 0..Float::INFINITY + s.lazy.chunk_while { |a, b| false }.first(100).should == + s.first(100).chunk_while { |a, b| false }.to_a + end + + it "should return a lazy enumerator" do + s = 0..Float::INFINITY + s.lazy.chunk_while { |a, b| false }.should be_kind_of(Enumerator::Lazy) + end +end diff --git a/spec/ruby/core/enumerator/lazy/collect_concat_spec.rb b/spec/ruby/core/enumerator/lazy/collect_concat_spec.rb new file mode 100644 index 0000000000..8765bb2190 --- /dev/null +++ b/spec/ruby/core/enumerator/lazy/collect_concat_spec.rb @@ -0,0 +1,8 @@ +# -*- encoding: us-ascii -*- + +require_relative '../../../spec_helper' +require_relative 'shared/collect_concat' + +describe "Enumerator::Lazy#collect_concat" do + it_behaves_like :enumerator_lazy_collect_concat, :collect_concat +end diff --git a/spec/ruby/core/enumerator/lazy/collect_spec.rb b/spec/ruby/core/enumerator/lazy/collect_spec.rb new file mode 100644 index 0000000000..14b79ce16d --- /dev/null +++ b/spec/ruby/core/enumerator/lazy/collect_spec.rb @@ -0,0 +1,8 @@ +# -*- encoding: us-ascii -*- + +require_relative '../../../spec_helper' +require_relative 'shared/collect' + +describe "Enumerator::Lazy#collect" do + it_behaves_like :enumerator_lazy_collect, :collect +end diff --git a/spec/ruby/core/enumerator/lazy/compact_spec.rb b/spec/ruby/core/enumerator/lazy/compact_spec.rb new file mode 100644 index 0000000000..65c544f062 --- /dev/null +++ b/spec/ruby/core/enumerator/lazy/compact_spec.rb @@ -0,0 +1,14 @@ +require_relative '../../../spec_helper' +require_relative 'fixtures/classes' + +describe "Enumerator::Lazy#compact" do + it 'returns array without nil elements' do + arr = [1, nil, 3, false, 5].to_enum.lazy.compact + arr.should be_an_instance_of(Enumerator::Lazy) + arr.force.should == [1, 3, false, 5] + end + + it "sets #size to nil" do + Enumerator::Lazy.new(Object.new, 100) {}.compact.size.should == nil + end +end diff --git a/spec/ruby/core/enumerator/lazy/drop_spec.rb b/spec/ruby/core/enumerator/lazy/drop_spec.rb new file mode 100644 index 0000000000..822b8034fb --- /dev/null +++ b/spec/ruby/core/enumerator/lazy/drop_spec.rb @@ -0,0 +1,58 @@ +# -*- encoding: us-ascii -*- + +require_relative '../../../spec_helper' +require_relative 'fixtures/classes' + +describe "Enumerator::Lazy#drop" do + before :each do + @yieldsmixed = EnumeratorLazySpecs::YieldsMixed.new.to_enum.lazy + @eventsmixed = EnumeratorLazySpecs::EventsMixed.new.to_enum.lazy + ScratchPad.record [] + end + + after :each do + ScratchPad.clear + end + + it "returns a new instance of Enumerator::Lazy" do + ret = @yieldsmixed.drop(1) + ret.should be_an_instance_of(Enumerator::Lazy) + ret.should_not equal(@yieldsmixed) + end + + it "sets difference of given count with old size to new size" do + Enumerator::Lazy.new(Object.new, 100) {}.drop(20).size.should == 80 + Enumerator::Lazy.new(Object.new, 100) {}.drop(200).size.should == 0 + end + + describe "when the returned lazy enumerator is evaluated by Enumerable#first" do + it "stops after specified times" do + (0..Float::INFINITY).lazy.drop(2).first(2).should == [2, 3] + + @eventsmixed.drop(0).first(1) + ScratchPad.recorded.should == [:before_yield] + end + end + + describe "on a nested Lazy" do + it "sets difference of given count with old size to new size" do + Enumerator::Lazy.new(Object.new, 100) {}.drop(20).drop(50).size.should == 30 + Enumerator::Lazy.new(Object.new, 100) {}.drop(50).drop(20).size.should == 30 + end + + describe "when the returned lazy enumerator is evaluated by Enumerable#first" do + it "stops after specified times" do + (0..Float::INFINITY).lazy.drop(2).drop(2).first(2).should == [4, 5] + + @eventsmixed.drop(0).drop(0).first(1) + ScratchPad.recorded.should == [:before_yield] + end + end + end + + it "works with an infinite enumerable" do + s = 0..Float::INFINITY + s.lazy.drop(100).first(100).should == + s.first(200).drop(100) + end +end diff --git a/spec/ruby/core/enumerator/lazy/drop_while_spec.rb b/spec/ruby/core/enumerator/lazy/drop_while_spec.rb new file mode 100644 index 0000000000..4f6e366f88 --- /dev/null +++ b/spec/ruby/core/enumerator/lazy/drop_while_spec.rb @@ -0,0 +1,66 @@ +# -*- encoding: us-ascii -*- + +require_relative '../../../spec_helper' +require_relative 'fixtures/classes' + +describe "Enumerator::Lazy#drop_while" do + before :each do + @yieldsmixed = EnumeratorLazySpecs::YieldsMixed.new.to_enum.lazy + @eventsmixed = EnumeratorLazySpecs::EventsMixed.new.to_enum.lazy + ScratchPad.record [] + end + + after :each do + ScratchPad.clear + end + + it "returns a new instance of Enumerator::Lazy" do + ret = @yieldsmixed.drop_while {} + ret.should be_an_instance_of(Enumerator::Lazy) + ret.should_not equal(@yieldsmixed) + end + + it "sets #size to nil" do + Enumerator::Lazy.new(Object.new, 100) {}.drop_while { |v| v }.size.should == nil + end + + describe "when the returned lazy enumerator is evaluated by Enumerable#first" do + it "stops after specified times" do + (0..Float::INFINITY).lazy.drop_while { |n| n < 5 }.first(2).should == [5, 6] + + @eventsmixed.drop_while { false }.first(1) + ScratchPad.recorded.should == [:before_yield] + end + end + + it "calls the block with initial values when yield with multiple arguments" do + yields = [] + @yieldsmixed.drop_while { |v| yields << v; true }.force + yields.should == EnumeratorLazySpecs::YieldsMixed.initial_yields + end + + it "raises an ArgumentError when not given a block" do + -> { @yieldsmixed.drop_while }.should raise_error(ArgumentError) + end + + describe "on a nested Lazy" do + it "sets #size to nil" do + Enumerator::Lazy.new(Object.new, 100) {}.take(20).drop_while { |v| v }.size.should == nil + end + + describe "when the returned lazy enumerator is evaluated by Enumerable#first" do + it "stops after specified times" do + (0..Float::INFINITY).lazy.drop_while { |n| n < 5 }.drop_while { |n| n.odd? }.first(2).should == [6, 7] + + @eventsmixed.drop_while { false }.drop_while { false }.first(1) + ScratchPad.recorded.should == [:before_yield] + end + end + end + + it "works with an infinite enumerable" do + s = 0..Float::INFINITY + s.lazy.drop_while { |n| n < 100 }.first(100).should == + s.first(200).drop_while { |n| n < 100 } + end +end diff --git a/spec/ruby/core/enumerator/lazy/eager_spec.rb b/spec/ruby/core/enumerator/lazy/eager_spec.rb new file mode 100644 index 0000000000..592da4fd8c --- /dev/null +++ b/spec/ruby/core/enumerator/lazy/eager_spec.rb @@ -0,0 +1,27 @@ +require_relative '../../../spec_helper' + +describe "Enumerator::Lazy#eager" do + it "returns a non-lazy Enumerator converted from the lazy enumerator" do + enum = [1, 2, 3].lazy + + enum.class.should == Enumerator::Lazy + enum.eager.class.should == Enumerator + end + + it "does not enumerate an enumerator" do + ScratchPad.record [] + + sequence = [1, 2, 3] + enum_lazy = Enumerator::Lazy.new(sequence) do |yielder, value| + yielder << value + ScratchPad << value + end + + ScratchPad.recorded.should == [] + enum = enum_lazy.eager + ScratchPad.recorded.should == [] + + enum.map { |i| i }.should == [1, 2, 3] + ScratchPad.recorded.should == [1, 2, 3] + end +end diff --git a/spec/ruby/core/enumerator/lazy/enum_for_spec.rb b/spec/ruby/core/enumerator/lazy/enum_for_spec.rb new file mode 100644 index 0000000000..7e7783f6f1 --- /dev/null +++ b/spec/ruby/core/enumerator/lazy/enum_for_spec.rb @@ -0,0 +1,8 @@ +# -*- encoding: us-ascii -*- + +require_relative '../../../spec_helper' +require_relative 'shared/to_enum' + +describe "Enumerator::Lazy#enum_for" do + it_behaves_like :enumerator_lazy_to_enum, :enum_for +end diff --git a/spec/ruby/core/enumerator/lazy/filter_map_spec.rb b/spec/ruby/core/enumerator/lazy/filter_map_spec.rb new file mode 100644 index 0000000000..99dd59cebe --- /dev/null +++ b/spec/ruby/core/enumerator/lazy/filter_map_spec.rb @@ -0,0 +1,14 @@ +# -*- encoding: us-ascii -*- + +require_relative '../../../spec_helper' +require_relative 'fixtures/classes' + +describe "Enumerator::Lazy#filter_map" do + it "maps only truthy results" do + (1..Float::INFINITY).lazy.filter_map { |i| i if i.odd? }.first(4).should == [1, 3, 5, 7] + end + + it "does not map false results" do + (1..Float::INFINITY).lazy.filter_map { |i| i.odd? ? i : false }.first(4).should == [1, 3, 5, 7] + end +end diff --git a/spec/ruby/core/enumerator/lazy/filter_spec.rb b/spec/ruby/core/enumerator/lazy/filter_spec.rb new file mode 100644 index 0000000000..43128241e0 --- /dev/null +++ b/spec/ruby/core/enumerator/lazy/filter_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../../spec_helper' +require_relative 'shared/select' + +describe "Enumerator::Lazy#filter" do + it_behaves_like :enumerator_lazy_select, :filter +end diff --git a/spec/ruby/core/enumerator/lazy/find_all_spec.rb b/spec/ruby/core/enumerator/lazy/find_all_spec.rb new file mode 100644 index 0000000000..8b05c53803 --- /dev/null +++ b/spec/ruby/core/enumerator/lazy/find_all_spec.rb @@ -0,0 +1,8 @@ +# -*- encoding: us-ascii -*- + +require_relative '../../../spec_helper' +require_relative 'shared/select' + +describe "Enumerator::Lazy#find_all" do + it_behaves_like :enumerator_lazy_select, :find_all +end diff --git a/spec/ruby/core/enumerator/lazy/fixtures/classes.rb b/spec/ruby/core/enumerator/lazy/fixtures/classes.rb new file mode 100644 index 0000000000..e35592ba1c --- /dev/null +++ b/spec/ruby/core/enumerator/lazy/fixtures/classes.rb @@ -0,0 +1,54 @@ +# -*- encoding: us-ascii -*- + +module EnumeratorLazySpecs + class SpecificError < Exception; end + + class YieldsMixed + def self.initial_yields + [nil, 0, 0, 0, 0, nil, :default_arg, [], [], [0], [0, 1], [0, 1, 2]] + end + + def self.gathered_yields + [nil, 0, [0, 1], [0, 1, 2], [0, 1, 2], nil, :default_arg, [], [], [0], [0, 1], [0, 1, 2]] + end + + def self.gathered_non_array_yields + [nil, 0, nil, :default_arg] + end + + def self.gathered_yields_with_args(arg, *args) + [nil, 0, [0, 1], [0, 1, 2], [0, 1, 2], nil, arg, args, [], [0], [0, 1], [0, 1, 2]] + end + + def each(arg=:default_arg, *args) + yield + yield 0 + yield 0, 1 + yield 0, 1, 2 + yield(*[0, 1, 2]) + yield nil + yield arg + yield args + yield [] + yield [0] + yield [0, 1] + yield [0, 1, 2] + end + end + + class EventsMixed + def each + ScratchPad << :before_yield + + yield 0 + + ScratchPad << :after_yield + + raise SpecificError + + ScratchPad << :after_error + + :should_not_reach_here + end + end +end diff --git a/spec/ruby/core/enumerator/lazy/flat_map_spec.rb b/spec/ruby/core/enumerator/lazy/flat_map_spec.rb new file mode 100644 index 0000000000..5dcaa8bfa1 --- /dev/null +++ b/spec/ruby/core/enumerator/lazy/flat_map_spec.rb @@ -0,0 +1,16 @@ +# -*- encoding: us-ascii -*- + +require_relative '../../../spec_helper' +require_relative 'shared/collect_concat' + +describe "Enumerator::Lazy#flat_map" do + it_behaves_like :enumerator_lazy_collect_concat, :flat_map + + it "properly unwraps nested yields" do + s = Enumerator.new do |y| loop do y << [1, 2] end end + + expected = s.take(3).flat_map { |x| x }.to_a + actual = s.lazy.take(3).flat_map{ |x| x }.force + actual.should == expected + end +end diff --git a/spec/ruby/core/enumerator/lazy/force_spec.rb b/spec/ruby/core/enumerator/lazy/force_spec.rb new file mode 100644 index 0000000000..a7fa029135 --- /dev/null +++ b/spec/ruby/core/enumerator/lazy/force_spec.rb @@ -0,0 +1,36 @@ +# -*- encoding: us-ascii -*- + +require_relative '../../../spec_helper' +require_relative 'fixtures/classes' + +describe "Enumerator::Lazy#force" do + before :each do + @yieldsmixed = EnumeratorLazySpecs::YieldsMixed.new.to_enum.lazy + @eventsmixed = EnumeratorLazySpecs::EventsMixed.new.to_enum.lazy + ScratchPad.record [] + end + + after :each do + ScratchPad.clear + end + + it "passes given arguments to receiver.each" do + @yieldsmixed.force(:arg1, :arg2, :arg3).should == + EnumeratorLazySpecs::YieldsMixed.gathered_yields_with_args(:arg1, :arg2, :arg3) + end + + describe "on a nested Lazy" do + it "calls all block and returns an Array" do + (0..Float::INFINITY).lazy.map(&:succ).take(2).force.should == [1, 2] + + @eventsmixed.take(1).map(&:succ).force.should == [1] + ScratchPad.recorded.should == [:before_yield] + end + end + + it "works with an infinite enumerable" do + s = 0..Float::INFINITY + s.lazy.take(100).force.should == + s.take(100) + end +end diff --git a/spec/ruby/core/enumerator/lazy/grep_spec.rb b/spec/ruby/core/enumerator/lazy/grep_spec.rb new file mode 100644 index 0000000000..e67686c9a3 --- /dev/null +++ b/spec/ruby/core/enumerator/lazy/grep_spec.rb @@ -0,0 +1,121 @@ +# -*- encoding: us-ascii -*- + +require_relative '../../../spec_helper' +require_relative 'fixtures/classes' + +describe "Enumerator::Lazy#grep" do + before :each do + @yieldsmixed = EnumeratorLazySpecs::YieldsMixed.new.to_enum.lazy + @eventsmixed = EnumeratorLazySpecs::EventsMixed.new.to_enum.lazy + ScratchPad.record [] + end + + after :each do + ScratchPad.clear + end + + it "requires an argument" do + Enumerator::Lazy.instance_method(:grep).arity.should == 1 + end + + it "returns a new instance of Enumerator::Lazy" do + ret = @yieldsmixed.grep(Object) {} + ret.should be_an_instance_of(Enumerator::Lazy) + ret.should_not equal(@yieldsmixed) + + ret = @yieldsmixed.grep(Object) + ret.should be_an_instance_of(Enumerator::Lazy) + ret.should_not equal(@yieldsmixed) + end + + it "sets #size to nil" do + Enumerator::Lazy.new(Object.new, 100) {}.grep(Object) {}.size.should == nil + Enumerator::Lazy.new(Object.new, 100) {}.grep(Object).size.should == nil + end + + it "sets $~ in the block" do + "z" =~ /z/ # Reset $~ + ["abc", "def"].lazy.grep(/b/) { |e| + e.should == "abc" + $&.should == "b" + }.force + + # Set by the failed match of "def" + $~.should == nil + end + + it "sets $~ in the next block with each" do + "z" =~ /z/ # Reset $~ + ["abc", "def"].lazy.grep(/b/).each { |e| + e.should == "abc" + $&.should == "b" + } + + # Set by the failed match of "def" + $~.should == nil + end + + it "sets $~ in the next block with map" do + "z" =~ /z/ # Reset $~ + ["abc", "def"].lazy.grep(/b/).map { |e| + e.should == "abc" + $&.should == "b" + }.force + + # Set by the failed match of "def" + $~.should == nil + end + + describe "when the returned lazy enumerator is evaluated by Enumerable#first" do + it "stops after specified times when not given a block" do + (0..Float::INFINITY).lazy.grep(Integer).first(3).should == [0, 1, 2] + + @eventsmixed.grep(BasicObject).first(1) + ScratchPad.recorded.should == [:before_yield] + end + + it "stops after specified times when given a block" do + (0..Float::INFINITY).lazy.grep(Integer, &:succ).first(3).should == [1, 2, 3] + + @eventsmixed.grep(BasicObject) {}.first(1) + ScratchPad.recorded.should == [:before_yield] + end + end + + it "calls the block with a gathered array when yield with multiple arguments" do + yields = [] + @yieldsmixed.grep(BasicObject) { |v| yields << v }.force + yields.should == EnumeratorLazySpecs::YieldsMixed.gathered_yields + + @yieldsmixed.grep(BasicObject).force.should == yields + end + + describe "on a nested Lazy" do + it "sets #size to nil" do + Enumerator::Lazy.new(Object.new, 100) {}.grep(Object) {}.size.should == nil + Enumerator::Lazy.new(Object.new, 100) {}.grep(Object).size.should == nil + end + + describe "when the returned lazy enumerator is evaluated by Enumerable#first" do + it "stops after specified times when not given a block" do + (0..Float::INFINITY).lazy.grep(Integer).grep(Object).first(3).should == [0, 1, 2] + + @eventsmixed.grep(BasicObject).grep(Object).first(1) + ScratchPad.recorded.should == [:before_yield] + end + + it "stops after specified times when given a block" do + (0..Float::INFINITY).lazy.grep(Integer) { |n| n > 3 ? n : false }.grep(Integer) { |n| n.even? ? n : false }.first(3).should == [4, false, 6] + + @eventsmixed.grep(BasicObject) {}.grep(Object) {}.first(1) + ScratchPad.recorded.should == [:before_yield] + end + end + end + + it "works with an infinite enumerable" do + s = 0..Float::INFINITY + s.lazy.grep(Numeric).first(100).should == + s.first(100).grep(Numeric) + end +end diff --git a/spec/ruby/core/enumerator/lazy/grep_v_spec.rb b/spec/ruby/core/enumerator/lazy/grep_v_spec.rb new file mode 100644 index 0000000000..67173021bb --- /dev/null +++ b/spec/ruby/core/enumerator/lazy/grep_v_spec.rb @@ -0,0 +1,123 @@ +require_relative '../../../spec_helper' +require_relative 'fixtures/classes' + +describe "Enumerator::Lazy#grep_v" do + before(:each) do + @yieldsmixed = EnumeratorLazySpecs::YieldsMixed.new.to_enum.lazy + @eventsmixed = EnumeratorLazySpecs::EventsMixed.new.to_enum.lazy + ScratchPad.record [] + end + + after(:each) do + ScratchPad.clear + end + + it "requires an argument" do + Enumerator::Lazy.instance_method(:grep_v).arity.should == 1 + end + + it "returns a new instance of Enumerator::Lazy" do + ret = @yieldsmixed.grep_v(Object) {} + ret.should be_an_instance_of(Enumerator::Lazy) + ret.should_not equal(@yieldsmixed) + + ret = @yieldsmixed.grep_v(Object) + ret.should be_an_instance_of(Enumerator::Lazy) + ret.should_not equal(@yieldsmixed) + end + + it "sets #size to nil" do + Enumerator::Lazy.new(Object.new, 100) {}.grep_v(Object) {}.size.should == nil + Enumerator::Lazy.new(Object.new, 100) {}.grep_v(Object).size.should == nil + end + + it "sets $~ in the block" do + "z" =~ /z/ # Reset $~ + ["abc", "def"].lazy.grep_v(/e/) { |e| + e.should == "abc" + $~.should == nil + }.force + + # Set by the match of "def" + $&.should == "e" + end + + it "sets $~ in the next block with each" do + "z" =~ /z/ # Reset $~ + ["abc", "def"].lazy.grep_v(/e/).each { |e| + e.should == "abc" + $~.should == nil + } + + # Set by the match of "def" + $&.should == "e" + end + + it "sets $~ in the next block with map" do + "z" =~ /z/ # Reset $~ + ["abc", "def"].lazy.grep_v(/e/).map { |e| + e.should == "abc" + $~.should == nil + }.force + + # Set by the match of "def" + $&.should == "e" + end + + describe "when the returned lazy enumerator is evaluated by Enumerable#first" do + it "stops after specified times when not given a block" do + (0..Float::INFINITY).lazy.grep_v(3..5).first(3).should == [0, 1, 2] + + @eventsmixed.grep_v(Symbol).first(1) + ScratchPad.recorded.should == [:before_yield] + end + + it "stops after specified times when given a block" do + (0..Float::INFINITY).lazy.grep_v(4..8, &:succ).first(3).should == [1, 2, 3] + + @eventsmixed.grep_v(Symbol) {}.first(1) + ScratchPad.recorded.should == [:before_yield] + end + end + + it "calls the block with a gathered array when yield with multiple arguments" do + yields = [] + @yieldsmixed.grep_v(Array) { |v| yields << v }.force + yields.should == EnumeratorLazySpecs::YieldsMixed.gathered_non_array_yields + + @yieldsmixed.grep_v(Array).force.should == yields + end + + describe "on a nested Lazy" do + it "sets #size to nil" do + Enumerator::Lazy.new(Object.new, 100) {}.grep_v(Object).grep_v(Object) {}.size.should == nil + Enumerator::Lazy.new(Object.new, 100) {}.grep_v(Object).grep_v(Object).size.should == nil + end + + describe "when the returned lazy enumerator is evaluated by Enumerable#first" do + it "stops after specified times when not given a block" do + (0..Float::INFINITY).lazy.grep_v(3..5).grep_v(6..10).first(3).should == [0, 1, 2] + + @eventsmixed.grep_v(Symbol).grep_v(String).first(1) + ScratchPad.recorded.should == [:before_yield] + end + + it "stops after specified times when given a block" do + (0..Float::INFINITY).lazy + .grep_v(1..2) { |n| n > 3 ? n : false } + .grep_v(false) { |n| n.even? ? n : false } + .first(3) + .should == [4, false, 6] + + @eventsmixed.grep_v(Symbol) {}.grep_v(String) {}.first(1) + ScratchPad.recorded.should == [:before_yield] + end + end + end + + it "works with an infinite enumerable" do + s = 0..Float::INFINITY + s.lazy.grep_v(String).first(100).should == + s.first(100).grep_v(String) + end +end diff --git a/spec/ruby/core/enumerator/lazy/initialize_spec.rb b/spec/ruby/core/enumerator/lazy/initialize_spec.rb new file mode 100644 index 0000000000..e1e0b1d608 --- /dev/null +++ b/spec/ruby/core/enumerator/lazy/initialize_spec.rb @@ -0,0 +1,63 @@ +# -*- encoding: us-ascii -*- + +require_relative '../../../spec_helper' + +describe "Enumerator::Lazy#initialize" do + before :each do + @receiver = receiver = Object.new + + def receiver.each + yield 0 + yield 1 + yield 2 + end + + @uninitialized = Enumerator::Lazy.allocate + end + + it "is a private method" do + Enumerator::Lazy.should have_private_instance_method(:initialize, false) + end + + it "returns self" do + @uninitialized.send(:initialize, @receiver) {}.should equal(@uninitialized) + end + + describe "when the returned lazy enumerator is evaluated by Enumerable#first" do + it "stops after specified times" do + @uninitialized.send(:initialize, @receiver) do |yielder, *values| + yielder.<<(*values) + end.first(2).should == [0, 1] + end + end + + it "sets #size to nil if not given a size" do + @uninitialized.send(:initialize, @receiver) {}.size.should be_nil + end + + it "sets #size to nil if given size is nil" do + @uninitialized.send(:initialize, @receiver, nil) {}.size.should be_nil + end + + it "sets given size to own size if the given size is Float::INFINITY" do + @uninitialized.send(:initialize, @receiver, Float::INFINITY) {}.size.should equal(Float::INFINITY) + end + + it "sets given size to own size if the given size is an Integer" do + @uninitialized.send(:initialize, @receiver, 100) {}.size.should == 100 + end + + it "sets given size to own size if the given size is a Proc" do + @uninitialized.send(:initialize, @receiver, -> { 200 }) {}.size.should == 200 + end + + it "raises an ArgumentError when block is not given" do + -> { @uninitialized.send :initialize, @receiver }.should raise_error(ArgumentError) + end + + describe "on frozen instance" do + it "raises a FrozenError" do + -> { @uninitialized.freeze.send(:initialize, @receiver) {} }.should raise_error(FrozenError) + end + end +end diff --git a/spec/ruby/core/enumerator/lazy/lazy_spec.rb b/spec/ruby/core/enumerator/lazy/lazy_spec.rb new file mode 100644 index 0000000000..b43864b673 --- /dev/null +++ b/spec/ruby/core/enumerator/lazy/lazy_spec.rb @@ -0,0 +1,27 @@ +# -*- encoding: us-ascii -*- + +require_relative '../../../spec_helper' + +describe "Enumerator::Lazy" do + it "is a subclass of Enumerator" do + Enumerator::Lazy.superclass.should equal(Enumerator) + end + + it "defines lazy versions of a whitelist of Enumerator methods" do + lazy_methods = [ + :chunk, :chunk_while, :collect, :collect_concat, :compact, :drop, :drop_while, :enum_for, + :find_all, :flat_map, :force, :grep, :grep_v, :lazy, :map, :reject, + :select, :slice_after, :slice_before, :slice_when, :take, :take_while, + :to_enum, :uniq, :zip + ] + + Enumerator::Lazy.instance_methods(false).should include(*lazy_methods) + end +end + +describe "Enumerator::Lazy#lazy" do + it "returns self" do + lazy = (1..3).to_enum.lazy + lazy.lazy.should equal(lazy) + end +end diff --git a/spec/ruby/core/enumerator/lazy/map_spec.rb b/spec/ruby/core/enumerator/lazy/map_spec.rb new file mode 100644 index 0000000000..5cb998f5f7 --- /dev/null +++ b/spec/ruby/core/enumerator/lazy/map_spec.rb @@ -0,0 +1,12 @@ +# -*- encoding: us-ascii -*- + +require_relative '../../../spec_helper' +require_relative 'shared/collect' + +describe "Enumerator::Lazy#map" do + it_behaves_like :enumerator_lazy_collect, :map + + it "doesn't unwrap Arrays" do + Enumerator.new {|y| y.yield([1])}.lazy.to_a.should == [[1]] + end +end diff --git a/spec/ruby/core/enumerator/lazy/reject_spec.rb b/spec/ruby/core/enumerator/lazy/reject_spec.rb new file mode 100644 index 0000000000..0e1632d667 --- /dev/null +++ b/spec/ruby/core/enumerator/lazy/reject_spec.rb @@ -0,0 +1,78 @@ +# -*- encoding: us-ascii -*- + +require_relative '../../../spec_helper' +require_relative 'fixtures/classes' + +describe "Enumerator::Lazy#reject" do + before :each do + @yieldsmixed = EnumeratorLazySpecs::YieldsMixed.new.to_enum.lazy + @eventsmixed = EnumeratorLazySpecs::EventsMixed.new.to_enum.lazy + ScratchPad.record [] + end + + after :each do + ScratchPad.clear + end + + it "returns a new instance of Enumerator::Lazy" do + ret = @yieldsmixed.reject {} + ret.should be_an_instance_of(Enumerator::Lazy) + ret.should_not equal(@yieldsmixed) + end + + it "sets #size to nil" do + Enumerator::Lazy.new(Object.new, 100) {}.reject {}.size.should == nil + end + + describe "when the returned lazy enumerator is evaluated by Enumerable#first" do + it "stops after specified times" do + (0..Float::INFINITY).lazy.reject(&:even?).first(3).should == [1, 3, 5] + + @eventsmixed.reject { false }.first(1) + ScratchPad.recorded.should == [:before_yield] + end + end + + it "lets exceptions raised in the block go through" do + lazy = 10.times.lazy.map do |i| + raise "foo" + end + + lazy = lazy.reject(&:nil?) + + -> { + lazy.first + }.should raise_error(RuntimeError, "foo") + end + + it "calls the block with a gathered array when yield with multiple arguments" do + yields = [] + @yieldsmixed.reject { |v| yields << v }.force + yields.should == EnumeratorLazySpecs::YieldsMixed.gathered_yields + end + + it "raises an ArgumentError when not given a block" do + -> { @yieldsmixed.reject }.should raise_error(ArgumentError) + end + + describe "on a nested Lazy" do + it "sets #size to nil" do + Enumerator::Lazy.new(Object.new, 100) {}.take(20).reject {}.size.should == nil + end + + describe "when the returned lazy enumerator is evaluated by Enumerable#first" do + it "stops after specified times" do + (0..Float::INFINITY).lazy.reject { |n| n < 4 }.reject(&:even?).first(3).should == [5, 7, 9] + + @eventsmixed.reject { false }.reject { false }.first(1) + ScratchPad.recorded.should == [:before_yield] + end + end + end + + it "works with an infinite enumerable" do + s = 0..Float::INFINITY + s.lazy.reject { |n| false }.first(100).should == + s.first(100).reject { |n| false } + end +end diff --git a/spec/ruby/core/enumerator/lazy/select_spec.rb b/spec/ruby/core/enumerator/lazy/select_spec.rb new file mode 100644 index 0000000000..3773d8f0a8 --- /dev/null +++ b/spec/ruby/core/enumerator/lazy/select_spec.rb @@ -0,0 +1,47 @@ +# -*- encoding: us-ascii -*- + +require_relative '../../../spec_helper' +require_relative 'shared/select' + +describe "Enumerator::Lazy#select" do + it_behaves_like :enumerator_lazy_select, :select + + it "doesn't pre-evaluate the next element" do + eval_count = 0 + enum = %w[Text1 Text2 Text3].lazy.select do + eval_count += 1 + true + end + + eval_count.should == 0 + enum.next + eval_count.should == 1 + end + + it "doesn't over-evaluate when peeked" do + eval_count = 0 + enum = %w[Text1 Text2 Text3].lazy.select do + eval_count += 1 + true + end + + eval_count.should == 0 + enum.peek + enum.peek + eval_count.should == 1 + end + + it "doesn't re-evaluate after peek" do + eval_count = 0 + enum = %w[Text1 Text2 Text3].lazy.select do + eval_count += 1 + true + end + + eval_count.should == 0 + enum.peek + eval_count.should == 1 + enum.next + eval_count.should == 1 + end +end diff --git a/spec/ruby/core/enumerator/lazy/shared/collect.rb b/spec/ruby/core/enumerator/lazy/shared/collect.rb new file mode 100644 index 0000000000..5690255a0c --- /dev/null +++ b/spec/ruby/core/enumerator/lazy/shared/collect.rb @@ -0,0 +1,62 @@ +# -*- encoding: us-ascii -*- + +require_relative '../../../../spec_helper' +require_relative '../fixtures/classes' + +describe :enumerator_lazy_collect, shared: true do + before :each do + @yieldsmixed = EnumeratorLazySpecs::YieldsMixed.new.to_enum.lazy + @eventsmixed = EnumeratorLazySpecs::EventsMixed.new.to_enum.lazy + ScratchPad.record [] + end + + after :each do + ScratchPad.clear + end + + it "returns a new instance of Enumerator::Lazy" do + ret = @yieldsmixed.send(@method) {} + ret.should be_an_instance_of(Enumerator::Lazy) + ret.should_not equal(@yieldsmixed) + end + + it "keeps size" do + Enumerator::Lazy.new(Object.new, 100) {}.send(@method) {}.size.should == 100 + end + + describe "when the returned lazy enumerator is evaluated by Enumerable#first" do + it "stops after specified times" do + (0..Float::INFINITY).lazy.send(@method, &:succ).first(3).should == [1, 2, 3] + + @eventsmixed.send(@method) {}.first(1) + ScratchPad.recorded.should == [:before_yield] + end + end + + it "calls the block with initial values when yield with multiple arguments" do + yields = [] + @yieldsmixed.send(@method) { |v| yields << v }.force + yields.should == EnumeratorLazySpecs::YieldsMixed.initial_yields + end + + describe "on a nested Lazy" do + it "keeps size" do + Enumerator::Lazy.new(Object.new, 100) {}.send(@method) {}.send(@method) {}.size.should == 100 + end + + describe "when the returned lazy enumerator is evaluated by Enumerable#first" do + it "stops after specified times" do + (0..Float::INFINITY).lazy.send(@method, &:succ).send(@method, &:succ).first(3).should == [2, 3, 4] + + @eventsmixed.send(@method) {}.send(@method) {}.first(1) + ScratchPad.recorded.should == [:before_yield] + end + end + end + + it "works with an infinite enumerable" do + s = 0..Float::INFINITY + s.lazy.send(@method) { |n| n }.first(100).should == + s.first(100).send(@method) { |n| n }.to_a + end +end diff --git a/spec/ruby/core/enumerator/lazy/shared/collect_concat.rb b/spec/ruby/core/enumerator/lazy/shared/collect_concat.rb new file mode 100644 index 0000000000..00d7941a61 --- /dev/null +++ b/spec/ruby/core/enumerator/lazy/shared/collect_concat.rb @@ -0,0 +1,78 @@ +# -*- encoding: us-ascii -*- + +require_relative '../../../../spec_helper' +require_relative '../fixtures/classes' + +describe :enumerator_lazy_collect_concat, shared: true do + before :each do + @yieldsmixed = EnumeratorLazySpecs::YieldsMixed.new.to_enum.lazy + @eventsmixed = EnumeratorLazySpecs::EventsMixed.new.to_enum.lazy + ScratchPad.record [] + end + + after :each do + ScratchPad.clear + end + + it "returns a new instance of Enumerator::Lazy" do + ret = @yieldsmixed.send(@method) {} + ret.should be_an_instance_of(Enumerator::Lazy) + ret.should_not equal(@yieldsmixed) + end + + it "sets #size to nil" do + Enumerator::Lazy.new(Object.new, 100) {}.send(@method) { true }.size.should == nil + end + + describe "when the returned lazy enumerator is evaluated by Enumerable#first" do + it "stops after specified times" do + (0..Float::INFINITY).lazy.send(@method) { |n| (n * 10).to_s }.first(6).should == %w[0 10 20 30 40 50] + + @eventsmixed.send(@method) {}.first(1) + ScratchPad.recorded.should == [:before_yield] + end + + it "flattens elements when the given block returned an array or responding to .each and .force" do + (0..Float::INFINITY).lazy.send(@method) { |n| (n * 10).to_s.chars }.first(6).should == %w[0 1 0 2 0 3] + (0..Float::INFINITY).lazy.send(@method) { |n| (n * 10).to_s.each_char }.first(6).all? { |o| o.instance_of? Enumerator }.should be_true + (0..Float::INFINITY).lazy.send(@method) { |n| (n * 10).to_s.each_char.lazy }.first(6).should == %w[0 1 0 2 0 3] + end + end + + it "calls the block with initial values when yield with multiple arguments" do + yields = [] + @yieldsmixed.send(@method) { |v| yields << v }.force + yields.should == EnumeratorLazySpecs::YieldsMixed.initial_yields + end + + it "raises an ArgumentError when not given a block" do + -> { @yieldsmixed.send(@method) }.should raise_error(ArgumentError) + end + + describe "on a nested Lazy" do + it "sets #size to nil" do + Enumerator::Lazy.new(Object.new, 100) {}.take(50) {}.send(@method) {}.size.should == nil + end + + describe "when the returned lazy enumerator is evaluated by Enumerable#first" do + it "stops after specified times" do + (0..Float::INFINITY).lazy.map {|n| n * 10 }.send(@method) { |n| n.to_s }.first(6).should == %w[0 10 20 30 40 50] + + @eventsmixed.send(@method) {}.send(@method) {}.first(1) + ScratchPad.recorded.should == [:before_yield] + end + + it "flattens elements when the given block returned an array or responding to .each and .force" do + (0..Float::INFINITY).lazy.map {|n| n * 10 }.send(@method) { |n| n.to_s.chars }.first(6).should == %w[0 1 0 2 0 3] + (0..Float::INFINITY).lazy.map {|n| n * 10 }.send(@method) { |n| n.to_s.each_char }.first(6).all? { |o| o.instance_of? Enumerator }.should be_true + (0..Float::INFINITY).lazy.map {|n| n * 10 }.send(@method) { |n| n.to_s.each_char.lazy }.first(6).should == %w[0 1 0 2 0 3] + end + end + end + + it "works with an infinite enumerable" do + s = 0..Float::INFINITY + s.lazy.send(@method) { |n| [-n, +n] }.first(200).should == + s.first(100).send(@method) { |n| [-n, +n] }.to_a + end +end diff --git a/spec/ruby/core/enumerator/lazy/shared/select.rb b/spec/ruby/core/enumerator/lazy/shared/select.rb new file mode 100644 index 0000000000..50a00bcbf4 --- /dev/null +++ b/spec/ruby/core/enumerator/lazy/shared/select.rb @@ -0,0 +1,66 @@ +# -*- encoding: us-ascii -*- + +require_relative '../../../../spec_helper' +require_relative '../fixtures/classes' + +describe :enumerator_lazy_select, shared: true do + before :each do + @yieldsmixed = EnumeratorLazySpecs::YieldsMixed.new.to_enum.lazy + @eventsmixed = EnumeratorLazySpecs::EventsMixed.new.to_enum.lazy + ScratchPad.record [] + end + + after :each do + ScratchPad.clear + end + + it "returns a new instance of Enumerator::Lazy" do + ret = @yieldsmixed.send(@method) {} + ret.should be_an_instance_of(Enumerator::Lazy) + ret.should_not equal(@yieldsmixed) + end + + it "sets #size to nil" do + Enumerator::Lazy.new(Object.new, 100) {}.send(@method) { true }.size.should == nil + end + + describe "when the returned lazy enumerator is evaluated by Enumerable#first" do + it "stops after specified times" do + (0..Float::INFINITY).lazy.send(@method, &:even?).first(3).should == [0, 2, 4] + + @eventsmixed.send(@method) { true }.first(1) + ScratchPad.recorded.should == [:before_yield] + end + end + + it "calls the block with a gathered array when yield with multiple arguments" do + yields = [] + @yieldsmixed.send(@method) { |v| yields << v }.force + yields.should == EnumeratorLazySpecs::YieldsMixed.gathered_yields + end + + it "raises an ArgumentError when not given a block" do + -> { @yieldsmixed.send(@method) }.should raise_error(ArgumentError) + end + + describe "on a nested Lazy" do + it "sets #size to nil" do + Enumerator::Lazy.new(Object.new, 100) {}.take(50) {}.send(@method) { true }.size.should == nil + end + + describe "when the returned lazy enumerator is evaluated by Enumerable#first" do + it "stops after specified times" do + (0..Float::INFINITY).lazy.send(@method) { |n| n > 5 }.send(@method, &:even?).first(3).should == [6, 8, 10] + + @eventsmixed.send(@method) { true }.send(@method) { true }.first(1) + ScratchPad.recorded.should == [:before_yield] + end + end + end + + it "works with an infinite enumerable" do + s = 0..Float::INFINITY + s.lazy.send(@method) { |n| true }.first(100).should == + s.first(100).send(@method) { |n| true } + end +end diff --git a/spec/ruby/core/enumerator/lazy/shared/to_enum.rb b/spec/ruby/core/enumerator/lazy/shared/to_enum.rb new file mode 100644 index 0000000000..0c91ea55b9 --- /dev/null +++ b/spec/ruby/core/enumerator/lazy/shared/to_enum.rb @@ -0,0 +1,55 @@ +# -*- encoding: us-ascii -*- + +require_relative '../../../../spec_helper' + +describe :enumerator_lazy_to_enum, shared: true do + before :each do + @infinite = (0..Float::INFINITY).lazy + end + + it "requires multiple arguments" do + Enumerator::Lazy.instance_method(@method).arity.should < 0 + end + + it "returns a new instance of Enumerator::Lazy" do + ret = @infinite.send @method + ret.should be_an_instance_of(Enumerator::Lazy) + ret.should_not equal(@infinite) + end + + it "sets #size to nil when not given a block" do + Enumerator::Lazy.new(Object.new, 100) {}.send(@method).size.should == nil + end + + it "sets given block to size when given a block" do + Enumerator::Lazy.new(Object.new, 100) {}.send(@method) { 30 }.size.should == 30 + end + + it "generates a lazy enumerator from the given name" do + @infinite.send(@method, :with_index, 10).first(3).should == [[0, 10], [1, 11], [2, 12]] + end + + it "passes given arguments to wrapped method" do + @infinite.send(@method, :each_slice, 2).map { |assoc| assoc.first * assoc.last }.first(4).should == [0, 6, 20, 42] + end + + it "used by some parent's methods though returning Lazy" do + { each_with_index: [], + with_index: [], + cycle: [1], + each_with_object: [Object.new], + with_object: [Object.new], + each_slice: [2], + each_entry: [], + each_cons: [2] + }.each_pair do |method, args| + @infinite.send(method, *args).should be_an_instance_of(Enumerator::Lazy) + end + end + + it "works with an infinite enumerable" do + s = 0..Float::INFINITY + s.lazy.send(@method, :with_index).first(100).should == + s.first(100).to_enum.send(@method, :with_index).to_a + end +end diff --git a/spec/ruby/core/enumerator/lazy/slice_after_spec.rb b/spec/ruby/core/enumerator/lazy/slice_after_spec.rb new file mode 100644 index 0000000000..8b08a1ecfd --- /dev/null +++ b/spec/ruby/core/enumerator/lazy/slice_after_spec.rb @@ -0,0 +1,14 @@ +require_relative '../../../spec_helper' + +describe "Enumerator::Lazy#slice_after" do + it "works with an infinite enumerable" do + s = 0..Float::INFINITY + s.lazy.slice_after { |n| true }.first(100).should == + s.first(100).slice_after { |n| true }.to_a + end + + it "should return a lazy enumerator" do + s = 0..Float::INFINITY + s.lazy.slice_after { |n| true }.should be_kind_of(Enumerator::Lazy) + end +end diff --git a/spec/ruby/core/enumerator/lazy/slice_before_spec.rb b/spec/ruby/core/enumerator/lazy/slice_before_spec.rb new file mode 100644 index 0000000000..9c1ec9ba4a --- /dev/null +++ b/spec/ruby/core/enumerator/lazy/slice_before_spec.rb @@ -0,0 +1,14 @@ +require_relative '../../../spec_helper' + +describe "Enumerator::Lazy#slice_before" do + it "works with an infinite enumerable" do + s = 0..Float::INFINITY + s.lazy.slice_before { |n| true }.first(100).should == + s.first(100).slice_before { |n| true }.to_a + end + + it "should return a lazy enumerator" do + s = 0..Float::INFINITY + s.lazy.slice_before { |n| true }.should be_kind_of(Enumerator::Lazy) + end +end diff --git a/spec/ruby/core/enumerator/lazy/slice_when_spec.rb b/spec/ruby/core/enumerator/lazy/slice_when_spec.rb new file mode 100644 index 0000000000..f83403425d --- /dev/null +++ b/spec/ruby/core/enumerator/lazy/slice_when_spec.rb @@ -0,0 +1,14 @@ +require_relative '../../../spec_helper' + +describe "Enumerator::Lazy#slice_when" do + it "works with an infinite enumerable" do + s = 0..Float::INFINITY + s.lazy.slice_when { |a, b| true }.first(100).should == + s.first(100).slice_when { |a, b| true }.to_a + end + + it "should return a lazy enumerator" do + s = 0..Float::INFINITY + s.lazy.slice_when { |a, b| true }.should be_kind_of(Enumerator::Lazy) + end +end diff --git a/spec/ruby/core/enumerator/lazy/take_spec.rb b/spec/ruby/core/enumerator/lazy/take_spec.rb new file mode 100644 index 0000000000..9fc17e969f --- /dev/null +++ b/spec/ruby/core/enumerator/lazy/take_spec.rb @@ -0,0 +1,66 @@ +# -*- encoding: us-ascii -*- + +require_relative '../../../spec_helper' +require_relative 'fixtures/classes' + +describe "Enumerator::Lazy#take" do + before :each do + @yieldsmixed = EnumeratorLazySpecs::YieldsMixed.new.to_enum.lazy + @eventsmixed = EnumeratorLazySpecs::EventsMixed.new.to_enum.lazy + ScratchPad.record [] + end + + after :each do + ScratchPad.clear + end + + it "returns a new instance of Enumerator::Lazy" do + ret = @yieldsmixed.take(1) + ret.should be_an_instance_of(Enumerator::Lazy) + ret.should_not equal(@yieldsmixed) + end + + it "sets given count to size if the given count is less than old size" do + Enumerator::Lazy.new(Object.new, 100) {}.take(20).size.should == 20 + Enumerator::Lazy.new(Object.new, 100) {}.take(200).size.should == 100 + end + + it "sets given count to size if the old size is Infinity" do + loop.lazy.take(20).size.should == 20 + end + + describe "when the returned lazy enumerator is evaluated by .force" do + it "stops after specified times" do + (0..Float::INFINITY).lazy.take(2).force.should == [0, 1] + + @eventsmixed.take(1).force + ScratchPad.recorded.should == [:before_yield] + end + + it "stops without iterations if the given argument is 0" do + @eventsmixed.take(0).force + ScratchPad.recorded.should == [] + end + end + + describe "on a nested Lazy" do + it "sets given count to size if the given count is less than old size" do + Enumerator::Lazy.new(Object.new, 100) {}.take(20).take(50).size.should == 20 + Enumerator::Lazy.new(Object.new, 100) {}.take(50).take(20).size.should == 20 + end + + describe "when the returned lazy enumerator is evaluated by .force" do + it "stops after specified times" do + (0..Float::INFINITY).lazy.map(&:succ).take(2).force.should == [1, 2] + + @eventsmixed.take(10).take(1).force + ScratchPad.recorded.should == [:before_yield] + end + + it "stops without iterations if the given argument is 0" do + @eventsmixed.take(10).take(0).force + ScratchPad.recorded.should == [] + end + end + end +end diff --git a/spec/ruby/core/enumerator/lazy/take_while_spec.rb b/spec/ruby/core/enumerator/lazy/take_while_spec.rb new file mode 100644 index 0000000000..bcea0b1419 --- /dev/null +++ b/spec/ruby/core/enumerator/lazy/take_while_spec.rb @@ -0,0 +1,60 @@ +# -*- encoding: us-ascii -*- + +require_relative '../../../spec_helper' +require_relative 'fixtures/classes' + +describe "Enumerator::Lazy#take_while" do + before :each do + @yieldsmixed = EnumeratorLazySpecs::YieldsMixed.new.to_enum.lazy + @eventsmixed = EnumeratorLazySpecs::EventsMixed.new.to_enum.lazy + ScratchPad.record [] + end + + after :each do + ScratchPad.clear + end + + it "returns a new instance of Enumerator::Lazy" do + ret = @yieldsmixed.take_while {} + ret.should be_an_instance_of(Enumerator::Lazy) + ret.should_not equal(@yieldsmixed) + end + + it "sets #size to nil" do + Enumerator::Lazy.new(Object.new, 100) {}.take_while { true }.size.should == nil + end + + describe "when the returned lazy enumerator is evaluated by .force" do + it "stops after specified times" do + (0..Float::INFINITY).lazy.take_while { |n| n < 3 }.force.should == [0, 1, 2] + + @eventsmixed.take_while { false }.force + ScratchPad.recorded.should == [:before_yield] + end + end + + it "calls the block with initial values when yield with multiple arguments" do + yields = [] + @yieldsmixed.take_while { |v| yields << v; true }.force + yields.should == EnumeratorLazySpecs::YieldsMixed.initial_yields + end + + it "raises an ArgumentError when not given a block" do + -> { @yieldsmixed.take_while }.should raise_error(ArgumentError) + end + + describe "on a nested Lazy" do + it "sets #size to nil" do + Enumerator::Lazy.new(Object.new, 100) {}.take(20).take_while { true }.size.should == nil + end + + describe "when the returned lazy enumerator is evaluated by .force" do + it "stops after specified times" do + (0..Float::INFINITY).lazy.take_while { |n| n < 3 }.take_while(&:even?).force.should == [0] + + @eventsmixed.take_while { true }.take_while { false }.force + ScratchPad.recorded.should == [:before_yield] + end + end + end +end diff --git a/spec/ruby/core/enumerator/lazy/to_enum_spec.rb b/spec/ruby/core/enumerator/lazy/to_enum_spec.rb new file mode 100644 index 0000000000..210e5294b7 --- /dev/null +++ b/spec/ruby/core/enumerator/lazy/to_enum_spec.rb @@ -0,0 +1,8 @@ +# -*- encoding: us-ascii -*- + +require_relative '../../../spec_helper' +require_relative 'shared/to_enum' + +describe "Enumerator::Lazy#to_enum" do + it_behaves_like :enumerator_lazy_to_enum, :to_enum +end diff --git a/spec/ruby/core/enumerator/lazy/uniq_spec.rb b/spec/ruby/core/enumerator/lazy/uniq_spec.rb new file mode 100644 index 0000000000..ce67ace5ab --- /dev/null +++ b/spec/ruby/core/enumerator/lazy/uniq_spec.rb @@ -0,0 +1,74 @@ +require_relative '../../../spec_helper' +require_relative 'fixtures/classes' + +describe 'Enumerator::Lazy#uniq' do + context 'without block' do + before :each do + @lazy = [0, 1, 0, 1].to_enum.lazy.uniq + end + + it 'returns a lazy enumerator' do + @lazy.should be_an_instance_of(Enumerator::Lazy) + @lazy.force.should == [0, 1] + end + + it 'return same value after rewind' do + @lazy.force.should == [0, 1] + @lazy.force.should == [0, 1] + end + + it 'sets the size to nil' do + @lazy.size.should == nil + end + end + + context 'when yielded with an argument' do + before :each do + @lazy = [0, 1, 2, 3].to_enum.lazy.uniq(&:even?) + end + + it 'returns a lazy enumerator' do + @lazy.should be_an_instance_of(Enumerator::Lazy) + @lazy.force.should == [0, 1] + end + + it 'return same value after rewind' do + @lazy.force.should == [0, 1] + @lazy.force.should == [0, 1] + end + + it 'sets the size to nil' do + @lazy.size.should == nil + end + end + + context 'when yielded with multiple arguments' do + before :each do + enum = Object.new.to_enum + class << enum + def each + yield 0, 'foo' + yield 1, 'FOO' + yield 2, 'bar' + end + end + @lazy = enum.lazy + end + + it 'return same value after rewind' do + enum = @lazy.uniq { |_, label| label.downcase } + enum.force.should == [[0, 'foo'], [2, 'bar']] + enum.force.should == [[0, 'foo'], [2, 'bar']] + end + + it 'returns all yield arguments as an array' do + @lazy.uniq { |_, label| label.downcase }.force.should == [[0, 'foo'], [2, 'bar']] + end + end + + it "works with an infinite enumerable" do + s = 0..Float::INFINITY + s.lazy.uniq.first(100).should == + s.first(100).uniq + end +end diff --git a/spec/ruby/core/enumerator/lazy/with_index_spec.rb b/spec/ruby/core/enumerator/lazy/with_index_spec.rb new file mode 100644 index 0000000000..a6b5c38777 --- /dev/null +++ b/spec/ruby/core/enumerator/lazy/with_index_spec.rb @@ -0,0 +1,36 @@ +# -*- encoding: us-ascii -*- + +require_relative '../../../spec_helper' +require_relative 'fixtures/classes' + +describe "Enumerator::Lazy#with_index" do + it "enumerates with an index" do + (0..Float::INFINITY).lazy.with_index.map { |i, idx| [i, idx] }.first(3).should == [[0, 0], [1, 1], [2, 2]] + end + + it "enumerates with an index starting at a given offset" do + (0..Float::INFINITY).lazy.with_index(3).map { |i, idx| [i, idx] }.first(3).should == [[0, 3], [1, 4], [2, 5]] + end + + it "enumerates with an index starting at 0 when offset is nil" do + (0..Float::INFINITY).lazy.with_index(nil).map { |i, idx| [i, idx] }.first(3).should == [[0, 0], [1, 1], [2, 2]] + end + + it "raises TypeError when offset does not convert to Integer" do + -> { (0..Float::INFINITY).lazy.with_index(false).map { |i, idx| i }.first(3) }.should raise_error(TypeError) + end + + it "enumerates with a given block" do + result = [] + (0..Float::INFINITY).lazy.with_index { |i, idx| result << [i * 2, idx] }.first(3) + result.should == [[0,0],[2,1],[4,2]] + end + + it "resets after a new call to each" do + enum = (0..2).lazy.with_index.map { |i, idx| [i, idx] } + result = [] + enum.each { |i, idx| result << [i, idx] } + enum.each { |i, idx| result << [i, idx] } + result.should == [[0,0], [1,1], [2,2], [0,0], [1,1], [2,2]] + end +end diff --git a/spec/ruby/core/enumerator/lazy/zip_spec.rb b/spec/ruby/core/enumerator/lazy/zip_spec.rb new file mode 100644 index 0000000000..5a828c1dcc --- /dev/null +++ b/spec/ruby/core/enumerator/lazy/zip_spec.rb @@ -0,0 +1,86 @@ +# -*- encoding: us-ascii -*- + +require_relative '../../../spec_helper' +require_relative 'fixtures/classes' + +describe "Enumerator::Lazy#zip" do + before :each do + @yieldsmixed = EnumeratorLazySpecs::YieldsMixed.new.to_enum.lazy + @eventsmixed = EnumeratorLazySpecs::EventsMixed.new.to_enum.lazy + ScratchPad.record [] + end + + after :each do + ScratchPad.clear + end + + it "returns a new instance of Enumerator::Lazy" do + ret = @yieldsmixed.zip [] + ret.should be_an_instance_of(Enumerator::Lazy) + ret.should_not equal(@yieldsmixed) + end + + it "keeps size" do + Enumerator::Lazy.new(Object.new, 100) {}.zip([], []).size.should == 100 + end + + describe "when the returned lazy enumerator is evaluated by Enumerable#first" do + it "stops after specified times" do + (0..Float::INFINITY).lazy.zip([4, 5], [8]).first(2).should == [[0, 4, 8], [1, 5, nil]] + + @eventsmixed.zip([0, 1]).first(1) + ScratchPad.recorded.should == [:before_yield] + end + end + + it "calls the block with a gathered array when yield with multiple arguments" do + yields = @yieldsmixed.zip(EnumeratorLazySpecs::YieldsMixed.new.to_enum).force + yields.should == [EnumeratorLazySpecs::YieldsMixed.gathered_yields, + EnumeratorLazySpecs::YieldsMixed.gathered_yields].transpose + end + + it "returns a Lazy when no arguments given" do + @yieldsmixed.zip.should be_an_instance_of(Enumerator::Lazy) + end + + it "raises a TypeError if arguments contain non-list object" do + -> { @yieldsmixed.zip [], Object.new, [] }.should raise_error(TypeError) + end + + describe "on a nested Lazy" do + it "keeps size" do + Enumerator::Lazy.new(Object.new, 100) {}.map {}.zip([], []).size.should == 100 + end + + it "behaves as Enumerable#zip when given a block" do + lazy_yields = [] + lazy_ret = @yieldsmixed.zip(EnumeratorLazySpecs::YieldsMixed.new.to_enum) { |lists| lazy_yields << lists } + enum_yields = [] + enum_ret = EnumeratorLazySpecs::YieldsMixed.new.to_enum.zip(EnumeratorLazySpecs::YieldsMixed.new.to_enum) { |lists| enum_yields << lists } + + lazy_yields.should == enum_yields + lazy_ret.should == enum_ret + end + + describe "when the returned lazy enumerator is evaluated by Enumerable#first" do + it "stops after specified times" do + (0..Float::INFINITY).lazy.map(&:succ).zip([4, 5], [8]).first(2).should == [[1, 4, 8], [2, 5, nil]] + + @eventsmixed.zip([0, 1]).zip([0, 1]).first(1) + ScratchPad.recorded.should == [:before_yield] + end + end + end + + it "works with an infinite enumerable and an array" do + s = 0..Float::INFINITY + s.lazy.zip(0..1000).first(100).should == + s.first(100).zip(0..100) + end + + it "works with two infinite enumerables" do + s = 0..Float::INFINITY + s.lazy.zip(s).first(100).should == + s.first(100).zip(s) + end +end diff --git a/spec/ruby/core/enumerator/new_spec.rb b/spec/ruby/core/enumerator/new_spec.rb new file mode 100644 index 0000000000..671912224f --- /dev/null +++ b/spec/ruby/core/enumerator/new_spec.rb @@ -0,0 +1,76 @@ +require_relative '../../spec_helper' + +describe "Enumerator.new" do + context "no block given" do + it "raises" do + -> { Enumerator.new(1, :upto, 3) }.should raise_error(ArgumentError) + end + end + + context "when passed a block" do + it "defines iteration with block, yielder argument and calling << method" do + enum = Enumerator.new do |yielder| + a = 1 + + loop do + yielder << a + a = a + 1 + end + end + + enum.take(3).should == [1, 2, 3] + end + + it "defines iteration with block, yielder argument and calling yield method" do + enum = Enumerator.new do |yielder| + a = 1 + + loop do + yielder.yield(a) + a = a + 1 + end + end + + enum.take(3).should == [1, 2, 3] + end + + it "defines iteration with block, yielder argument and treating it as a proc" do + enum = Enumerator.new do |yielder| + "a\nb\nc".each_line(&yielder) + end + + enum.to_a.should == ["a\n", "b\n", "c"] + end + + describe 'yielded values' do + it 'handles yield arguments properly' do + Enumerator.new { |y| y.yield(1) }.to_a.should == [1] + Enumerator.new { |y| y.yield(1) }.first.should == 1 + + Enumerator.new { |y| y.yield([1]) }.to_a.should == [[1]] + Enumerator.new { |y| y.yield([1]) }.first.should == [1] + + Enumerator.new { |y| y.yield(1, 2) }.to_a.should == [[1, 2]] + Enumerator.new { |y| y.yield(1, 2) }.first.should == [1, 2] + + Enumerator.new { |y| y.yield([1, 2]) }.to_a.should == [[1, 2]] + Enumerator.new { |y| y.yield([1, 2]) }.first.should == [1, 2] + end + + it 'handles << arguments properly' do + Enumerator.new { |y| y.<<(1) }.to_a.should == [1] + Enumerator.new { |y| y.<<(1) }.first.should == 1 + + Enumerator.new { |y| y.<<([1]) }.to_a.should == [[1]] + Enumerator.new { |y| y.<<([1]) }.first.should == [1] + + # << doesn't accept multiple arguments + # Enumerator.new { |y| y.<<(1, 2) }.to_a.should == [[1, 2]] + # Enumerator.new { |y| y.<<(1, 2) }.first.should == [1, 2] + + Enumerator.new { |y| y.<<([1, 2]) }.to_a.should == [[1, 2]] + Enumerator.new { |y| y.<<([1, 2]) }.first.should == [1, 2] + end + end + end +end diff --git a/spec/ruby/core/enumerator/next_spec.rb b/spec/ruby/core/enumerator/next_spec.rb new file mode 100644 index 0000000000..a5e01a399d --- /dev/null +++ b/spec/ruby/core/enumerator/next_spec.rb @@ -0,0 +1,38 @@ +require_relative '../../spec_helper' + +describe "Enumerator#next" do + before :each do + @enum = 1.upto(3) + end + + it "returns the next element of the enumeration" do + @enum.next.should == 1 + @enum.next.should == 2 + @enum.next.should == 3 + end + + it "raises a StopIteration exception at the end of the stream" do + 3.times { @enum.next } + -> { @enum.next }.should raise_error(StopIteration) + end + + it "cannot be called again until the enumerator is rewound" do + 3.times { @enum.next } + -> { @enum.next }.should raise_error(StopIteration) + -> { @enum.next }.should raise_error(StopIteration) + -> { @enum.next }.should raise_error(StopIteration) + @enum.rewind + @enum.next.should == 1 + end + + it "restarts the enumerator if an exception terminated a previous iteration" do + exception = StandardError.new + enum = Enumerator.new do + raise exception + end + + result = 2.times.map { enum.next rescue $! } + + result.should == [exception, exception] + end +end diff --git a/spec/ruby/core/enumerator/next_values_spec.rb b/spec/ruby/core/enumerator/next_values_spec.rb new file mode 100644 index 0000000000..2202700c58 --- /dev/null +++ b/spec/ruby/core/enumerator/next_values_spec.rb @@ -0,0 +1,61 @@ +require_relative '../../spec_helper' + +describe "Enumerator#next_values" do + before :each do + o = Object.new + def o.each + yield :a + yield :b1, :b2 + yield :c + yield :d1, :d2 + yield :e1, :e2, :e3 + yield nil + yield + yield [:f1, :f2] + end + + @e = o.to_enum + end + + it "returns the next element in self" do + @e.next_values.should == [:a] + end + + it "advances the position of the current element" do + @e.next.should == :a + @e.next_values.should == [:b1, :b2] + @e.next.should == :c + end + + it "advances the position of the enumerator each time when called multiple times" do + 2.times { @e.next_values } + @e.next_values.should == [:c] + @e.next.should == [:d1, :d2] + end + + it "works in concert with #rewind" do + 2.times { @e.next } + @e.rewind + @e.next_values.should == [:a] + end + + it "returns an array with only nil if yield is called with nil" do + 5.times { @e.next } + @e.next_values.should == [nil] + end + + it "returns an empty array if yield is called without arguments" do + 6.times { @e.next } + @e.next_values.should == [] + end + + it "returns an array of array if yield is called with an array" do + 7.times { @e.next } + @e.next_values.should == [[:f1, :f2]] + end + + it "raises StopIteration if called on a finished enumerator" do + 8.times { @e.next } + -> { @e.next_values }.should raise_error(StopIteration) + end +end diff --git a/spec/ruby/core/enumerator/peek_spec.rb b/spec/ruby/core/enumerator/peek_spec.rb new file mode 100644 index 0000000000..2334385437 --- /dev/null +++ b/spec/ruby/core/enumerator/peek_spec.rb @@ -0,0 +1,36 @@ +require_relative '../../spec_helper' + +describe "Enumerator#peek" do + before :each do + @e = (1..5).to_a.to_enum + end + + it "returns the next element in self" do + @e.peek.should == 1 + end + + it "does not advance the position of the current element" do + @e.next.should == 1 + @e.peek.should == 2 + @e.next.should == 2 + end + + it "can be called repeatedly without advancing the position of the current element" do + @e.peek + @e.peek + @e.peek.should == 1 + @e.next.should == 1 + end + + it "works in concert with #rewind" do + @e.next + @e.next + @e.rewind + @e.peek.should == 1 + end + + it "raises StopIteration if called on a finished enumerator" do + 5.times { @e.next } + -> { @e.peek }.should raise_error(StopIteration) + end +end diff --git a/spec/ruby/core/enumerator/peek_values_spec.rb b/spec/ruby/core/enumerator/peek_values_spec.rb new file mode 100644 index 0000000000..8b84fc8afc --- /dev/null +++ b/spec/ruby/core/enumerator/peek_values_spec.rb @@ -0,0 +1,63 @@ +require_relative '../../spec_helper' + +describe "Enumerator#peek_values" do + before :each do + o = Object.new + def o.each + yield :a + yield :b1, :b2 + yield :c + yield :d1, :d2 + yield :e1, :e2, :e3 + yield nil + yield + yield [:f1, :f2] + end + + @e = o.to_enum + end + + it "returns the next element in self" do + @e.peek_values.should == [:a] + end + + it "does not advance the position of the current element" do + @e.next.should == :a + @e.peek_values.should == [:b1, :b2] + @e.next.should == [:b1, :b2] + end + + it "can be called repeatedly without advancing the position of the current element" do + @e.peek_values + @e.peek_values + @e.peek_values.should == [:a] + @e.next.should == :a + end + + it "works in concert with #rewind" do + @e.next + @e.next + @e.rewind + @e.peek_values.should == [:a] + end + + it "returns an array with only nil if yield is called with nil" do + 5.times { @e.next } + @e.peek_values.should == [nil] + end + + it "returns an empty array if yield is called without arguments" do + 6.times { @e.next } + @e.peek_values.should == [] + end + + it "returns an array of array if yield is called with an array" do + 7.times { @e.next } + @e.peek_values.should == [[:f1, :f2]] + end + + it "raises StopIteration if called on a finished enumerator" do + 8.times { @e.next } + -> { @e.peek_values }.should raise_error(StopIteration) + end +end diff --git a/spec/ruby/core/enumerator/plus_spec.rb b/spec/ruby/core/enumerator/plus_spec.rb new file mode 100644 index 0000000000..755be5b4eb --- /dev/null +++ b/spec/ruby/core/enumerator/plus_spec.rb @@ -0,0 +1,33 @@ +require_relative '../../spec_helper' + +describe "Enumerator#+" do + before :each do + ScratchPad.record [] + end + + it "returns a chain of self and provided enumerators" do + one = Enumerator.new { |y| y << 1 } + two = Enumerator.new { |y| y << 2 } + three = Enumerator.new { |y| y << 3 } + + chain = one + two + three + + chain.should be_an_instance_of(Enumerator::Chain) + chain.each { |item| ScratchPad << item } + ScratchPad.recorded.should == [1, 2, 3] + end + + it "calls #each on each argument" do + enum = Enumerator.new { |y| y << "one" } + + obj1 = mock("obj1") + obj1.should_receive(:each).once.and_yield("two") + + obj2 = mock("obj2") + obj2.should_receive(:each).once.and_yield("three") + + chain = enum + obj1 + obj2 + chain.each { |item| ScratchPad << item } + ScratchPad.recorded.should == ["one", "two", "three"] + end +end diff --git a/spec/ruby/core/enumerator/produce_spec.rb b/spec/ruby/core/enumerator/produce_spec.rb new file mode 100644 index 0000000000..c69fb49303 --- /dev/null +++ b/spec/ruby/core/enumerator/produce_spec.rb @@ -0,0 +1,34 @@ +require_relative '../../spec_helper' + +describe "Enumerator.produce" do + it "creates an infinite enumerator" do + enum = Enumerator.produce(0) { |prev| prev + 1 } + enum.take(5).should == [0, 1, 2, 3, 4] + end + + it "terminates iteration when block raises StopIteration exception" do + enum = Enumerator.produce(0) do | prev| + raise StopIteration if prev >= 2 + prev + 1 + end + + enum.to_a.should == [0, 1, 2] + end + + context "when initial value skipped" do + it "uses nil instead" do + ScratchPad.record [] + enum = Enumerator.produce { |prev| ScratchPad << prev; (prev || 0) + 1 } + + enum.take(3).should == [1, 2, 3] + ScratchPad.recorded.should == [nil, 1, 2] + end + + it "starts enumerable from result of first block call" do + array = "a\nb\nc\nd".lines + lines = Enumerator.produce { array.shift }.take_while { |s| s } + + lines.should == ["a\n", "b\n", "c\n", "d"] + end + end +end diff --git a/spec/ruby/core/enumerator/product/each_spec.rb b/spec/ruby/core/enumerator/product/each_spec.rb new file mode 100644 index 0000000000..88f115a712 --- /dev/null +++ b/spec/ruby/core/enumerator/product/each_spec.rb @@ -0,0 +1,71 @@ +require_relative '../../../spec_helper' +require_relative '../../enumerable/shared/enumeratorized' + +describe "Enumerator::Product#each" do + it_behaves_like :enumeratorized_with_origin_size, :each, Enumerator::Product.new([1, 2], [:a, :b]) + + it "yields each element of Cartesian product of enumerators" do + enum = Enumerator::Product.new([1, 2], [:a, :b]) + acc = [] + enum.each { |e| acc << e } + acc.should == [[1, :a], [1, :b], [2, :a], [2, :b]] + end + + it "calls #each_entry method on enumerators" do + object1 = Object.new + def object1.each_entry + yield 1 + yield 2 + end + + object2 = Object.new + def object2.each_entry + yield :a + yield :b + end + + enum = Enumerator::Product.new(object1, object2) + acc = [] + enum.each { |e| acc << e } + acc.should == [[1, :a], [1, :b], [2, :a], [2, :b]] + end + + it "raises a NoMethodError if the object doesn't respond to #each_entry" do + -> { + Enumerator::Product.new(Object.new).each {} + }.should raise_error(NoMethodError, /undefined method [`']each_entry' for/) + end + + it "returns enumerator if not given a block" do + enum = Enumerator::Product.new([1, 2], [:a, :b]) + enum.each.should.kind_of?(Enumerator) + + enum = Enumerator::Product.new([1, 2], [:a, :b]) + enum.each.to_a.should == [[1, :a], [1, :b], [2, :a], [2, :b]] + end + + it "returns self if given a block" do + enum = Enumerator::Product.new([1, 2], [:a, :b]) + enum.each {}.should.equal?(enum) + end + + it "doesn't accept arguments" do + Enumerator::Product.instance_method(:each).arity.should == 0 + end + + it "yields each element to a block that takes multiple arguments" do + enum = Enumerator::Product.new([1, 2], [:a, :b]) + + acc = [] + enum.each { |x, y| acc << x } + acc.should == [1, 1, 2, 2] + + acc = [] + enum.each { |x, y| acc << y } + acc.should == [:a, :b, :a, :b] + + acc = [] + enum.each { |x, y, z| acc << z } + acc.should == [nil, nil, nil, nil] + end +end diff --git a/spec/ruby/core/enumerator/product/initialize_copy_spec.rb b/spec/ruby/core/enumerator/product/initialize_copy_spec.rb new file mode 100644 index 0000000000..b1b9f3ca9b --- /dev/null +++ b/spec/ruby/core/enumerator/product/initialize_copy_spec.rb @@ -0,0 +1,52 @@ +require_relative '../../../spec_helper' + +describe "Enumerator::Product#initialize_copy" do + it "replaces content of the receiver with content of the other object" do + enum = Enumerator::Product.new([true, false]) + enum2 = Enumerator::Product.new([1, 2], [:a, :b]) + + enum.send(:initialize_copy, enum2) + enum.each.to_a.should == [[1, :a], [1, :b], [2, :a], [2, :b]] + end + + it "returns self" do + enum = Enumerator::Product.new([true, false]) + enum2 = Enumerator::Product.new([1, 2], [:a, :b]) + + enum.send(:initialize_copy, enum2).should.equal?(enum) + end + + it "is a private method" do + Enumerator::Product.should have_private_instance_method(:initialize_copy, false) + end + + it "does nothing if the argument is the same as the receiver" do + enum = Enumerator::Product.new(1..2) + enum.send(:initialize_copy, enum).should.equal?(enum) + + enum.freeze + enum.send(:initialize_copy, enum).should.equal?(enum) + end + + it "raises FrozenError if the receiver is frozen" do + enum = Enumerator::Product.new(1..2) + enum2 = Enumerator::Product.new(3..4) + + -> { enum.freeze.send(:initialize_copy, enum2) }.should raise_error(FrozenError) + end + + it "raises TypeError if the objects are of different class" do + enum = Enumerator::Product.new(1..2) + enum2 = Class.new(Enumerator::Product).new(3..4) + + -> { enum.send(:initialize_copy, enum2) }.should raise_error(TypeError, 'initialize_copy should take same class object') + -> { enum2.send(:initialize_copy, enum) }.should raise_error(TypeError, 'initialize_copy should take same class object') + end + + it "raises ArgumentError if the argument is not initialized yet" do + enum = Enumerator::Product.new(1..2) + enum2 = Enumerator::Product.allocate + + -> { enum.send(:initialize_copy, enum2) }.should raise_error(ArgumentError, 'uninitialized product') + end +end diff --git a/spec/ruby/core/enumerator/product/initialize_spec.rb b/spec/ruby/core/enumerator/product/initialize_spec.rb new file mode 100644 index 0000000000..ed2a8a2a13 --- /dev/null +++ b/spec/ruby/core/enumerator/product/initialize_spec.rb @@ -0,0 +1,31 @@ +require_relative '../../../spec_helper' + +describe "Enumerator::Product#initialize" do + before :each do + @uninitialized = Enumerator::Product.allocate + end + + it "is a private method" do + Enumerator::Product.should have_private_instance_method(:initialize, false) + end + + it "returns self" do + @uninitialized.send(:initialize).should equal(@uninitialized) + end + + it "accepts many arguments" do + @uninitialized.send(:initialize, 0..1, 2..3, 4..5).should equal(@uninitialized) + end + + it "accepts arguments that are not Enumerable nor responding to :each_entry" do + @uninitialized.send(:initialize, Object.new).should equal(@uninitialized) + end + + describe "on frozen instance" do + it "raises a FrozenError" do + -> { + @uninitialized.freeze.send(:initialize, 0..1) + }.should raise_error(FrozenError) + end + end +end diff --git a/spec/ruby/core/enumerator/product/inspect_spec.rb b/spec/ruby/core/enumerator/product/inspect_spec.rb new file mode 100644 index 0000000000..e0d7441f26 --- /dev/null +++ b/spec/ruby/core/enumerator/product/inspect_spec.rb @@ -0,0 +1,20 @@ +require_relative '../../../spec_helper' + +describe "Enumerator::Product#inspect" do + it "returns a String including enumerators" do + enum = Enumerator::Product.new([1, 2], [:a, :b]) + enum.inspect.should == "#<Enumerator::Product: [[1, 2], [:a, :b]]>" + end + + it "represents a recursive element with '[...]'" do + enum = [1, 2] + enum_recursive = Enumerator::Product.new(enum) + + enum << enum_recursive + enum_recursive.inspect.should == "#<Enumerator::Product: [[1, 2, #<Enumerator::Product: ...>]]>" + end + + it "returns a not initialized representation if #initialized is not called yet" do + Enumerator::Product.allocate.inspect.should == "#<Enumerator::Product: uninitialized>" + end +end diff --git a/spec/ruby/core/enumerator/product/rewind_spec.rb b/spec/ruby/core/enumerator/product/rewind_spec.rb new file mode 100644 index 0000000000..2beffaf5c1 --- /dev/null +++ b/spec/ruby/core/enumerator/product/rewind_spec.rb @@ -0,0 +1,62 @@ +require_relative '../../../spec_helper' + +describe "Enumerator::Product#rewind" do + before :each do + @enum = Enumerator::Product.new([1, 2].each.to_enum, [:a, :b].each.to_enum) + end + + it "resets the enumerator to its initial state" do + @enum.each.to_a.should == [[1, :a], [1, :b], [2, :a], [2, :b]] + @enum.rewind + @enum.each.to_a.should == [[1, :a], [1, :b], [2, :a], [2, :b]] + end + + it "returns self" do + @enum.rewind.should.equal? @enum + end + + it "has no effect on a new enumerator" do + @enum.rewind + @enum.each.to_a.should == [[1, :a], [1, :b], [2, :a], [2, :b]] + end + + it "has no effect if called multiple, consecutive times" do + @enum.each.to_a.should == [[1, :a], [1, :b], [2, :a], [2, :b]] + @enum.rewind + @enum.rewind + @enum.each.to_a.should == [[1, :a], [1, :b], [2, :a], [2, :b]] + end + + it "calls the enclosed object's rewind method if one exists" do + obj = mock('rewinder') + enum = Enumerator::Product.new(obj.to_enum) + + obj.should_receive(:rewind) + enum.rewind + end + + it "does nothing if the object doesn't have a #rewind method" do + obj = mock('rewinder') + enum = Enumerator::Product.new(obj.to_enum) + + enum.rewind.should == enum + end + + it "calls a rewind method on each enumerable in direct order" do + ScratchPad.record [] + + object1 = Object.new + def object1.rewind; ScratchPad << :object1; end + + object2 = Object.new + def object2.rewind; ScratchPad << :object2; end + + object3 = Object.new + def object3.rewind; ScratchPad << :object3; end + + enum = Enumerator::Product.new(object1, object2, object3) + enum.rewind + + ScratchPad.recorded.should == [:object1, :object2, :object3] + end +end diff --git a/spec/ruby/core/enumerator/product/size_spec.rb b/spec/ruby/core/enumerator/product/size_spec.rb new file mode 100644 index 0000000000..96632d6eee --- /dev/null +++ b/spec/ruby/core/enumerator/product/size_spec.rb @@ -0,0 +1,54 @@ +require_relative '../../../spec_helper' + +describe "Enumerator::Product#size" do + it "returns the total size of the enumerator product calculated by multiplying the sizes of enumerables in the product" do + product = Enumerator::Product.new(1..2, 1..3, 1..4) + product.size.should == 24 # 2 * 3 * 4 + end + + it "returns nil if any enumerable reports its size as nil" do + enum = Object.new + def enum.size; nil; end + + product = Enumerator::Product.new(1..2, enum) + product.size.should == nil + end + + it "returns Float::INFINITY if any enumerable reports its size as Float::INFINITY" do + enum = Object.new + def enum.size; Float::INFINITY; end + + product = Enumerator::Product.new(1..2, enum) + product.size.should == Float::INFINITY + end + + it "returns nil if any enumerable reports its size as Float::NAN" do + enum = Object.new + def enum.size; Float::NAN; end + + product = Enumerator::Product.new(1..2, enum) + product.size.should == nil + end + + it "returns nil if any enumerable doesn't respond to #size" do + enum = Object.new + product = Enumerator::Product.new(1..2, enum) + product.size.should == nil + end + + it "returns nil if any enumerable reports a not-convertible to Integer" do + enum = Object.new + def enum.size; :symbol; end + + product = Enumerator::Product.new(1..2, enum) + product.size.should == nil + end + + it "returns nil if any enumerable reports a non-Integer but convertible to Integer size" do + enum = Object.new + def enum.size; 1.0; end + + product = Enumerator::Product.new(1..2, enum) + product.size.should == nil + end +end diff --git a/spec/ruby/core/enumerator/product_spec.rb b/spec/ruby/core/enumerator/product_spec.rb new file mode 100644 index 0000000000..83c7cb8e0e --- /dev/null +++ b/spec/ruby/core/enumerator/product_spec.rb @@ -0,0 +1,91 @@ +require_relative '../../spec_helper' + +describe "Enumerator.product" do + it "returns a Cartesian product of enumerators" do + enum = Enumerator.product(1..2, ["A", "B"]) + enum.to_a.should == [[1, "A"], [1, "B"], [2, "A"], [2, "B"]] + end + + it "accepts a list of enumerators of any length" do + enum = Enumerator.product(1..2) + enum.to_a.should == [[1], [2]] + + enum = Enumerator.product(1..2, ["A"]) + enum.to_a.should == [[1, "A"], [2, "A"]] + + enum = Enumerator.product(1..2, ["A"], ["B"]) + enum.to_a.should == [[1, "A", "B"], [2, "A", "B"]] + + enum = Enumerator.product(2..3, ["A"], ["B"], ["C"]) + enum.to_a.should == [[2, "A", "B", "C"], [3, "A", "B", "C"]] + end + + it "returns an enumerator with an empty array when no arguments passed" do + enum = Enumerator.product + enum.to_a.should == [[]] + end + + it "returns an instance of Enumerator::Product" do + enum = Enumerator.product + enum.class.should == Enumerator::Product + end + + it "accepts infinite enumerators and returns infinite enumerator" do + enum = Enumerator.product(1.., ["A", "B"]) + enum.take(5).should == [[1, "A"], [1, "B"], [2, "A"], [2, "B"], [3, "A"]] + enum.size.should == Float::INFINITY + end + + it "accepts a block" do + elems = [] + enum = Enumerator.product(1..2, ["X", "Y"]) { elems << _1 } + + elems.should == [[1, "X"], [1, "Y"], [2, "X"], [2, "Y"]] + end + + it "returns nil when a block passed" do + Enumerator.product(1..2) {}.should == nil + end + + # https://bugs.ruby-lang.org/issues/19829 + it "reject keyword arguments" do + -> { + Enumerator.product(1..3, foo: 1, bar: 2) + }.should raise_error(ArgumentError, "unknown keywords: :foo, :bar") + end + + it "calls only #each_entry method on arguments" do + object = Object.new + def object.each_entry + yield 1 + yield 2 + end + + enum = Enumerator.product(object, ["A", "B"]) + enum.to_a.should == [[1, "A"], [1, "B"], [2, "A"], [2, "B"]] + end + + it "raises NoMethodError when argument doesn't respond to #each_entry" do + -> { + Enumerator.product(Object.new).to_a + }.should raise_error(NoMethodError, /undefined method [`']each_entry' for/) + end + + it "calls #each_entry lazily" do + Enumerator.product(Object.new).should be_kind_of(Enumerator) + end + + it "iterates through consuming enumerator elements only once" do + a = [1, 2, 3] + i = 0 + + enum = Enumerator.new do |y| + while i < a.size + y << a[i] + i += 1 + end + end + + Enumerator.product(['a', 'b'], enum).to_a.should == [["a", 1], ["a", 2], ["a", 3]] + end +end diff --git a/spec/ruby/core/enumerator/rewind_spec.rb b/spec/ruby/core/enumerator/rewind_spec.rb new file mode 100644 index 0000000000..6ba0edf174 --- /dev/null +++ b/spec/ruby/core/enumerator/rewind_spec.rb @@ -0,0 +1,70 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' + +describe "Enumerator#rewind" do + before :each do + @enum = 1.upto(3) + end + + it "resets the enumerator to its initial state" do + @enum.next.should == 1 + @enum.next.should == 2 + @enum.rewind + @enum.next.should == 1 + end + + it "returns self" do + @enum.rewind.should.equal? @enum + end + + it "has no effect on a new enumerator" do + @enum.rewind + @enum.next.should == 1 + end + + it "has no effect if called multiple, consecutive times" do + @enum.next.should == 1 + @enum.rewind + @enum.rewind + @enum.next.should == 1 + end + + it "works with peek to reset the position" do + @enum.next + @enum.next + @enum.rewind + @enum.next + @enum.peek.should == 2 + end + + it "calls the enclosed object's rewind method if one exists" do + obj = mock('rewinder') + enum = obj.to_enum + obj.should_receive(:each).at_most(1) + obj.should_receive(:rewind) + enum.rewind + end + + it "does nothing if the object doesn't have a #rewind method" do + obj = mock('rewinder') + enum = obj.to_enum + obj.should_receive(:each).at_most(1) + enum.rewind.should == enum + end +end + +describe "Enumerator#rewind" do + before :each do + ScratchPad.record [] + @enum = EnumeratorSpecs::Feed.new.to_enum(:each) + end + + it "clears a pending #feed value" do + @enum.next + @enum.feed :a + @enum.rewind + @enum.next + @enum.next + ScratchPad.recorded.should == [nil] + end +end diff --git a/spec/ruby/core/enumerator/shared/enum_for.rb b/spec/ruby/core/enumerator/shared/enum_for.rb new file mode 100644 index 0000000000..a67a76c461 --- /dev/null +++ b/spec/ruby/core/enumerator/shared/enum_for.rb @@ -0,0 +1,57 @@ +describe :enum_for, shared: true do + it "is defined in Kernel" do + Kernel.method_defined?(@method).should be_true + end + + it "returns a new enumerator" do + "abc".send(@method).should be_an_instance_of(Enumerator) + end + + it "defaults the first argument to :each" do + enum = [1,2].send(@method) + enum.map { |v| v }.should == [1,2].each { |v| v } + end + + it "sets regexp matches in the caller" do + "wawa".send(@method, :scan, /./).map {|o| $& }.should == ["w", "a", "w", "a"] + a = [] + "wawa".send(@method, :scan, /./).each {|o| a << $& } + a.should == ["w", "a", "w", "a"] + end + + it "exposes multi-arg yields as an array" do + o = Object.new + def o.each + yield :a + yield :b1, :b2 + yield [:c] + yield :d1, :d2 + yield :e1, :e2, :e3 + end + + enum = o.send(@method) + enum.next.should == :a + enum.next.should == [:b1, :b2] + enum.next.should == [:c] + enum.next.should == [:d1, :d2] + enum.next.should == [:e1, :e2, :e3] + end + + it "uses the passed block's value to calculate the size of the enumerator" do + Object.new.enum_for { 100 }.size.should == 100 + end + + it "defers the evaluation of the passed block until #size is called" do + ScratchPad.record [] + + enum = Object.new.enum_for do + ScratchPad << :called + 100 + end + + ScratchPad.recorded.should be_empty + + enum.size + ScratchPad.recorded.should == [:called] + end +end diff --git a/spec/ruby/core/enumerator/shared/with_index.rb b/spec/ruby/core/enumerator/shared/with_index.rb new file mode 100644 index 0000000000..78771ffe82 --- /dev/null +++ b/spec/ruby/core/enumerator/shared/with_index.rb @@ -0,0 +1,33 @@ +require_relative '../../../spec_helper' + +describe :enum_with_index, shared: true do + + require_relative '../fixtures/classes' + + before :each do + @origin = [1, 2, 3, 4] + @enum = @origin.to_enum + end + + it "passes each element and its index to block" do + a = [] + @enum.send(@method) { |o, i| a << [o, i] } + a.should == [[1, 0], [2, 1], [3, 2], [4, 3]] + end + + it "returns the object being enumerated when given a block" do + @enum.send(@method) { |o, i| :glark }.should equal(@origin) + end + + it "binds splat arguments properly" do + acc = [] + @enum.send(@method) { |*b| c,d = b; acc << c; acc << d } + [1, 0, 2, 1, 3, 2, 4, 3].should == acc + end + + it "returns an enumerator if no block is supplied" do + ewi = @enum.send(@method) + ewi.should be_an_instance_of(Enumerator) + ewi.to_a.should == [[1, 0], [2, 1], [3, 2], [4, 3]] + end +end diff --git a/spec/ruby/core/enumerator/shared/with_object.rb b/spec/ruby/core/enumerator/shared/with_object.rb new file mode 100644 index 0000000000..d8e9d8e9f7 --- /dev/null +++ b/spec/ruby/core/enumerator/shared/with_object.rb @@ -0,0 +1,42 @@ +require_relative '../../../spec_helper' + +describe :enum_with_object, shared: true do + before :each do + @enum = [:a, :b].to_enum + @memo = '' + @block_params = @enum.send(@method, @memo).to_a + end + + it "receives an argument" do + @enum.method(@method).arity.should == 1 + end + + context "with block" do + it "returns the given object" do + ret = @enum.send(@method, @memo) do |elm, memo| + # nothing + end + ret.should equal(@memo) + end + + context "the block parameter" do + it "passes each element to first parameter" do + @block_params[0][0].should equal(:a) + @block_params[1][0].should equal(:b) + end + + it "passes the given object to last parameter" do + @block_params[0][1].should equal(@memo) + @block_params[1][1].should equal(@memo) + end + end + end + + context "without block" do + it "returns new Enumerator" do + ret = @enum.send(@method, @memo) + ret.should be_an_instance_of(Enumerator) + ret.should_not equal(@enum) + end + end +end diff --git a/spec/ruby/core/enumerator/size_spec.rb b/spec/ruby/core/enumerator/size_spec.rb new file mode 100644 index 0000000000..6accd26a4e --- /dev/null +++ b/spec/ruby/core/enumerator/size_spec.rb @@ -0,0 +1,26 @@ +require_relative '../../spec_helper' + +describe "Enumerator#size" do + it "returns same value if set size is an Integer" do + Enumerator.new(100) {}.size.should == 100 + end + + it "returns nil if set size is nil" do + Enumerator.new(nil) {}.size.should be_nil + end + + it "returns returning value from size.call if set size is a Proc" do + base_size = 100 + enum = Enumerator.new(-> { base_size + 1 }) {} + base_size = 200 + enum.size.should == 201 + base_size = 300 + enum.size.should == 301 + end + + it "returns the result from size.call if the size respond to call" do + obj = mock('call') + obj.should_receive(:call).and_return(42) + Enumerator.new(obj) {}.size.should == 42 + end +end diff --git a/spec/ruby/core/enumerator/to_enum_spec.rb b/spec/ruby/core/enumerator/to_enum_spec.rb new file mode 100644 index 0000000000..7fb73d0c3c --- /dev/null +++ b/spec/ruby/core/enumerator/to_enum_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/enum_for' + +describe "Enumerator#to_enum" do + it_behaves_like :enum_for, :to_enum +end diff --git a/spec/ruby/core/enumerator/with_index_spec.rb b/spec/ruby/core/enumerator/with_index_spec.rb new file mode 100644 index 0000000000..e49aa7a939 --- /dev/null +++ b/spec/ruby/core/enumerator/with_index_spec.rb @@ -0,0 +1,89 @@ +require_relative '../../spec_helper' +require_relative 'shared/with_index' +require_relative '../enumerable/shared/enumeratorized' + +describe "Enumerator#with_index" do + it_behaves_like :enum_with_index, :with_index + it_behaves_like :enumeratorized_with_origin_size, :with_index, [1,2,3].select + + it "returns a new Enumerator when no block is given" do + enum1 = [1,2,3].select + enum2 = enum1.with_index + enum2.should be_an_instance_of(Enumerator) + enum1.should_not === enum2 + end + + it "accepts an optional argument when given a block" do + -> do + @enum.with_index(1) { |f| f} + end.should_not raise_error(ArgumentError) + end + + it "accepts an optional argument when not given a block" do + -> do + @enum.with_index(1) + end.should_not raise_error(ArgumentError) + end + + it "numbers indices from the given index when given an offset but no block" do + @enum.with_index(1).to_a.should == [[1,1], [2,2], [3,3], [4,4]] + end + + it "numbers indices from the given index when given an offset and block" do + acc = [] + @enum.with_index(1) {|e,i| acc << [e,i] } + acc.should == [[1,1], [2,2], [3,3], [4,4]] + end + + it "raises a TypeError when the argument cannot be converted to numeric" do + -> do + @enum.with_index('1') {|*i| i} + end.should raise_error(TypeError) + end + + it "converts non-numeric arguments to Integer via #to_int" do + (o = mock('1')).should_receive(:to_int).and_return(1) + @enum.with_index(o).to_a.should == [[1,1], [2,2], [3,3], [4,4]] + end + + it "coerces the given numeric argument to an Integer" do + @enum.with_index(1.678).to_a.should == [[1,1], [2,2], [3,3], [4,4]] + + res = [] + @enum.with_index(1.001) { |*x| res << x} + res.should == [[1,1], [2,2], [3,3], [4,4]] + end + + it "treats nil argument as no argument" do + @enum.with_index(nil).to_a.should == [[1,0], [2,1], [3,2], [4,3]] + + res = [] + @enum.with_index(nil) { |*x| res << x} + res.should == [[1,0], [2,1], [3,2], [4,3]] + end + + it "accepts negative argument" do + @enum.with_index(-1).to_a.should == [[1,-1], [2,0], [3,1], [4,2]] + + res = [] + @enum.with_index(-1) { |*x| res << x} + res.should == [[1,-1], [2,0], [3,1], [4,2]] + end + + it "passes on the given block's return value" do + arr = [1,2,3] + arr.delete_if.with_index { |a,b| false } + arr.should == [1,2,3] + + arr.delete_if.with_index { |a,b| true } + arr.should == [] + end + + it "returns the iterator's return value" do + @enum.select.with_index { |a,b| false }.should == [] + end + + it "returns the correct value if chained with itself" do + [:a].each.with_index.with_index.to_a.should == [[[:a,0],0]] + end +end diff --git a/spec/ruby/core/enumerator/with_object_spec.rb b/spec/ruby/core/enumerator/with_object_spec.rb new file mode 100644 index 0000000000..58031fd765 --- /dev/null +++ b/spec/ruby/core/enumerator/with_object_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/with_object' + +describe "Enumerator#with_object" do + it_behaves_like :enum_with_object, :with_object +end diff --git a/spec/ruby/core/enumerator/yielder/append_spec.rb b/spec/ruby/core/enumerator/yielder/append_spec.rb new file mode 100644 index 0000000000..a36e5d64b6 --- /dev/null +++ b/spec/ruby/core/enumerator/yielder/append_spec.rb @@ -0,0 +1,35 @@ +require_relative '../../../spec_helper' + +describe "Enumerator::Yielder#<<" do + # TODO: There's some common behavior between yield and <<; move to a shared spec + it "yields the value to the block" do + ary = [] + y = Enumerator::Yielder.new {|x| ary << x} + y << 1 + + ary.should == [1] + end + + it "doesn't double-wrap Arrays" do + yields = [] + y = Enumerator::Yielder.new {|args| yields << args } + y << [1] + yields.should == [[1]] + end + + it "returns self" do + y = Enumerator::Yielder.new {|x| x + 1} + (y << 1).should equal(y) + end + + context "when multiple arguments passed" do + it "raises an ArgumentError" do + ary = [] + y = Enumerator::Yielder.new { |*x| ary << x } + + -> { + y.<<(1, 2) + }.should raise_error(ArgumentError, /wrong number of arguments/) + end + end +end diff --git a/spec/ruby/core/enumerator/yielder/initialize_spec.rb b/spec/ruby/core/enumerator/yielder/initialize_spec.rb new file mode 100644 index 0000000000..5a6eee2d0f --- /dev/null +++ b/spec/ruby/core/enumerator/yielder/initialize_spec.rb @@ -0,0 +1,18 @@ +# -*- encoding: us-ascii -*- + +require_relative '../../../spec_helper' + +describe "Enumerator::Yielder#initialize" do + before :each do + @class = Enumerator::Yielder + @uninitialized = @class.allocate + end + + it "is a private method" do + @class.should have_private_instance_method(:initialize, false) + end + + it "returns self when given a block" do + @uninitialized.send(:initialize) {}.should equal(@uninitialized) + end +end diff --git a/spec/ruby/core/enumerator/yielder/to_proc_spec.rb b/spec/ruby/core/enumerator/yielder/to_proc_spec.rb new file mode 100644 index 0000000000..1d3681ab50 --- /dev/null +++ b/spec/ruby/core/enumerator/yielder/to_proc_spec.rb @@ -0,0 +1,16 @@ +require_relative '../../../spec_helper' + +describe "Enumerator::Yielder#to_proc" do + it "returns a Proc object that takes an argument and yields it to the block" do + ScratchPad.record [] + y = Enumerator::Yielder.new { |*args| ScratchPad << args; "foobar" } + + callable = y.to_proc + callable.class.should == Proc + + result = callable.call(1, 2) + ScratchPad.recorded.should == [[1, 2]] + + result.should == "foobar" + end +end diff --git a/spec/ruby/core/enumerator/yielder/yield_spec.rb b/spec/ruby/core/enumerator/yielder/yield_spec.rb new file mode 100644 index 0000000000..acfdf114b6 --- /dev/null +++ b/spec/ruby/core/enumerator/yielder/yield_spec.rb @@ -0,0 +1,33 @@ +require_relative '../../../spec_helper' + +describe "Enumerator::Yielder#yield" do + it "yields the value to the block" do + ary = [] + y = Enumerator::Yielder.new {|x| ary << x} + y.yield 1 + + ary.should == [1] + end + + it "yields with passed arguments" do + yields = [] + y = Enumerator::Yielder.new {|*args| yields << args } + y.yield 1, 2 + yields.should == [[1, 2]] + end + + it "returns the result of the block for the given value" do + y = Enumerator::Yielder.new {|x| x + 1} + y.yield(1).should == 2 + end + + context "when multiple arguments passed" do + it "yields the arguments list to the block" do + ary = [] + y = Enumerator::Yielder.new { |*x| ary << x } + y.yield(1, 2) + + ary.should == [[1, 2]] + end + end +end diff --git a/spec/ruby/core/env/assoc_spec.rb b/spec/ruby/core/env/assoc_spec.rb new file mode 100644 index 0000000000..c7a388db75 --- /dev/null +++ b/spec/ruby/core/env/assoc_spec.rb @@ -0,0 +1,31 @@ +require_relative '../../spec_helper' + +describe "ENV.assoc" do + before :each do + @foo = ENV["foo"] + end + + after :each do + ENV["foo"] = @foo + end + + it "returns an array of the key and value of the environment variable with the given key" do + ENV["foo"] = "bar" + ENV.assoc("foo").should == ["foo", "bar"] + end + + it "returns nil if no environment variable with the given key exists" do + ENV.assoc("foo").should == nil + end + + it "returns the key element coerced with #to_str" do + ENV["foo"] = "bar" + k = mock('key') + k.should_receive(:to_str).and_return("foo") + ENV.assoc(k).should == ["foo", "bar"] + end + + it "raises TypeError if the argument is not a String and does not respond to #to_str" do + -> { ENV.assoc(Object.new) }.should raise_error(TypeError, "no implicit conversion of Object into String") + end +end diff --git a/spec/ruby/core/env/clear_spec.rb b/spec/ruby/core/env/clear_spec.rb new file mode 100644 index 0000000000..48b034ba1d --- /dev/null +++ b/spec/ruby/core/env/clear_spec.rb @@ -0,0 +1,20 @@ +require_relative '../../spec_helper' + +describe "ENV.clear" do + it "deletes all environment variables" do + orig = ENV.to_hash + begin + ENV.clear.should equal(ENV) + + # This used 'env' the helper before. That shells out to 'env' which + # itself sets up certain environment variables before it runs, because + # the shell sets them up before it runs any command. + # + # Thusly, you can ONLY test this by asking through ENV itself. + ENV.size.should == 0 + ensure + ENV.replace orig + end + end + +end diff --git a/spec/ruby/core/env/clone_spec.rb b/spec/ruby/core/env/clone_spec.rb new file mode 100644 index 0000000000..01a29c6ab4 --- /dev/null +++ b/spec/ruby/core/env/clone_spec.rb @@ -0,0 +1,21 @@ +require_relative '../../spec_helper' + +describe "ENV#clone" do + it "raises ArgumentError when keyword argument 'freeze' is neither nil nor boolean" do + -> { + ENV.clone(freeze: 1) + }.should raise_error(ArgumentError) + end + + it "raises ArgumentError when keyword argument is not 'freeze'" do + -> { + ENV.clone(foo: nil) + }.should raise_error(ArgumentError) + end + + it "raises TypeError" do + -> { + ENV.clone + }.should raise_error(TypeError, /Cannot clone ENV, use ENV.to_h to get a copy of ENV as a hash/) + end +end diff --git a/spec/ruby/core/env/delete_if_spec.rb b/spec/ruby/core/env/delete_if_spec.rb new file mode 100644 index 0000000000..d2de51c225 --- /dev/null +++ b/spec/ruby/core/env/delete_if_spec.rb @@ -0,0 +1,54 @@ +require_relative '../../spec_helper' +require_relative '../enumerable/shared/enumeratorized' + +describe "ENV.delete_if" do + before :each do + @foo = ENV["foo"] + @bar = ENV["bar"] + + ENV["foo"] = "0" + ENV["bar"] = "1" + end + + after :each do + ENV["foo"] = @foo + ENV["bar"] = @bar + end + + it "deletes pairs if the block returns true" do + ENV.delete_if { |k, v| ["foo", "bar"].include?(k) } + ENV["foo"].should == nil + ENV["bar"].should == nil + end + + it "returns ENV when block given" do + ENV.delete_if { |k, v| ["foo", "bar"].include?(k) }.should equal(ENV) + end + + it "returns ENV even if nothing deleted" do + ENV.delete_if { false }.should equal(ENV) + end + + it "returns an Enumerator if no block given" do + ENV.delete_if.should be_an_instance_of(Enumerator) + end + + it "deletes pairs through enumerator" do + enum = ENV.delete_if + enum.each { |k, v| ["foo", "bar"].include?(k) } + ENV["foo"].should == nil + ENV["bar"].should == nil + end + + it "returns ENV from enumerator" do + enum = ENV.delete_if + enum.each { |k, v| ["foo", "bar"].include?(k) }.should equal(ENV) + end + + it "returns ENV from enumerator even if nothing deleted" do + enum = ENV.delete_if + enum.each { false }.should equal(ENV) + end + + it_behaves_like :enumeratorized_with_origin_size, :delete_if, ENV +end diff --git a/spec/ruby/core/env/delete_spec.rb b/spec/ruby/core/env/delete_spec.rb new file mode 100644 index 0000000000..f28ac97911 --- /dev/null +++ b/spec/ruby/core/env/delete_spec.rb @@ -0,0 +1,55 @@ +require_relative '../../spec_helper' + +describe "ENV.delete" do + before :each do + @saved_foo = ENV["foo"] + end + after :each do + ENV["foo"] = @saved_foo + end + + it "removes the variable from the environment" do + ENV["foo"] = "bar" + ENV.delete("foo") + ENV["foo"].should == nil + end + + it "returns the previous value" do + ENV["foo"] = "bar" + ENV.delete("foo").should == "bar" + end + + it "returns nil if the named environment variable does not exist and no block given" do + ENV.delete("foo") + ENV.delete("foo").should == nil + end + + it "yields the name to the given block if the named environment variable does not exist" do + ENV.delete("foo") + ENV.delete("foo") { |name| ScratchPad.record name } + ScratchPad.recorded.should == "foo" + end + + it "returns the result of given block if the named environment variable does not exist" do + ENV.delete("foo") + ENV.delete("foo") { |name| "bar" }.should == "bar" + end + + it "does not evaluate the block if the environment variable exists" do + ENV["foo"] = "bar" + ENV.delete("foo") { |name| fail "Should not happen" } + ENV["foo"].should == nil + end + + it "removes the variable coerced with #to_str" do + ENV["foo"] = "bar" + k = mock('key') + k.should_receive(:to_str).and_return("foo") + ENV.delete(k) + ENV["foo"].should == nil + end + + it "raises TypeError if the argument is not a String and does not respond to #to_str" do + -> { ENV.delete(Object.new) }.should raise_error(TypeError, "no implicit conversion of Object into String") + end +end diff --git a/spec/ruby/core/env/dup_spec.rb b/spec/ruby/core/env/dup_spec.rb new file mode 100644 index 0000000000..ac66b455cd --- /dev/null +++ b/spec/ruby/core/env/dup_spec.rb @@ -0,0 +1,9 @@ +require_relative '../../spec_helper' + +describe "ENV#dup" do + it "raises TypeError" do + -> { + ENV.dup + }.should raise_error(TypeError, /Cannot dup ENV, use ENV.to_h to get a copy of ENV as a hash/) + end +end diff --git a/spec/ruby/core/env/each_key_spec.rb b/spec/ruby/core/env/each_key_spec.rb new file mode 100644 index 0000000000..0efcb09900 --- /dev/null +++ b/spec/ruby/core/env/each_key_spec.rb @@ -0,0 +1,34 @@ +require_relative '../../spec_helper' +require_relative '../enumerable/shared/enumeratorized' + +describe "ENV.each_key" do + + it "returns each key" do + e = [] + orig = ENV.to_hash + begin + ENV.clear + ENV["1"] = "3" + ENV["2"] = "4" + ENV.each_key { |k| e << k }.should equal(ENV) + e.should include("1") + e.should include("2") + ensure + ENV.replace orig + end + end + + it "returns an Enumerator if called without a block" do + enum = ENV.each_key + enum.should be_an_instance_of(Enumerator) + enum.to_a.should == ENV.keys + end + + it "returns keys in the locale encoding" do + ENV.each_key do |key| + key.encoding.should == Encoding.find('locale') + end + end + + it_behaves_like :enumeratorized_with_origin_size, :each_key, ENV +end diff --git a/spec/ruby/core/env/each_pair_spec.rb b/spec/ruby/core/env/each_pair_spec.rb new file mode 100644 index 0000000000..2d7ed5faa0 --- /dev/null +++ b/spec/ruby/core/env/each_pair_spec.rb @@ -0,0 +1,6 @@ +require_relative 'spec_helper' +require_relative 'shared/each' + +describe "ENV.each_pair" do + it_behaves_like :env_each, :each_pair +end diff --git a/spec/ruby/core/env/each_spec.rb b/spec/ruby/core/env/each_spec.rb new file mode 100644 index 0000000000..d1e06f55b6 --- /dev/null +++ b/spec/ruby/core/env/each_spec.rb @@ -0,0 +1,6 @@ +require_relative 'spec_helper' +require_relative 'shared/each' + +describe "ENV.each" do + it_behaves_like :env_each, :each +end diff --git a/spec/ruby/core/env/each_value_spec.rb b/spec/ruby/core/env/each_value_spec.rb new file mode 100644 index 0000000000..cc3c9ebfb8 --- /dev/null +++ b/spec/ruby/core/env/each_value_spec.rb @@ -0,0 +1,34 @@ +require_relative 'spec_helper' +require_relative '../enumerable/shared/enumeratorized' + +describe "ENV.each_value" do + + it "returns each value" do + e = [] + orig = ENV.to_hash + begin + ENV.clear + ENV["1"] = "3" + ENV["2"] = "4" + ENV.each_value { |v| e << v }.should equal(ENV) + e.should include("3") + e.should include("4") + ensure + ENV.replace orig + end + end + + it "returns an Enumerator if called without a block" do + enum = ENV.each_value + enum.should be_an_instance_of(Enumerator) + enum.to_a.should == ENV.values + end + + it "uses the locale encoding" do + ENV.each_value do |value| + value.should.be_locale_env + end + end + + it_behaves_like :enumeratorized_with_origin_size, :each_value, ENV +end diff --git a/spec/ruby/core/env/element_reference_spec.rb b/spec/ruby/core/env/element_reference_spec.rb new file mode 100644 index 0000000000..66a9bc9690 --- /dev/null +++ b/spec/ruby/core/env/element_reference_spec.rb @@ -0,0 +1,76 @@ +# encoding: binary +require_relative '../../spec_helper' +require_relative 'fixtures/common' + +describe "ENV.[]" do + before :each do + @variable = "returns_only_frozen_values" + end + + after :each do + ENV.delete @variable + end + + it "returns nil if the variable isn't found" do + ENV["this_var_is_never_set"].should == nil + end + + it "returns only frozen values" do + ENV[@variable] = "a non-frozen string" + ENV[@variable].should.frozen? + end + + it "coerces a non-string name with #to_str" do + ENV[@variable] = "bar" + k = mock('key') + k.should_receive(:to_str).and_return(@variable) + ENV[k].should == "bar" + end + + it "raises TypeError if the argument is not a String and does not respond to #to_str" do + -> { ENV[Object.new] }.should raise_error(TypeError, "no implicit conversion of Object into String") + end + + platform_is :windows do + it "looks up values case-insensitively" do + ENV[@variable] = "bar" + ENV[@variable.upcase].should == "bar" + end + end +end + +describe "ENV.[]" do + before :each do + @variable = "env_element_reference_encoding_specs" + + @external = Encoding.default_external + @internal = Encoding.default_internal + + Encoding.default_external = Encoding::BINARY + end + + after :each do + Encoding.default_external = @external + Encoding.default_internal = @internal + + ENV.delete @variable + end + + it "uses the locale encoding if Encoding.default_internal is nil" do + Encoding.default_internal = nil + + locale = ENVSpecs.encoding + locale = Encoding::BINARY if locale == Encoding::US_ASCII + ENV[@variable] = "\xC3\xB8" + ENV[@variable].encoding.should == locale + end + + it "transcodes from the locale encoding to Encoding.default_internal if set" do + # We cannot reliably know the locale encoding, so we merely check that + # the result string has the expected encoding. + ENV[@variable] = "" + Encoding.default_internal = Encoding::IBM437 + + ENV[@variable].encoding.should equal(Encoding::IBM437) + end +end diff --git a/spec/ruby/core/env/element_set_spec.rb b/spec/ruby/core/env/element_set_spec.rb new file mode 100644 index 0000000000..26dfee1ade --- /dev/null +++ b/spec/ruby/core/env/element_set_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/store' + +describe "ENV.[]=" do + it_behaves_like :env_store, :[]= +end diff --git a/spec/ruby/core/env/empty_spec.rb b/spec/ruby/core/env/empty_spec.rb new file mode 100644 index 0000000000..afeb406a9e --- /dev/null +++ b/spec/ruby/core/env/empty_spec.rb @@ -0,0 +1,23 @@ +require_relative '../../spec_helper' + +describe "ENV.empty?" do + + it "returns true if the Environment is empty" do + if ENV.keys.size > 0 + ENV.should_not.empty? + end + orig = ENV.to_hash + begin + ENV.clear + ENV.should.empty? + ensure + ENV.replace orig + end + end + + it "returns false if not empty" do + if ENV.keys.size > 0 + ENV.should_not.empty? + end + end +end diff --git a/spec/ruby/core/env/except_spec.rb b/spec/ruby/core/env/except_spec.rb new file mode 100644 index 0000000000..fb8f3b7536 --- /dev/null +++ b/spec/ruby/core/env/except_spec.rb @@ -0,0 +1,34 @@ +require_relative 'spec_helper' +require_relative 'shared/to_hash' + +describe "ENV.except" do + before do + @orig_hash = ENV.to_hash + end + + after do + ENV.replace @orig_hash + end + + # Testing the method without arguments is covered via + it_behaves_like :env_to_hash, :except + + it "returns a hash without the requested subset" do + ENV.clear + + ENV['one'] = '1' + ENV['two'] = '2' + ENV['three'] = '3' + + ENV.except('one', 'three').should == { 'two' => '2' } + end + + it "ignores keys not present in the original hash" do + ENV.clear + + ENV['one'] = '1' + ENV['two'] = '2' + + ENV.except('one', 'three').should == { 'two' => '2' } + end +end diff --git a/spec/ruby/core/env/fetch_spec.rb b/spec/ruby/core/env/fetch_spec.rb new file mode 100644 index 0000000000..2c5d7cc3a0 --- /dev/null +++ b/spec/ruby/core/env/fetch_spec.rb @@ -0,0 +1,63 @@ +require_relative '../../spec_helper' +require_relative '../../shared/hash/key_error' +require_relative 'fixtures/common' + +describe "ENV.fetch" do + before :each do + @foo_saved = ENV.delete("foo") + end + after :each do + ENV["foo"] = @saved_foo + end + + it "returns a value" do + ENV["foo"] = "bar" + ENV.fetch("foo").should == "bar" + end + + it "raises a TypeError if the key is not a String" do + -> { ENV.fetch Object.new }.should raise_error(TypeError, "no implicit conversion of Object into String") + end + + context "when the key is not found" do + it_behaves_like :key_error, -> obj, key { obj.fetch(key) }, ENV + + it "formats the object with #inspect in the KeyError message" do + -> { + ENV.fetch('foo') + }.should raise_error(KeyError, 'key not found: "foo"') + end + end + + it "provides the given default parameter" do + ENV.fetch("foo", "default").should == "default" + end + + it "does not insist that the default be a String" do + ENV.fetch("foo", :default).should == :default + end + + it "provides a default value from a block" do + ENV.fetch("foo") { |k| "wanted #{k}" }.should == "wanted foo" + end + + it "does not insist that the block return a String" do + ENV.fetch("foo") { |k| k.to_sym }.should == :foo + end + + it "warns on block and default parameter given" do + -> do + ENV.fetch("foo", "default") { "bar" }.should == "bar" + end.should complain(/block supersedes default value argument/) + end + + it "does not evaluate the block when key found" do + ENV["foo"] = "bar" + ENV.fetch("foo") { fail "should not get here"}.should == "bar" + end + + it "uses the locale encoding" do + ENV["foo"] = "bar" + ENV.fetch("foo").encoding.should == ENVSpecs.encoding + end +end diff --git a/spec/ruby/core/env/filter_spec.rb b/spec/ruby/core/env/filter_spec.rb new file mode 100644 index 0000000000..52f8b79a0b --- /dev/null +++ b/spec/ruby/core/env/filter_spec.rb @@ -0,0 +1,13 @@ +require_relative '../../spec_helper' +require_relative '../enumerable/shared/enumeratorized' +require_relative 'shared/select' + +describe "ENV.filter!" do + it_behaves_like :env_select!, :filter! + it_behaves_like :enumeratorized_with_origin_size, :filter!, ENV +end + +describe "ENV.filter" do + it_behaves_like :env_select, :filter + it_behaves_like :enumeratorized_with_origin_size, :filter, ENV +end diff --git a/spec/ruby/core/env/fixtures/common.rb b/spec/ruby/core/env/fixtures/common.rb new file mode 100644 index 0000000000..8d5057614d --- /dev/null +++ b/spec/ruby/core/env/fixtures/common.rb @@ -0,0 +1,9 @@ +module ENVSpecs + def self.encoding + locale = Encoding.find('locale') + if ruby_version_is '3' and platform_is :windows + locale = Encoding::UTF_8 + end + locale + end +end diff --git a/spec/ruby/core/env/has_key_spec.rb b/spec/ruby/core/env/has_key_spec.rb new file mode 100644 index 0000000000..798668105d --- /dev/null +++ b/spec/ruby/core/env/has_key_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/include' + +describe "ENV.has_key?" do + it_behaves_like :env_include, :has_key? +end diff --git a/spec/ruby/core/env/has_value_spec.rb b/spec/ruby/core/env/has_value_spec.rb new file mode 100644 index 0000000000..a2bf3eb877 --- /dev/null +++ b/spec/ruby/core/env/has_value_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/value' + +describe "ENV.has_value?" do + it_behaves_like :env_value, :has_value? +end diff --git a/spec/ruby/core/env/include_spec.rb b/spec/ruby/core/env/include_spec.rb new file mode 100644 index 0000000000..3975f095ac --- /dev/null +++ b/spec/ruby/core/env/include_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/include' + +describe "ENV.include?" do + it_behaves_like :env_include, :include? +end diff --git a/spec/ruby/core/env/inspect_spec.rb b/spec/ruby/core/env/inspect_spec.rb new file mode 100644 index 0000000000..7dd92b120f --- /dev/null +++ b/spec/ruby/core/env/inspect_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' + +describe "ENV.inspect" do + + it "returns a String that looks like a Hash with real data" do + ENV["foo"] = "bar" + ENV.inspect.should =~ /\{.*"foo" *=> *"bar".*\}/ + ENV.delete "foo" + end + +end diff --git a/spec/ruby/core/env/invert_spec.rb b/spec/ruby/core/env/invert_spec.rb new file mode 100644 index 0000000000..c095374d95 --- /dev/null +++ b/spec/ruby/core/env/invert_spec.rb @@ -0,0 +1,16 @@ +require_relative '../../spec_helper' + +describe "ENV.invert" do + before :each do + ENV["foo"] = "bar" + end + + after :each do + ENV.delete "foo" + end + + it "returns a hash with ENV.keys as the values and vice versa" do + ENV.invert["bar"].should == "foo" + ENV["foo"].should == "bar" + end +end diff --git a/spec/ruby/core/env/keep_if_spec.rb b/spec/ruby/core/env/keep_if_spec.rb new file mode 100644 index 0000000000..64b6a207d0 --- /dev/null +++ b/spec/ruby/core/env/keep_if_spec.rb @@ -0,0 +1,54 @@ +require_relative '../../spec_helper' +require_relative '../enumerable/shared/enumeratorized' + +describe "ENV.keep_if" do + before :each do + @foo = ENV["foo"] + @bar = ENV["bar"] + + ENV["foo"] = "0" + ENV["bar"] = "1" + end + + after :each do + ENV["foo"] = @foo + ENV["bar"] = @bar + end + + it "deletes pairs if the block returns false" do + ENV.keep_if { |k, v| !["foo", "bar"].include?(k) } + ENV["foo"].should == nil + ENV["bar"].should == nil + end + + it "returns ENV when block given" do + ENV.keep_if { |k, v| !["foo", "bar"].include?(k) }.should equal(ENV) + end + + it "returns ENV even if nothing deleted" do + ENV.keep_if { true }.should equal(ENV) + end + + it "returns an Enumerator if no block given" do + ENV.keep_if.should be_an_instance_of(Enumerator) + end + + it "deletes pairs through enumerator" do + enum = ENV.keep_if + enum.each { |k, v| !["foo", "bar"].include?(k) } + ENV["foo"].should == nil + ENV["bar"].should == nil + end + + it "returns ENV from enumerator" do + enum = ENV.keep_if + enum.each { |k, v| !["foo", "bar"].include?(k) }.should equal(ENV) + end + + it "returns ENV from enumerator even if nothing deleted" do + enum = ENV.keep_if + enum.each { true }.should equal(ENV) + end + + it_behaves_like :enumeratorized_with_origin_size, :keep_if, ENV +end diff --git a/spec/ruby/core/env/key_spec.rb b/spec/ruby/core/env/key_spec.rb new file mode 100644 index 0000000000..cf70286409 --- /dev/null +++ b/spec/ruby/core/env/key_spec.rb @@ -0,0 +1,39 @@ +require_relative '../../spec_helper' +require_relative 'shared/include' + +describe "ENV.key?" do + it_behaves_like :env_include, :key? +end + +describe "ENV.key" do + before :each do + @saved_foo = ENV["foo"] + end + + after :each do + ENV["foo"] = @saved_foo + end + + it "returns the index associated with the passed value" do + ENV["foo"] = "bar" + ENV.key("bar").should == "foo" + end + + it "returns nil if the passed value is not found" do + ENV.delete("foo") + ENV.key("foo").should be_nil + end + + it "coerces the key element with #to_str" do + ENV["foo"] = "bar" + k = mock('key') + k.should_receive(:to_str).and_return("bar") + ENV.key(k).should == "foo" + end + + it "raises TypeError if the argument is not a String and does not respond to #to_str" do + -> { + ENV.key(Object.new) + }.should raise_error(TypeError, "no implicit conversion of Object into String") + end +end diff --git a/spec/ruby/core/env/keys_spec.rb b/spec/ruby/core/env/keys_spec.rb new file mode 100644 index 0000000000..b074a8f7c7 --- /dev/null +++ b/spec/ruby/core/env/keys_spec.rb @@ -0,0 +1,14 @@ +require_relative '../../spec_helper' + +describe "ENV.keys" do + + it "returns an array of the keys" do + ENV.keys.should == ENV.to_hash.keys + end + + it "returns the keys in the locale encoding" do + ENV.keys.each do |key| + key.encoding.should == Encoding.find('locale') + end + end +end diff --git a/spec/ruby/core/env/length_spec.rb b/spec/ruby/core/env/length_spec.rb new file mode 100644 index 0000000000..c6f9062892 --- /dev/null +++ b/spec/ruby/core/env/length_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/length' + +describe "ENV.length" do + it_behaves_like :env_length, :length +end diff --git a/spec/ruby/core/env/member_spec.rb b/spec/ruby/core/env/member_spec.rb new file mode 100644 index 0000000000..9119022ae5 --- /dev/null +++ b/spec/ruby/core/env/member_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/include' + +describe "ENV.member?" do + it_behaves_like :env_include, :member? +end diff --git a/spec/ruby/core/env/merge_spec.rb b/spec/ruby/core/env/merge_spec.rb new file mode 100644 index 0000000000..f10662cf79 --- /dev/null +++ b/spec/ruby/core/env/merge_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/update' + +describe "ENV.merge!" do + it_behaves_like :env_update, :merge! +end diff --git a/spec/ruby/core/env/rassoc_spec.rb b/spec/ruby/core/env/rassoc_spec.rb new file mode 100644 index 0000000000..ab9fe68088 --- /dev/null +++ b/spec/ruby/core/env/rassoc_spec.rb @@ -0,0 +1,42 @@ +require_relative '../../spec_helper' + +describe "ENV.rassoc" do + before :each do + @foo = ENV["foo"] + @baz = ENV["baz"] + end + + after :each do + ENV["foo"] = @foo + ENV["baz"] = @baz + end + + it "returns an array of the key and value of the environment variable with the given value" do + ENV["foo"] = "bar" + ENV.rassoc("bar").should == ["foo", "bar"] + end + + it "returns a single array even if there are multiple such environment variables" do + ENV["foo"] = "bar" + ENV["baz"] = "bar" + [ + ["foo", "bar"], + ["baz", "bar"], + ].should include(ENV.rassoc("bar")) + end + + it "returns nil if no environment variable with the given value exists" do + ENV.rassoc("bar").should == nil + end + + it "returns the value element coerced with #to_str" do + ENV["foo"] = "bar" + v = mock('value') + v.should_receive(:to_str).and_return("bar") + ENV.rassoc(v).should == ["foo", "bar"] + end + + it "returns nil if the argument is not a String and does not respond to #to_str" do + ENV.rassoc(Object.new).should == nil + end +end diff --git a/spec/ruby/core/env/rehash_spec.rb b/spec/ruby/core/env/rehash_spec.rb new file mode 100644 index 0000000000..3782e4b727 --- /dev/null +++ b/spec/ruby/core/env/rehash_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' + +describe "ENV.rehash" do + it "returns nil" do + ENV.rehash.should == nil + end +end diff --git a/spec/ruby/core/env/reject_spec.rb b/spec/ruby/core/env/reject_spec.rb new file mode 100644 index 0000000000..6a9794925d --- /dev/null +++ b/spec/ruby/core/env/reject_spec.rb @@ -0,0 +1,101 @@ +require_relative '../../spec_helper' +require_relative '../enumerable/shared/enumeratorized' + +describe "ENV.reject!" do + before :each do + @foo = ENV["foo"] + end + + after :each do + ENV["foo"] = @foo + end + + it "rejects entries based on key" do + ENV["foo"] = "bar" + ENV.reject! { |k, v| k == "foo" } + ENV["foo"].should == nil + end + + it "rejects entries based on value" do + ENV["foo"] = "bar" + ENV.reject! { |k, v| v == "bar" } + ENV["foo"].should == nil + end + + it "returns itself or nil" do + ENV.reject! { false }.should == nil + ENV["foo"] = "bar" + ENV.reject! { |k, v| k == "foo" }.should equal(ENV) + ENV["foo"].should == nil + end + + it "returns an Enumerator if called without a block" do + ENV["foo"] = "bar" + enum = ENV.reject! + enum.should be_an_instance_of(Enumerator) + enum.each { |k, v| k == "foo" }.should equal(ENV) + ENV["foo"].should == nil + end + + it "doesn't raise if empty" do + orig = ENV.to_hash + begin + ENV.clear + -> { ENV.reject! }.should_not raise_error(LocalJumpError) + ensure + ENV.replace orig + end + end + + it_behaves_like :enumeratorized_with_origin_size, :reject!, ENV +end + +describe "ENV.reject" do + before :each do + @foo = ENV["foo"] + end + + after :each do + ENV["foo"] = @foo + end + + it "rejects entries based on key" do + ENV["foo"] = "bar" + e = ENV.reject { |k, v| k == "foo" } + e["foo"].should == nil + ENV["foo"].should == "bar" + ENV["foo"] = nil + end + + it "rejects entries based on value" do + ENV["foo"] = "bar" + e = ENV.reject { |k, v| v == "bar" } + e["foo"].should == nil + ENV["foo"].should == "bar" + ENV["foo"] = nil + end + + it "returns a Hash" do + ENV.reject { false }.should be_kind_of(Hash) + end + + it "returns an Enumerator if called without a block" do + ENV["foo"] = "bar" + enum = ENV.reject + enum.should be_an_instance_of(Enumerator) + enum.each { |k, v| k == "foo"} + ENV["foo"] = nil + end + + it "doesn't raise if empty" do + orig = ENV.to_hash + begin + ENV.clear + -> { ENV.reject }.should_not raise_error(LocalJumpError) + ensure + ENV.replace orig + end + end + + it_behaves_like :enumeratorized_with_origin_size, :reject, ENV +end diff --git a/spec/ruby/core/env/replace_spec.rb b/spec/ruby/core/env/replace_spec.rb new file mode 100644 index 0000000000..9fc67643d1 --- /dev/null +++ b/spec/ruby/core/env/replace_spec.rb @@ -0,0 +1,51 @@ +require_relative '../../spec_helper' + +describe "ENV.replace" do + before :each do + @orig = ENV.to_hash + ENV.delete("foo") + end + + after :each do + ENV.replace(@orig) + end + + it "replaces ENV with a Hash" do + ENV.replace("foo" => "0", "bar" => "1").should equal(ENV) + ENV.size.should == 2 + ENV["foo"].should == "0" + ENV["bar"].should == "1" + end + + it "raises TypeError if the argument is not a Hash" do + -> { ENV.replace(Object.new) }.should raise_error(TypeError, "no implicit conversion of Object into Hash") + ENV.to_hash.should == @orig + end + + it "raises TypeError if a key is not a String" do + -> { ENV.replace(Object.new => "0") }.should raise_error(TypeError, "no implicit conversion of Object into String") + ENV.to_hash.should == @orig + end + + it "raises TypeError if a value is not a String" do + -> { ENV.replace("foo" => Object.new) }.should raise_error(TypeError, "no implicit conversion of Object into String") + ENV.to_hash.should == @orig + end + + it "raises Errno::EINVAL when the key contains the '=' character" do + -> { ENV.replace("foo=" =>"bar") }.should raise_error(Errno::EINVAL) + end + + it "raises Errno::EINVAL when the key is an empty string" do + -> { ENV.replace("" => "bar") }.should raise_error(Errno::EINVAL) + end + + it "does not accept good data preceding an error" do + -> { ENV.replace("foo" => "1", Object.new => Object.new) }.should raise_error(TypeError, "no implicit conversion of Object into String") + end + + it "does not accept good data following an error" do + -> { ENV.replace(Object.new => Object.new, "foo" => "0") }.should raise_error(TypeError, "no implicit conversion of Object into String") + ENV.to_hash.should == @orig + end +end diff --git a/spec/ruby/core/env/select_spec.rb b/spec/ruby/core/env/select_spec.rb new file mode 100644 index 0000000000..c3a76f4434 --- /dev/null +++ b/spec/ruby/core/env/select_spec.rb @@ -0,0 +1,13 @@ +require_relative '../../spec_helper' +require_relative '../enumerable/shared/enumeratorized' +require_relative 'shared/select' + +describe "ENV.select!" do + it_behaves_like :env_select!, :select! + it_behaves_like :enumeratorized_with_origin_size, :select!, ENV +end + +describe "ENV.select" do + it_behaves_like :env_select, :select + it_behaves_like :enumeratorized_with_origin_size, :select, ENV +end diff --git a/spec/ruby/core/env/shared/each.rb b/spec/ruby/core/env/shared/each.rb new file mode 100644 index 0000000000..d901b854c4 --- /dev/null +++ b/spec/ruby/core/env/shared/each.rb @@ -0,0 +1,65 @@ +require_relative '../../enumerable/shared/enumeratorized' + +describe :env_each, shared: true do + it "returns each pair" do + orig = ENV.to_hash + e = [] + begin + ENV.clear + ENV["foo"] = "bar" + ENV["baz"] = "boo" + ENV.send(@method) { |k, v| e << [k, v] }.should equal(ENV) + e.should include(["foo", "bar"]) + e.should include(["baz", "boo"]) + ensure + ENV.replace orig + end + end + + it "returns an Enumerator if called without a block" do + enum = ENV.send(@method) + enum.should be_an_instance_of(Enumerator) + enum.each do |name, value| + ENV[name].should == value + end + end + + before :all do + @object = ENV + end + it_should_behave_like :enumeratorized_with_origin_size + + describe "with encoding" do + before :each do + @external = Encoding.default_external + @internal = Encoding.default_internal + + Encoding.default_external = Encoding::BINARY + end + + after :each do + Encoding.default_external = @external + Encoding.default_internal = @internal + end + + it "uses the locale encoding when Encoding.default_internal is nil" do + Encoding.default_internal = nil + + ENV.send(@method) do |key, value| + key.should.be_locale_env + value.should.be_locale_env + end + end + + it "transcodes from the locale encoding to Encoding.default_internal if set" do + Encoding.default_internal = internal = Encoding::IBM437 + + ENV.send(@method) do |key, value| + key.encoding.should equal(internal) + if value.ascii_only? + value.encoding.should equal(internal) + end + end + end + end +end diff --git a/spec/ruby/core/env/shared/include.rb b/spec/ruby/core/env/shared/include.rb new file mode 100644 index 0000000000..70aa555301 --- /dev/null +++ b/spec/ruby/core/env/shared/include.rb @@ -0,0 +1,30 @@ +describe :env_include, shared: true do + before :each do + @saved_foo = ENV["foo"] + end + + after :each do + ENV["foo"] = @saved_foo + end + + it "returns true if ENV has the key" do + ENV["foo"] = "bar" + ENV.send(@method, "foo").should == true + end + + it "returns false if ENV doesn't include the key" do + ENV.delete("foo") + ENV.send(@method, "foo").should == false + end + + it "coerces the key with #to_str" do + ENV["foo"] = "bar" + k = mock('key') + k.should_receive(:to_str).and_return("foo") + ENV.send(@method, k).should == true + end + + it "raises TypeError if the argument is not a String and does not respond to #to_str" do + -> { ENV.send(@method, Object.new) }.should raise_error(TypeError, "no implicit conversion of Object into String") + end +end diff --git a/spec/ruby/core/env/shared/length.rb b/spec/ruby/core/env/shared/length.rb new file mode 100644 index 0000000000..6d788a3f4a --- /dev/null +++ b/spec/ruby/core/env/shared/length.rb @@ -0,0 +1,13 @@ +describe :env_length, shared: true do + it "returns the number of ENV entries" do + orig = ENV.to_hash + begin + ENV.clear + ENV["foo"] = "bar" + ENV["baz"] = "boo" + ENV.send(@method).should == 2 + ensure + ENV.replace orig + end + end +end diff --git a/spec/ruby/core/env/shared/select.rb b/spec/ruby/core/env/shared/select.rb new file mode 100644 index 0000000000..75ba112a32 --- /dev/null +++ b/spec/ruby/core/env/shared/select.rb @@ -0,0 +1,61 @@ +describe :env_select, shared: true do + before :each do + @saved_foo = ENV["foo"] + end + + after :each do + ENV["foo"] = @saved_foo + end + + it "returns a Hash of names and values for which block return true" do + ENV["foo"] = "bar" + (ENV.send(@method) { |k, v| k == "foo" }).should == { "foo" => "bar" } + end + + it "returns an Enumerator when no block is given" do + enum = ENV.send(@method) + enum.should be_an_instance_of(Enumerator) + end + + it "selects via the enumerator" do + enum = ENV.send(@method) + ENV["foo"] = "bar" + enum.each { |k, v| k == "foo" }.should == { "foo" => "bar"} + end +end + +describe :env_select!, shared: true do + before :each do + @saved_foo = ENV["foo"] + end + + after :each do + ENV["foo"] = @saved_foo + end + + it "removes environment variables for which the block returns true" do + ENV["foo"] = "bar" + ENV.send(@method) { |k, v| k != "foo" } + ENV["foo"].should == nil + end + + it "returns self if any changes were made" do + ENV["foo"] = "bar" + (ENV.send(@method) { |k, v| k != "foo" }).should == ENV + end + + it "returns nil if no changes were made" do + (ENV.send(@method) { true }).should == nil + end + + it "returns an Enumerator if called without a block" do + ENV.send(@method).should be_an_instance_of(Enumerator) + end + + it "selects via the enumerator" do + enum = ENV.send(@method) + ENV["foo"] = "bar" + enum.each { |k, v| k != "foo" } + ENV["foo"].should == nil + end +end diff --git a/spec/ruby/core/env/shared/store.rb b/spec/ruby/core/env/shared/store.rb new file mode 100644 index 0000000000..d6265c66a5 --- /dev/null +++ b/spec/ruby/core/env/shared/store.rb @@ -0,0 +1,60 @@ +describe :env_store, shared: true do + before :each do + @saved_foo = ENV["foo"] + end + + after :each do + ENV["foo"] = @saved_foo + end + + it "sets the environment variable to the given value" do + ENV.send(@method, "foo", "bar") + ENV["foo"].should == "bar" + end + + it "returns the value" do + value = "bar" + ENV.send(@method, "foo", value).should equal(value) + end + + it "deletes the environment variable when the value is nil" do + ENV["foo"] = "bar" + ENV.send(@method, "foo", nil) + ENV.key?("foo").should be_false + end + + it "coerces the key argument with #to_str" do + k = mock("key") + k.should_receive(:to_str).and_return("foo") + ENV.send(@method, k, "bar") + ENV["foo"].should == "bar" + end + + it "coerces the value argument with #to_str" do + v = mock("value") + v.should_receive(:to_str).and_return("bar") + ENV.send(@method, "foo", v) + ENV["foo"].should == "bar" + end + + it "raises TypeError when the key is not coercible to String" do + -> { ENV.send(@method, Object.new, "bar") }.should raise_error(TypeError, "no implicit conversion of Object into String") + end + + it "raises TypeError when the value is not coercible to String" do + -> { ENV.send(@method, "foo", Object.new) }.should raise_error(TypeError, "no implicit conversion of Object into String") + end + + it "raises Errno::EINVAL when the key contains the '=' character" do + -> { ENV.send(@method, "foo=", "bar") }.should raise_error(Errno::EINVAL) + end + + it "raises Errno::EINVAL when the key is an empty string" do + -> { ENV.send(@method, "", "bar") }.should raise_error(Errno::EINVAL) + end + + it "does nothing when the key is not a valid environment variable key and the value is nil" do + ENV.send(@method, "foo=", nil) + ENV.key?("foo=").should be_false + end +end diff --git a/spec/ruby/core/env/shared/to_hash.rb b/spec/ruby/core/env/shared/to_hash.rb new file mode 100644 index 0000000000..a0d4d7ce69 --- /dev/null +++ b/spec/ruby/core/env/shared/to_hash.rb @@ -0,0 +1,33 @@ +describe :env_to_hash, shared: true do + before :each do + @saved_foo = ENV["foo"] + end + + after :each do + ENV["foo"]= @saved_foo + end + + it "returns the ENV as a hash" do + ENV["foo"] = "bar" + h = ENV.send(@method) + h.should be_an_instance_of(Hash) + h["foo"].should == "bar" + end + + it "uses the locale encoding for keys" do + ENV.send(@method).keys.each {|k| k.should.be_locale_env } + end + + it "uses the locale encoding for values" do + ENV.send(@method).values.each {|k| k.should.be_locale_env } + end + + it "duplicates the ENV when converting to a Hash" do + h = ENV.send(@method) + h.should_not equal ENV + h.size.should == ENV.size + h.each_pair do |k, v| + ENV[k].should == v + end + end +end diff --git a/spec/ruby/core/env/shared/update.rb b/spec/ruby/core/env/shared/update.rb new file mode 100644 index 0000000000..e1b1c9c290 --- /dev/null +++ b/spec/ruby/core/env/shared/update.rb @@ -0,0 +1,104 @@ +describe :env_update, shared: true do + before :each do + @saved_foo = ENV["foo"] + @saved_bar = ENV["bar"] + end + + after :each do + ENV["foo"] = @saved_foo + ENV["bar"] = @saved_bar + end + + it "adds the parameter hash to ENV, returning ENV" do + ENV.send(@method, "foo" => "0", "bar" => "1").should equal(ENV) + ENV["foo"].should == "0" + ENV["bar"].should == "1" + end + + it "adds the multiple parameter hashes to ENV, returning ENV" do + ENV.send(@method, {"foo" => "multi1"}, {"bar" => "multi2"}).should equal(ENV) + ENV["foo"].should == "multi1" + ENV["bar"].should == "multi2" + end + + it "returns ENV when no block given" do + ENV.send(@method, {"foo" => "0", "bar" => "1"}).should equal(ENV) + end + + it "yields key, the old value and the new value when replacing an entry" do + ENV.send @method, {"foo" => "0", "bar" => "3"} + a = [] + ENV.send @method, {"foo" => "1", "bar" => "4"} do |key, old, new| + a << [key, old, new] + new + end + a[0].should == ["foo", "0", "1"] + a[1].should == ["bar", "3", "4"] + end + + it "yields key, the old value and the new value when replacing an entry" do + ENV.send @method, {"foo" => "0", "bar" => "3"} + ENV.send @method, {"foo" => "1", "bar" => "4"} do |key, old, new| + (new.to_i + 1).to_s + end + ENV["foo"].should == "2" + ENV["bar"].should == "5" + end + + # BUG: https://bugs.ruby-lang.org/issues/16192 + it "does not evaluate the block when the name is new" do + ENV.delete("bar") + ENV.send @method, {"foo" => "0"} + ENV.send(@method, "bar" => "1") { |key, old, new| fail "Should not get here" } + ENV["bar"].should == "1" + end + + # BUG: https://bugs.ruby-lang.org/issues/16192 + it "does not use the block's return value as the value when the name is new" do + ENV.delete("bar") + ENV.send @method, {"foo" => "0"} + ENV.send(@method, "bar" => "1") { |key, old, new| "Should not use this value" } + ENV["foo"].should == "0" + ENV["bar"].should == "1" + end + + it "returns ENV when block given" do + ENV.send(@method, {"foo" => "0", "bar" => "1"}){}.should equal(ENV) + end + + it "raises TypeError when a name is not coercible to String" do + -> { ENV.send @method, Object.new => "0" }.should raise_error(TypeError, "no implicit conversion of Object into String") + end + + it "raises TypeError when a value is not coercible to String" do + -> { ENV.send @method, "foo" => Object.new }.should raise_error(TypeError, "no implicit conversion of Object into String") + end + + it "raises Errno::EINVAL when a name contains the '=' character" do + -> { ENV.send(@method, "foo=" => "bar") }.should raise_error(Errno::EINVAL) + end + + it "raises Errno::EINVAL when a name is an empty string" do + -> { ENV.send(@method, "" => "bar") }.should raise_error(Errno::EINVAL) + end + + it "updates good data preceding an error" do + ENV["foo"] = "0" + begin + ENV.send @method, {"foo" => "2", Object.new => "1"} + rescue TypeError + ensure + ENV["foo"].should == "2" + end + end + + it "does not update good data following an error" do + ENV["foo"] = "0" + begin + ENV.send @method, {Object.new => "1", "foo" => "2"} + rescue TypeError + ensure + ENV["foo"].should == "0" + end + end +end diff --git a/spec/ruby/core/env/shared/value.rb b/spec/ruby/core/env/shared/value.rb new file mode 100644 index 0000000000..c2b5025465 --- /dev/null +++ b/spec/ruby/core/env/shared/value.rb @@ -0,0 +1,29 @@ +describe :env_value, shared: true do + before :each do + @saved_foo = ENV["foo"] + end + + after :each do + ENV["foo"] = @saved_foo + end + + it "returns true if ENV has the value" do + ENV["foo"] = "bar" + ENV.send(@method, "bar").should == true + end + + it "returns false if ENV doesn't have the value" do + ENV.send(@method, "foo").should == false + end + + it "coerces the value element with #to_str" do + ENV["foo"] = "bar" + v = mock('value') + v.should_receive(:to_str).and_return("bar") + ENV.send(@method, v).should == true + end + + it "returns nil if the argument is not a String and does not respond to #to_str" do + ENV.send(@method, Object.new).should == nil + end +end diff --git a/spec/ruby/core/env/shift_spec.rb b/spec/ruby/core/env/shift_spec.rb new file mode 100644 index 0000000000..1b92e5d1e4 --- /dev/null +++ b/spec/ruby/core/env/shift_spec.rb @@ -0,0 +1,47 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' + +describe "ENV.shift" do + before :each do + @orig = ENV.to_hash + @external = Encoding.default_external + @internal = Encoding.default_internal + + Encoding.default_external = Encoding::BINARY + ENV.replace({"FOO"=>"BAR"}) + end + + after :each do + Encoding.default_external = @external + Encoding.default_internal = @internal + ENV.replace @orig + end + + it "returns a pair and deletes it" do + ENV.should.has_key?("FOO") + pair = ENV.shift + pair.should == ["FOO", "BAR"] + ENV.should_not.has_key?("FOO") + end + + it "returns nil if ENV.empty?" do + ENV.clear + ENV.shift.should == nil + end + + it "uses the locale encoding if Encoding.default_internal is nil" do + Encoding.default_internal = nil + + pair = ENV.shift + pair.first.encoding.should equal(ENVSpecs.encoding) + pair.last.encoding.should equal(ENVSpecs.encoding) + end + + it "transcodes from the locale encoding to Encoding.default_internal if set" do + Encoding.default_internal = Encoding::IBM437 + + pair = ENV.shift + pair.first.encoding.should equal(Encoding::IBM437) + pair.last.encoding.should equal(Encoding::IBM437) + end +end diff --git a/spec/ruby/core/env/size_spec.rb b/spec/ruby/core/env/size_spec.rb new file mode 100644 index 0000000000..7c8072481e --- /dev/null +++ b/spec/ruby/core/env/size_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/length' + +describe "ENV.size" do + it_behaves_like :env_length, :size +end diff --git a/spec/ruby/core/env/slice_spec.rb b/spec/ruby/core/env/slice_spec.rb new file mode 100644 index 0000000000..959239d2b2 --- /dev/null +++ b/spec/ruby/core/env/slice_spec.rb @@ -0,0 +1,37 @@ +require_relative '../../spec_helper' + +describe "ENV.slice" do + before :each do + @saved_foo = ENV["foo"] + @saved_bar = ENV["bar"] + ENV["foo"] = "0" + ENV["bar"] = "1" + end + + after :each do + ENV["foo"] = @saved_foo + ENV["bar"] = @saved_bar + end + + it "returns a hash of the given environment variable names and their values" do + ENV.slice("foo", "bar").should == {"foo" => "0", "bar" => "1"} + end + + it "ignores each String that is not an environment variable name" do + ENV.slice("foo", "boo", "bar").should == {"foo" => "0", "bar" => "1"} + end + + it "returns the values for the keys coerced with #to_str, but keeps the original objects as result keys" do + foo = mock('key 1') + foo.should_receive(:to_str).and_return("foo") + boo = mock('key 2') + boo.should_receive(:to_str).and_return("boo") + bar = mock('key 3') + bar.should_receive(:to_str).and_return("bar") + ENV.slice(foo, boo, bar).should == {foo => "0", bar => "1"} + end + + it "raises TypeError if any argument is not a String and does not respond to #to_str" do + -> { ENV.slice(Object.new) }.should raise_error(TypeError, "no implicit conversion of Object into String") + end +end diff --git a/spec/ruby/core/env/spec_helper.rb b/spec/ruby/core/env/spec_helper.rb new file mode 100644 index 0000000000..470ffa58bc --- /dev/null +++ b/spec/ruby/core/env/spec_helper.rb @@ -0,0 +1,26 @@ +require_relative '../../spec_helper' + +locale_env_matcher = Class.new do + def initialize(name = 'locale') + encoding = Encoding.find(name) + @encodings = (encoding = Encoding::US_ASCII) ? + [encoding, Encoding::ASCII_8BIT] : [encoding] + end + + def matches?(actual) + @actual = actual = actual.encoding + @encodings.include?(actual) + end + + def failure_message + ["Expected #{@actual} to be #{@encodings.join(' or ')}"] + end + + def negative_failure_message + ["Expected #{@actual} not to be #{@encodings.join(' or ')}"] + end +end + +String.__send__(:define_method, :be_locale_env) do |expected = 'locale'| + locale_env_matcher.new(expected) +end diff --git a/spec/ruby/core/env/store_spec.rb b/spec/ruby/core/env/store_spec.rb new file mode 100644 index 0000000000..b4700e0a96 --- /dev/null +++ b/spec/ruby/core/env/store_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/store' + +describe "ENV.store" do + it_behaves_like :env_store, :store +end diff --git a/spec/ruby/core/env/to_a_spec.rb b/spec/ruby/core/env/to_a_spec.rb new file mode 100644 index 0000000000..2b1649281f --- /dev/null +++ b/spec/ruby/core/env/to_a_spec.rb @@ -0,0 +1,21 @@ +require_relative 'spec_helper' + +describe "ENV.to_a" do + + it "returns the ENV as an array" do + a = ENV.to_a + a.is_a?(Array).should == true + a.size.should == ENV.size + a.each { |k,v| ENV[k].should == v } + + a.first.should.is_a?(Array) + a.first.size.should == 2 + end + + it "returns the entries in the locale encoding" do + ENV.to_a.each do |key, value| + key.should.be_locale_env + value.should.be_locale_env + end + end +end diff --git a/spec/ruby/core/env/to_h_spec.rb b/spec/ruby/core/env/to_h_spec.rb new file mode 100644 index 0000000000..58ea2d2030 --- /dev/null +++ b/spec/ruby/core/env/to_h_spec.rb @@ -0,0 +1,70 @@ +require_relative 'spec_helper' +require_relative 'shared/to_hash' + +describe "ENV.to_h" do + it_behaves_like :env_to_hash, :to_h + + context "with block" do + before do + @orig_hash = ENV.to_hash + end + + after do + ENV.replace @orig_hash + end + + it "converts [key, value] pairs returned by the block to a hash" do + ENV.replace("a" => "b", "c" => "d") + ENV.to_h { |k, v| [k, v.upcase] }.should == { 'a' => "B", 'c' => "D" } + end + + it "passes to a block each pair's key and value as separate arguments" do + ENV.replace("a" => "b", "c" => "d") + + ScratchPad.record [] + ENV.to_h { |k, v| ScratchPad << [k, v]; [k, v] } + ScratchPad.recorded.sort.should == [["a", "b"], ["c", "d"]] + + ScratchPad.record [] + ENV.to_h { |*args| ScratchPad << args; [args[0], args[1]] } + ScratchPad.recorded.sort.should == [["a", "b"], ["c", "d"]] + end + + it "does not require the array elements to be strings" do + ENV.replace("a" => "b", "c" => "d") + ENV.to_h { |k, v| [k.to_sym, v.to_sym] }.should == { :a => :b, :c => :d } + end + + it "raises ArgumentError if block returns longer or shorter array" do + -> do + ENV.to_h { |k, v| [k, v.upcase, 1] } + end.should raise_error(ArgumentError, /element has wrong array length/) + + -> do + ENV.to_h { |k, v| [k] } + end.should raise_error(ArgumentError, /element has wrong array length/) + end + + it "raises TypeError if block returns something other than Array" do + -> do + ENV.to_h { |k, v| "not-array" } + end.should raise_error(TypeError, /wrong element type String/) + end + + it "coerces returned pair to Array with #to_ary" do + x = mock('x') + x.stub!(:to_ary).and_return([:b, 'b']) + + ENV.to_h { |k| x }.should == { :b => 'b' } + end + + it "does not coerce returned pair to Array with #to_a" do + x = mock('x') + x.stub!(:to_a).and_return([:b, 'b']) + + -> do + ENV.to_h { |k| x } + end.should raise_error(TypeError, /wrong element type MockObject/) + end + end +end diff --git a/spec/ruby/core/env/to_hash_spec.rb b/spec/ruby/core/env/to_hash_spec.rb new file mode 100644 index 0000000000..306572c353 --- /dev/null +++ b/spec/ruby/core/env/to_hash_spec.rb @@ -0,0 +1,6 @@ +require_relative 'spec_helper' +require_relative 'shared/to_hash' + +describe "ENV.to_hash" do + it_behaves_like :env_to_hash, :to_hash +end diff --git a/spec/ruby/core/env/to_s_spec.rb b/spec/ruby/core/env/to_s_spec.rb new file mode 100644 index 0000000000..0bd92cf217 --- /dev/null +++ b/spec/ruby/core/env/to_s_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' + +describe "ENV.to_s" do + it "returns \"ENV\"" do + ENV.to_s.should == "ENV" + end +end diff --git a/spec/ruby/core/env/update_spec.rb b/spec/ruby/core/env/update_spec.rb new file mode 100644 index 0000000000..95a8a2eb49 --- /dev/null +++ b/spec/ruby/core/env/update_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/update' + +describe "ENV.update" do + it_behaves_like :env_update, :update +end diff --git a/spec/ruby/core/env/value_spec.rb b/spec/ruby/core/env/value_spec.rb new file mode 100644 index 0000000000..906e86ab39 --- /dev/null +++ b/spec/ruby/core/env/value_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/value' + +describe "ENV.value?" do + it_behaves_like :env_value, :value? +end diff --git a/spec/ruby/core/env/values_at_spec.rb b/spec/ruby/core/env/values_at_spec.rb new file mode 100644 index 0000000000..338680e820 --- /dev/null +++ b/spec/ruby/core/env/values_at_spec.rb @@ -0,0 +1,38 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' + +describe "ENV.values_at" do + before :each do + @saved_foo = ENV["foo"] + @saved_bar = ENV["bar"] + end + + after :each do + ENV["foo"] = @saved_foo + ENV["bar"] = @saved_bar + end + + it "returns an array of the values corresponding to the given keys" do + ENV["foo"] = "oof" + ENV["bar"] = "rab" + ENV.values_at("bar", "foo").should == ["rab", "oof"] + end + + it "returns an empty array if no keys specified" do + ENV.values_at.should == [] + end + + it "returns nil for each key that is not a name" do + ENV["foo"] = "oof" + ENV["bar"] = "rab" + ENV.values_at("x", "bar", "y", "foo", "z").should == [nil, "rab", nil, "oof", nil] + end + + it "uses the locale encoding" do + ENV.values_at(ENV.keys.first).first.encoding.should == ENVSpecs.encoding + end + + it "raises TypeError when a key is not coercible to String" do + -> { ENV.values_at("foo", Object.new) }.should raise_error(TypeError, "no implicit conversion of Object into String") + end +end diff --git a/spec/ruby/core/env/values_spec.rb b/spec/ruby/core/env/values_spec.rb new file mode 100644 index 0000000000..71bc877d31 --- /dev/null +++ b/spec/ruby/core/env/values_spec.rb @@ -0,0 +1,14 @@ +require_relative 'spec_helper' + +describe "ENV.values" do + + it "returns an array of the values" do + ENV.values.should == ENV.to_hash.values + end + + it "uses the locale encoding" do + ENV.values.each do |value| + value.should.be_locale_env + end + end +end diff --git a/spec/ruby/core/exception/backtrace_locations_spec.rb b/spec/ruby/core/exception/backtrace_locations_spec.rb new file mode 100644 index 0000000000..86eb9d3413 --- /dev/null +++ b/spec/ruby/core/exception/backtrace_locations_spec.rb @@ -0,0 +1,39 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' + +describe "Exception#backtrace_locations" do + before :each do + @backtrace = ExceptionSpecs::Backtrace.backtrace_locations + end + + it "returns nil if no backtrace was set" do + Exception.new.backtrace_locations.should be_nil + end + + it "returns an Array" do + @backtrace.should be_an_instance_of(Array) + end + + it "sets each element to a Thread::Backtrace::Location" do + @backtrace.each {|l| l.should be_an_instance_of(Thread::Backtrace::Location)} + end + + it "produces a backtrace for an exception captured using $!" do + exception = begin + raise + rescue RuntimeError + $! + end + + exception.backtrace_locations.first.path.should =~ /backtrace_locations_spec/ + end + + it "returns an Array that can be updated" do + begin + raise + rescue RuntimeError => e + e.backtrace_locations.unshift "backtrace first" + e.backtrace_locations[0].should == "backtrace first" + end + end +end diff --git a/spec/ruby/core/exception/backtrace_spec.rb b/spec/ruby/core/exception/backtrace_spec.rb new file mode 100644 index 0000000000..9a65ea3820 --- /dev/null +++ b/spec/ruby/core/exception/backtrace_spec.rb @@ -0,0 +1,106 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' + +describe "Exception#backtrace" do + before :each do + @backtrace = ExceptionSpecs::Backtrace.backtrace + end + + it "returns nil if no backtrace was set" do + Exception.new.backtrace.should be_nil + end + + it "returns an Array" do + @backtrace.should be_an_instance_of(Array) + end + + it "sets each element to a String" do + @backtrace.each {|l| l.should be_an_instance_of(String)} + end + + it "includes the filename of the location where self raised in the first element" do + @backtrace.first.should =~ /common\.rb/ + end + + it "includes the line number of the location where self raised in the first element" do + @backtrace.first.should =~ /:7:in / + end + + it "includes the name of the method from where self raised in the first element" do + @backtrace.first.should =~ /in [`'](?:ExceptionSpecs::Backtrace\.)?backtrace'/ + end + + it "includes the filename of the location immediately prior to where self raised in the second element" do + @backtrace[1].should =~ /backtrace_spec\.rb/ + end + + it "includes the line number of the location immediately prior to where self raised in the second element" do + @backtrace[1].should =~ /:6(:in )?/ + end + + ruby_version_is ""..."3.4" do + it "contains lines of the same format for each prior position in the stack" do + @backtrace[2..-1].each do |line| + # This regexp is deliberately imprecise to account for the need to abstract out + # the paths of the included mspec files and the desire to avoid specifying in any + # detail what the in `...' portion looks like. + line.should =~ /^.+:\d+:in `[^`]+'$/ + end + end + end + + ruby_version_is "3.4" do + it "contains lines of the same format for each prior position in the stack" do + @backtrace[2..-1].each do |line| + # This regexp is deliberately imprecise to account for the need to abstract out + # the paths of the included mspec files and the desire to avoid specifying in any + # detail what the in '...' portion looks like. + line.should =~ /^.+:\d+:in '[^`]+'$/ + end + end + end + + it "captures the backtrace for an exception into $!" do + exception = begin + raise + rescue RuntimeError + $! + end + + exception.backtrace.first.should =~ /backtrace_spec/ + end + + it "captures the backtrace for an exception into $@" do + backtrace = begin + raise + rescue RuntimeError + $@ + end + + backtrace.first.should =~ /backtrace_spec/ + end + + it "returns an Array that can be updated" do + begin + raise + rescue RuntimeError => e + e.backtrace.unshift "backtrace first" + e.backtrace[0].should == "backtrace first" + end + end + + it "returns the same array after duping" do + begin + raise + rescue RuntimeError => err + bt = err.backtrace + err.dup.backtrace.should equal(bt) + + new_bt = ['hi'] + err.set_backtrace new_bt + + err.backtrace.should == new_bt + err.dup.backtrace.should equal(new_bt) + end + end +end diff --git a/spec/ruby/core/exception/case_compare_spec.rb b/spec/ruby/core/exception/case_compare_spec.rb new file mode 100644 index 0000000000..5fd11ae741 --- /dev/null +++ b/spec/ruby/core/exception/case_compare_spec.rb @@ -0,0 +1,37 @@ +require_relative '../../spec_helper' + +describe "SystemCallError.===" do + before :all do + @example_errno_class = Errno::EINVAL + @example_errno = @example_errno_class::Errno + end + + it "returns true for an instance of the same class" do + Errno::EINVAL.should === Errno::EINVAL.new + end + + it "returns true if errnos same" do + e = SystemCallError.new('foo', @example_errno) + @example_errno_class.===(e).should == true + end + + it "returns false if errnos different" do + e = SystemCallError.new('foo', @example_errno + 1) + @example_errno_class.===(e).should == false + end + + it "returns false if arg is not kind of SystemCallError" do + e = Object.new + @example_errno_class.===(e).should == false + end + + it "returns true if receiver is generic and arg is kind of SystemCallError" do + e = SystemCallError.new('foo', @example_errno) + SystemCallError.===(e).should == true + end + + it "returns false if receiver is generic and arg is not kind of SystemCallError" do + e = Object.new + SystemCallError.===(e).should == false + end +end diff --git a/spec/ruby/core/exception/cause_spec.rb b/spec/ruby/core/exception/cause_spec.rb new file mode 100644 index 0000000000..cf4aaeb188 --- /dev/null +++ b/spec/ruby/core/exception/cause_spec.rb @@ -0,0 +1,56 @@ +require_relative '../../spec_helper' + +describe "Exception#cause" do + it "returns the active exception when an exception is raised" do + begin + raise Exception, "the cause" + rescue Exception + begin + raise RuntimeError, "the consequence" + rescue RuntimeError => e + e.should be_an_instance_of(RuntimeError) + e.message.should == "the consequence" + + e.cause.should be_an_instance_of(Exception) + e.cause.message.should == "the cause" + end + end + end + + it "is set for user errors caused by internal errors" do + -> { + begin + 1 / 0 + rescue + raise "foo" + end + }.should raise_error(RuntimeError) { |e| + e.cause.should be_kind_of(ZeroDivisionError) + } + end + + it "is set for internal errors caused by user errors" do + cause = RuntimeError.new "cause" + -> { + begin + raise cause + rescue + 1 / 0 + end + }.should raise_error(ZeroDivisionError) { |e| + e.cause.should equal(cause) + } + end + + it "is not set to the exception itself when it is re-raised" do + -> { + begin + raise RuntimeError + rescue RuntimeError => e + raise e + end + }.should raise_error(RuntimeError) { |e| + e.cause.should == nil + } + end +end diff --git a/spec/ruby/core/exception/detailed_message_spec.rb b/spec/ruby/core/exception/detailed_message_spec.rb new file mode 100644 index 0000000000..9df164a1cf --- /dev/null +++ b/spec/ruby/core/exception/detailed_message_spec.rb @@ -0,0 +1,50 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' + +describe "Exception#detailed_message" do + it "returns decorated message" do + RuntimeError.new("new error").detailed_message.should == "new error (RuntimeError)" + end + + it "is called by #full_message to allow message customization" do + exception = Exception.new("new error") + def exception.detailed_message(**) + "<prefix>#{message}<suffix>" + end + exception.full_message(highlight: false).should.include? "<prefix>new error<suffix>" + end + + it "returns just a message if exception class is anonymous" do + Class.new(RuntimeError).new("message").detailed_message.should == "message" + end + + it "returns 'unhandled exception' for an instance of RuntimeError with empty message" do + RuntimeError.new("").detailed_message.should == "unhandled exception" + end + + it "returns just class name for an instance other than RuntimeError with empty message" do + DetailedMessageSpec::C.new("").detailed_message.should == "DetailedMessageSpec::C" + StandardError.new("").detailed_message.should == "StandardError" + end + + it "returns a generated class name for an instance of RuntimeError anonymous subclass with empty message" do + klass = Class.new(RuntimeError) + klass.new("").detailed_message.should =~ /\A#<Class:0x\h+>\z/ + end + + it "accepts highlight keyword argument and adds escape control sequences" do + RuntimeError.new("new error").detailed_message(highlight: true).should == "\e[1mnew error (\e[1;4mRuntimeError\e[m\e[1m)\e[m" + end + + it "accepts highlight keyword argument and adds escape control sequences for an instance of RuntimeError with empty message" do + RuntimeError.new("").detailed_message(highlight: true).should == "\e[1;4munhandled exception\e[m" + end + + it "accepts highlight keyword argument and adds escape control sequences for an instance other than RuntimeError with empty message" do + StandardError.new("").detailed_message(highlight: true).should == "\e[1;4mStandardError\e[m" + end + + it "allows and ignores other keyword arguments" do + RuntimeError.new("new error").detailed_message(foo: true).should == "new error (RuntimeError)" + end +end diff --git a/spec/ruby/core/exception/dup_spec.rb b/spec/ruby/core/exception/dup_spec.rb new file mode 100644 index 0000000000..edd54bfb37 --- /dev/null +++ b/spec/ruby/core/exception/dup_spec.rb @@ -0,0 +1,74 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' + +describe "Exception#dup" do + before :each do + @obj = ExceptionSpecs::InitializeException.new("my exception") + end + + it "calls #initialize_copy on the new instance" do + dup = @obj.dup + ScratchPad.recorded.should_not == @obj.object_id + ScratchPad.recorded.should == dup.object_id + end + + it "copies instance variables" do + dup = @obj.dup + dup.ivar.should == 1 + end + + it "does not copy singleton methods" do + def @obj.special() :the_one end + dup = @obj.dup + -> { dup.special }.should raise_error(NameError) + end + + it "does not copy modules included in the singleton class" do + class << @obj + include ExceptionSpecs::ExceptionModule + end + + dup = @obj.dup + -> { dup.repr }.should raise_error(NameError) + end + + it "does not copy constants defined in the singleton class" do + class << @obj + CLONE = :clone + end + + dup = @obj.dup + -> { class << dup; CLONE; end }.should raise_error(NameError) + end + + it "does copy the message" do + @obj.dup.message.should == @obj.message + end + + it "does copy the backtrace" do + begin + # Explicitly raise so a backtrace is associated with the exception. + # It's tempting to call `set_backtrace` instead, but that complicates + # the test because it might affect other state (e.g., instance variables) + # on some implementations. + raise ExceptionSpecs::InitializeException.new("my exception") + rescue => e + @obj = e + end + + @obj.dup.backtrace.should == @obj.backtrace + end + + it "does copy the cause" do + begin + raise StandardError, "the cause" + rescue StandardError => cause + begin + raise RuntimeError, "the consequence" + rescue RuntimeError => e + e.cause.should equal(cause) + e.dup.cause.should equal(cause) + end + end + end +end diff --git a/spec/ruby/core/exception/equal_value_spec.rb b/spec/ruby/core/exception/equal_value_spec.rb new file mode 100644 index 0000000000..e8f3ce0f8d --- /dev/null +++ b/spec/ruby/core/exception/equal_value_spec.rb @@ -0,0 +1,68 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' + +describe "Exception#==" do + it "returns true if both exceptions are the same object" do + e = ArgumentError.new + e.should == e + end + + it "returns true if one exception is the dup'd copy of the other" do + e = ArgumentError.new + e.should == e.dup + end + + it "returns true if both exceptions have the same class, no message, and no backtrace" do + RuntimeError.new.should == RuntimeError.new + end + + it "returns true if both exceptions have the same class, the same message, and no backtrace" do + TypeError.new("message").should == TypeError.new("message") + end + + it "returns true if both exceptions have the same class, the same message, and the same backtrace" do + one = TypeError.new("message") + one.set_backtrace [__dir__] + two = TypeError.new("message") + two.set_backtrace [__dir__] + one.should == two + end + + it "returns false if the two exceptions inherit from Exception but have different classes" do + one = RuntimeError.new("message") + one.set_backtrace [__dir__] + one.should be_kind_of(Exception) + two = TypeError.new("message") + two.set_backtrace [__dir__] + two.should be_kind_of(Exception) + one.should_not == two + end + + it "returns true if the two objects subclass Exception and have the same message and backtrace" do + one = ExceptionSpecs::UnExceptional.new + two = ExceptionSpecs::UnExceptional.new + one.message.should == two.message + two.backtrace.should == two.backtrace + one.should == two + end + + it "returns false if the argument is not an Exception" do + ArgumentError.new.should_not == String.new + end + + it "returns false if the two exceptions differ only in their backtrace" do + one = RuntimeError.new("message") + one.set_backtrace [__dir__] + two = RuntimeError.new("message") + two.set_backtrace nil + one.should_not == two + end + + it "returns false if the two exceptions differ only in their message" do + one = RuntimeError.new("message") + one.set_backtrace [__dir__] + two = RuntimeError.new("message2") + two.set_backtrace [__dir__] + one.should_not == two + end +end diff --git a/spec/ruby/core/exception/errno_spec.rb b/spec/ruby/core/exception/errno_spec.rb new file mode 100644 index 0000000000..1ab4277700 --- /dev/null +++ b/spec/ruby/core/exception/errno_spec.rb @@ -0,0 +1,69 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' + +describe "Errno::EINVAL.new" do + it "can be called with no arguments" do + exc = Errno::EINVAL.new + exc.should be_an_instance_of(Errno::EINVAL) + exc.errno.should == Errno::EINVAL::Errno + exc.message.should == "Invalid argument" + end + + it "accepts an optional custom message" do + exc = Errno::EINVAL.new('custom message') + exc.should be_an_instance_of(Errno::EINVAL) + exc.errno.should == Errno::EINVAL::Errno + exc.message.should == "Invalid argument - custom message" + end + + it "accepts an optional custom message and location" do + exc = Errno::EINVAL.new('custom message', 'location') + exc.should be_an_instance_of(Errno::EINVAL) + exc.errno.should == Errno::EINVAL::Errno + exc.message.should == "Invalid argument @ location - custom message" + end +end + +describe "Errno::EMFILE" do + it "can be subclassed" do + ExceptionSpecs::EMFILESub = Class.new(Errno::EMFILE) + exc = ExceptionSpecs::EMFILESub.new + exc.should be_an_instance_of(ExceptionSpecs::EMFILESub) + ensure + ExceptionSpecs.send(:remove_const, :EMFILESub) + end +end + +describe "Errno::EAGAIN" do + # From http://jira.codehaus.org/browse/JRUBY-4747 + it "is the same class as Errno::EWOULDBLOCK if they represent the same errno value" do + if Errno::EAGAIN::Errno == Errno::EWOULDBLOCK::Errno + Errno::EAGAIN.should == Errno::EWOULDBLOCK + else + Errno::EAGAIN.should_not == Errno::EWOULDBLOCK + end + end +end + +describe "Errno::ENOTSUP" do + it "is defined" do + Errno.should have_constant(:ENOTSUP) + end + + it "is the same class as Errno::EOPNOTSUPP if they represent the same errno value" do + if Errno::ENOTSUP::Errno == Errno::EOPNOTSUPP::Errno + Errno::ENOTSUP.should == Errno::EOPNOTSUPP + else + Errno::ENOTSUP.should_not == Errno::EOPNOTSUPP + end + end +end + +describe "Errno::ENOENT" do + it "lets subclasses inherit the default error message" do + c = Class.new(Errno::ENOENT) + raise c, "custom message" + rescue => e + e.message.should == "No such file or directory - custom message" + end +end diff --git a/spec/ruby/core/exception/exception_spec.rb b/spec/ruby/core/exception/exception_spec.rb new file mode 100644 index 0000000000..d6f5283bd9 --- /dev/null +++ b/spec/ruby/core/exception/exception_spec.rb @@ -0,0 +1,69 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' +require_relative 'shared/new' + +describe "Exception.exception" do + it_behaves_like :exception_new, :exception +end + +describe "Exception#exception" do + it "returns self when passed no argument" do + e = RuntimeError.new + e.should == e.exception + end + + it "returns self when passed self as an argument" do + e = RuntimeError.new + e.should == e.exception(e) + end + + it "returns an exception of the same class as self with the message given as argument" do + e = RuntimeError.new + e2 = e.exception("message") + e2.should be_an_instance_of(RuntimeError) + e2.message.should == "message" + end + + it "when raised will be rescued as the new exception" do + begin + begin + raised_first = StandardError.new('first') + raise raised_first + rescue => caught_first + raised_second = raised_first.exception('second') + raise raised_second + end + rescue => caught_second + end + + raised_first.should == caught_first + raised_second.should == caught_second + end + + it "captures an exception into $!" do + exception = begin + raise + rescue RuntimeError + $! + end + + exception.class.should == RuntimeError + exception.message.should == "" + exception.backtrace.first.should =~ /exception_spec/ + end + + class CustomArgumentError < StandardError + attr_reader :val + def initialize(val) + @val = val + end + end + + it "returns an exception of the same class as self with the message given as argument, but without reinitializing" do + e = CustomArgumentError.new(:boom) + e2 = e.exception("message") + e2.should be_an_instance_of(CustomArgumentError) + e2.val.should == :boom + e2.message.should == "message" + end +end diff --git a/spec/ruby/core/exception/exit_value_spec.rb b/spec/ruby/core/exception/exit_value_spec.rb new file mode 100644 index 0000000000..99987dd1bc --- /dev/null +++ b/spec/ruby/core/exception/exit_value_spec.rb @@ -0,0 +1,13 @@ +require_relative '../../spec_helper' + +describe "LocalJumpError#exit_value" do + def get_me_a_return + Proc.new { return 42 } + end + + it "returns the value given to return" do + -> { get_me_a_return.call }.should raise_error(LocalJumpError) { |e| + e.exit_value.should == 42 + } + end +end diff --git a/spec/ruby/core/exception/fixtures/common.rb b/spec/ruby/core/exception/fixtures/common.rb new file mode 100644 index 0000000000..3d8a3c3430 --- /dev/null +++ b/spec/ruby/core/exception/fixtures/common.rb @@ -0,0 +1,102 @@ +module ExceptionSpecs + class Exceptional < Exception; end + + class Backtrace + def self.backtrace + begin + raise # If you move this line, update backtrace_spec.rb + rescue RuntimeError => e + e.backtrace + end + end + + def self.backtrace_locations + begin + raise + rescue RuntimeError => e + e.backtrace_locations + end + end + end + + class UnExceptional < Exception + def backtrace + nil + end + def message + nil + end + end + + class ConstructorException < Exception + + def initialize + end + + end + + class OverrideToS < RuntimeError + def to_s + "this is from #to_s" + end + end + + class EmptyToS < RuntimeError + def to_s + "" + end + end + + class InitializeException < StandardError + attr_reader :ivar + + def initialize(message = nil) + super + @ivar = 1 + end + + def initialize_copy(other) + super + ScratchPad.record object_id + end + end + + module ExceptionModule + def repr + 1 + end + end +end + +module NoMethodErrorSpecs + class NoMethodErrorA; end + + class NoMethodErrorB; end + + class NoMethodErrorC; + protected + def a_protected_method;end + private + def a_private_method; end + end + + class NoMethodErrorD; end + + class InstanceException < Exception + end + + class AClass; end + module AModule; end +end + +class NameErrorSpecs + class ReceiverClass + def call_undefined_class_variable + @@doesnt_exist + end + end +end + +module DetailedMessageSpec + C = Class.new(RuntimeError) +end diff --git a/spec/ruby/core/exception/fixtures/syntax_error.rb b/spec/ruby/core/exception/fixtures/syntax_error.rb new file mode 100644 index 0000000000..ccec62f7a1 --- /dev/null +++ b/spec/ruby/core/exception/fixtures/syntax_error.rb @@ -0,0 +1,3 @@ +# rubocop:disable Lint/Syntax +1+1=2 +# rubocop:enable Lint/Syntax diff --git a/spec/ruby/core/exception/fixtures/thread_fiber_ensure.rb b/spec/ruby/core/exception/fixtures/thread_fiber_ensure.rb new file mode 100644 index 0000000000..c109ec6247 --- /dev/null +++ b/spec/ruby/core/exception/fixtures/thread_fiber_ensure.rb @@ -0,0 +1,22 @@ +ready = false +t = Thread.new do + f = Fiber.new do + begin + Fiber.yield + ensure + STDERR.puts "suspended fiber ensure" + end + end + f.resume + + begin + ready = true + sleep + ensure + STDERR.puts "current fiber ensure" + end +end + +Thread.pass until ready && t.stop? + +# let the program end, it's the same as #exit or an exception for this behavior diff --git a/spec/ruby/core/exception/fixtures/thread_fiber_ensure_non_root_fiber.rb b/spec/ruby/core/exception/fixtures/thread_fiber_ensure_non_root_fiber.rb new file mode 100644 index 0000000000..3364ed06d0 --- /dev/null +++ b/spec/ruby/core/exception/fixtures/thread_fiber_ensure_non_root_fiber.rb @@ -0,0 +1,25 @@ +ready = false +t = Thread.new do + f = Fiber.new do + begin + Fiber.yield + ensure + STDERR.puts "suspended fiber ensure" + end + end + f.resume + + f2 = Fiber.new do + begin + ready = true + sleep + ensure + STDERR.puts "current fiber ensure" + end + end + f2.resume +end + +Thread.pass until ready && t.stop? + +# let the program end, it's the same as #exit or an exception for this behavior diff --git a/spec/ruby/core/exception/frozen_error_spec.rb b/spec/ruby/core/exception/frozen_error_spec.rb new file mode 100644 index 0000000000..af2e925661 --- /dev/null +++ b/spec/ruby/core/exception/frozen_error_spec.rb @@ -0,0 +1,54 @@ +require_relative '../../spec_helper' + +describe "FrozenError.new" do + it "should take optional receiver argument" do + o = Object.new + FrozenError.new("msg", receiver: o).receiver.should equal(o) + end +end + +describe "FrozenError#receiver" do + it "should return frozen object that modification was attempted on" do + o = Object.new.freeze + begin + def o.x; end + rescue => e + e.should be_kind_of(FrozenError) + e.receiver.should equal(o) + else + raise + end + end +end + +describe "FrozenError#message" do + it "includes a receiver" do + object = Object.new + object.freeze + + msg_class = ruby_version_is("4.0") ? "Object" : "object" + + -> { + def object.x; end + }.should raise_error(FrozenError, "can't modify frozen #{msg_class}: #{object}") + + object = [].freeze + -> { object << nil }.should raise_error(FrozenError, "can't modify frozen Array: []") + end +end + +describe "Modifying a frozen object" do + context "#inspect is redefined and modifies the object" do + it "returns ... instead of String representation of object" do + object = Object.new + def object.inspect; @a = 1 end + def object.modify; @a = 2 end + + object.freeze + + # CRuby's message contains multiple whitespaces before '...'. + # So handle both multiple and single whitespace. + -> { object.modify }.should raise_error(FrozenError, /can't modify frozen .*?: \s*.../) + end + end +end diff --git a/spec/ruby/core/exception/full_message_spec.rb b/spec/ruby/core/exception/full_message_spec.rb new file mode 100644 index 0000000000..0761d2b40c --- /dev/null +++ b/spec/ruby/core/exception/full_message_spec.rb @@ -0,0 +1,226 @@ +require_relative '../../spec_helper' + +describe "Exception#full_message" do + it "returns formatted string of exception using the same format that is used to print an uncaught exceptions to stderr" do + e = RuntimeError.new("Some runtime error") + e.set_backtrace(["a.rb:1", "b.rb:2"]) + + full_message = e.full_message + full_message.should include "RuntimeError" + full_message.should include "Some runtime error" + full_message.should include "a.rb:1" + full_message.should include "b.rb:2" + end + + it "supports :highlight option and adds escape sequences to highlight some strings" do + e = RuntimeError.new("Some runtime error") + + full_message = e.full_message(highlight: true, order: :top).lines + full_message[0].should.end_with? "\e[1mSome runtime error (\e[1;4mRuntimeError\e[m\e[1m)\e[m\n" + + full_message = e.full_message(highlight: true, order: :bottom).lines + full_message[0].should == "\e[1mTraceback\e[m (most recent call last):\n" + full_message[-1].should.end_with? "\e[1mSome runtime error (\e[1;4mRuntimeError\e[m\e[1m)\e[m\n" + + full_message = e.full_message(highlight: false, order: :top).lines + full_message[0].should.end_with? "Some runtime error (RuntimeError)\n" + + full_message = e.full_message(highlight: false, order: :bottom).lines + full_message[0].should == "Traceback (most recent call last):\n" + full_message[-1].should.end_with? "Some runtime error (RuntimeError)\n" + end + + it "supports :order option and places the error message and the backtrace at the top or the bottom" do + e = RuntimeError.new("Some runtime error") + e.set_backtrace(["a.rb:1", "b.rb:2"]) + + e.full_message(order: :top, highlight: false).should =~ /a.rb:1.*b.rb:2/m + e.full_message(order: :bottom, highlight: false).should =~ /b.rb:2.*a.rb:1/m + end + + it "shows the caller if the exception has no backtrace" do + e = RuntimeError.new("Some runtime error") + e.backtrace.should == nil + full_message = e.full_message(highlight: false, order: :top).lines + full_message[0].should.start_with?("#{__FILE__}:#{__LINE__-1}:in ") + full_message[0].should.end_with?("': Some runtime error (RuntimeError)\n") + end + + describe "includes details about whether an exception was handled" do + describe "RuntimeError" do + it "should report as unhandled if message is empty" do + err = RuntimeError.new("") + + err.full_message.should =~ /unhandled exception/ + err.full_message(highlight: true).should =~ /unhandled exception/ + err.full_message(highlight: false).should =~ /unhandled exception/ + end + + it "should not report as unhandled if the message is not empty" do + err = RuntimeError.new("non-empty") + + err.full_message.should !~ /unhandled exception/ + err.full_message(highlight: true).should !~ /unhandled exception/ + err.full_message(highlight: false).should !~ /unhandled exception/ + end + + it "should not report as unhandled if the message is nil" do + err = RuntimeError.new(nil) + + err.full_message.should !~ /unhandled exception/ + err.full_message(highlight: true).should !~ /unhandled exception/ + err.full_message(highlight: false).should !~ /unhandled exception/ + end + + it "should not report as unhandled if the message is not specified" do + err = RuntimeError.new() + + err.full_message.should !~ /unhandled exception/ + err.full_message(highlight: true).should !~ /unhandled exception/ + err.full_message(highlight: false).should !~ /unhandled exception/ + end + + it "adds escape sequences to highlight some strings if the message is not specified and :highlight option is specified" do + e = RuntimeError.new("") + + full_message = e.full_message(highlight: true, order: :top).lines + full_message[0].should.end_with? "\e[1;4munhandled exception\e[m\n" + + full_message = e.full_message(highlight: true, order: :bottom).lines + full_message[0].should == "\e[1mTraceback\e[m (most recent call last):\n" + full_message[-1].should.end_with? "\e[1;4munhandled exception\e[m\n" + + full_message = e.full_message(highlight: false, order: :top).lines + full_message[0].should.end_with? "unhandled exception\n" + + full_message = e.full_message(highlight: false, order: :bottom).lines + full_message[0].should == "Traceback (most recent call last):\n" + full_message[-1].should.end_with? "unhandled exception\n" + end + end + + describe "generic Error" do + it "should not report as unhandled in any event" do + StandardError.new("").full_message.should !~ /unhandled exception/ + StandardError.new("non-empty").full_message.should !~ /unhandled exception/ + end + end + end + + it "shows the exception class at the end of the first line of the message when the message contains multiple lines" do + begin + line = __LINE__; raise "first line\nsecond line" + rescue => e + full_message = e.full_message(highlight: false, order: :top).lines + full_message[0].should.start_with?("#{__FILE__}:#{line}:in ") + full_message[0].should.end_with?(": first line (RuntimeError)\n") + full_message[1].should == "second line\n" + end + end + + it "highlights the entire message when the message contains multiple lines" do + begin + line = __LINE__; raise "first line\nsecond line\nthird line" + rescue => e + full_message = e.full_message(highlight: true, order: :top).lines + full_message[0].should.start_with?("#{__FILE__}:#{line}:in ") + full_message[0].should.end_with?(": \e[1mfirst line (\e[1;4mRuntimeError\e[m\e[1m)\e[m\n") + full_message[1].should == "\e[1msecond line\e[m\n" + full_message[2].should == "\e[1mthird line\e[m\n" + end + end + + it "contains cause of exception" do + begin + begin + raise 'the cause' + rescue + raise 'main exception' + end + rescue => e + exception = e + end + + exception.full_message.should include "main exception" + exception.full_message.should include "the cause" + end + + it 'contains all the chain of exceptions' do + begin + begin + begin + raise 'origin exception' + rescue + raise 'intermediate exception' + end + rescue + raise 'last exception' + end + rescue => e + exception = e + end + + exception.full_message.should include "last exception" + exception.full_message.should include "intermediate exception" + exception.full_message.should include "origin exception" + end + + it "relies on #detailed_message" do + e = RuntimeError.new("new error") + e.define_singleton_method(:detailed_message) { |**| "DETAILED MESSAGE" } + + e.full_message.lines.first.should =~ /DETAILED MESSAGE/ + end + + it "passes all its own keyword arguments (with :highlight default value and without :order default value) to #detailed_message" do + e = RuntimeError.new("new error") + options_passed = nil + e.define_singleton_method(:detailed_message) do |**options| + options_passed = options + "DETAILED MESSAGE" + end + + e.full_message(foo: "bar") + options_passed.should == { foo: "bar", highlight: Exception.to_tty? } + end + + it "converts #detailed_message returned value to String if it isn't a String" do + message = Object.new + def message.to_str; "DETAILED MESSAGE"; end + + e = RuntimeError.new("new error") + e.define_singleton_method(:detailed_message) { |**| message } + + e.full_message.lines.first.should =~ /DETAILED MESSAGE/ + end + + it "uses class name if #detailed_message returns nil" do + e = RuntimeError.new("new error") + e.define_singleton_method(:detailed_message) { |**| nil } + + e.full_message(highlight: false).lines.first.should =~ /RuntimeError/ + e.full_message(highlight: true).lines.first.should =~ /#{Regexp.escape("\e[1;4mRuntimeError\e[m")}/ + end + + it "uses class name if exception object doesn't respond to #detailed_message" do + e = RuntimeError.new("new error") + class << e + undef :detailed_message + end + + e.full_message(highlight: false).lines.first.should =~ /RuntimeError/ + e.full_message(highlight: true).lines.first.should =~ /#{Regexp.escape("\e[1;4mRuntimeError\e[m")}/ + end + + it "allows cause with empty backtrace" do + begin + raise RuntimeError.new("Some runtime error"), cause: RuntimeError.new("Some other runtime error") + rescue => e + end + + full_message = e.full_message + full_message.should include "RuntimeError" + full_message.should include "Some runtime error" + full_message.should include "Some other runtime error" + end +end diff --git a/spec/ruby/core/exception/hierarchy_spec.rb b/spec/ruby/core/exception/hierarchy_spec.rb new file mode 100644 index 0000000000..6514eb1994 --- /dev/null +++ b/spec/ruby/core/exception/hierarchy_spec.rb @@ -0,0 +1,62 @@ +require_relative '../../spec_helper' + +describe "Exception" do + it "has the right class hierarchy" do + hierarchy = { + Exception => { + NoMemoryError => nil, + ScriptError => { + LoadError => nil, + NotImplementedError => nil, + SyntaxError => nil, + }, + SecurityError => nil, + SignalException => { + Interrupt => nil, + }, + StandardError => { + ArgumentError => { + UncaughtThrowError => nil, + }, + EncodingError => nil, + FiberError => nil, + IOError => { + EOFError => nil, + }, + IndexError => { + KeyError => nil, + StopIteration => { + ClosedQueueError => nil, + }, + }, + LocalJumpError => nil, + NameError => { + NoMethodError => nil, + }, + RangeError => { + FloatDomainError => nil, + }, + RegexpError => nil, + RuntimeError => { + FrozenError => nil, + }, + SystemCallError => nil, + ThreadError => nil, + TypeError => nil, + ZeroDivisionError => nil, + }, + SystemExit => nil, + SystemStackError => nil, + }, + } + + traverse = -> parent_class, parent_subclass_hash { + parent_subclass_hash.each do |child_class, child_subclass_hash| + child_class.class.should == Class + child_class.superclass.should == parent_class + traverse.call(child_class, child_subclass_hash) if child_subclass_hash + end + } + traverse.call(Object, hierarchy) + end +end diff --git a/spec/ruby/core/exception/inspect_spec.rb b/spec/ruby/core/exception/inspect_spec.rb new file mode 100644 index 0000000000..6f380a36c7 --- /dev/null +++ b/spec/ruby/core/exception/inspect_spec.rb @@ -0,0 +1,24 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' + +describe "Exception#inspect" do + it "returns '#<Exception: Exception>' when no message given" do + Exception.new.inspect.should == "#<Exception: Exception>" + end + + it "keeps message encoding" do + Exception.new('Ã¥').inspect.should == "#<Exception: Ã¥>" + end + + it "includes #to_s when the result is non-empty" do + ExceptionSpecs::OverrideToS.new.inspect.should == "#<ExceptionSpecs::OverrideToS: this is from #to_s>" + end + + it "returns the class name when #to_s returns an empty string" do + ExceptionSpecs::EmptyToS.new.inspect.should == "ExceptionSpecs::EmptyToS" + end + + it "returns the derived class name with a subclassed Exception" do + ExceptionSpecs::UnExceptional.new.inspect.should == "#<ExceptionSpecs::UnExceptional: ExceptionSpecs::UnExceptional>" + end +end diff --git a/spec/ruby/core/exception/interrupt_spec.rb b/spec/ruby/core/exception/interrupt_spec.rb new file mode 100644 index 0000000000..90d261e470 --- /dev/null +++ b/spec/ruby/core/exception/interrupt_spec.rb @@ -0,0 +1,60 @@ +require_relative '../../spec_helper' + +describe "Interrupt.new" do + it "returns an instance of interrupt with no message given" do + e = Interrupt.new + e.signo.should == Signal.list["INT"] + e.signm.should == "Interrupt" + end + + it "takes an optional message argument" do + e = Interrupt.new("message") + e.signo.should == Signal.list["INT"] + e.signm.should == "message" + end +end + +describe "rescuing Interrupt" do + before do + @original_sigint_proc = Signal.trap(:INT, :SIG_DFL) + end + + after do + Signal.trap(:INT, @original_sigint_proc) + end + + it "raises an Interrupt when sent a signal SIGINT" do + begin + Process.kill :INT, Process.pid + sleep + rescue Interrupt => e + e.signo.should == Signal.list["INT"] + ["", "Interrupt"].should.include?(e.message) + end + end +end + +describe "Interrupt" do + # This spec is basically the same as above, + # but it does not rely on Signal.trap(:INT, :SIG_DFL) which can be tricky + it "is raised on the main Thread by the default SIGINT handler" do + out = ruby_exe(<<-'RUBY', args: "2>&1") + begin + Process.kill :INT, Process.pid + sleep + rescue Interrupt => e + puts "Interrupt: #{e.signo}" + end + RUBY + out.should == "Interrupt: #{Signal.list["INT"]}\n" + end + + platform_is_not :windows do + it "shows the backtrace and has a signaled exit status" do + err = IO.popen([*ruby_exe, '-e', 'Process.kill :INT, Process.pid; sleep'], err: [:child, :out], &:read) + $?.termsig.should == Signal.list.fetch('INT') + err.should.include? ': Interrupt' + err.should =~ /from -e:1:in [`']<main>'/ + end + end +end diff --git a/spec/ruby/core/exception/io_error_spec.rb b/spec/ruby/core/exception/io_error_spec.rb new file mode 100644 index 0000000000..ab8a72518f --- /dev/null +++ b/spec/ruby/core/exception/io_error_spec.rb @@ -0,0 +1,45 @@ +require_relative '../../spec_helper' + +describe "IO::EAGAINWaitReadable" do + it "combines Errno::EAGAIN and IO::WaitReadable" do + IO::EAGAINWaitReadable.superclass.should == Errno::EAGAIN + IO::EAGAINWaitReadable.ancestors.should include IO::WaitReadable + end + + it "is the same as IO::EWOULDBLOCKWaitReadable if Errno::EAGAIN is the same as Errno::EWOULDBLOCK" do + if Errno::EAGAIN.equal? Errno::EWOULDBLOCK + IO::EAGAINWaitReadable.should equal IO::EWOULDBLOCKWaitReadable + else + IO::EAGAINWaitReadable.should_not equal IO::EWOULDBLOCKWaitReadable + end + end +end + +describe "IO::EWOULDBLOCKWaitReadable" do + it "combines Errno::EWOULDBLOCK and IO::WaitReadable" do + IO::EWOULDBLOCKWaitReadable.superclass.should == Errno::EWOULDBLOCK + IO::EAGAINWaitReadable.ancestors.should include IO::WaitReadable + end +end + +describe "IO::EAGAINWaitWritable" do + it "combines Errno::EAGAIN and IO::WaitWritable" do + IO::EAGAINWaitWritable.superclass.should == Errno::EAGAIN + IO::EAGAINWaitWritable.ancestors.should include IO::WaitWritable + end + + it "is the same as IO::EWOULDBLOCKWaitWritable if Errno::EAGAIN is the same as Errno::EWOULDBLOCK" do + if Errno::EAGAIN.equal? Errno::EWOULDBLOCK + IO::EAGAINWaitWritable.should equal IO::EWOULDBLOCKWaitWritable + else + IO::EAGAINWaitWritable.should_not equal IO::EWOULDBLOCKWaitWritable + end + end +end + +describe "IO::EWOULDBLOCKWaitWritable" do + it "combines Errno::EWOULDBLOCK and IO::WaitWritable" do + IO::EWOULDBLOCKWaitWritable.superclass.should == Errno::EWOULDBLOCK + IO::EAGAINWaitWritable.ancestors.should include IO::WaitWritable + end +end diff --git a/spec/ruby/core/exception/key_error_spec.rb b/spec/ruby/core/exception/key_error_spec.rb new file mode 100644 index 0000000000..c5e2b1efbc --- /dev/null +++ b/spec/ruby/core/exception/key_error_spec.rb @@ -0,0 +1,19 @@ +require_relative '../../spec_helper' + +describe "KeyError" do + it "accepts :receiver and :key options" do + receiver = mock("receiver") + key = mock("key") + + error = KeyError.new(receiver: receiver, key: key) + + error.receiver.should == receiver + error.key.should == key + + error = KeyError.new("message", receiver: receiver, key: key) + + error.message.should == "message" + error.receiver.should == receiver + error.key.should == key + end +end diff --git a/spec/ruby/core/exception/load_error_spec.rb b/spec/ruby/core/exception/load_error_spec.rb new file mode 100644 index 0000000000..0056403e58 --- /dev/null +++ b/spec/ruby/core/exception/load_error_spec.rb @@ -0,0 +1,21 @@ +require_relative '../../spec_helper' + +describe "LoadError#path" do + before :each do + @le = LoadError.new + end + + it "is nil when constructed directly" do + @le.path.should == nil + end +end + +describe "LoadError raised by load or require" do + it "provides the failing path in its #path attribute" do + begin + require 'file_that_does_not_exist' + rescue LoadError => le + le.path.should == 'file_that_does_not_exist' + end + end +end diff --git a/spec/ruby/core/exception/message_spec.rb b/spec/ruby/core/exception/message_spec.rb new file mode 100644 index 0000000000..8d7476126e --- /dev/null +++ b/spec/ruby/core/exception/message_spec.rb @@ -0,0 +1,27 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' + +describe "Exception#message" do + it "returns the class name if there is no message" do + Exception.new.message.should == "Exception" + end + + it "returns the message passed to #initialize" do + Exception.new("Ouch!").message.should == "Ouch!" + end + + it "calls #to_s on self" do + exc = ExceptionSpecs::OverrideToS.new("you won't see this") + exc.message.should == "this is from #to_s" + end + + context "when #backtrace is redefined" do + it "returns the Exception message" do + e = Exception.new + e.message.should == 'Exception' + + def e.backtrace; []; end + e.message.should == 'Exception' + end + end +end diff --git a/spec/ruby/core/exception/name_error_spec.rb b/spec/ruby/core/exception/name_error_spec.rb new file mode 100644 index 0000000000..ddd51a92e5 --- /dev/null +++ b/spec/ruby/core/exception/name_error_spec.rb @@ -0,0 +1,28 @@ +require_relative '../../spec_helper' + +describe "NameError.new" do + it "should take optional name argument" do + NameError.new("msg","name").name.should == "name" + end + + it "accepts a :receiver keyword argument" do + receiver = mock("receiver") + + error = NameError.new("msg", :name, receiver: receiver) + + error.receiver.should == receiver + error.name.should == :name + end +end + +describe "NameError#dup" do + it "copies the name and receiver" do + begin + foo + rescue NameError => ne + name_error_dup = ne.dup + name_error_dup.name.should == :foo + name_error_dup.receiver.should == self + end + end +end diff --git a/spec/ruby/core/exception/name_spec.rb b/spec/ruby/core/exception/name_spec.rb new file mode 100644 index 0000000000..c8a49b40e2 --- /dev/null +++ b/spec/ruby/core/exception/name_spec.rb @@ -0,0 +1,43 @@ +require_relative '../../spec_helper' + +describe "NameError#name" do + it "returns a method name as a symbol" do + -> { + doesnt_exist + }.should raise_error(NameError) {|e| e.name.should == :doesnt_exist } + end + + it "returns a constant name as a symbol" do + -> { + DoesntExist + }.should raise_error(NameError) {|e| e.name.should == :DoesntExist } + end + + it "returns a constant name without namespace as a symbol" do + -> { + Object::DoesntExist + }.should raise_error(NameError) {|e| e.name.should == :DoesntExist } + end + + it "returns a class variable name as a symbol" do + -> { + eval("class singleton_class::A; @@doesnt_exist end", binding, __FILE__, __LINE__) + }.should raise_error(NameError) { |e| e.name.should == :@@doesnt_exist } + end + + it "returns the first argument passed to the method when a NameError is raised from #instance_variable_get" do + invalid_ivar_name = "invalid_ivar_name" + + -> { + Object.new.instance_variable_get(invalid_ivar_name) + }.should raise_error(NameError) {|e| e.name.should equal(invalid_ivar_name) } + end + + it "returns the first argument passed to the method when a NameError is raised from #class_variable_get" do + invalid_cvar_name = "invalid_cvar_name" + + -> { + Object.class_variable_get(invalid_cvar_name) + }.should raise_error(NameError) {|e| e.name.should equal(invalid_cvar_name) } + end +end diff --git a/spec/ruby/core/exception/new_spec.rb b/spec/ruby/core/exception/new_spec.rb new file mode 100644 index 0000000000..100dbb0a24 --- /dev/null +++ b/spec/ruby/core/exception/new_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' +require_relative 'shared/new' + +describe "Exception.new" do + it_behaves_like :exception_new, :new +end diff --git a/spec/ruby/core/exception/no_method_error_spec.rb b/spec/ruby/core/exception/no_method_error_spec.rb new file mode 100644 index 0000000000..772c569f67 --- /dev/null +++ b/spec/ruby/core/exception/no_method_error_spec.rb @@ -0,0 +1,283 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' + +describe "NoMethodError.new" do + it "allows passing method args" do + NoMethodError.new("msg", "name", ["args"]).args.should == ["args"] + end + + it "does not require a name" do + NoMethodError.new("msg").message.should == "msg" + end + + it "accepts a :receiver keyword argument" do + receiver = mock("receiver") + + error = NoMethodError.new("msg", :name, receiver: receiver) + + error.receiver.should == receiver + error.name.should == :name + end +end + +describe "NoMethodError#args" do + it "returns an empty array if the caller method had no arguments" do + begin + NoMethodErrorSpecs::NoMethodErrorB.new.foo + rescue Exception => e + e.args.should == [] + end + end + + it "returns an array with the same elements as passed to the method" do + begin + a = NoMethodErrorSpecs::NoMethodErrorA.new + NoMethodErrorSpecs::NoMethodErrorB.new.foo(1,a) + rescue Exception => e + e.args.should == [1,a] + e.args[1].should equal a + end + end +end + +describe "NoMethodError#message" do + it "for an undefined method match /undefined method/" do + begin + NoMethodErrorSpecs::NoMethodErrorD.new.foo + rescue Exception => e + e.should be_kind_of(NoMethodError) + end + end + + it "for an protected method match /protected method/" do + begin + NoMethodErrorSpecs::NoMethodErrorC.new.a_protected_method + rescue Exception => e + e.should be_kind_of(NoMethodError) + end + end + + it "for private method match /private method/" do + begin + NoMethodErrorSpecs::NoMethodErrorC.new.a_private_method + rescue Exception => e + e.should be_kind_of(NoMethodError) + e.message.lines[0].should =~ /private method [`']a_private_method' called for / + end + end + + ruby_version_is ""..."3.3" do + it "calls #inspect when calling Exception#message" do + ScratchPad.record [] + test_class = Class.new do + def inspect + ScratchPad << :inspect_called + "<inspect>" + end + end + instance = test_class.new + + begin + instance.bar + rescue NoMethodError => error + error.message.should =~ /\Aundefined method [`']bar' for <inspect>:#<Class:0x\h+>$/ + ScratchPad.recorded.should == [:inspect_called] + end + end + + it "fallbacks to a simpler representation of the receiver when receiver.inspect raises an exception" do + test_class = Class.new do + def inspect + raise NoMethodErrorSpecs::InstanceException + end + end + instance = test_class.new + + begin + instance.bar + rescue NoMethodError => error + message = error.message + message.should =~ /undefined method.+\bbar\b/ + message.should include test_class.inspect + end + end + + it "uses #name to display the receiver if it is a class" do + klass = Class.new { def self.name; "MyClass"; end } + + begin + klass.foo + rescue NoMethodError => error + error.message.should =~ /\Aundefined method [`']foo' for MyClass:Class$/ + end + end + + it "uses #name to display the receiver if it is a module" do + mod = Module.new { def self.name; "MyModule"; end } + + begin + mod.foo + rescue NoMethodError => error + error.message.should =~ /\Aundefined method [`']foo' for MyModule:Module$/ + end + end + end + + ruby_version_is "3.3" do + it "uses a literal name when receiver is nil" do + begin + nil.foo + rescue NoMethodError => error + error.message.should =~ /\Aundefined method [`']foo' for nil\Z/ + end + end + + it "uses a literal name when receiver is true" do + begin + true.foo + rescue NoMethodError => error + error.message.should =~ /\Aundefined method [`']foo' for true\Z/ + end + end + + it "uses a literal name when receiver is false" do + begin + false.foo + rescue NoMethodError => error + error.message.should =~ /\Aundefined method [`']foo' for false\Z/ + end + end + + it "uses #name when receiver is a class" do + klass = Class.new { def self.name; "MyClass"; end } + + begin + klass.foo + rescue NoMethodError => error + error.message.should =~ /\Aundefined method [`']foo' for class MyClass\Z/ + end + end + + it "uses class' string representation when receiver is an anonymous class" do + klass = Class.new + + begin + klass.foo + rescue NoMethodError => error + error.message.should =~ /\Aundefined method [`']foo' for class #<Class:0x\h+>\Z/ + end + end + + it "uses class' string representation when receiver is a singleton class" do + obj = Object.new + singleton_class = obj.singleton_class + + begin + singleton_class.foo + rescue NoMethodError => error + error.message.should =~ /\Aundefined method [`']foo' for class #<Class:#<Object:0x\h+>>\Z/ + end + end + + it "uses #name when receiver is a module" do + mod = Module.new { def self.name; "MyModule"; end } + + begin + mod.foo + rescue NoMethodError => error + error.message.should =~ /\Aundefined method [`']foo' for module MyModule\Z/ + end + end + + it "uses module's string representation when receiver is an anonymous module" do + m = Module.new + + begin + m.foo + rescue NoMethodError => error + error.message.should =~ /\Aundefined method [`']foo' for module #<Module:0x\h+>\Z/ + end + end + + it "uses class #name when receiver is an ordinary object" do + klass = Class.new { def self.name; "MyClass"; end } + instance = klass.new + + begin + instance.bar + rescue NoMethodError => error + error.message.should =~ /\Aundefined method [`']bar' for an instance of MyClass\Z/ + end + end + + it "uses class string representation when receiver is an instance of anonymous class" do + klass = Class.new + instance = klass.new + + begin + instance.bar + rescue NoMethodError => error + error.message.should =~ /\Aundefined method [`']bar' for an instance of #<Class:0x\h+>\Z/ + end + end + + it "uses class name when receiver has a singleton class" do + instance = NoMethodErrorSpecs::NoMethodErrorA.new + def instance.foo; end + + begin + instance.bar + rescue NoMethodError => error + error.message.should =~ /\Aundefined method [`']bar' for #<NoMethodErrorSpecs::NoMethodErrorA:0x\h+>\Z/ + end + end + + it "does not call #inspect when calling Exception#message" do + ScratchPad.record [] + test_class = Class.new do + def inspect + ScratchPad << :inspect_called + "<inspect>" + end + end + instance = test_class.new + + begin + instance.bar + rescue NoMethodError => error + error.message.should =~ /\Aundefined method [`']bar' for an instance of #<Class:0x\h+>\Z/ + ScratchPad.recorded.should == [] + end + end + + it "does not truncate long class names" do + class_name = 'ExceptionSpecs::A' + 'a'*100 + + begin + eval <<~RUBY + class #{class_name} + end + + obj = #{class_name}.new + obj.foo + RUBY + rescue NoMethodError => error + error.message.should =~ /\Aundefined method [`']foo' for an instance of #{class_name}\Z/ + end + end + end +end + +describe "NoMethodError#dup" do + it "copies the name, arguments and receiver" do + begin + receiver = Object.new + receiver.foo(:one, :two) + rescue NoMethodError => nme + no_method_error_dup = nme.dup + no_method_error_dup.name.should == :foo + no_method_error_dup.receiver.should == receiver + no_method_error_dup.args.should == [:one, :two] + end + end +end diff --git a/spec/ruby/core/exception/reason_spec.rb b/spec/ruby/core/exception/reason_spec.rb new file mode 100644 index 0000000000..210bbc9725 --- /dev/null +++ b/spec/ruby/core/exception/reason_spec.rb @@ -0,0 +1,13 @@ +require_relative '../../spec_helper' + +describe "LocalJumpError#reason" do + def get_me_a_return + Proc.new { return 42 } + end + + it "returns 'return' for a return" do + -> { get_me_a_return.call }.should raise_error(LocalJumpError) { |e| + e.reason.should == :return + } + end +end diff --git a/spec/ruby/core/exception/receiver_spec.rb b/spec/ruby/core/exception/receiver_spec.rb new file mode 100644 index 0000000000..d1c23b67be --- /dev/null +++ b/spec/ruby/core/exception/receiver_spec.rb @@ -0,0 +1,58 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' + +describe "NameError#receiver" do + class ::ReceiverClass + def call_undefined_class_variable; @@doesnt_exist end + end + + it "returns the object that raised the exception" do + receiver = Object.new + + -> { + receiver.doesnt_exist + }.should raise_error(NameError) {|e| e.receiver.should equal(receiver) } + end + + it "returns the Object class when an undefined constant is called without namespace" do + -> { + DoesntExist + }.should raise_error(NameError) {|e| e.receiver.should equal(Object) } + end + + it "returns a class when an undefined constant is called" do + -> { + NameErrorSpecs::ReceiverClass::DoesntExist + }.should raise_error(NameError) {|e| e.receiver.should equal(NameErrorSpecs::ReceiverClass) } + end + + it "returns the Object class when an undefined class variable is called" do + -> { + eval("class singleton_class::A; @@doesnt_exist end", binding, __FILE__, __LINE__) + }.should raise_error(NameError) {|e| e.receiver.should equal(singleton_class::A) } + end + + it "returns a class when an undefined class variable is called in a subclass' namespace" do + -> { + NameErrorSpecs::ReceiverClass.new.call_undefined_class_variable + }.should raise_error(NameError) {|e| e.receiver.should equal(NameErrorSpecs::ReceiverClass) } + end + + it "returns the receiver when raised from #instance_variable_get" do + receiver = Object.new + + -> { + receiver.instance_variable_get("invalid_ivar_name") + }.should raise_error(NameError) {|e| e.receiver.should equal(receiver) } + end + + it "returns the receiver when raised from #class_variable_get" do + -> { + Object.class_variable_get("invalid_cvar_name") + }.should raise_error(NameError) {|e| e.receiver.should equal(Object) } + end + + it "raises an ArgumentError when the receiver is none" do + -> { NameError.new.receiver }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/exception/result_spec.rb b/spec/ruby/core/exception/result_spec.rb new file mode 100644 index 0000000000..d42fcdffcb --- /dev/null +++ b/spec/ruby/core/exception/result_spec.rb @@ -0,0 +1,21 @@ +require_relative '../../spec_helper' + +describe "StopIteration#result" do + before :each do + obj = Object.new + def obj.each + yield :yield_returned_1 + yield :yield_returned_2 + :method_returned + end + @enum = obj.to_enum + end + + it "returns the method-returned-object from an Enumerator" do + @enum.next + @enum.next + -> { @enum.next }.should raise_error(StopIteration) { |error| + error.result.should equal(:method_returned) + } + end +end diff --git a/spec/ruby/core/exception/set_backtrace_spec.rb b/spec/ruby/core/exception/set_backtrace_spec.rb new file mode 100644 index 0000000000..2cd93326ec --- /dev/null +++ b/spec/ruby/core/exception/set_backtrace_spec.rb @@ -0,0 +1,23 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' +require_relative 'shared/set_backtrace' + +describe "Exception#set_backtrace" do + it "allows the user to set the backtrace from a rescued exception" do + bt = ExceptionSpecs::Backtrace.backtrace + err = RuntimeError.new + err.backtrace.should == nil + err.backtrace_locations.should == nil + + err.set_backtrace bt + + err.backtrace.should == bt + err.backtrace_locations.should == nil + end + + it_behaves_like :exception_set_backtrace, -> backtrace { + err = RuntimeError.new + err.set_backtrace(backtrace) + err + } +end diff --git a/spec/ruby/core/exception/shared/new.rb b/spec/ruby/core/exception/shared/new.rb new file mode 100644 index 0000000000..bcde8ee4b2 --- /dev/null +++ b/spec/ruby/core/exception/shared/new.rb @@ -0,0 +1,18 @@ +describe :exception_new, shared: true do + it "creates a new instance of Exception" do + Exception.should be_ancestor_of(Exception.send(@method).class) + end + + it "sets the message of the Exception when passes a message" do + Exception.send(@method, "I'm broken.").message.should == "I'm broken." + end + + it "returns 'Exception' for message when no message given" do + Exception.send(@method).message.should == "Exception" + end + + it "returns the exception when it has a custom constructor" do + ExceptionSpecs::ConstructorException.send(@method).should be_kind_of(ExceptionSpecs::ConstructorException) + end + +end diff --git a/spec/ruby/core/exception/shared/set_backtrace.rb b/spec/ruby/core/exception/shared/set_backtrace.rb new file mode 100644 index 0000000000..c6213b42b4 --- /dev/null +++ b/spec/ruby/core/exception/shared/set_backtrace.rb @@ -0,0 +1,64 @@ +require_relative '../fixtures/common' + +describe :exception_set_backtrace, shared: true do + it "accepts an Array of Strings" do + err = @method.call(["unhappy"]) + err.backtrace.should == ["unhappy"] + end + + it "allows the user to set the backtrace from a rescued exception" do + bt = ExceptionSpecs::Backtrace.backtrace + err = @method.call(bt) + err.backtrace.should == bt + end + + ruby_version_is "3.4" do + it "allows the user to set backtrace locations from a rescued exception" do + bt_locations = ExceptionSpecs::Backtrace.backtrace_locations + err = @method.call(bt_locations) + err.backtrace_locations.size.should == bt_locations.size + err.backtrace_locations.each_with_index do |loc, index| + other_loc = bt_locations[index] + + loc.path.should == other_loc.path + loc.label.should == other_loc.label + loc.base_label.should == other_loc.base_label + loc.lineno.should == other_loc.lineno + loc.absolute_path.should == other_loc.absolute_path + loc.to_s.should == other_loc.to_s + end + err.backtrace.size.should == err.backtrace_locations.size + end + end + + it "accepts an empty Array" do + err = @method.call([]) + err.backtrace.should == [] + end + + it "accepts a String" do + err = @method.call("unhappy") + err.backtrace.should == ["unhappy"] + end + + it "accepts nil" do + err = @method.call(nil) + err.backtrace.should be_nil + end + + it "raises a TypeError when passed a Symbol" do + -> { @method.call(:unhappy) }.should raise_error(TypeError) + end + + it "raises a TypeError when the Array contains a Symbol" do + -> { @method.call(["String", :unhappy]) }.should raise_error(TypeError) + end + + it "raises a TypeError when the array contains nil" do + -> { @method.call(["String", nil]) }.should raise_error(TypeError) + end + + it "raises a TypeError when the argument is a nested array" do + -> { @method.call(["String", ["String"]]) }.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/exception/signal_exception_spec.rb b/spec/ruby/core/exception/signal_exception_spec.rb new file mode 100644 index 0000000000..1a0940743f --- /dev/null +++ b/spec/ruby/core/exception/signal_exception_spec.rb @@ -0,0 +1,123 @@ +require_relative '../../spec_helper' + +describe "SignalException.new" do + it "takes a signal number as the first argument" do + exc = SignalException.new(Signal.list["INT"]) + exc.signo.should == Signal.list["INT"] + exc.signm.should == "SIGINT" + exc.message.should == "SIGINT" + end + + it "raises an exception with an invalid signal number" do + -> { SignalException.new(100000) }.should raise_error(ArgumentError) + end + + it "takes a signal name without SIG prefix as the first argument" do + exc = SignalException.new("INT") + exc.signo.should == Signal.list["INT"] + exc.signm.should == "SIGINT" + exc.message.should == "SIGINT" + end + + it "takes a signal name with SIG prefix as the first argument" do + exc = SignalException.new("SIGINT") + exc.signo.should == Signal.list["INT"] + exc.signm.should == "SIGINT" + exc.message.should == "SIGINT" + end + + it "raises an exception with an invalid signal name" do + -> { SignalException.new("NONEXISTENT") }.should raise_error(ArgumentError) + end + + it "raises an exception with an invalid first argument type" do + -> { SignalException.new(Object.new) }.should raise_error(ArgumentError) + end + + it "takes a signal symbol without SIG prefix as the first argument" do + exc = SignalException.new(:INT) + exc.signo.should == Signal.list["INT"] + exc.signm.should == "SIGINT" + exc.message.should == "SIGINT" + end + + it "takes a signal symbol with SIG prefix as the first argument" do + exc = SignalException.new(:SIGINT) + exc.signo.should == Signal.list["INT"] + exc.signm.should == "SIGINT" + exc.message.should == "SIGINT" + end + + it "raises an exception with an invalid signal name" do + -> { SignalException.new(:NONEXISTENT) }.should raise_error(ArgumentError) + end + + it "takes an optional message argument with a signal number" do + exc = SignalException.new(Signal.list["INT"], "name") + exc.signo.should == Signal.list["INT"] + exc.signm.should == "name" + exc.message.should == "name" + end + + it "raises an exception for an optional argument with a signal name" do + -> { SignalException.new("INT","name") }.should raise_error(ArgumentError) + end +end + +describe "rescuing SignalException" do + it "raises a SignalException when sent a signal" do + begin + Process.kill :TERM, Process.pid + sleep + rescue SignalException => e + e.signo.should == Signal.list["TERM"] + e.signm.should == "SIGTERM" + e.message.should == "SIGTERM" + end + end +end + +describe "SignalException" do + it "can be rescued" do + ruby_exe(<<-RUBY) + begin + raise SignalException, 'SIGKILL' + rescue SignalException + exit(0) + end + exit(1) + RUBY + + $?.exitstatus.should == 0 + end + + platform_is_not :windows do + it "runs after at_exit" do + output = ruby_exe(<<-RUBY, exit_status: :SIGKILL) + at_exit do + puts "hello" + $stdout.flush + end + + raise SignalException, 'SIGKILL' + RUBY + + $?.termsig.should == Signal.list.fetch("KILL") + output.should == "hello\n" + end + + it "cannot be trapped with Signal.trap" do + ruby_exe(<<-RUBY, exit_status: :SIGPROF) + Signal.trap("PROF") {} + raise(SignalException, "PROF") + RUBY + + $?.termsig.should == Signal.list.fetch("PROF") + end + + it "self-signals for USR1" do + ruby_exe("raise(SignalException, 'USR1')", exit_status: :SIGUSR1) + $?.termsig.should == Signal.list.fetch('USR1') + end + end +end diff --git a/spec/ruby/core/exception/signm_spec.rb b/spec/ruby/core/exception/signm_spec.rb new file mode 100644 index 0000000000..4adff3b5ee --- /dev/null +++ b/spec/ruby/core/exception/signm_spec.rb @@ -0,0 +1,9 @@ +require_relative '../../spec_helper' + +describe "SignalException#signm" do + it "returns the signal name" do + -> { Process.kill(:TERM, Process.pid) }.should raise_error(SignalException) { |e| + e.signm.should == 'SIGTERM' + } + end +end diff --git a/spec/ruby/core/exception/signo_spec.rb b/spec/ruby/core/exception/signo_spec.rb new file mode 100644 index 0000000000..62fc321516 --- /dev/null +++ b/spec/ruby/core/exception/signo_spec.rb @@ -0,0 +1,9 @@ +require_relative '../../spec_helper' + +describe "SignalException#signo" do + it "returns the signal number" do + -> { Process.kill(:TERM, Process.pid) }.should raise_error(SignalException) { |e| + e.signo.should == Signal.list['TERM'] + } + end +end diff --git a/spec/ruby/core/exception/standard_error_spec.rb b/spec/ruby/core/exception/standard_error_spec.rb new file mode 100644 index 0000000000..17e98ce7f0 --- /dev/null +++ b/spec/ruby/core/exception/standard_error_spec.rb @@ -0,0 +1,23 @@ +require_relative '../../spec_helper' + +describe "StandardError" do + it "rescues StandardError" do + begin + raise StandardError + rescue => exception + exception.class.should == StandardError + end + end + + it "rescues subclass of StandardError" do + begin + raise RuntimeError + rescue => exception + exception.class.should == RuntimeError + end + end + + it "does not rescue superclass of StandardError" do + -> { begin; raise Exception; rescue; end }.should raise_error(Exception) + end +end diff --git a/spec/ruby/core/exception/status_spec.rb b/spec/ruby/core/exception/status_spec.rb new file mode 100644 index 0000000000..8ace00fe10 --- /dev/null +++ b/spec/ruby/core/exception/status_spec.rb @@ -0,0 +1,9 @@ +require_relative '../../spec_helper' + +describe "SystemExit#status" do + it "returns the exit status" do + -> { exit 42 }.should raise_error(SystemExit) { |e| + e.status.should == 42 + } + end +end diff --git a/spec/ruby/core/exception/success_spec.rb b/spec/ruby/core/exception/success_spec.rb new file mode 100644 index 0000000000..6f21743340 --- /dev/null +++ b/spec/ruby/core/exception/success_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../spec_helper' + +describe "SystemExit#success?" do + it "returns true if the process exited successfully" do + -> { exit 0 }.should raise_error(SystemExit) { |e| + e.should.success? + } + end + + it "returns false if the process exited unsuccessfully" do + -> { exit(-1) }.should raise_error(SystemExit) { |e| + e.should_not.success? + } + end +end diff --git a/spec/ruby/core/exception/syntax_error_spec.rb b/spec/ruby/core/exception/syntax_error_spec.rb new file mode 100644 index 0000000000..4c713a3507 --- /dev/null +++ b/spec/ruby/core/exception/syntax_error_spec.rb @@ -0,0 +1,25 @@ +require_relative '../../spec_helper' + +describe "SyntaxError#path" do + it "returns the file path provided to eval" do + filename = "speccing.rb" + + -> { + eval("if true", TOPLEVEL_BINDING, filename) + }.should raise_error(SyntaxError) { |e| + e.path.should == filename + } + end + + it "returns the file path that raised an exception" do + expected_path = fixture(__FILE__, "syntax_error.rb") + + -> { + require_relative "fixtures/syntax_error" + }.should raise_error(SyntaxError) { |e| e.path.should == expected_path } + end + + it "returns nil when constructed directly" do + SyntaxError.new.path.should == nil + end +end diff --git a/spec/ruby/core/exception/system_call_error_spec.rb b/spec/ruby/core/exception/system_call_error_spec.rb new file mode 100644 index 0000000000..4fe51901c8 --- /dev/null +++ b/spec/ruby/core/exception/system_call_error_spec.rb @@ -0,0 +1,163 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' + +describe "SystemCallError" do + before :each do + ScratchPad.clear + end + + it "can be subclassed" do + ExceptionSpecs::SCESub = Class.new(SystemCallError) do + def initialize + ScratchPad.record :initialize + end + end + + exc = ExceptionSpecs::SCESub.new + ScratchPad.recorded.should equal(:initialize) + exc.should be_an_instance_of(ExceptionSpecs::SCESub) + ensure + ExceptionSpecs.send(:remove_const, :SCESub) + end +end + +describe "SystemCallError.new" do + before :all do + @example_errno = Errno::EINVAL::Errno + @example_errno_class = Errno::EINVAL + @last_known_errno = Errno.constants.size - 1 + @unknown_errno = Errno.constants.size + @some_human_readable = /[[:graph:]]+/ + end + + it "requires at least one argument" do + -> { SystemCallError.new }.should raise_error(ArgumentError) + end + + it "accepts single Integer argument as errno" do + SystemCallError.new(-2**24).errno.should == -2**24 + SystemCallError.new(-1).errno.should == -1 + SystemCallError.new(0).errno.should == 0 + SystemCallError.new(@last_known_errno).errno.should == @last_known_errno + SystemCallError.new(@unknown_errno).errno.should == @unknown_errno + SystemCallError.new(2**24).errno.should == 2**24 + end + + it "constructs a SystemCallError for an unknown error number" do + SystemCallError.new(-2**24).should be_an_instance_of(SystemCallError) + SystemCallError.new(-1).should be_an_instance_of(SystemCallError) + SystemCallError.new(@unknown_errno).should be_an_instance_of(SystemCallError) + SystemCallError.new(2**24).should be_an_instance_of(SystemCallError) + end + + it "constructs the appropriate Errno class" do + e = SystemCallError.new(@example_errno) + e.should be_kind_of(SystemCallError) + e.should be_an_instance_of(@example_errno_class) + end + + it "sets an error message corresponding to an appropriate Errno class" do + e = SystemCallError.new(@example_errno) + e.message.should == 'Invalid argument' + end + + it "accepts an optional custom message preceding the errno" do + exc = SystemCallError.new("custom message", @example_errno) + exc.should be_an_instance_of(@example_errno_class) + exc.errno.should == @example_errno + exc.message.should == 'Invalid argument - custom message' + end + + it "accepts an optional third argument specifying the location" do + exc = SystemCallError.new("custom message", @example_errno, "location") + exc.should be_an_instance_of(@example_errno_class) + exc.errno.should == @example_errno + exc.message.should == 'Invalid argument @ location - custom message' + end + + it "coerces location if it is not a String" do + e = SystemCallError.new('foo', 1, :not_a_string) + e.message.should =~ /@ not_a_string - foo/ + end + + it "returns an arity of -1 for the initialize method" do + SystemCallError.instance_method(:initialize).arity.should == -1 + end + + it "converts to Integer if errno is a Float" do + SystemCallError.new('foo', 2.0).should == SystemCallError.new('foo', 2) + SystemCallError.new('foo', 2.9).should == SystemCallError.new('foo', 2) + end + + it "treats nil errno as unknown error value" do + SystemCallError.new(nil).should be_an_instance_of(SystemCallError) + end + + it "treats nil custom message as if it is not passed at all" do + exc = SystemCallError.new(nil, @example_errno) + exc.message.should == 'Invalid argument' + end + + it "sets an 'unknown error' message when an unknown error number" do + SystemCallError.new(-1).message.should =~ @some_human_readable + end + + it "adds a custom error message to an 'unknown error' message when an unknown error number and a custom message specified" do + SystemCallError.new("custom message", -1).message.should =~ /#{@some_human_readable}.* - custom message/ + end + + it "converts to Integer if errno is a Complex convertible to Integer" do + SystemCallError.new('foo', Complex(2.9, 0)).should == SystemCallError.new('foo', 2) + end + + it "raises TypeError if message is not a String" do + -> { SystemCallError.new(:foo, 1) }.should raise_error(TypeError, /no implicit conversion of Symbol into String/) + end + + it "raises TypeError if errno is not an Integer" do + -> { SystemCallError.new('foo', 'bar') }.should raise_error(TypeError, /no implicit conversion of String into Integer/) + end + + it "raises RangeError if errno is a Complex not convertible to Integer" do + -> { SystemCallError.new('foo', Complex(2.9, 1)) }.should raise_error(RangeError, /can't convert/) + end +end + +describe "SystemCallError#errno" do + it "returns nil when no errno given" do + SystemCallError.new("message").errno.should == nil + end + + it "returns the errno given as optional argument to new" do + SystemCallError.new("message", -2**20).errno.should == -2**20 + SystemCallError.new("message", -1).errno.should == -1 + SystemCallError.new("message", 0).errno.should == 0 + SystemCallError.new("message", 1).errno.should == 1 + SystemCallError.new("message", 42).errno.should == 42 + SystemCallError.new("message", 2**20).errno.should == 2**20 + end +end + +describe "SystemCallError#message" do + it "returns the default message when no message is given" do + SystemCallError.new(2**28).message.should =~ @some_human_readable + end + + it "returns the message given as an argument to new" do + SystemCallError.new("message", 1).message.should =~ /message/ + SystemCallError.new("XXX").message.should =~ /XXX/ + end +end + +describe "SystemCallError#dup" do + it "copies the errno" do + dup_sce = SystemCallError.new("message", 42).dup + dup_sce.errno.should == 42 + end +end + +describe "SystemCallError#backtrace" do + it "is nil if not raised" do + SystemCallError.new("message", 42).backtrace.should == nil + end +end diff --git a/spec/ruby/core/exception/system_exit_spec.rb b/spec/ruby/core/exception/system_exit_spec.rb new file mode 100644 index 0000000000..d899844c4e --- /dev/null +++ b/spec/ruby/core/exception/system_exit_spec.rb @@ -0,0 +1,59 @@ +require_relative '../../spec_helper' + +describe "SystemExit" do + describe "#initialize" do + it "accepts a status and message" do + exc = SystemExit.new(42, "message") + exc.status.should == 42 + exc.message.should == "message" + + exc = SystemExit.new(true, "message") + exc.status.should == 0 + exc.message.should == "message" + + exc = SystemExit.new(false, "message") + exc.status.should == 1 + exc.message.should == "message" + end + + it "accepts a status only" do + exc = SystemExit.new(42) + exc.status.should == 42 + exc.message.should == "SystemExit" + + exc = SystemExit.new(true) + exc.status.should == 0 + exc.message.should == "SystemExit" + + exc = SystemExit.new(false) + exc.status.should == 1 + exc.message.should == "SystemExit" + end + + it "accepts a message only" do + exc = SystemExit.new("message") + exc.status.should == 0 + exc.message.should == "message" + end + + it "accepts no arguments" do + exc = SystemExit.new + exc.status.should == 0 + exc.message.should == "SystemExit" + end + end + + it "sets the exit status and exits silently when raised" do + code = 'raise SystemExit.new(7)' + result = ruby_exe(code, args: "2>&1", exit_status: 7) + result.should == "" + $?.exitstatus.should == 7 + end + + it "sets the exit status and exits silently when raised when subclassed" do + code = 'class CustomExit < SystemExit; end; raise CustomExit.new(8)' + result = ruby_exe(code, args: "2>&1", exit_status: 8) + result.should == "" + $?.exitstatus.should == 8 + end +end diff --git a/spec/ruby/core/exception/to_s_spec.rb b/spec/ruby/core/exception/to_s_spec.rb new file mode 100644 index 0000000000..65c0d73a98 --- /dev/null +++ b/spec/ruby/core/exception/to_s_spec.rb @@ -0,0 +1,37 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' + +describe "Exception#to_s" do + it "returns the self's name if no message is set" do + Exception.new.to_s.should == 'Exception' + ExceptionSpecs::Exceptional.new.to_s.should == 'ExceptionSpecs::Exceptional' + end + + it "returns self's message if set" do + ExceptionSpecs::Exceptional.new('!!').to_s.should == '!!' + end + + it "calls #to_s on the message" do + message = mock("message") + message.should_receive(:to_s).and_return("message") + ExceptionSpecs::Exceptional.new(message).to_s.should == "message" + end +end + +describe "NameError#to_s" do + it "raises its own message for an undefined variable" do + begin + puts not_defined + rescue => exception + exception.message.should =~ /undefined local variable or method [`']not_defined'/ + end + end + + it "raises its own message for an undefined constant" do + begin + puts NotDefined + rescue => exception + exception.message.should =~ /uninitialized constant NotDefined/ + end + end +end diff --git a/spec/ruby/core/exception/top_level_spec.rb b/spec/ruby/core/exception/top_level_spec.rb new file mode 100644 index 0000000000..cc961d06d5 --- /dev/null +++ b/spec/ruby/core/exception/top_level_spec.rb @@ -0,0 +1,65 @@ +require_relative '../../spec_helper' + +describe "An Exception reaching the top level" do + it "is printed on STDERR" do + ruby_exe('raise "foo"', args: "2>&1", exit_status: 1).should =~ /in [`']<main>': foo \(RuntimeError\)/ + end + + it "the Exception#cause is printed to STDERR with backtraces" do + code = <<-RUBY + def raise_cause + raise "the cause" # 2 + end + def raise_wrapped + raise "wrapped" # 5 + end + begin + raise_cause # 8 + rescue + raise_wrapped # 10 + end + RUBY + lines = ruby_exe(code, args: "2>&1", exit_status: 1).lines + + lines.map! { |l| l.chomp[/:(\d+:in.+)/, 1] } + lines[0].should =~ /\A5:in [`'](?:Object#)?raise_wrapped': wrapped \(RuntimeError\)\z/ + if lines[1].include? 'rescue in' + # CRuby < 3.4 has an extra 'rescue in' backtrace entry + lines[1].should =~ /\A10:in [`']rescue in <main>'\z/ + lines.delete_at 1 + lines[1].should =~ /\A7:in [`']<main>'\z/ + else + lines[1].should =~ /\A10:in [`']<main>'\z/ + end + lines[2].should =~ /\A2:in [`'](?:Object#)?raise_cause': the cause \(RuntimeError\)\z/ + lines[3].should =~ /\A8:in [`']<main>'\z/ + lines.size.should == 4 + end + + describe "with a custom backtrace" do + it "is printed on STDERR" do + code = <<-RUBY + raise RuntimeError, "foo", [ + "/dir/foo.rb:10:in `raising'", + "/dir/bar.rb:20:in `caller'", + ] + RUBY + ruby_exe(code, args: "2>&1", exit_status: 1).should == <<-EOS +/dir/foo.rb:10:in `raising': foo (RuntimeError) +\tfrom /dir/bar.rb:20:in `caller' + EOS + end + end + + describe "kills all threads and fibers, ensure clauses are only run for threads current fibers, not for suspended fibers" do + it "with ensure on the root fiber" do + file = fixture(__FILE__, "thread_fiber_ensure.rb") + ruby_exe(file, args: "2>&1", exit_status: 0).should == "current fiber ensure\n" + end + + it "with ensure on non-root fiber" do + file = fixture(__FILE__, "thread_fiber_ensure_non_root_fiber.rb") + ruby_exe(file, args: "2>&1", exit_status: 0).should == "current fiber ensure\n" + end + end +end diff --git a/spec/ruby/core/exception/uncaught_throw_error_spec.rb b/spec/ruby/core/exception/uncaught_throw_error_spec.rb new file mode 100644 index 0000000000..9267df6670 --- /dev/null +++ b/spec/ruby/core/exception/uncaught_throw_error_spec.rb @@ -0,0 +1,12 @@ +require_relative '../../spec_helper' + +describe "UncaughtThrowError#tag" do + it "returns the object thrown" do + begin + throw :abc + + rescue UncaughtThrowError => e + e.tag.should == :abc + end + end +end diff --git a/spec/ruby/core/false/and_spec.rb b/spec/ruby/core/false/and_spec.rb new file mode 100644 index 0000000000..0b02ae62c5 --- /dev/null +++ b/spec/ruby/core/false/and_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' + +describe "FalseClass#&" do + it "returns false" do + (false & false).should == false + (false & true).should == false + (false & nil).should == false + (false & "").should == false + (false & mock('x')).should == false + end +end diff --git a/spec/ruby/core/false/case_compare_spec.rb b/spec/ruby/core/false/case_compare_spec.rb new file mode 100644 index 0000000000..0bd0ab44ae --- /dev/null +++ b/spec/ruby/core/false/case_compare_spec.rb @@ -0,0 +1,14 @@ +require_relative '../../spec_helper' + +describe "FalseClass#===" do + it "returns true for false" do + (false === false).should == true + end + + it "returns false for non-false object" do + (false === 0).should == false + (false === "").should == false + (false === Object).should == false + (false === nil).should == false + end +end diff --git a/spec/ruby/core/false/dup_spec.rb b/spec/ruby/core/false/dup_spec.rb new file mode 100644 index 0000000000..1a569a2f4f --- /dev/null +++ b/spec/ruby/core/false/dup_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' + +describe "FalseClass#dup" do + it "returns self" do + false.dup.should equal(false) + end +end diff --git a/spec/ruby/core/false/falseclass_spec.rb b/spec/ruby/core/false/falseclass_spec.rb new file mode 100644 index 0000000000..c018ef2421 --- /dev/null +++ b/spec/ruby/core/false/falseclass_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../spec_helper' + +describe "FalseClass" do + it ".allocate raises a TypeError" do + -> do + FalseClass.allocate + end.should raise_error(TypeError) + end + + it ".new is undefined" do + -> do + FalseClass.new + end.should raise_error(NoMethodError) + end +end diff --git a/spec/ruby/core/false/inspect_spec.rb b/spec/ruby/core/false/inspect_spec.rb new file mode 100644 index 0000000000..4cbb55d434 --- /dev/null +++ b/spec/ruby/core/false/inspect_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' + +describe "FalseClass#inspect" do + it "returns the string 'false'" do + false.inspect.should == "false" + end +end diff --git a/spec/ruby/core/false/or_spec.rb b/spec/ruby/core/false/or_spec.rb new file mode 100644 index 0000000000..f3ee1a3439 --- /dev/null +++ b/spec/ruby/core/false/or_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' + +describe "FalseClass#|" do + it "returns false if other is nil or false, otherwise true" do + (false | false).should == false + (false | true).should == true + (false | nil).should == false + (false | "").should == true + (false | mock('x')).should == true + end +end diff --git a/spec/ruby/core/false/singleton_method_spec.rb b/spec/ruby/core/false/singleton_method_spec.rb new file mode 100644 index 0000000000..738794b46c --- /dev/null +++ b/spec/ruby/core/false/singleton_method_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../spec_helper' + +describe "FalseClass#singleton_method" do + ruby_version_is '3.3' do + it "raises regardless of whether FalseClass defines the method" do + -> { false.singleton_method(:foo) }.should raise_error(NameError) + begin + def (false).foo; end + -> { false.singleton_method(:foo) }.should raise_error(NameError) + ensure + FalseClass.send(:remove_method, :foo) + end + end + end +end diff --git a/spec/ruby/core/false/to_s_spec.rb b/spec/ruby/core/false/to_s_spec.rb new file mode 100644 index 0000000000..62f67f6f55 --- /dev/null +++ b/spec/ruby/core/false/to_s_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../spec_helper' + +describe "FalseClass#to_s" do + it "returns the string 'false'" do + false.to_s.should == "false" + end + + it "returns a frozen string" do + false.to_s.should.frozen? + end + + it "always returns the same string" do + false.to_s.should equal(false.to_s) + end +end diff --git a/spec/ruby/core/false/xor_spec.rb b/spec/ruby/core/false/xor_spec.rb new file mode 100644 index 0000000000..1b87b9f412 --- /dev/null +++ b/spec/ruby/core/false/xor_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' + +describe "FalseClass#^" do + it "returns false if other is nil or false, otherwise true" do + (false ^ false).should == false + (false ^ true).should == true + (false ^ nil).should == false + (false ^ "").should == true + (false ^ mock('x')).should == true + end +end diff --git a/spec/ruby/core/fiber/alive_spec.rb b/spec/ruby/core/fiber/alive_spec.rb new file mode 100644 index 0000000000..a1df582435 --- /dev/null +++ b/spec/ruby/core/fiber/alive_spec.rb @@ -0,0 +1,44 @@ +require_relative '../../spec_helper' + +describe "Fiber#alive?" do + it "returns true for a Fiber that hasn't had #resume called" do + fiber = Fiber.new { true } + fiber.alive?.should be_true + end + + # FIXME: Better description? + it "returns true for a Fiber that's yielded to the caller" do + fiber = Fiber.new { Fiber.yield } + fiber.resume + fiber.alive?.should be_true + end + + it "returns true when called from its Fiber" do + fiber = Fiber.new { fiber.alive?.should be_true } + fiber.resume + end + + it "doesn't invoke the block associated with the Fiber" do + offthehook = mock('do not call') + offthehook.should_not_receive(:ring) + fiber = Fiber.new { offthehook.ring } + fiber.alive? + end + + it "returns false for a Fiber that's dead" do + fiber = Fiber.new { true } + fiber.resume + -> { fiber.resume }.should raise_error(FiberError) + fiber.alive?.should be_false + end + + it "always returns false for a dead Fiber" do + fiber = Fiber.new { true } + fiber.resume + -> { fiber.resume }.should raise_error(FiberError) + fiber.alive?.should be_false + -> { fiber.resume }.should raise_error(FiberError) + fiber.alive?.should be_false + fiber.alive?.should be_false + end +end diff --git a/spec/ruby/core/fiber/blocking_spec.rb b/spec/ruby/core/fiber/blocking_spec.rb new file mode 100644 index 0000000000..d5caf81fbe --- /dev/null +++ b/spec/ruby/core/fiber/blocking_spec.rb @@ -0,0 +1,73 @@ +require_relative '../../spec_helper' +require_relative 'shared/blocking' + +describe "Fiber.blocking?" do + it_behaves_like :non_blocking_fiber, -> { Fiber.blocking? } + + context "when fiber is blocking" do + context "root Fiber of the main thread" do + it "returns 1 for blocking: true" do + fiber = Fiber.new(blocking: true) { Fiber.blocking? } + blocking = fiber.resume + + blocking.should == 1 + end + end + + context "root Fiber of a new thread" do + it "returns 1 for blocking: true" do + thread = Thread.new do + fiber = Fiber.new(blocking: true) { Fiber.blocking? } + blocking = fiber.resume + + blocking.should == 1 + end + + thread.join + end + end + end +end + +describe "Fiber#blocking?" do + it_behaves_like :non_blocking_fiber, -> { Fiber.current.blocking? } + + context "when fiber is blocking" do + context "root Fiber of the main thread" do + it "returns true for blocking: true" do + fiber = Fiber.new(blocking: true) { Fiber.current.blocking? } + blocking = fiber.resume + + blocking.should == true + end + end + + context "root Fiber of a new thread" do + it "returns true for blocking: true" do + thread = Thread.new do + fiber = Fiber.new(blocking: true) { Fiber.current.blocking? } + blocking = fiber.resume + + blocking.should == true + end + + thread.join + end + end + end +end + +describe "Fiber.blocking" do + context "when fiber is non-blocking" do + it "can become blocking" do + fiber = Fiber.new(blocking: false) do + Fiber.blocking do |f| + f.blocking? ? :blocking : :non_blocking + end + end + + blocking = fiber.resume + blocking.should == :blocking + end + end +end diff --git a/spec/ruby/core/fiber/current_spec.rb b/spec/ruby/core/fiber/current_spec.rb new file mode 100644 index 0000000000..b93df77a89 --- /dev/null +++ b/spec/ruby/core/fiber/current_spec.rb @@ -0,0 +1,50 @@ +require_relative '../../spec_helper' + +describe "Fiber.current" do + it "returns the root Fiber when called outside of a Fiber" do + root = Fiber.current + root.should be_an_instance_of(Fiber) + # We can always transfer to the root Fiber; it will never die + 5.times do + root.transfer.should be_nil + root.alive?.should be_true + end + end + + it "returns the current Fiber when called from a Fiber" do + fiber = Fiber.new do + this = Fiber.current + this.should be_an_instance_of(Fiber) + this.should == fiber + this.alive?.should be_true + end + fiber.resume + end + + it "returns the current Fiber when called from a Fiber that transferred to another" do + states = [] + fiber = Fiber.new do + states << :fiber + this = Fiber.current + this.should be_an_instance_of(Fiber) + this.should == fiber + this.alive?.should be_true + end + + fiber2 = Fiber.new do + states << :fiber2 + fiber.transfer + flunk + end + + fiber3 = Fiber.new do + states << :fiber3 + fiber2.transfer + states << :fiber3_terminated + end + + fiber3.resume + + states.should == [:fiber3, :fiber2, :fiber, :fiber3_terminated] + end +end diff --git a/spec/ruby/core/fiber/fixtures/classes.rb b/spec/ruby/core/fiber/fixtures/classes.rb new file mode 100644 index 0000000000..6b0e0fbc42 --- /dev/null +++ b/spec/ruby/core/fiber/fixtures/classes.rb @@ -0,0 +1,22 @@ +module FiberSpecs + + class NewFiberToRaise + def self.raise(*args, **kwargs, &block) + fiber = Fiber.new do + if block_given? + block.call do + Fiber.yield + end + else + Fiber.yield + end + end + + fiber.resume + + fiber.raise(*args, **kwargs) + end + end + + class CustomError < StandardError; end +end diff --git a/spec/ruby/core/fiber/fixtures/scheduler.rb b/spec/ruby/core/fiber/fixtures/scheduler.rb new file mode 100644 index 0000000000..16bd2f6b44 --- /dev/null +++ b/spec/ruby/core/fiber/fixtures/scheduler.rb @@ -0,0 +1,35 @@ +module FiberSpecs + + class LoggingScheduler + attr_reader :events + def initialize + @events = [] + end + + def block(*args) + @events << { event: :block, fiber: Fiber.current, args: args } + Fiber.yield + end + + def io_wait(*args) + @events << { event: :io_wait, fiber: Fiber.current, args: args } + Fiber.yield + end + + def kernel_sleep(*args) + @events << { event: :kernel_sleep, fiber: Fiber.current, args: args } + Fiber.yield + end + + def unblock(*args) + @events << { event: :unblock, fiber: Fiber.current, args: args } + Fiber.yield + end + + def fiber_interrupt(*args) + @events << { event: :fiber_interrupt, fiber: Fiber.current, args: args } + Fiber.yield + end + end + +end diff --git a/spec/ruby/core/fiber/inspect_spec.rb b/spec/ruby/core/fiber/inspect_spec.rb new file mode 100644 index 0000000000..fcfef20716 --- /dev/null +++ b/spec/ruby/core/fiber/inspect_spec.rb @@ -0,0 +1,35 @@ +require_relative '../../spec_helper' + +describe "Fiber#inspect" do + describe "status" do + it "is resumed for the root Fiber of a Thread" do + inspected = Thread.new { Fiber.current.inspect }.value + inspected.should =~ /\A#<Fiber:0x\h+ .*\(resumed\)>\z/ + end + + it "is created for a Fiber which did not run yet" do + inspected = Fiber.new {}.inspect + inspected.should =~ /\A#<Fiber:0x\h+ .+ \(created\)>\z/ + end + + it "is resumed for a Fiber which was resumed" do + inspected = Fiber.new { Fiber.current.inspect }.resume + inspected.should =~ /\A#<Fiber:0x\h+ .+ \(resumed\)>\z/ + end + + it "is resumed for a Fiber which was transferred" do + inspected = Fiber.new { Fiber.current.inspect }.transfer + inspected.should =~ /\A#<Fiber:0x\h+ .+ \(resumed\)>\z/ + end + + it "is suspended for a Fiber which was resumed and yielded" do + inspected = Fiber.new { Fiber.yield }.tap(&:resume).inspect + inspected.should =~ /\A#<Fiber:0x\h+ .+ \(suspended\)>\z/ + end + + it "is terminated for a Fiber which has terminated" do + inspected = Fiber.new {}.tap(&:resume).inspect + inspected.should =~ /\A#<Fiber:0x\h+ .+ \(terminated\)>\z/ + end + end +end diff --git a/spec/ruby/core/fiber/kill_spec.rb b/spec/ruby/core/fiber/kill_spec.rb new file mode 100644 index 0000000000..2f4c499280 --- /dev/null +++ b/spec/ruby/core/fiber/kill_spec.rb @@ -0,0 +1,90 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative '../../shared/kernel/raise' + +ruby_version_is "3.3" do + describe "Fiber#kill" do + it "kills a non-resumed fiber" do + fiber = Fiber.new{} + + fiber.alive?.should == true + + fiber.kill + fiber.alive?.should == false + end + + it "kills a resumed fiber" do + fiber = Fiber.new{while true; Fiber.yield; end} + fiber.resume + + fiber.alive?.should == true + + fiber.kill + fiber.alive?.should == false + end + + it "can kill itself" do + fiber = Fiber.new do + Fiber.current.kill + end + + fiber.alive?.should == true + + fiber.resume + fiber.alive?.should == false + end + + it "kills a resumed fiber from a child" do + parent = Fiber.new do + child = Fiber.new do + parent.kill + parent.alive?.should == true + end + + child.resume + end + + parent.resume + parent.alive?.should == false + end + + it "executes the ensure block" do + ensure_executed = false + + fiber = Fiber.new do + while true; Fiber.yield; end + ensure + ensure_executed = true + end + + fiber.resume + fiber.kill + ensure_executed.should == true + end + + it "does not execute rescue block" do + rescue_executed = false + + fiber = Fiber.new do + while true; Fiber.yield; end + rescue Exception + rescue_executed = true + end + + fiber.resume + fiber.kill + rescue_executed.should == false + end + + it "repeatedly kills a fiber" do + fiber = Fiber.new do + while true; Fiber.yield; end + ensure + while true; Fiber.yield; end + end + + fiber.kill + fiber.alive?.should == false + end + end +end diff --git a/spec/ruby/core/fiber/new_spec.rb b/spec/ruby/core/fiber/new_spec.rb new file mode 100644 index 0000000000..b43c1386be --- /dev/null +++ b/spec/ruby/core/fiber/new_spec.rb @@ -0,0 +1,39 @@ +require_relative '../../spec_helper' + +describe "Fiber.new" do + it "creates a fiber from the given block" do + fiber = Fiber.new {} + fiber.resume + fiber.should be_an_instance_of(Fiber) + end + + it "creates a fiber from a subclass" do + class MyFiber < Fiber + end + fiber = MyFiber.new {} + fiber.resume + fiber.should be_an_instance_of(MyFiber) + end + + it "raises an ArgumentError if called without a block" do + -> { Fiber.new }.should raise_error(ArgumentError) + end + + it "does not invoke the block" do + invoked = false + fiber = Fiber.new { invoked = true } + invoked.should be_false + fiber.resume + end + + it "closes over lexical environments" do + o = Object.new + def o.f + a = 1 + f = Fiber.new { a = 2 } + f.resume + a + end + o.f.should == 2 + end +end diff --git a/spec/ruby/core/fiber/raise_spec.rb b/spec/ruby/core/fiber/raise_spec.rb new file mode 100644 index 0000000000..896f760290 --- /dev/null +++ b/spec/ruby/core/fiber/raise_spec.rb @@ -0,0 +1,139 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative '../../shared/kernel/raise' + +describe "Fiber#raise" do + it_behaves_like :kernel_raise, :raise, FiberSpecs::NewFiberToRaise + it_behaves_like :kernel_raise_across_contexts, :raise, FiberSpecs::NewFiberToRaise +end + +describe "Fiber#raise" do + it 'raises RuntimeError by default' do + -> { FiberSpecs::NewFiberToRaise.raise }.should raise_error(RuntimeError) + end + + it "raises FiberError if Fiber is not born" do + fiber = Fiber.new { true } + -> { fiber.raise }.should raise_error(FiberError, "cannot raise exception on unborn fiber") + end + + it "raises FiberError if Fiber is dead" do + fiber = Fiber.new { true } + fiber.resume + -> { fiber.raise }.should raise_error(FiberError, /dead fiber called|attempt to resume a terminated fiber/) + end + + it 'accepts error class' do + -> { FiberSpecs::NewFiberToRaise.raise FiberSpecs::CustomError }.should raise_error(FiberSpecs::CustomError) + end + + it 'accepts error message' do + -> { FiberSpecs::NewFiberToRaise.raise "error message" }.should raise_error(RuntimeError, "error message") + end + + it 'does not accept array of backtrace information only' do + -> { FiberSpecs::NewFiberToRaise.raise ['foo'] }.should raise_error(TypeError) + end + + it 'does not accept integer' do + -> { FiberSpecs::NewFiberToRaise.raise 100 }.should raise_error(TypeError) + end + + it 'accepts error class with error message' do + -> { FiberSpecs::NewFiberToRaise.raise FiberSpecs::CustomError, 'test error' }.should raise_error(FiberSpecs::CustomError, 'test error') + end + + it 'accepts error class with error message and backtrace information' do + -> { + FiberSpecs::NewFiberToRaise.raise FiberSpecs::CustomError, 'test error', ['foo', 'boo'] + }.should raise_error(FiberSpecs::CustomError) { |e| + e.message.should == 'test error' + e.backtrace.should == ['foo', 'boo'] + } + end + + it 'does not accept only error message and backtrace information' do + -> { FiberSpecs::NewFiberToRaise.raise 'test error', ['foo', 'boo'] }.should raise_error(TypeError) + end + + it "raises a FiberError if invoked from a different Thread" do + fiber = Fiber.new { Fiber.yield } + fiber.resume + Thread.new do + -> { + fiber.raise + }.should raise_error(FiberError, "fiber called across threads") + end.join + end + + it "kills Fiber" do + fiber = Fiber.new { Fiber.yield :first; :second } + fiber.resume + -> { fiber.raise }.should raise_error + -> { fiber.resume }.should raise_error(FiberError, /dead fiber called|attempt to resume a terminated fiber/) + end + + it "returns to calling fiber after raise" do + fiber_one = Fiber.new do + Fiber.yield :yield_one + :unreachable + end + + fiber_two = Fiber.new do + results = [] + results << fiber_one.resume + begin + fiber_one.raise + rescue + results << :rescued + end + results + end + + fiber_two.resume.should == [:yield_one, :rescued] + end + + ruby_version_is "3.4" do + it "raises on the resumed fiber" do + root_fiber = Fiber.current + f1 = Fiber.new { root_fiber.transfer } + f2 = Fiber.new { f1.resume } + f2.transfer + + -> do + f2.raise(RuntimeError, "Expected error") + end.should raise_error(RuntimeError, "Expected error") + end + + it "raises on itself" do + -> do + Fiber.current.raise(RuntimeError, "Expected error") + end.should raise_error(RuntimeError, "Expected error") + end + + it "should raise on parent fiber" do + f2 = nil + f1 = Fiber.new do + # This is equivalent to Kernel#raise: + f2.raise(RuntimeError, "Expected error") + end + f2 = Fiber.new do + f1.resume + end + + -> do + f2.resume + end.should raise_error(RuntimeError, "Expected error") + end + end +end + + +describe "Fiber#raise" do + it "transfers and raises on a transferring fiber" do + root = Fiber.current + fiber = Fiber.new { root.transfer } + fiber.transfer + -> { fiber.raise "msg" }.should raise_error(RuntimeError, "msg") + end +end diff --git a/spec/ruby/core/fiber/resume_spec.rb b/spec/ruby/core/fiber/resume_spec.rb new file mode 100644 index 0000000000..4b20f4b4bf --- /dev/null +++ b/spec/ruby/core/fiber/resume_spec.rb @@ -0,0 +1,83 @@ +require_relative '../../spec_helper' +require_relative 'shared/resume' + +describe "Fiber#resume" do + it_behaves_like :fiber_resume, :resume +end + +describe "Fiber#resume" do + it "runs until Fiber.yield" do + obj = mock('obj') + obj.should_not_receive(:do) + fiber = Fiber.new { 1 + 2; Fiber.yield; obj.do } + fiber.resume + end + + it "resumes from the last call to Fiber.yield on subsequent invocations" do + fiber = Fiber.new { Fiber.yield :first; :second } + fiber.resume.should == :first + fiber.resume.should == :second + end + + it "sets the block parameters to its arguments on the first invocation" do + first = mock('first') + first.should_receive(:arg).with(:first).twice + + fiber = Fiber.new { |arg| first.arg arg; Fiber.yield; first.arg arg; } + fiber.resume :first + fiber.resume :second + end + + it "raises a FiberError if the Fiber tries to resume itself" do + fiber = Fiber.new { fiber.resume } + -> { fiber.resume }.should raise_error(FiberError, /current fiber/) + end + + it "returns control to the calling Fiber if called from one" do + fiber1 = Fiber.new { :fiber1 } + fiber2 = Fiber.new { fiber1.resume; :fiber2 } + fiber2.resume.should == :fiber2 + end + + # Redmine #595 + it "executes the ensure clause" do + code = <<-RUBY + f = Fiber.new do + begin + Fiber.yield + ensure + puts "ensure executed" + end + end + + # The apparent issue is that when Fiber.yield executes, control + # "leaves" the "ensure block" and so the ensure clause should run. But + # control really does NOT leave the ensure block when Fiber.yield + # executes. It merely pauses there. To require ensure to run when a + # Fiber is suspended then makes ensure-in-a-Fiber-context different + # than ensure-in-a-Thread-context and this would be very confusing. + f.resume + + # When we execute the second #resume call, the ensure block DOES exit, + # the ensure clause runs. + f.resume + + exit 0 + RUBY + + ruby_exe(code).should == "ensure executed\n" + end + + it "can work with Fiber#transfer" do + fiber1 = Fiber.new { true } + fiber2 = Fiber.new { fiber1.transfer; Fiber.yield 10 ; Fiber.yield 20; raise } + fiber2.resume.should == 10 + fiber2.resume.should == 20 + end + + it "raises a FiberError if the Fiber attempts to resume a resuming fiber" do + root_fiber = Fiber.current + fiber1 = Fiber.new { root_fiber.resume } + -> { fiber1.resume }.should raise_error(FiberError, /attempt to resume a resuming fiber/) + end +end diff --git a/spec/ruby/core/fiber/scheduler_spec.rb b/spec/ruby/core/fiber/scheduler_spec.rb new file mode 100644 index 0000000000..15a03c1479 --- /dev/null +++ b/spec/ruby/core/fiber/scheduler_spec.rb @@ -0,0 +1,8 @@ +require_relative '../../spec_helper' +require_relative 'shared/scheduler' + +require "fiber" + +describe "Fiber.scheduler" do + it_behaves_like :scheduler, :scheduler +end diff --git a/spec/ruby/core/fiber/set_scheduler_spec.rb b/spec/ruby/core/fiber/set_scheduler_spec.rb new file mode 100644 index 0000000000..82f6acbe86 --- /dev/null +++ b/spec/ruby/core/fiber/set_scheduler_spec.rb @@ -0,0 +1,8 @@ +require_relative '../../spec_helper' +require_relative 'shared/scheduler' + +require "fiber" + +describe "Fiber.scheduler" do + it_behaves_like :scheduler, :set_scheduler +end diff --git a/spec/ruby/core/fiber/shared/blocking.rb b/spec/ruby/core/fiber/shared/blocking.rb new file mode 100644 index 0000000000..21707e1ea7 --- /dev/null +++ b/spec/ruby/core/fiber/shared/blocking.rb @@ -0,0 +1,41 @@ +describe :non_blocking_fiber, shared: true do + context "root Fiber of the main thread" do + it "returns false" do + fiber = Fiber.new { @method.call } + blocking = fiber.resume + + blocking.should == false + end + + it "returns false for blocking: false" do + fiber = Fiber.new(blocking: false) { @method.call } + blocking = fiber.resume + + blocking.should == false + end + end + + context "root Fiber of a new thread" do + it "returns false" do + thread = Thread.new do + fiber = Fiber.new { @method.call } + blocking = fiber.resume + + blocking.should == false + end + + thread.join + end + + it "returns false for blocking: false" do + thread = Thread.new do + fiber = Fiber.new(blocking: false) { @method.call } + blocking = fiber.resume + + blocking.should == false + end + + thread.join + end + end +end diff --git a/spec/ruby/core/fiber/shared/resume.rb b/spec/ruby/core/fiber/shared/resume.rb new file mode 100644 index 0000000000..5ee27d1d24 --- /dev/null +++ b/spec/ruby/core/fiber/shared/resume.rb @@ -0,0 +1,58 @@ +describe :fiber_resume, shared: true do + it "can be invoked from the root Fiber" do + fiber = Fiber.new { :fiber } + fiber.send(@method).should == :fiber + end + + it "raises a FiberError if invoked from a different Thread" do + fiber = Fiber.new { 42 } + Thread.new do + -> { + fiber.send(@method) + }.should raise_error(FiberError) + end.join + + # Check the Fiber can still be used + fiber.send(@method).should == 42 + end + + it "passes control to the beginning of the block on first invocation" do + invoked = false + fiber = Fiber.new { invoked = true } + fiber.send(@method) + invoked.should be_true + end + + it "returns the last value encountered on first invocation" do + fiber = Fiber.new { 1+1; true } + fiber.send(@method).should be_true + end + + it "runs until the end of the block" do + obj = mock('obj') + obj.should_receive(:do).once + fiber = Fiber.new { 1 + 2; a = "glark"; obj.do } + fiber.send(@method) + end + + it "accepts any number of arguments" do + fiber = Fiber.new { |a| } + -> { fiber.send(@method, *(1..10).to_a) }.should_not raise_error + end + + it "raises a FiberError if the Fiber is dead" do + fiber = Fiber.new { true } + fiber.send(@method) + -> { fiber.send(@method) }.should raise_error(FiberError) + end + + it "raises a LocalJumpError if the block includes a return statement" do + fiber = Fiber.new { return; } + -> { fiber.send(@method) }.should raise_error(LocalJumpError) + end + + it "raises a LocalJumpError if the block includes a break statement" do + fiber = Fiber.new { break; } + -> { fiber.send(@method) }.should raise_error(LocalJumpError) + end +end diff --git a/spec/ruby/core/fiber/shared/scheduler.rb b/spec/ruby/core/fiber/shared/scheduler.rb new file mode 100644 index 0000000000..19bfb75e3e --- /dev/null +++ b/spec/ruby/core/fiber/shared/scheduler.rb @@ -0,0 +1,51 @@ +describe :scheduler, shared: true do + it "validates the scheduler for required methods" do + required_methods = [:block, :unblock, :kernel_sleep, :io_wait] + required_methods.each do |missing_method| + scheduler = Object.new + required_methods.difference([missing_method]).each do |method| + scheduler.define_singleton_method(method) {} + end + -> { + suppress_warning { Fiber.set_scheduler(scheduler) } + }.should raise_error(ArgumentError, /Scheduler must implement ##{missing_method}/) + end + end + + it "can set and get the scheduler" do + required_methods = [:block, :unblock, :kernel_sleep, :io_wait] + scheduler = Object.new + required_methods.each do |method| + scheduler.define_singleton_method(method) {} + end + suppress_warning { Fiber.set_scheduler(scheduler) } + Fiber.scheduler.should == scheduler + end + + it "returns the scheduler after setting it" do + required_methods = [:block, :unblock, :kernel_sleep, :io_wait] + scheduler = Object.new + required_methods.each do |method| + scheduler.define_singleton_method(method) {} + end + result = suppress_warning { Fiber.set_scheduler(scheduler) } + result.should == scheduler + end + + it "can remove the scheduler" do + required_methods = [:block, :unblock, :kernel_sleep, :io_wait] + scheduler = Object.new + required_methods.each do |method| + scheduler.define_singleton_method(method) {} + end + suppress_warning { Fiber.set_scheduler(scheduler) } + Fiber.set_scheduler(nil) + Fiber.scheduler.should be_nil + end + + it "can assign a nil scheduler multiple times" do + Fiber.set_scheduler(nil) + Fiber.set_scheduler(nil) + Fiber.scheduler.should be_nil + end +end diff --git a/spec/ruby/core/fiber/storage_spec.rb b/spec/ruby/core/fiber/storage_spec.rb new file mode 100644 index 0000000000..015caaf3bb --- /dev/null +++ b/spec/ruby/core/fiber/storage_spec.rb @@ -0,0 +1,181 @@ +require_relative '../../spec_helper' + +describe "Fiber.new(storage:)" do + it "creates a Fiber with the given storage" do + storage = {life: 42} + fiber = Fiber.new(storage: storage) { Fiber.current.storage } + fiber.resume.should == storage + end + + it "creates a fiber with lazily initialized storage" do + Fiber.new(storage: nil) { Fiber[:x] = 10; Fiber.current.storage }.resume.should == {x: 10} + end + + it "creates a fiber by inheriting the storage of the parent fiber" do + fiber = Fiber.new(storage: {life: 42}) do + Fiber.new { Fiber.current.storage }.resume + end + fiber.resume.should == {life: 42} + end + + it "cannot create a fiber with non-hash storage" do + -> { Fiber.new(storage: 42) {} }.should raise_error(TypeError) + end + + it "cannot create a fiber with a frozen hash as storage" do + -> { Fiber.new(storage: {life: 43}.freeze) {} }.should raise_error(FrozenError) + end + + it "cannot create a fiber with a storage hash with non-symbol keys" do + -> { Fiber.new(storage: {life: 43, Object.new => 44}) {} }.should raise_error(TypeError) + end +end + +describe "Fiber#storage" do + it "cannot be accessed from a different fiber" do + f = Fiber.new(storage: {life: 42}) { nil } + -> { + f.storage + }.should raise_error(ArgumentError, /Fiber storage can only be accessed from the Fiber it belongs to/) + end +end + +describe "Fiber#storage=" do + it "can clear the storage of the fiber" do + fiber = Fiber.new(storage: {life: 42}) do + Fiber.current.storage = nil + Fiber[:x] = 10 + Fiber.current.storage + end + fiber.resume.should == {x: 10} + end + + it "can set the storage of the fiber" do + fiber = Fiber.new(storage: {life: 42}) do + Fiber.current.storage = {life: 43} + Fiber.current.storage + end + fiber.resume.should == {life: 43} + end + + it "can't set the storage of the fiber to non-hash" do + -> { Fiber.current.storage = 42 }.should raise_error(TypeError) + end + + it "can't set the storage of the fiber to a frozen hash" do + -> { Fiber.current.storage = {life: 43}.freeze }.should raise_error(FrozenError) + end + + it "can't set the storage of the fiber to a hash with non-symbol keys" do + -> { Fiber.current.storage = {life: 43, Object.new => 44} }.should raise_error(TypeError) + end +end + +describe "Fiber.[]" do + it "returns the value of the given key in the storage of the current fiber" do + Fiber.new(storage: {life: 42}) { Fiber[:life] }.resume.should == 42 + end + + it "returns nil if the key is not present in the storage of the current fiber" do + Fiber.new(storage: {life: 42}) { Fiber[:death] }.resume.should be_nil + end + + it "returns nil if the current fiber has no storage" do + Fiber.new { Fiber[:life] }.resume.should be_nil + end + + ruby_version_is "3.2.3" do + it "can use dynamically defined keys" do + key = :"#{self.class.name}#.#{self.object_id}" + Fiber.new { Fiber[key] = 42; Fiber[key] }.resume.should == 42 + end + + it "can't use invalid keys" do + invalid_keys = [Object.new, 12] + invalid_keys.each do |key| + -> { Fiber[key] }.should raise_error(TypeError) + end + end + end + + ruby_bug "#20978", ""..."3.4" do + it "can use keys as strings" do + key = Object.new + def key.to_str; "Foo"; end + Fiber.new { Fiber[key] = 42; Fiber["Foo"] }.resume.should == 42 + end + + it "converts a String key into a Symbol" do + Fiber.new { Fiber["key"] = 42; Fiber[:key] }.resume.should == 42 + Fiber.new { Fiber[:key] = 42; Fiber["key"] }.resume.should == 42 + end + + it "can use any object that responds to #to_str as a key" do + key = mock("key") + key.should_receive(:to_str).twice.and_return("key") + Fiber.new { Fiber[key] = 42; Fiber[key] }.resume.should == 42 + end + end + + it "does not call #to_sym on the key" do + key = mock("key") + key.should_not_receive(:to_sym) + -> { Fiber[key] }.should raise_error(TypeError) + end + + it "can access the storage of the parent fiber" do + f = Fiber.new(storage: {life: 42}) do + Fiber.new { Fiber[:life] }.resume + end + f.resume.should == 42 + end + + it "can't access the storage of the fiber with non-symbol keys" do + -> { Fiber[Object.new] }.should raise_error(TypeError) + end +end + +describe "Fiber.[]=" do + it "sets the value of the given key in the storage of the current fiber" do + Fiber.new(storage: {life: 42}) { Fiber[:life] = 43; Fiber[:life] }.resume.should == 43 + end + + it "sets the value of the given key in the storage of the current fiber" do + Fiber.new(storage: {life: 42}) { Fiber[:death] = 43; Fiber[:death] }.resume.should == 43 + end + + it "sets the value of the given key in the storage of the current fiber" do + Fiber.new { Fiber[:life] = 43; Fiber[:life] }.resume.should == 43 + end + + it "does not overwrite the storage of the parent fiber" do + f = Fiber.new(storage: {life: 42}) do + Fiber.yield Fiber.new { Fiber[:life] = 43; Fiber[:life] }.resume + Fiber[:life] + end + f.resume.should == 43 # Value of the inner fiber + f.resume.should == 42 # Value of the outer fiber + end + + it "can't access the storage of the fiber with non-symbol keys" do + -> { Fiber[Object.new] = 44 }.should raise_error(TypeError) + end + + ruby_version_is "3.3" do + it "deletes the fiber storage key when assigning nil" do + Fiber.new(storage: {life: 42}) { + Fiber[:life] = nil + Fiber.current.storage + }.resume.should == {} + end + end +end + +describe "Thread.new" do + it "creates a thread with the storage of the current fiber" do + fiber = Fiber.new(storage: {life: 42}) do + Thread.new { Fiber.current.storage }.value + end + fiber.resume.should == {life: 42} + end +end diff --git a/spec/ruby/core/fiber/transfer_spec.rb b/spec/ruby/core/fiber/transfer_spec.rb new file mode 100644 index 0000000000..238721475d --- /dev/null +++ b/spec/ruby/core/fiber/transfer_spec.rb @@ -0,0 +1,84 @@ +require_relative '../../spec_helper' +require_relative 'shared/resume' + +describe "Fiber#transfer" do + it_behaves_like :fiber_resume, :transfer +end + +describe "Fiber#transfer" do + it "transfers control from one Fiber to another when called from a Fiber" do + fiber1 = Fiber.new { :fiber1 } + fiber2 = Fiber.new { fiber1.transfer; :fiber2 } + fiber2.resume.should == :fiber2 + end + + it "returns to the root Fiber when finished" do + f1 = Fiber.new { :fiber_1 } + f2 = Fiber.new { f1.transfer; :fiber_2 } + + f2.transfer.should == :fiber_1 + f2.transfer.should == :fiber_2 + end + + it "can be invoked from the same Fiber it transfers control to" do + states = [] + fiber = Fiber.new { states << :start; fiber.transfer; states << :end } + fiber.transfer + states.should == [:start, :end] + + states = [] + fiber = Fiber.new { states << :start; fiber.transfer; states << :end } + fiber.resume + states.should == [:start, :end] + end + + it "can not transfer control to a Fiber that has suspended by Fiber.yield" do + states = [] + fiber1 = Fiber.new { states << :fiber1 } + fiber2 = Fiber.new { states << :fiber2_start; Fiber.yield fiber1.transfer; states << :fiber2_end} + fiber2.resume.should == [:fiber2_start, :fiber1] + -> { fiber2.transfer }.should raise_error(FiberError) + end + + it "raises a FiberError when transferring to a Fiber which resumes itself" do + fiber = Fiber.new { fiber.resume } + -> { fiber.transfer }.should raise_error(FiberError) + end + + it "works if Fibers in different Threads each transfer to a Fiber in the same Thread" do + # This catches a bug where Fibers are running on a thread-pool + # and Fibers from a different Ruby Thread reuse the same native thread. + # Caching the Ruby Thread based on the native thread is not correct in that case, + # and the check for "fiber called across threads" in Fiber#transfer + # might be incorrect based on that. + 2.times do + Thread.new do + io_fiber = Fiber.new do |calling_fiber| + calling_fiber.transfer + end + io_fiber.transfer(Fiber.current) + value = Object.new + io_fiber.transfer(value).should equal value + end.join + end + end + + it "transfers control between a non-main thread's root fiber to a child fiber and back again" do + states = [] + thread = Thread.new do + f1 = Fiber.new do |f0| + states << 0 + value2 = f0.transfer(1) + states << value2 + 3 + end + + value1 = f1.transfer(Fiber.current) + states << value1 + value3 = f1.transfer(2) + states << value3 + end + thread.join + states.should == [0, 1, 2, 3] + end +end diff --git a/spec/ruby/core/fiber/yield_spec.rb b/spec/ruby/core/fiber/yield_spec.rb new file mode 100644 index 0000000000..b010912c87 --- /dev/null +++ b/spec/ruby/core/fiber/yield_spec.rb @@ -0,0 +1,49 @@ +require_relative '../../spec_helper' + +describe "Fiber.yield" do + it "passes control to the Fiber's caller" do + step = 0 + fiber = Fiber.new { step = 1; Fiber.yield; step = 2; Fiber.yield; step = 3 } + fiber.resume + step.should == 1 + fiber.resume + step.should == 2 + end + + it "returns its arguments to the caller" do + fiber = Fiber.new { true; Fiber.yield :glark; true } + fiber.resume.should == :glark + fiber.resume + end + + it "returns nil to the caller if given no arguments" do + fiber = Fiber.new { true; Fiber.yield; true } + fiber.resume.should be_nil + fiber.resume + end + + it "returns to the Fiber the value of the #resume call that invoked it" do + fiber = Fiber.new { Fiber.yield.should == :caller } + fiber.resume + fiber.resume :caller + end + + it "does not propagate or reraise a rescued exception" do + fiber = Fiber.new do + begin + raise "an error in a Fiber" + rescue + Fiber.yield :first + end + + :second + end + + fiber.resume.should == :first + fiber.resume.should == :second + end + + it "raises a FiberError if called from the root Fiber" do + ->{ Fiber.yield }.should raise_error(FiberError) + end +end diff --git a/spec/ruby/core/file/absolute_path_spec.rb b/spec/ruby/core/file/absolute_path_spec.rb new file mode 100644 index 0000000000..315eead34f --- /dev/null +++ b/spec/ruby/core/file/absolute_path_spec.rb @@ -0,0 +1,94 @@ +require_relative '../../spec_helper' + +describe "File.absolute_path?" do + before :each do + @abs = File.expand_path(__FILE__) + end + + it "returns true if it's an absolute pathname" do + File.absolute_path?(@abs).should be_true + end + + it "returns false if it's a relative path" do + File.absolute_path?(File.basename(__FILE__)).should be_false + end + + it "returns false if it's a tricky relative path" do + File.absolute_path?("C:foo\\bar").should be_false + end + + it "does not expand '~' to a home directory." do + File.absolute_path?('~').should be_false + end + + it "does not expand '~user' to a home directory." do + path = File.dirname(@abs) + Dir.chdir(path) do + File.absolute_path?('~user').should be_false + end + end + + it "calls #to_path on its argument" do + mock = mock_to_path(File.expand_path(__FILE__)) + + File.absolute_path?(mock).should be_true + end + + platform_is_not :windows do + it "takes into consideration the platform's root" do + File.absolute_path?("C:\\foo\\bar").should be_false + File.absolute_path?("C:/foo/bar").should be_false + File.absolute_path?("/foo/bar\\baz").should be_true + end + end + + platform_is :windows do + it "takes into consideration the platform path separator(s)" do + File.absolute_path?("C:\\foo\\bar").should be_true + File.absolute_path?("C:/foo/bar").should be_true + File.absolute_path?("/foo/bar\\baz").should be_false + end + end +end + +describe "File.absolute_path" do + before :each do + @abs = File.expand_path(__FILE__) + end + + it "returns the argument if it's an absolute pathname" do + File.absolute_path(@abs).should == @abs + end + + it "resolves paths relative to the current working directory" do + path = File.dirname(@abs) + Dir.chdir(path) do + File.absolute_path('hello.txt').should == File.join(Dir.pwd, 'hello.txt') + end + end + + it "does not expand '~' to a home directory." do + File.absolute_path('~').should_not == File.expand_path('~') + end + + platform_is_not :windows do + it "does not expand '~' when given dir argument" do + File.absolute_path('~', '/').should == '/~' + end + end + + it "does not expand '~user' to a home directory." do + path = File.dirname(@abs) + Dir.chdir(path) do + File.absolute_path('~user').should == File.join(Dir.pwd, '~user') + end + end + + it "accepts a second argument of a directory from which to resolve the path" do + File.absolute_path(__FILE__, __dir__).should == @abs + end + + it "calls #to_path on its argument" do + File.absolute_path(mock_to_path(@abs)).should == @abs + end +end diff --git a/spec/ruby/core/file/atime_spec.rb b/spec/ruby/core/file/atime_spec.rb new file mode 100644 index 0000000000..e47e70e5ac --- /dev/null +++ b/spec/ruby/core/file/atime_spec.rb @@ -0,0 +1,60 @@ +require_relative '../../spec_helper' + +describe "File.atime" do + before :each do + @file = tmp('test.txt') + touch @file + end + + after :each do + rm_r @file + end + + it "returns the last access time for the named file as a Time object" do + File.atime(@file) + File.atime(@file).should be_kind_of(Time) + end + + platform_is :linux, :windows do + unless ENV.key?('TRAVIS') # https://bugs.ruby-lang.org/issues/17926 + ## NOTE also that some Linux systems disable atime (e.g. via mount params) for better filesystem speed. + it "returns the last access time for the named file with microseconds" do + supports_subseconds = Integer(`stat -c%x '#{__FILE__}'`[/\.(\d{1,6})/, 1], 10) + if supports_subseconds != 0 + expected_time = Time.at(Time.now.to_i + 0.123456) + File.utime expected_time, 0, @file + File.atime(@file).usec.should == expected_time.usec + else + File.atime(__FILE__).usec.should == 0 + end + rescue Errno::ENOENT => e + # Native Windows don't have stat command. + skip e.message + end + end + end + + it "raises an Errno::ENOENT exception if the file is not found" do + -> { File.atime('a_fake_file') }.should raise_error(Errno::ENOENT) + end + + it "accepts an object that has a #to_path method" do + File.atime(mock_to_path(@file)) + end +end + +describe "File#atime" do + before :each do + @name = File.expand_path(__FILE__) + @file = File.open(@name) + end + + after :each do + @file.close rescue nil + end + + it "returns the last access time to self" do + @file.atime + @file.atime.should be_kind_of(Time) + end +end diff --git a/spec/ruby/core/file/basename_spec.rb b/spec/ruby/core/file/basename_spec.rb new file mode 100644 index 0000000000..989409d76b --- /dev/null +++ b/spec/ruby/core/file/basename_spec.rb @@ -0,0 +1,183 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' + +# TODO: Fix these +describe "File.basename" do + it "returns the basename of a path (basic cases)" do + File.basename("/Some/path/to/test.txt").should == "test.txt" + File.basename(File.join("/tmp")).should == "tmp" + File.basename(File.join(*%w( g f d s a b))).should == "b" + File.basename("/tmp", ".*").should == "tmp" + File.basename("/tmp", ".c").should == "tmp" + File.basename("/tmp.c", ".c").should == "tmp" + File.basename("/tmp.c", ".*").should == "tmp" + File.basename("/tmp.c", ".?").should == "tmp.c" + File.basename("/tmp.cpp", ".*").should == "tmp" + File.basename("/tmp.cpp", ".???").should == "tmp.cpp" + File.basename("/tmp.o", ".c").should == "tmp.o" + File.basename(File.join("/tmp/")).should == "tmp" + File.basename("/").should == "/" + File.basename("//").should == "/" + File.basename("dir///base", ".*").should == "base" + File.basename("dir///base", ".c").should == "base" + File.basename("dir///base.c", ".c").should == "base" + File.basename("dir///base.c", ".*").should == "base" + File.basename("dir///base.o", ".c").should == "base.o" + File.basename("dir///base///").should == "base" + File.basename("dir//base/", ".*").should == "base" + File.basename("dir//base/", ".c").should == "base" + File.basename("dir//base.c/", ".c").should == "base" + File.basename("dir//base.c/", ".*").should == "base" + end + + it "returns the last component of the filename" do + File.basename('a').should == 'a' + File.basename('/a').should == 'a' + File.basename('/a/b').should == 'b' + File.basename('/ab/ba/bag').should == 'bag' + File.basename('/ab/ba/bag.txt').should == 'bag.txt' + File.basename('/').should == '/' + File.basename('/foo/bar/baz.rb', '.rb').should == 'baz' + File.basename('baz.rb', 'z.rb').should == 'ba' + end + + it "returns an string" do + File.basename("foo").should be_kind_of(String) + end + + it "returns the basename for unix format" do + File.basename("/foo/bar").should == "bar" + File.basename("/foo/bar.txt").should == "bar.txt" + File.basename("bar.c").should == "bar.c" + File.basename("/bar").should == "bar" + File.basename("/bar/").should == "bar" + + # Considered UNC paths on Windows + platform_is :windows do + File.basename("baz//foo").should =="foo" + File.basename("//foo/bar/baz").should == "baz" + end + end + + it "returns the basename for edge cases" do + File.basename("").should == "" + File.basename(".").should == "." + File.basename("..").should == ".." + platform_is_not :windows do + File.basename("//foo/").should == "foo" + File.basename("//foo//").should == "foo" + end + File.basename("foo/").should == "foo" + end + + it "ignores a trailing directory separator" do + File.basename("foo.rb/", '.rb').should == "foo" + File.basename("bar.rb///", '.*').should == "bar" + end + + it "returns the basename for unix suffix" do + File.basename("bar.c", ".c").should == "bar" + File.basename("bar.txt", ".txt").should == "bar" + File.basename("/bar.txt", ".txt").should == "bar" + File.basename("/foo/bar.txt", ".txt").should == "bar" + File.basename("bar.txt", ".exe").should == "bar.txt" + File.basename("bar.txt.exe", ".exe").should == "bar.txt" + File.basename("bar.txt.exe", ".txt").should == "bar.txt.exe" + File.basename("bar.txt", ".*").should == "bar" + File.basename("bar.txt.exe", ".*").should == "bar.txt" + File.basename("bar.txt.exe", ".txt.exe").should == "bar" + end + + platform_is_not :windows do + it "takes into consideration the platform path separator(s)" do + File.basename("C:\\foo\\bar").should == "C:\\foo\\bar" + File.basename("C:/foo/bar").should == "bar" + File.basename("/foo/bar\\baz").should == "bar\\baz" + end + end + + platform_is :windows do + it "takes into consideration the platform path separator(s)" do + File.basename("C:\\foo\\bar").should == "bar" + File.basename("C:/foo/bar").should == "bar" + File.basename("/foo/bar\\baz").should == "baz" + end + end + + it "raises a TypeError if the arguments are not String types" do + -> { File.basename(nil) }.should raise_error(TypeError) + -> { File.basename(1) }.should raise_error(TypeError) + -> { File.basename("bar.txt", 1) }.should raise_error(TypeError) + -> { File.basename(true) }.should raise_error(TypeError) + end + + it "accepts an object that has a #to_path method" do + File.basename(mock_to_path("foo.txt")) + end + + it "raises an ArgumentError if passed more than two arguments" do + -> { File.basename('bar.txt', '.txt', '.txt') }.should raise_error(ArgumentError) + end + + # specific to MS Windows + platform_is :windows do + it "returns the basename for windows" do + File.basename("C:\\foo\\bar\\baz.txt").should == "baz.txt" + File.basename("C:\\foo\\bar").should == "bar" + File.basename("C:\\foo\\bar\\").should == "bar" + File.basename("C:\\foo").should == "foo" + File.basename("C:\\").should == "\\" + end + + it "returns basename windows unc" do + File.basename("\\\\foo\\bar\\baz.txt").should == "baz.txt" + File.basename("\\\\foo\\bar\\baz").should =="baz" + end + + it "returns basename windows forward slash" do + File.basename("C:/").should == "/" + File.basename("C:/foo").should == "foo" + File.basename("C:/foo/bar").should == "bar" + File.basename("C:/foo/bar/").should == "bar" + File.basename("C:/foo/bar//").should == "bar" + end + + it "returns basename with windows suffix" do + File.basename("c:\\bar.txt", ".txt").should == "bar" + File.basename("c:\\foo\\bar.txt", ".txt").should == "bar" + File.basename("c:\\bar.txt", ".exe").should == "bar.txt" + File.basename("c:\\bar.txt.exe", ".exe").should == "bar.txt" + File.basename("c:\\bar.txt.exe", ".txt").should == "bar.txt.exe" + File.basename("c:\\bar.txt", ".*").should == "bar" + File.basename("c:\\bar.txt.exe", ".*").should == "bar.txt" + end + end + + + it "returns the extension for a multibyte filename" do + File.basename('/path/ОфиÑ.m4a').should == "ОфиÑ.m4a" + end + + it "returns the basename with the same encoding as the original" do + basename = File.basename('C:/Users/Scuby Pagrubý'.encode(Encoding::Windows_1250)) + basename.should == 'Scuby Pagrubý'.encode(Encoding::Windows_1250) + basename.encoding.should == Encoding::Windows_1250 + end + + it "returns a new unfrozen String" do + exts = [nil, '.rb', '.*', '.txt'] + ['foo.rb','//', '/test/', 'test'].each do |example| + exts.each do |ext| + original = example.freeze + result = if ext + File.basename(original, ext) + else + File.basename(original) + end + result.should_not equal(original) + result.frozen?.should == false + end + end + end + +end diff --git a/spec/ruby/core/file/birthtime_spec.rb b/spec/ruby/core/file/birthtime_spec.rb new file mode 100644 index 0000000000..f82eaf7cca --- /dev/null +++ b/spec/ruby/core/file/birthtime_spec.rb @@ -0,0 +1,56 @@ +require_relative '../../spec_helper' + +platform_is :windows, :darwin, :freebsd, :netbsd, :linux do + not_implemented_messages = [ + "birthtime() function is unimplemented", # unsupported OS/version + "birthtime is unimplemented", # unsupported filesystem + ] + + describe "File.birthtime" do + before :each do + @file = __FILE__ + end + + after :each do + @file = nil + end + + it "returns the birth time for the named file as a Time object" do + File.birthtime(@file) + File.birthtime(@file).should be_kind_of(Time) + rescue NotImplementedError => e + e.message.should.start_with?(*not_implemented_messages) + end + + it "accepts an object that has a #to_path method" do + File.birthtime(@file) # Avoid to failure of mock object with old Kernel and glibc + File.birthtime(mock_to_path(@file)) + rescue NotImplementedError => e + e.message.should.start_with?(*not_implemented_messages) + end + + it "raises an Errno::ENOENT exception if the file is not found" do + -> { File.birthtime('bogus') }.should raise_error(Errno::ENOENT) + rescue NotImplementedError => e + e.message.should.start_with?(*not_implemented_messages) + end + end + + describe "File#birthtime" do + before :each do + @file = File.open(__FILE__) + end + + after :each do + @file.close + @file = nil + end + + it "returns the birth time for self" do + @file.birthtime + @file.birthtime.should be_kind_of(Time) + rescue NotImplementedError => e + e.message.should.start_with?(*not_implemented_messages) + end + end +end diff --git a/spec/ruby/core/file/blockdev_spec.rb b/spec/ruby/core/file/blockdev_spec.rb new file mode 100644 index 0000000000..9ba9afc251 --- /dev/null +++ b/spec/ruby/core/file/blockdev_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/blockdev' + +describe "File.blockdev?" do + it_behaves_like :file_blockdev, :blockdev?, File +end diff --git a/spec/ruby/core/file/chardev_spec.rb b/spec/ruby/core/file/chardev_spec.rb new file mode 100644 index 0000000000..1fc932ee4e --- /dev/null +++ b/spec/ruby/core/file/chardev_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/chardev' + +describe "File.chardev?" do + it_behaves_like :file_chardev, :chardev?, File +end diff --git a/spec/ruby/core/file/chmod_spec.rb b/spec/ruby/core/file/chmod_spec.rb new file mode 100644 index 0000000000..5ca15c9748 --- /dev/null +++ b/spec/ruby/core/file/chmod_spec.rb @@ -0,0 +1,185 @@ +require_relative '../../spec_helper' + +describe "File#chmod" do + before :each do + @filename = tmp('i_exist.exe') + @file = File.open(@filename, 'w') + end + + after :each do + @file.close + rm_r @filename + end + + it "returns 0 if successful" do + @file.chmod(0755).should == 0 + end + + it "raises RangeError with too large values" do + -> { @file.chmod(2**64) }.should raise_error(RangeError) + -> { @file.chmod(-2**63 - 1) }.should raise_error(RangeError) + end + + it "invokes to_int on non-integer argument" do + mode = File.stat(@filename).mode + (obj = mock('mode')).should_receive(:to_int).and_return(mode) + @file.chmod(obj) + File.stat(@filename).mode.should == mode + end + + platform_is :windows do + it "with '0444' makes file readable and executable but not writable" do + @file.chmod(0444) + File.readable?(@filename).should == true + File.writable?(@filename).should == false + File.executable?(@filename).should == true + end + + it "with '0644' makes file readable and writable and also executable" do + @file.chmod(0644) + File.readable?(@filename).should == true + File.writable?(@filename).should == true + File.executable?(@filename).should == true + end + end + + platform_is_not :windows do + as_user do + it "with '0222' makes file writable but not readable or executable" do + @file.chmod(0222) + File.readable?(@filename).should == false + File.writable?(@filename).should == true + File.executable?(@filename).should == false + end + + it "with '0444' makes file readable but not writable or executable" do + @file.chmod(0444) + File.readable?(@filename).should == true + File.writable?(@filename).should == false + File.executable?(@filename).should == false + end + + it "with '0666' makes file readable and writable but not executable" do + @file.chmod(0666) + File.readable?(@filename).should == true + File.writable?(@filename).should == true + File.executable?(@filename).should == false + end + + it "with '0111' makes file executable but not readable or writable" do + @file.chmod(0111) + File.readable?(@filename).should == false + File.writable?(@filename).should == false + File.executable?(@filename).should == true + end + + it "modifies the permission bits of the files specified" do + @file.chmod(0755) + File.stat(@filename).mode.should == 33261 + end + end + end +end + +describe "File.chmod" do + before :each do + @file = tmp('i_exist.exe') + touch @file + @count = File.chmod(0755, @file) + end + + after :each do + rm_r @file + end + + it "returns the number of files modified" do + @count.should == 1 + end + + it "raises RangeError with too large values" do + -> { File.chmod(2**64, @file) }.should raise_error(RangeError) + -> { File.chmod(-2**63 - 1, @file) }.should raise_error(RangeError) + end + + it "accepts an object that has a #to_path method" do + File.chmod(0, mock_to_path(@file)) + end + + it "throws a TypeError if the given path is not coercible into a string" do + -> { File.chmod(0, []) }.should raise_error(TypeError) + end + + it "raises an error for a non existent path" do + -> { + File.chmod(0644, "#{@file}.not.existing") + }.should raise_error(Errno::ENOENT) + end + + it "invokes to_int on non-integer argument" do + mode = File.stat(@file).mode + (obj = mock('mode')).should_receive(:to_int).and_return(mode) + File.chmod(obj, @file) + File.stat(@file).mode.should == mode + end + + it "invokes to_str on non-string file names" do + mode = File.stat(@file).mode + (obj = mock('path')).should_receive(:to_str).and_return(@file) + File.chmod(mode, obj) + File.stat(@file).mode.should == mode + end + + platform_is :windows do + it "with '0444' makes file readable and executable but not writable" do + File.chmod(0444, @file) + File.readable?(@file).should == true + File.writable?(@file).should == false + File.executable?(@file).should == true + end + + it "with '0644' makes file readable and writable and also executable" do + File.chmod(0644, @file) + File.readable?(@file).should == true + File.writable?(@file).should == true + File.executable?(@file).should == true + end + end + + platform_is_not :windows do + as_user do + it "with '0222' makes file writable but not readable or executable" do + File.chmod(0222, @file) + File.readable?(@file).should == false + File.writable?(@file).should == true + File.executable?(@file).should == false + end + + it "with '0444' makes file readable but not writable or executable" do + File.chmod(0444, @file) + File.readable?(@file).should == true + File.writable?(@file).should == false + File.executable?(@file).should == false + end + end + + it "with '0666' makes file readable and writable but not executable" do + File.chmod(0666, @file) + File.readable?(@file).should == true + File.writable?(@file).should == true + File.executable?(@file).should == false + end + + as_user do + it "with '0111' makes file executable but not readable or writable" do + File.chmod(0111, @file) + File.readable?(@file).should == false + File.writable?(@file).should == false + File.executable?(@file).should == true + end + end + + it "modifies the permission bits of the files specified" do + File.stat(@file).mode.should == 33261 + end + end +end diff --git a/spec/ruby/core/file/chown_spec.rb b/spec/ruby/core/file/chown_spec.rb new file mode 100644 index 0000000000..4db0e3712c --- /dev/null +++ b/spec/ruby/core/file/chown_spec.rb @@ -0,0 +1,144 @@ +require_relative '../../spec_helper' + +describe "File.chown" do + before :each do + @fname = tmp('file_chown_test') + touch @fname + end + + after :each do + rm_r @fname + end + + as_superuser do + platform_is :windows do + it "does not modify the owner id of the file" do + File.chown 0, nil, @fname + File.stat(@fname).uid.should == 0 + File.chown 501, nil, @fname + File.stat(@fname).uid.should == 0 + end + + it "does not modify the group id of the file" do + File.chown nil, 0, @fname + File.stat(@fname).gid.should == 0 + File.chown nil, 501, @fname + File.stat(@fname).gid.should == 0 + end + end + + platform_is_not :windows do + it "changes the owner id of the file" do + File.chown 501, nil, @fname + File.stat(@fname).uid.should == 501 + File.chown 0, nil, @fname + File.stat(@fname).uid.should == 0 + end + + it "changes the group id of the file" do + File.chown nil, 501, @fname + File.stat(@fname).gid.should == 501 + File.chown nil, 0, @fname + File.stat(@fname).uid.should == 0 + end + + it "does not modify the owner id of the file if passed nil or -1" do + File.chown 501, nil, @fname + File.chown nil, nil, @fname + File.stat(@fname).uid.should == 501 + File.chown nil, -1, @fname + File.stat(@fname).uid.should == 501 + end + + it "does not modify the group id of the file if passed nil or -1" do + File.chown nil, 501, @fname + File.chown nil, nil, @fname + File.stat(@fname).gid.should == 501 + File.chown nil, -1, @fname + File.stat(@fname).gid.should == 501 + end + end + end + + it "returns the number of files processed" do + File.chown(nil, nil, @fname, @fname).should == 2 + end + + platform_is_not :windows do + it "raises an error for a non existent path" do + -> { + File.chown(nil, nil, "#{@fname}_not_existing") + }.should raise_error(Errno::ENOENT) + end + end + + it "accepts an object that has a #to_path method" do + File.chown(nil, nil, mock_to_path(@fname)).should == 1 + end +end + +describe "File#chown" do + before :each do + @fname = tmp('file_chown_test') + @file = File.open(@fname, 'w') + end + + after :each do + @file.close unless @file.closed? + rm_r @fname + end + + as_superuser do + platform_is :windows do + it "does not modify the owner id of the file" do + @file.chown 0, nil + @file.stat.uid.should == 0 + @file.chown 501, nil + @file.stat.uid.should == 0 + end + + it "does not modify the group id of the file" do + @file.chown nil, 0 + @file.stat.gid.should == 0 + @file.chown nil, 501 + @file.stat.gid.should == 0 + end + end + + platform_is_not :windows do + it "changes the owner id of the file" do + @file.chown 501, nil + @file.stat.uid.should == 501 + @file.chown 0, nil + @file.stat.uid.should == 0 + end + + it "changes the group id of the file" do + @file.chown nil, 501 + @file.stat.gid.should == 501 + @file.chown nil, 0 + @file.stat.uid.should == 0 + end + + it "does not modify the owner id of the file if passed nil or -1" do + @file.chown 501, nil + @file.chown nil, nil + @file.stat.uid.should == 501 + @file.chown nil, -1 + @file.stat.uid.should == 501 + end + + it "does not modify the group id of the file if passed nil or -1" do + @file.chown nil, 501 + @file.chown nil, nil + @file.stat.gid.should == 501 + @file.chown nil, -1 + @file.stat.gid.should == 501 + end + end + end + + it "returns 0" do + @file.chown(nil, nil).should == 0 + end +end diff --git a/spec/ruby/core/file/constants/constants_spec.rb b/spec/ruby/core/file/constants/constants_spec.rb new file mode 100644 index 0000000000..bba248c21e --- /dev/null +++ b/spec/ruby/core/file/constants/constants_spec.rb @@ -0,0 +1,31 @@ +require_relative '../../../spec_helper' + +["APPEND", "CREAT", "EXCL", "FNM_CASEFOLD", + "FNM_DOTMATCH", "FNM_EXTGLOB", "FNM_NOESCAPE", "FNM_PATHNAME", + "FNM_SYSCASE", "LOCK_EX", "LOCK_NB", "LOCK_SH", + "LOCK_UN", "NONBLOCK", "RDONLY", + "RDWR", "TRUNC", "WRONLY", "SHARE_DELETE"].each do |const| + describe "File::Constants::#{const}" do + it "is defined" do + File::Constants.const_defined?(const).should be_true + end + end +end + +platform_is :windows do + describe "File::Constants::BINARY" do + it "is defined" do + File::Constants.const_defined?(:BINARY).should be_true + end + end +end + +platform_is_not :windows do + ["NOCTTY", "SYNC"].each do |const| + describe "File::Constants::#{const}" do + it "is defined" do + File::Constants.const_defined?(const).should be_true + end + end + end +end diff --git a/spec/ruby/core/file/constants_spec.rb b/spec/ruby/core/file/constants_spec.rb new file mode 100644 index 0000000000..5f058a7f40 --- /dev/null +++ b/spec/ruby/core/file/constants_spec.rb @@ -0,0 +1,141 @@ +require_relative '../../spec_helper' + +# TODO: migrate these to constants/constants_spec.rb + +describe "File::Constants" do + it "matches mode constants" do + File::FNM_NOESCAPE.should_not == nil + File::FNM_PATHNAME.should_not == nil + File::FNM_DOTMATCH.should_not == nil + File::FNM_CASEFOLD.should_not == nil + File::FNM_SYSCASE.should_not == nil + + platform_is :windows do #|| VMS + File::FNM_SYSCASE.should == 8 + end + end + + # Only these constants are not inherited from the IO class + it "the separator constant" do + File::SEPARATOR.should_not == nil + File::Separator.should_not == nil + File::PATH_SEPARATOR.should_not == nil + File::SEPARATOR.should == "/" + + platform_is :windows do #|| VMS + File::ALT_SEPARATOR.should_not == nil + File::PATH_SEPARATOR.should == ";" + end + + platform_is_not :windows do + File::ALT_SEPARATOR.should == nil + File::PATH_SEPARATOR.should == ":" + end + end + + it "the open mode constants" do + File::APPEND.should_not == nil + File::CREAT.should_not == nil + File::EXCL.should_not == nil + File::NONBLOCK.should_not == nil + File::RDONLY.should_not == nil + File::RDWR.should_not == nil + File::TRUNC.should_not == nil + File::WRONLY.should_not == nil + + platform_is_not :windows do # Not sure about VMS here + File::NOCTTY.should_not == nil + end + end + + it "lock mode constants" do + File::LOCK_EX.should_not == nil + File::LOCK_NB.should_not == nil + File::LOCK_SH.should_not == nil + File::LOCK_UN.should_not == nil + end +end + +describe "File::Constants" do + # These mode and permission bits are platform dependent + it "File::RDONLY" do + defined?(File::RDONLY).should == "constant" + end + + it "File::WRONLY" do + defined?(File::WRONLY).should == "constant" + end + + it "File::CREAT" do + defined?(File::CREAT).should == "constant" + end + + it "File::RDWR" do + defined?(File::RDWR).should == "constant" + end + + it "File::APPEND" do + defined?(File::APPEND).should == "constant" + end + + it "File::TRUNC" do + defined?(File::TRUNC).should == "constant" + end + + platform_is_not :windows do # Not sure about VMS here + it "File::NOCTTY" do + defined?(File::NOCTTY).should == "constant" + end + end + + it "File::NONBLOCK" do + defined?(File::NONBLOCK).should == "constant" + end + + it "File::LOCK_EX" do + defined?(File::LOCK_EX).should == "constant" + end + + it "File::LOCK_NB" do + defined?(File::LOCK_NB).should == "constant" + end + + it "File::LOCK_SH" do + defined?(File::LOCK_SH).should == "constant" + end + + it "File::LOCK_UN" do + defined?(File::LOCK_UN).should == "constant" + end + + it "File::SEPARATOR" do + defined?(File::SEPARATOR).should == "constant" + end + it "File::Separator" do + defined?(File::Separator).should == "constant" + end + + it "File::PATH_SEPARATOR" do + defined?(File::PATH_SEPARATOR).should == "constant" + end + + it "File::SEPARATOR" do + defined?(File::SEPARATOR).should == "constant" + File::SEPARATOR.should == "/" + end + + platform_is :windows do #|| VMS + it "File::ALT_SEPARATOR" do + defined?(File::ALT_SEPARATOR).should == "constant" + File::PATH_SEPARATOR.should == ";" + end + end + + platform_is_not :windows do + it "File::PATH_SEPARATOR" do + defined?(File::PATH_SEPARATOR).should == "constant" + File::PATH_SEPARATOR.should == ":" + end + end + +end diff --git a/spec/ruby/core/file/ctime_spec.rb b/spec/ruby/core/file/ctime_spec.rb new file mode 100644 index 0000000000..718f26d5cc --- /dev/null +++ b/spec/ruby/core/file/ctime_spec.rb @@ -0,0 +1,54 @@ +require_relative '../../spec_helper' + +describe "File.ctime" do + before :each do + @file = __FILE__ + end + + after :each do + @file = nil + end + + it "returns the change time for the named file (the time at which directory information about the file was changed, not the file itself)." do + File.ctime(@file) + File.ctime(@file).should be_kind_of(Time) + end + + platform_is :linux, :windows do + it "returns the change time for the named file (the time at which directory information about the file was changed, not the file itself) with microseconds." do + supports_subseconds = Integer(`stat -c%z '#{__FILE__}'`[/\.(\d{1,6})/, 1], 10) + if supports_subseconds != 0 + File.ctime(__FILE__).usec.should > 0 + else + File.ctime(__FILE__).usec.should == 0 + end + rescue Errno::ENOENT => e + # Windows don't have stat command. + skip e.message + end + end + + it "accepts an object that has a #to_path method" do + File.ctime(mock_to_path(@file)) + end + + it "raises an Errno::ENOENT exception if the file is not found" do + -> { File.ctime('bogus') }.should raise_error(Errno::ENOENT) + end +end + +describe "File#ctime" do + before :each do + @file = File.open(__FILE__) + end + + after :each do + @file.close + @file = nil + end + + it "returns the change time for the named file (the time at which directory information about the file was changed, not the file itself)." do + @file.ctime + @file.ctime.should be_kind_of(Time) + end +end diff --git a/spec/ruby/core/file/delete_spec.rb b/spec/ruby/core/file/delete_spec.rb new file mode 100644 index 0000000000..4098499942 --- /dev/null +++ b/spec/ruby/core/file/delete_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/unlink' + +describe "File.delete" do + it_behaves_like :file_unlink, :delete +end diff --git a/spec/ruby/core/file/directory_spec.rb b/spec/ruby/core/file/directory_spec.rb new file mode 100644 index 0000000000..8014a7a03d --- /dev/null +++ b/spec/ruby/core/file/directory_spec.rb @@ -0,0 +1,10 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/directory' + +describe "File.directory?" do + it_behaves_like :file_directory, :directory?, File +end + +describe "File.directory?" do + it_behaves_like :file_directory_io, :directory?, File +end diff --git a/spec/ruby/core/file/dirname_spec.rb b/spec/ruby/core/file/dirname_spec.rb new file mode 100644 index 0000000000..8e6016ce6f --- /dev/null +++ b/spec/ruby/core/file/dirname_spec.rb @@ -0,0 +1,137 @@ +require_relative '../../spec_helper' + +describe "File.dirname" do + it "returns all the components of filename except the last one" do + File.dirname('/home/jason').should == '/home' + File.dirname('/home/jason/poot.txt').should == '/home/jason' + File.dirname('poot.txt').should == '.' + File.dirname('/holy///schnikies//w00t.bin').should == '/holy///schnikies' + File.dirname('').should == '.' + File.dirname('/').should == '/' + File.dirname('/foo/foo').should == '/foo' + end + + context "when level is passed" do + it "returns all the components of filename except the last parts by the level" do + File.dirname('/home/jason', 2).should == '/' + File.dirname('/home/jason/poot.txt', 2).should == '/home' + end + + it "returns the same String if the level is 0" do + File.dirname('poot.txt', 0).should == 'poot.txt' + File.dirname('/', 0).should == '/' + end + + it "raises ArgumentError if the level is negative" do + -> { + File.dirname('/home/jason', -1) + }.should raise_error(ArgumentError, "negative level: -1") + end + + it "returns '/' when level exceeds the number of segments in the path" do + File.dirname("/home/jason", 100).should == '/' + end + + it "calls #to_int if passed not numeric value" do + object = Object.new + def object.to_int; 2; end + + File.dirname("/a/b/c/d", object).should == '/a/b' + end + end + + it "returns a String" do + File.dirname("foo").should be_kind_of(String) + end + + it "does not modify its argument" do + x = "/usr/bin" + File.dirname(x) + x.should == "/usr/bin" + end + + it "ignores a trailing /" do + File.dirname("/foo/bar/").should == "/foo" + end + + it "returns the return all the components of filename except the last one (unix format)" do + File.dirname("foo").should =="." + File.dirname("/foo").should =="/" + File.dirname("/foo/bar").should =="/foo" + File.dirname("/foo/bar.txt").should =="/foo" + File.dirname("/foo/bar/baz").should =="/foo/bar" + end + + it "returns all the components of filename except the last one (edge cases on all platforms)" do + File.dirname("").should == "." + File.dirname(".").should == "." + File.dirname("./").should == "." + File.dirname("./b/./").should == "./b" + File.dirname("..").should == "." + File.dirname("../").should == "." + File.dirname("/").should == "/" + File.dirname("/.").should == "/" + File.dirname("/foo/").should == "/" + File.dirname("/foo/.").should == "/foo" + File.dirname("/foo/./").should == "/foo" + File.dirname("/foo/../.").should == "/foo/.." + File.dirname("foo/../").should == "foo" + end + + platform_is_not :windows do + it "returns all the components of filename except the last one (edge cases on non-windows)" do + File.dirname('/////').should == '/' + File.dirname("//foo//").should == "/" + File.dirname('foo\bar').should == '.' + File.dirname('/foo\bar').should == '/' + File.dirname('foo/bar\baz').should == 'foo' + end + end + + platform_is :windows do + it "returns all the components of filename except the last one (edge cases on windows)" do + File.dirname("//foo").should == "//foo" + File.dirname("//foo//").should == "//foo" + File.dirname('/////').should == '//' + end + end + + it "accepts an object that has a #to_path method" do + File.dirname(mock_to_path("/")).should == "/" + end + + it "raises a TypeError if not passed a String type" do + -> { File.dirname(nil) }.should raise_error(TypeError) + -> { File.dirname(0) }.should raise_error(TypeError) + -> { File.dirname(true) }.should raise_error(TypeError) + -> { File.dirname(false) }.should raise_error(TypeError) + end + + # Windows specific tests + platform_is :windows do + it "returns the return all the components of filename except the last one (Windows format)" do + File.dirname("C:\\foo\\bar\\baz.txt").should =="C:\\foo\\bar" + File.dirname("C:\\foo\\bar").should =="C:\\foo" + File.dirname("C:\\foo\\bar\\").should == "C:\\foo" + File.dirname("C:\\foo").should == "C:\\" + File.dirname("C:\\").should =="C:\\" + end + + it "returns the return all the components of filename except the last one (windows unc)" do + File.dirname("\\\\foo\\bar\\baz.txt").should == "\\\\foo\\bar" + File.dirname("\\\\foo\\bar\\baz").should == "\\\\foo\\bar" + File.dirname("\\\\foo").should =="\\\\foo" + File.dirname("\\\\foo\\bar").should =="\\\\foo\\bar" + File.dirname("\\\\\\foo\\bar").should =="\\\\foo\\bar" + File.dirname("\\\\\\foo").should =="\\\\foo" + end + + it "returns the return all the components of filename except the last one (forward_slash)" do + File.dirname("C:/").should == "C:/" + File.dirname("C:/foo").should == "C:/" + File.dirname("C:/foo/bar").should == "C:/foo" + File.dirname("C:/foo/bar/").should == "C:/foo" + File.dirname("C:/foo/bar//").should == "C:/foo" + end + end +end diff --git a/spec/ruby/core/file/empty_spec.rb b/spec/ruby/core/file/empty_spec.rb new file mode 100644 index 0000000000..e8c9941676 --- /dev/null +++ b/spec/ruby/core/file/empty_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/zero' + +describe "File.empty?" do + it_behaves_like :file_zero, :empty?, File + it_behaves_like :file_zero_missing, :empty?, File +end diff --git a/spec/ruby/core/file/executable_real_spec.rb b/spec/ruby/core/file/executable_real_spec.rb new file mode 100644 index 0000000000..0cb848b201 --- /dev/null +++ b/spec/ruby/core/file/executable_real_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/executable_real' + +describe "File.executable_real?" do + it_behaves_like :file_executable_real, :executable_real?, File + it_behaves_like :file_executable_real_missing, :executable_real?, File +end diff --git a/spec/ruby/core/file/executable_spec.rb b/spec/ruby/core/file/executable_spec.rb new file mode 100644 index 0000000000..1dbb3b233d --- /dev/null +++ b/spec/ruby/core/file/executable_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/executable' + +describe "File.executable?" do + it_behaves_like :file_executable, :executable?, File + it_behaves_like :file_executable_missing, :executable?, File +end diff --git a/spec/ruby/core/file/exist_spec.rb b/spec/ruby/core/file/exist_spec.rb new file mode 100644 index 0000000000..b5600e5b07 --- /dev/null +++ b/spec/ruby/core/file/exist_spec.rb @@ -0,0 +1,12 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/exist' + +describe "File.exist?" do + it_behaves_like :file_exist, :exist?, File +end + +describe "File.exists?" do + it "has been removed" do + File.should_not.respond_to?(:exists?) + end +end diff --git a/spec/ruby/core/file/expand_path_spec.rb b/spec/ruby/core/file/expand_path_spec.rb new file mode 100644 index 0000000000..1abcf93900 --- /dev/null +++ b/spec/ruby/core/file/expand_path_spec.rb @@ -0,0 +1,265 @@ +# -*- encoding: utf-8 -*- + +require_relative '../../spec_helper' +require_relative 'fixtures/common' +require 'etc' + +describe "File.expand_path" do + before :each do + platform_is :windows do + @base = `cd`.chomp.tr '\\', '/' + @tmpdir = "c:/tmp" + @rootdir = "c:/" + end + + platform_is_not :windows do + @base = Dir.pwd + @tmpdir = "/tmp" + @rootdir = "/" + end + end + + before :each do + @external = Encoding.default_external + end + + after :each do + Encoding.default_external = @external + end + + it "converts a pathname to an absolute pathname" do + File.expand_path('').should == @base + File.expand_path('a').should == File.join(@base, 'a') + File.expand_path('a', nil).should == File.join(@base, 'a') + end + + it "converts a pathname to an absolute pathname, Ruby-Talk:18512" do + # See Ruby-Talk:18512 + File.expand_path('.a').should == File.join(@base, '.a') + File.expand_path('..a').should == File.join(@base, '..a') + File.expand_path('a../b').should == File.join(@base, 'a../b') + end + + platform_is_not :windows do + it "keeps trailing dots on absolute pathname" do + # See Ruby-Talk:18512 + File.expand_path('a.').should == File.join(@base, 'a.') + File.expand_path('a..').should == File.join(@base, 'a..') + end + end + + it "converts a pathname to an absolute pathname, using a complete path" do + File.expand_path("", "#{@tmpdir}").should == "#{@tmpdir}" + File.expand_path("a", "#{@tmpdir}").should =="#{@tmpdir}/a" + File.expand_path("../a", "#{@tmpdir}/xxx").should == "#{@tmpdir}/a" + File.expand_path(".", "#{@rootdir}").should == "#{@rootdir}" + end + + platform_is_not :windows do + before do + @var_home = ENV['HOME'].chomp('/') + @db_home = Dir.home(ENV['USER']) + end + + # FIXME: these are insane! + it "expand path with" do + File.expand_path("../../bin", "/tmp/x").should == "/bin" + File.expand_path("../../bin", "/tmp").should == "/bin" + File.expand_path("../../bin", "/").should == "/bin" + File.expand_path("../bin", "tmp/x").should == File.join(@base, 'tmp', 'bin') + File.expand_path("../bin", "x/../tmp").should == File.join(@base, 'bin') + end + + it "expand_path for common unix path gives a full path" do + File.expand_path('/tmp/').should =='/tmp' + File.expand_path('/tmp/../../../tmp').should == '/tmp' + File.expand_path('').should == Dir.pwd + File.expand_path('./////').should == Dir.pwd + File.expand_path('.').should == Dir.pwd + File.expand_path(Dir.pwd).should == Dir.pwd + File.expand_path('~/').should == @var_home + File.expand_path('~/..badfilename').should == "#{@var_home}/..badfilename" + File.expand_path('~/a','~/b').should == "#{@var_home}/a" + File.expand_path('..').should == File.dirname(Dir.pwd) + end + + it "does not replace multiple '/' at the beginning of the path" do + File.expand_path('////some/path').should == "////some/path" + end + + it "replaces multiple '/' with a single '/'" do + File.expand_path('/some////path').should == "/some/path" + end + + it "raises an ArgumentError if the path is not valid" do + -> { File.expand_path("~a_not_existing_user") }.should raise_error(ArgumentError) + end + + it "expands ~ENV['USER'] to the user's home directory" do + File.expand_path("~#{ENV['USER']}").should == @db_home + end + + it "expands ~ENV['USER']/a to a in the user's home directory" do + File.expand_path("~#{ENV['USER']}/a").should == "#{@db_home}/a" + end + + it "does not expand ~ENV['USER'] when it's not at the start" do + File.expand_path("/~#{ENV['USER']}/a").should == "/~#{ENV['USER']}/a" + end + + it "expands ../foo with ~/dir as base dir to /path/to/user/home/foo" do + File.expand_path('../foo', '~/dir').should == "#{@var_home}/foo" + end + end + + it "accepts objects that have a #to_path method" do + File.expand_path(mock_to_path("a"), mock_to_path("#{@tmpdir}")) + end + + it "raises a TypeError if not passed a String type" do + -> { File.expand_path(1) }.should raise_error(TypeError) + -> { File.expand_path(nil) }.should raise_error(TypeError) + -> { File.expand_path(true) }.should raise_error(TypeError) + end + + platform_is_not :windows do + it "expands /./dir to /dir" do + File.expand_path("/./dir").should == "/dir" + end + end + + platform_is :windows do + it "expands C:/./dir to C:/dir" do + File.expand_path("C:/./dir").should == "C:/dir" + end + end + + it "returns a String in the same encoding as the argument" do + Encoding.default_external = Encoding::SHIFT_JIS + + path = "./a".dup.force_encoding Encoding::CP1251 + File.expand_path(path).encoding.should equal(Encoding::CP1251) + + weird_path = [222, 173, 190, 175].pack('C*') + File.expand_path(weird_path).encoding.should equal(Encoding::BINARY) + end + + platform_is_not :windows do + it "expands a path when the default external encoding is BINARY" do + Encoding.default_external = Encoding::BINARY + path_8bit = [222, 173, 190, 175].pack('C*') + File.expand_path( path_8bit, @rootdir).should == "#{@rootdir}" + path_8bit + end + end + + it "expands a path with multi-byte characters" do + File.expand_path("AÌŠngström").should == "#{@base}/AÌŠngström" + end + + platform_is_not :windows do + it "raises an Encoding::CompatibilityError if the external encoding is not compatible" do + Encoding.default_external = Encoding::UTF_16BE + -> { File.expand_path("./a") }.should raise_error(Encoding::CompatibilityError) + end + end + + it "does not modify the string argument" do + str = "./a/b/../c" + File.expand_path(str, @base).should == "#{@base}/a/c" + str.should == "./a/b/../c" + end + + it "does not modify a HOME string argument" do + str = "~/a" + File.expand_path(str).should == "#{Dir.home}/a" + str.should == "~/a" + end + + it "returns a String when passed a String subclass" do + str = FileSpecs::SubString.new "./a/b/../c" + path = File.expand_path(str, @base) + path.should == "#{@base}/a/c" + path.should be_an_instance_of(String) + end +end + +platform_is_not :windows do + describe "File.expand_path when HOME is set" do + before :each do + @home = ENV["HOME"] + ENV["HOME"] = "/rubyspec_home" + end + + after :each do + ENV["HOME"] = @home + end + + it "converts a pathname to an absolute pathname, using ~ (home) as base" do + home = "/rubyspec_home" + File.expand_path('~').should == home + File.expand_path('~', '/tmp/gumby/ddd').should == home + File.expand_path('~/a', '/tmp/gumby/ddd').should == File.join(home, 'a') + end + + it "does not return a frozen string" do + home = "/rubyspec_home" + File.expand_path('~').should_not.frozen? + File.expand_path('~', '/tmp/gumby/ddd').should_not.frozen? + File.expand_path('~/a', '/tmp/gumby/ddd').should_not.frozen? + end + end + + + describe "File.expand_path when HOME is not set" do + before :each do + @home = ENV["HOME"] + end + + after :each do + ENV["HOME"] = @home + end + + guard -> { + # We need to check if getlogin(3) returns non-NULL, + # as MRI only checks getlogin(3) for expanding '~' if $HOME is not set. + user = ENV.delete("USER") + begin + Etc.getlogin != nil + rescue + false + ensure + ENV["USER"] = user + end + } do + it "uses the user database when passed '~' if HOME is nil" do + ENV.delete "HOME" + File.directory?(File.expand_path("~")).should == true + end + + it "uses the user database when passed '~/' if HOME is nil" do + ENV.delete "HOME" + File.directory?(File.expand_path("~/")).should == true + end + end + + it "raises an ArgumentError when passed '~' if HOME == ''" do + ENV["HOME"] = "" + -> { File.expand_path("~") }.should raise_error(ArgumentError) + end + end + + describe "File.expand_path with a non-absolute HOME" do + before :each do + @home = ENV["HOME"] + end + + after :each do + ENV["HOME"] = @home + end + + it "raises an ArgumentError" do + ENV["HOME"] = "non-absolute" + -> { File.expand_path("~") }.should raise_error(ArgumentError, 'non-absolute home') + end + end +end diff --git a/spec/ruby/core/file/extname_spec.rb b/spec/ruby/core/file/extname_spec.rb new file mode 100644 index 0000000000..d20cf813d9 --- /dev/null +++ b/spec/ruby/core/file/extname_spec.rb @@ -0,0 +1,76 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' + +describe "File.extname" do + it "returns the extension (the portion of file name in path after the period)" do + File.extname("foo.rb").should == ".rb" + File.extname("/foo/bar.rb").should == ".rb" + File.extname("/foo.rb/bar.c").should == ".c" + File.extname("bar").should == "" + File.extname(".bashrc").should == "" + File.extname("/foo.bar/baz").should == "" + File.extname(".app.conf").should == ".conf" + end + + it "returns unfrozen strings" do + File.extname("foo.rb").frozen?.should == false + File.extname("/foo/bar.rb").frozen?.should == false + File.extname("/foo.rb/bar.c").frozen?.should == false + File.extname("bar").frozen?.should == false + File.extname(".bashrc").frozen?.should == false + File.extname("/foo.bar/baz").frozen?.should == false + File.extname(".app.conf").frozen?.should == false + end + + it "returns the extension for edge cases" do + File.extname("").should == "" + File.extname(".").should == "" + File.extname("/").should == "" + File.extname("/.").should == "" + File.extname("..").should == "" + File.extname("...").should == "" + File.extname("....").should == "" + end + + describe "for a filename ending with a dot" do + platform_is :windows do + it "returns ''" do + File.extname(".foo.").should == "" + File.extname("foo.").should == "" + end + end + + platform_is_not :windows do + it "returns '.'" do + File.extname(".foo.").should == "." + File.extname("foo.").should == "." + end + end + end + + it "returns only the last extension of a file with several dots" do + File.extname("a.b.c.d.e").should == ".e" + end + + it "accepts an object that has a #to_path method" do + File.extname(mock_to_path("a.b.c.d.e")).should == ".e" + end + + it "raises a TypeError if not passed a String type" do + -> { File.extname(nil) }.should raise_error(TypeError) + -> { File.extname(0) }.should raise_error(TypeError) + -> { File.extname(true) }.should raise_error(TypeError) + -> { File.extname(false) }.should raise_error(TypeError) + end + + it "raises an ArgumentError if not passed one argument" do + -> { File.extname }.should raise_error(ArgumentError) + -> { File.extname("foo.bar", "foo.baz") }.should raise_error(ArgumentError) + end + + + it "returns the extension for a multibyte filename" do + File.extname('ИмÑ.m4a').should == ".m4a" + end + +end diff --git a/spec/ruby/core/file/file_spec.rb b/spec/ruby/core/file/file_spec.rb new file mode 100644 index 0000000000..8a9dfd5fe2 --- /dev/null +++ b/spec/ruby/core/file/file_spec.rb @@ -0,0 +1,16 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/file' + +describe "File" do + it "includes Enumerable" do + File.include?(Enumerable).should == true + end + + it "includes File::Constants" do + File.include?(File::Constants).should == true + end +end + +describe "File.file?" do + it_behaves_like :file_file, :file?, File +end diff --git a/spec/ruby/core/file/fixtures/common.rb b/spec/ruby/core/file/fixtures/common.rb new file mode 100644 index 0000000000..50721388ad --- /dev/null +++ b/spec/ruby/core/file/fixtures/common.rb @@ -0,0 +1,22 @@ +module FileSpecs + class SubString < String; end + + def self.make_closer(obj, exc=nil) + ScratchPad << :file_opened + + class << obj + attr_accessor :close_exception + + alias_method :original_close, :close + + def close + original_close + ScratchPad << :file_closed + + raise @close_exception if @close_exception + end + end + + obj.close_exception = exc + end +end diff --git a/spec/ruby/core/file/fixtures/do_not_remove b/spec/ruby/core/file/fixtures/do_not_remove new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/spec/ruby/core/file/fixtures/do_not_remove @@ -0,0 +1 @@ + diff --git a/spec/ruby/core/file/fixtures/file_types.rb b/spec/ruby/core/file/fixtures/file_types.rb new file mode 100644 index 0000000000..109bcfe42e --- /dev/null +++ b/spec/ruby/core/file/fixtures/file_types.rb @@ -0,0 +1,66 @@ +module FileSpecs + def self.configure_types + return if @configured + + @file = tmp("test.txt") + @dir = Dir.pwd + @fifo = tmp("test_fifo") + @link = tmp("test_link") + + platform_is_not :windows do + @block = `find /dev /devices -type b 2>/dev/null`.split("\n").first + @char = `{ tty || find /dev /devices -type c; } 2>/dev/null`.split("\n").last + end + + @configured = true + end + + def self.normal_file + touch(@file) + yield @file + ensure + rm_r @file + end + + def self.directory + yield @dir + end + + def self.fifo + File.mkfifo(@fifo) + yield @fifo + ensure + rm_r @fifo + end + + def self.block_device + raise "Could not find a block device" unless @block + yield @block + end + + def self.character_device + raise "Could not find a character device" unless @char + yield @char + end + + def self.symlink + touch(@file) + File.symlink(@file, @link) + yield @link + ensure + rm_r @file, @link + end + + def self.socket + require_relative '../../../library/socket/fixtures/classes.rb' + + name = SocketSpecs.socket_path + socket = UNIXServer.new name + begin + yield name + ensure + socket.close + rm_r name + end + end +end diff --git a/spec/ruby/core/file/flock_spec.rb b/spec/ruby/core/file/flock_spec.rb new file mode 100644 index 0000000000..23ddf89ed8 --- /dev/null +++ b/spec/ruby/core/file/flock_spec.rb @@ -0,0 +1,74 @@ +require_relative '../../spec_helper' + +describe "File#flock" do + before :each do + ScratchPad.record [] + + @name = tmp("flock_test") + touch(@name) + + @file = File.open @name, "w+" + end + + after :each do + @file.flock File::LOCK_UN + @file.close + + rm_r @name + end + + it "exclusively locks a file" do + @file.flock(File::LOCK_EX).should == 0 + @file.flock(File::LOCK_UN).should == 0 + end + + it "non-exclusively locks a file" do + @file.flock(File::LOCK_SH).should == 0 + @file.flock(File::LOCK_UN).should == 0 + end + + it "returns false if trying to lock an exclusively locked file" do + @file.flock File::LOCK_EX + + ruby_exe(<<-END_OF_CODE).should == "false" + File.open('#{@name}', "w") do |f2| + print f2.flock(File::LOCK_EX | File::LOCK_NB).to_s + end + END_OF_CODE + end + + it "blocks if trying to lock an exclusively locked file" do + @file.flock File::LOCK_EX + + out = ruby_exe(<<-END_OF_CODE) + running = false + + t = Thread.new do + File.open('#{@name}', "w") do |f2| + puts "before" + running = true + f2.flock(File::LOCK_EX) + puts "after" + end + end + + Thread.pass until running + Thread.pass while t.status and t.status != "sleep" + sleep 0.1 + + t.kill + t.join + END_OF_CODE + + out.should == "before\n" + end + + it "returns 0 if trying to lock a non-exclusively locked file" do + @file.flock File::LOCK_SH + + File.open(@name, "r") do |f2| + f2.flock(File::LOCK_SH | File::LOCK_NB).should == 0 + f2.flock(File::LOCK_UN).should == 0 + end + end +end diff --git a/spec/ruby/core/file/fnmatch_spec.rb b/spec/ruby/core/file/fnmatch_spec.rb new file mode 100644 index 0000000000..a1b7fa12b3 --- /dev/null +++ b/spec/ruby/core/file/fnmatch_spec.rb @@ -0,0 +1,10 @@ +require_relative '../../spec_helper' +require_relative 'shared/fnmatch' + +describe "File.fnmatch" do + it_behaves_like :file_fnmatch, :fnmatch +end + +describe "File.fnmatch?" do + it_behaves_like :file_fnmatch, :fnmatch? +end diff --git a/spec/ruby/core/file/ftype_spec.rb b/spec/ruby/core/file/ftype_spec.rb new file mode 100644 index 0000000000..cdddc404dc --- /dev/null +++ b/spec/ruby/core/file/ftype_spec.rb @@ -0,0 +1,82 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/file_types' + +describe "File.ftype" do + before :all do + FileSpecs.configure_types + end + + it "raises ArgumentError if not given exactly one filename" do + -> { File.ftype }.should raise_error(ArgumentError) + -> { File.ftype('blah', 'bleh') }.should raise_error(ArgumentError) + end + + it "raises Errno::ENOENT if the file is not valid" do + -> { + File.ftype("/#{$$}#{Time.now.to_f}") + }.should raise_error(Errno::ENOENT) + end + + it "returns a String" do + FileSpecs.normal_file do |file| + File.ftype(file).should be_kind_of(String) + end + end + + it "returns 'file' when the file is a file" do + FileSpecs.normal_file do |file| + File.ftype(file).should == 'file' + end + end + + it "returns 'directory' when the file is a dir" do + FileSpecs.directory do |dir| + File.ftype(dir).should == 'directory' + end + end + + it "uses to_path to convert arguments" do + FileSpecs.normal_file do |file| + obj = mock('path') + obj.should_receive(:to_path).and_return(file) + File.ftype(obj).should == 'file' + end + end + + # Both FreeBSD and Windows does not have block devices + platform_is_not :freebsd, :windows do + with_block_device do + it "returns 'blockSpecial' when the file is a block" do + FileSpecs.block_device do |block| + File.ftype(block).should == 'blockSpecial' + end + end + end + end + + platform_is_not :windows do + it "returns 'characterSpecial' when the file is a char" do + FileSpecs.character_device do |char| + File.ftype(char).should == 'characterSpecial' + end + end + + it "returns 'link' when the file is a link" do + FileSpecs.symlink do |link| + File.ftype(link).should == 'link' + end + end + + it "returns fifo when the file is a fifo" do + FileSpecs.fifo do |fifo| + File.ftype(fifo).should == 'fifo' + end + end + + it "returns 'socket' when the file is a socket" do + FileSpecs.socket do |socket| + File.ftype(socket).should == 'socket' + end + end + end +end diff --git a/spec/ruby/core/file/grpowned_spec.rb b/spec/ruby/core/file/grpowned_spec.rb new file mode 100644 index 0000000000..8ddac5237c --- /dev/null +++ b/spec/ruby/core/file/grpowned_spec.rb @@ -0,0 +1,10 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/grpowned' + +describe "File.grpowned?" do + it_behaves_like :file_grpowned, :grpowned?, File + + it "returns false if file the does not exist" do + File.grpowned?("i_am_a_bogus_file").should == false + end +end diff --git a/spec/ruby/core/file/identical_spec.rb b/spec/ruby/core/file/identical_spec.rb new file mode 100644 index 0000000000..bbeaef24d2 --- /dev/null +++ b/spec/ruby/core/file/identical_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/identical' + +describe "File.identical?" do + it_behaves_like :file_identical, :identical?, File +end diff --git a/spec/ruby/core/file/initialize_spec.rb b/spec/ruby/core/file/initialize_spec.rb new file mode 100644 index 0000000000..9a76a95260 --- /dev/null +++ b/spec/ruby/core/file/initialize_spec.rb @@ -0,0 +1,19 @@ +require_relative '../../spec_helper' + +describe "File#initialize" do + after :each do + @io.close if @io + end + + it "accepts encoding options in mode parameter" do + @io = File.new(__FILE__, 'r:UTF-8:iso-8859-1') + @io.external_encoding.to_s.should == 'UTF-8' + @io.internal_encoding.to_s.should == 'ISO-8859-1' + end + + it "accepts encoding options as a hash parameter" do + @io = File.new(__FILE__, 'r', encoding: 'UTF-8:iso-8859-1') + @io.external_encoding.to_s.should == 'UTF-8' + @io.internal_encoding.to_s.should == 'ISO-8859-1' + end +end diff --git a/spec/ruby/core/file/inspect_spec.rb b/spec/ruby/core/file/inspect_spec.rb new file mode 100644 index 0000000000..148e789c62 --- /dev/null +++ b/spec/ruby/core/file/inspect_spec.rb @@ -0,0 +1,17 @@ +require_relative '../../spec_helper' + +describe "File#inspect" do + before :each do + @name = tmp("file_inspect.txt") + @file = File.open @name, "w" + end + + after :each do + @file.close unless @file.closed? + rm_r @name + end + + it "returns a String" do + @file.inspect.should be_an_instance_of(String) + end +end diff --git a/spec/ruby/core/file/join_spec.rb b/spec/ruby/core/file/join_spec.rb new file mode 100644 index 0000000000..0feedbae93 --- /dev/null +++ b/spec/ruby/core/file/join_spec.rb @@ -0,0 +1,148 @@ +require_relative '../../spec_helper' + +describe "File.join" do + # see [ruby-core:46804] for the 4 following rules + it "changes only boundaries separators" do + File.join("file/\\/usr/", "/bin").should == "file/\\/usr/bin" + File.join("file://usr", "bin").should == "file://usr/bin" + end + + it "respects the given separator if only one part has a boundary separator" do + File.join("usr/", "bin").should == "usr/bin" + File.join("usr", "/bin").should == "usr/bin" + File.join("usr//", "bin").should == "usr//bin" + File.join("usr", "//bin").should == "usr//bin" + end + + it "joins parts using File::SEPARATOR if there are no boundary separators" do + File.join("usr", "bin").should == "usr/bin" + end + + it "prefers the separator of the right part if both parts have separators" do + File.join("usr/", "//bin").should == "usr//bin" + File.join("usr//", "/bin").should == "usr/bin" + end + + platform_is :windows do + it "respects given separator if only one part has a boundary separator" do + File.join("C:\\", 'windows').should == "C:\\windows" + File.join("C:", "\\windows").should == "C:\\windows" + File.join("\\\\", "usr").should == "\\\\usr" + end + + it "prefers the separator of the right part if both parts have separators" do + File.join("C:/", "\\windows").should == "C:\\windows" + File.join("C:\\", "/windows").should == "C:/windows" + end + end + + platform_is_not :windows do + it "does not treat \\ as a separator on non-Windows" do + File.join("usr\\", 'bin').should == "usr\\/bin" + File.join("usr", "\\bin").should == "usr/\\bin" + File.join("usr/", "\\bin").should == "usr/\\bin" + File.join("usr\\", "/bin").should == "usr\\/bin" + end + end + + it "returns an empty string when given no arguments" do + File.join.should == "" + end + + it "returns a duplicate string when given a single argument" do + str = "usr" + File.join(str).should == str + File.join(str).should_not equal(str) + end + + it "supports any number of arguments" do + File.join("a", "b", "c", "d").should == "a/b/c/d" + end + + it "flattens nested arrays" do + File.join(["a", "b", "c"]).should == "a/b/c" + File.join(["a", ["b", ["c"]]]).should == "a/b/c" + end + + it "inserts the separator in between empty strings and arrays" do + File.join("").should == "" + File.join("", "").should == "/" + File.join(["", ""]).should == "/" + File.join("a", "").should == "a/" + File.join("", "a").should == "/a" + + File.join([]).should == "" + File.join([], []).should == "/" + File.join([[], []]).should == "/" + File.join("a", []).should == "a/" + File.join([], "a").should == "/a" + end + + it "handles leading parts edge cases" do + File.join("/bin") .should == "/bin" + File.join("", "bin") .should == "/bin" + File.join("/", "bin") .should == "/bin" + File.join("/", "/bin").should == "/bin" + end + + it "handles trailing parts edge cases" do + File.join("bin", "") .should == "bin/" + File.join("bin/") .should == "bin/" + File.join("bin/", "") .should == "bin/" + File.join("bin", "/") .should == "bin/" + File.join("bin/", "/").should == "bin/" + end + + it "handles middle parts edge cases" do + File.join("usr", "", "bin") .should == "usr/bin" + File.join("usr/", "", "bin") .should == "usr/bin" + File.join("usr", "", "/bin").should == "usr/bin" + File.join("usr/", "", "/bin").should == "usr/bin" + end + + # TODO: See MRI svn r23306. Add patchlevel when there is a release. + it "raises an ArgumentError if passed a recursive array" do + a = ["a"] + a << a + -> { File.join a }.should raise_error(ArgumentError) + end + + it "raises a TypeError exception when args are nil" do + -> { File.join nil }.should raise_error(TypeError) + end + + it "calls #to_str" do + -> { File.join(mock('x')) }.should raise_error(TypeError) + + bin = mock("bin") + bin.should_receive(:to_str).exactly(:twice).and_return("bin") + File.join(bin).should == "bin" + File.join("usr", bin).should == "usr/bin" + end + + it "doesn't mutate the object when calling #to_str" do + usr = mock("usr") + str = "usr" + usr.should_receive(:to_str).and_return(str) + File.join(usr, "bin").should == "usr/bin" + str.should == "usr" + end + + it "calls #to_path" do + -> { File.join(mock('x')) }.should raise_error(TypeError) + + bin = mock("bin") + bin.should_receive(:to_path).exactly(:twice).and_return("bin") + File.join(bin).should == "bin" + File.join("usr", bin).should == "usr/bin" + end + + it "raises errors for null bytes" do + -> { File.join("\x00x", "metadata.gz") }.should raise_error(ArgumentError) { |e| + e.message.should == 'string contains null byte' + } + -> { File.join("metadata.gz", "\x00x") }.should raise_error(ArgumentError) { |e| + e.message.should == 'string contains null byte' + } + end +end diff --git a/spec/ruby/core/file/lchmod_spec.rb b/spec/ruby/core/file/lchmod_spec.rb new file mode 100644 index 0000000000..3c44374983 --- /dev/null +++ b/spec/ruby/core/file/lchmod_spec.rb @@ -0,0 +1,32 @@ +require_relative '../../spec_helper' + +describe "File.lchmod" do + platform_is_not :linux, :windows, :openbsd, :aix do + before :each do + @fname = tmp('file_chmod_test') + @lname = @fname + '.lnk' + + touch(@fname) { |f| f.write "rubinius" } + + rm_r @lname + File.symlink @fname, @lname + end + + after :each do + rm_r @lname, @fname + end + + it "changes the file mode of the link and not of the file" do + File.chmod(0222, @lname).should == 1 + File.lchmod(0755, @lname).should == 1 + + File.lstat(@lname).should.executable? + File.lstat(@lname).should.readable? + File.lstat(@lname).should.writable? + + File.stat(@lname).should_not.executable? + File.stat(@lname).should_not.readable? + File.stat(@lname).should.writable? + end + end +end diff --git a/spec/ruby/core/file/lchown_spec.rb b/spec/ruby/core/file/lchown_spec.rb new file mode 100644 index 0000000000..8d95d287ba --- /dev/null +++ b/spec/ruby/core/file/lchown_spec.rb @@ -0,0 +1,59 @@ +require_relative '../../spec_helper' + +as_superuser do + describe "File.lchown" do + platform_is_not :windows do + before :each do + @fname = tmp('file_chown_test') + @lname = @fname + '.lnk' + + touch(@fname) { |f| f.chown 501, 501 } + + rm_r @lname + File.symlink @fname, @lname + end + + after :each do + rm_r @lname, @fname + end + + it "changes the owner id of the file" do + File.lchown 502, nil, @lname + File.stat(@fname).uid.should == 501 + File.lstat(@lname).uid.should == 502 + File.lchown 0, nil, @lname + File.stat(@fname).uid.should == 501 + File.lstat(@lname).uid.should == 0 + end + + it "changes the group id of the file" do + File.lchown nil, 502, @lname + File.stat(@fname).gid.should == 501 + File.lstat(@lname).gid.should == 502 + File.lchown nil, 0, @lname + File.stat(@fname).uid.should == 501 + File.lstat(@lname).uid.should == 0 + end + + it "does not modify the owner id of the file if passed nil or -1" do + File.lchown 502, nil, @lname + File.lchown nil, nil, @lname + File.lstat(@lname).uid.should == 502 + File.lchown nil, -1, @lname + File.lstat(@lname).uid.should == 502 + end + + it "does not modify the group id of the file if passed nil or -1" do + File.lchown nil, 502, @lname + File.lchown nil, nil, @lname + File.lstat(@lname).gid.should == 502 + File.lchown nil, -1, @lname + File.lstat(@lname).gid.should == 502 + end + + it "returns the number of files processed" do + File.lchown(nil, nil, @lname, @lname).should == 2 + end + end + end +end diff --git a/spec/ruby/core/file/link_spec.rb b/spec/ruby/core/file/link_spec.rb new file mode 100644 index 0000000000..a5d5b4815f --- /dev/null +++ b/spec/ruby/core/file/link_spec.rb @@ -0,0 +1,39 @@ +require_relative '../../spec_helper' + +describe "File.link" do + before :each do + @file = tmp("file_link.txt") + @link = tmp("file_link.lnk") + + rm_r @link + touch @file + end + + after :each do + rm_r @link, @file + end + + platform_is_not :windows, :android do + it "link a file with another" do + File.link(@file, @link).should == 0 + File.should.exist?(@link) + File.identical?(@file, @link).should == true + end + + it "raises an Errno::EEXIST if the target already exists" do + File.link(@file, @link) + -> { File.link(@file, @link) }.should raise_error(Errno::EEXIST) + end + + it "raises an ArgumentError if not passed two arguments" do + -> { File.link }.should raise_error(ArgumentError) + -> { File.link(@file) }.should raise_error(ArgumentError) + -> { File.link(@file, @link, @file) }.should raise_error(ArgumentError) + end + + it "raises a TypeError if not passed String types" do + -> { File.link(@file, nil) }.should raise_error(TypeError) + -> { File.link(@file, 1) }.should raise_error(TypeError) + end + end +end diff --git a/spec/ruby/core/file/lstat_spec.rb b/spec/ruby/core/file/lstat_spec.rb new file mode 100644 index 0000000000..a5ea9d15a5 --- /dev/null +++ b/spec/ruby/core/file/lstat_spec.rb @@ -0,0 +1,33 @@ +require_relative '../../spec_helper' +require_relative 'shared/stat' + +describe "File.lstat" do + it_behaves_like :file_stat, :lstat +end + +describe "File.lstat" do + + before :each do + @file = tmp('i_exist') + @link = tmp('i_am_a_symlink') + touch(@file) { |f| f.write 'rubinius' } + File.symlink(@file, @link) + end + + after :each do + rm_r @link, @file + end + + platform_is_not :windows do + it "returns a File::Stat object with symlink properties for a symlink" do + st = File.lstat(@link) + + st.should.symlink? + st.should_not.file? + end + end +end + +describe "File#lstat" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/file/lutime_spec.rb b/spec/ruby/core/file/lutime_spec.rb new file mode 100644 index 0000000000..0f6df42ea3 --- /dev/null +++ b/spec/ruby/core/file/lutime_spec.rb @@ -0,0 +1,43 @@ +require_relative '../../spec_helper' +require_relative 'shared/update_time' + +platform_is_not :windows do + describe "File.lutime" do + it_behaves_like :update_time, :lutime + end + + describe "File.lutime" do + before :each do + @atime = Time.utc(2000) + @mtime = Time.utc(2001) + @file = tmp("specs_lutime_file") + @symlink = tmp("specs_lutime_symlink") + touch @file + File.symlink(@file, @symlink) + end + + after :each do + rm_r @file, @symlink + end + + it "sets the access and modification time for a regular file" do + File.lutime(@atime, @mtime, @file) + stat = File.stat(@file) + stat.atime.should == @atime + stat.mtime.should === @mtime + end + + it "sets the access and modification time for a symlink" do + original = File.stat(@file) + + File.lutime(@atime, @mtime, @symlink) + stat = File.lstat(@symlink) + stat.atime.should == @atime + stat.mtime.should === @mtime + + file = File.stat(@file) + file.atime.should == original.atime + file.mtime.should == original.mtime + end + end +end diff --git a/spec/ruby/core/file/mkfifo_spec.rb b/spec/ruby/core/file/mkfifo_spec.rb new file mode 100644 index 0000000000..19298c967c --- /dev/null +++ b/spec/ruby/core/file/mkfifo_spec.rb @@ -0,0 +1,51 @@ +require_relative '../../spec_helper' + +describe "File.mkfifo" do + platform_is_not :windows do + before do + @path = tmp('fifo') + end + + after do + rm_r(@path) + end + + context "when path passed responds to :to_path" do + it "creates a FIFO file at the path specified" do + File.mkfifo(@path) + File.ftype(@path).should == "fifo" + end + end + + context "when path passed is not a String value" do + it "raises a TypeError" do + -> { File.mkfifo(:"/tmp/fifo") }.should raise_error(TypeError) + end + end + + context "when path does not exist" do + it "raises an Errno::ENOENT exception" do + -> { File.mkfifo("/bogus/path") }.should raise_error(Errno::ENOENT) + end + end + + it "creates a FIFO file at the passed path" do + File.mkfifo(@path.to_s) + File.ftype(@path).should == "fifo" + end + + it "creates a FIFO file with passed mode & ~umask" do + File.mkfifo(@path, 0755) + File.stat(@path).mode.should == 010755 & ~File.umask + end + + it "creates a FIFO file with a default mode of 0666 & ~umask" do + File.mkfifo(@path) + File.stat(@path).mode.should == 010666 & ~File.umask + end + + it "returns 0 after creating the FIFO file" do + File.mkfifo(@path).should == 0 + end + end +end diff --git a/spec/ruby/core/file/mtime_spec.rb b/spec/ruby/core/file/mtime_spec.rb new file mode 100644 index 0000000000..0e9c95caee --- /dev/null +++ b/spec/ruby/core/file/mtime_spec.rb @@ -0,0 +1,56 @@ +require_relative '../../spec_helper' + +describe "File.mtime" do + before :each do + @filename = tmp('i_exist') + touch(@filename) { @mtime = Time.now } + end + + after :each do + rm_r @filename + end + + it "returns the modification Time of the file" do + File.mtime(@filename).should be_kind_of(Time) + File.mtime(@filename).should be_close(@mtime, TIME_TOLERANCE) + end + + platform_is :linux, :windows do + unless ENV.key?('TRAVIS') # https://bugs.ruby-lang.org/issues/17926 + it "returns the modification Time of the file with microseconds" do + supports_subseconds = Integer(`stat -c%y '#{__FILE__}'`[/\.(\d{1,6})/, 1], 10) + if supports_subseconds != 0 + expected_time = Time.at(Time.now.to_i + 0.123456) + File.utime 0, expected_time, @filename + File.mtime(@filename).usec.should == expected_time.usec + else + File.mtime(__FILE__).usec.should == 0 + end + rescue Errno::ENOENT => e + # Windows don't have stat command. + skip e.message + end + end + end + + it "raises an Errno::ENOENT exception if the file is not found" do + -> { File.mtime('bogus') }.should raise_error(Errno::ENOENT) + end +end + +describe "File#mtime" do + before :each do + @filename = tmp('i_exist') + @f = File.open(@filename, 'w') + end + + after :each do + @f.close + rm_r @filename + end + + it "returns the modification Time of the file" do + @f.mtime.should be_kind_of(Time) + end + +end diff --git a/spec/ruby/core/file/new_spec.rb b/spec/ruby/core/file/new_spec.rb new file mode 100644 index 0000000000..1e82a070b1 --- /dev/null +++ b/spec/ruby/core/file/new_spec.rb @@ -0,0 +1,223 @@ +require_relative '../../spec_helper' +require_relative 'shared/open' + +describe "File.new" do + before :each do + @file = tmp('test.txt') + @fh = nil + @flags = File::CREAT | File::TRUNC | File::WRONLY + touch @file + end + + after :each do + @fh.close if @fh + rm_r @file + end + + it "returns a new File with mode string" do + @fh = File.new(@file, 'w') + @fh.should be_kind_of(File) + File.should.exist?(@file) + end + + it "returns a new File with mode num" do + @fh = File.new(@file, @flags) + @fh.should be_kind_of(File) + File.should.exist?(@file) + end + + it "returns a new File with modus num and permissions" do + rm_r @file + File.umask(0011) + @fh = File.new(@file, @flags, 0755) + @fh.should be_kind_of(File) + platform_is_not :windows do + File.stat(@file).mode.to_s(8).should == "100744" + end + File.should.exist?(@file) + end + + it "creates the file and returns writable descriptor when called with 'w' mode and r-o permissions" do + # it should be possible to write to such a file via returned descriptor, + # even though the file permissions are r-r-r. + + rm_r @file + begin + f = File.new(@file, "w", 0444) + -> { f.puts("test") }.should_not raise_error(IOError) + ensure + f.close + end + File.should.exist?(@file) + File.read(@file).should == "test\n" + end + + platform_is_not :windows do + it "opens the existing file, does not change permissions even when they are specified" do + File.chmod(0644, @file) # r-w perms + orig_perms = File.stat(@file).mode & 0777 + begin + f = File.new(@file, "w", 0444) # r-o perms, but they should be ignored + f.puts("test") + ensure + f.close + end + perms = File.stat(@file).mode & 0777 + perms.should == orig_perms + + # it should be still possible to read from the file + File.read(@file).should == "test\n" + end + end + + it "returns a new File with modus fd" do + @fh = File.new(@file) + fh_copy = File.new(@fh.fileno) + fh_copy.autoclose = false + fh_copy.should be_kind_of(File) + File.should.exist?(@file) + end + + it "returns a new read-only File when mode is not specified" do + @fh = File.new(@file) + + -> { @fh.puts("test") }.should raise_error(IOError) + @fh.read.should == "" + File.should.exist?(@file) + end + + it "returns a new read-only File when mode is not specified but flags option is present" do + @fh = File.new(@file, flags: File::CREAT) + + -> { @fh.puts("test") }.should raise_error(IOError) + @fh.read.should == "" + File.should.exist?(@file) + end + + it "creates a new file when use File::EXCL mode" do + @fh = File.new(@file, File::EXCL) + @fh.should be_kind_of(File) + File.should.exist?(@file) + end + + it "raises an Errno::EEXIST if the file exists when create a new file with File::CREAT|File::EXCL" do + -> { @fh = File.new(@file, File::CREAT|File::EXCL) }.should raise_error(Errno::EEXIST) + end + + it "creates a new file when use File::WRONLY|File::APPEND mode" do + @fh = File.new(@file, File::WRONLY|File::APPEND) + @fh.should be_kind_of(File) + File.should.exist?(@file) + end + + it "returns a new File when use File::APPEND mode" do + @fh = File.new(@file, File::APPEND) + @fh.should be_kind_of(File) + File.should.exist?(@file) + end + + it "returns a new File when use File::RDONLY|File::APPEND mode" do + @fh = File.new(@file, File::RDONLY|File::APPEND) + @fh.should be_kind_of(File) + File.should.exist?(@file) + end + + it "returns a new File when use File::RDONLY|File::WRONLY mode" do + @fh = File.new(@file, File::RDONLY|File::WRONLY) + @fh.should be_kind_of(File) + File.should.exist?(@file) + end + + it "creates a new file when use File::WRONLY|File::TRUNC mode" do + @fh = File.new(@file, File::WRONLY|File::TRUNC) + @fh.should be_kind_of(File) + File.should.exist?(@file) + end + + it "returns a new read-only File when use File::RDONLY|File::CREAT mode" do + @fh = File.new(@file, File::RDONLY|File::CREAT) + @fh.should be_kind_of(File) + File.should.exist?(@file) + + # it's read-only + -> { @fh.puts("test") }.should raise_error(IOError) + @fh.read.should == "" + end + + it "returns a new read-only File when use File::CREAT mode" do + @fh = File.new(@file, File::CREAT) + @fh.should be_kind_of(File) + File.should.exist?(@file) + + # it's read-only + -> { @fh.puts("test") }.should raise_error(IOError) + @fh.read.should == "" + end + + it "coerces filename using to_str" do + name = mock("file") + name.should_receive(:to_str).and_return(@file) + @fh = File.new(name, "w") + File.should.exist?(@file) + end + + it "coerces filename using #to_path" do + name = mock("file") + name.should_receive(:to_path).and_return(@file) + @fh = File.new(name, "w") + File.should.exist?(@file) + end + + it "accepts options as a keyword argument" do + @fh = File.new(@file, 'w', 0755, flags: @flags) + @fh.should be_kind_of(File) + @fh.close + + -> { + @fh = File.new(@file, 'w', 0755, {flags: @flags}) + }.should raise_error(ArgumentError, "wrong number of arguments (given 4, expected 1..3)") + end + + it "bitwise-ORs mode and flags option" do + -> { + @fh = File.new(@file, 'w', flags: File::EXCL) + }.should raise_error(Errno::EEXIST, /File exists/) + + -> { + @fh = File.new(@file, mode: 'w', flags: File::EXCL) + }.should raise_error(Errno::EEXIST, /File exists/) + end + + it "does not use the given block and warns to use File::open" do + -> { + @fh = File.new(@file) { raise } + }.should complain(/warning: File::new\(\) does not take block; use File::open\(\) instead/) + end + + it "raises a TypeError if the first parameter can't be coerced to a string" do + -> { File.new(true) }.should raise_error(TypeError) + -> { File.new(false) }.should raise_error(TypeError) + end + + it "raises a TypeError if the first parameter is nil" do + -> { File.new(nil) }.should raise_error(TypeError) + end + + it "raises an Errno::EBADF if the first parameter is an invalid file descriptor" do + -> { File.new(-1) }.should raise_error(Errno::EBADF) + end + + platform_is_not :windows do + it "can't alter mode or permissions when opening a file" do + @fh = File.new(@file) + -> { + f = File.new(@fh.fileno, @flags) + f.autoclose = false + }.should raise_error(Errno::EINVAL) + end + end + + platform_is_not :windows do + it_behaves_like :open_directory, :new + end +end diff --git a/spec/ruby/core/file/null_spec.rb b/spec/ruby/core/file/null_spec.rb new file mode 100644 index 0000000000..355b72b799 --- /dev/null +++ b/spec/ruby/core/file/null_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../spec_helper' + +describe "File::NULL" do + platform_is :windows do + it "returns NUL as a string" do + File::NULL.should == 'NUL' + end + end + + platform_is_not :windows do + it "returns /dev/null as a string" do + File::NULL.should == '/dev/null' + end + end +end diff --git a/spec/ruby/core/file/open_spec.rb b/spec/ruby/core/file/open_spec.rb new file mode 100644 index 0000000000..6bfc16bbf9 --- /dev/null +++ b/spec/ruby/core/file/open_spec.rb @@ -0,0 +1,713 @@ +# encoding: utf-8 + +require_relative '../../spec_helper' +require_relative 'fixtures/common' +require_relative 'shared/open' + +describe "File.open" do + before :all do + @file = tmp("file_open.txt") + @unicode_path = tmp("ã“ã‚“ã«ã¡ã¯.txt") + @nonexistent = tmp("fake.txt") + rm_r @file, @nonexistent + end + + before :each do + ScratchPad.record [] + + @fh = @fd = nil + @flags = File::CREAT | File::TRUNC | File::WRONLY + touch @file + end + + after :each do + @fh.close if @fh and not @fh.closed? + rm_r @file, @unicode_path, @nonexistent + end + + describe "with a block" do + it "does not raise error when file is closed inside the block" do + @fh = File.open(@file) { |fh| fh.close; fh } + @fh.should.closed? + end + + it "invokes close on an opened file when exiting the block" do + File.open(@file, 'r') { |f| FileSpecs.make_closer f } + + ScratchPad.recorded.should == [:file_opened, :file_closed] + end + + it "propagates non-StandardErrors produced by close" do + -> { + File.open(@file, 'r') { |f| FileSpecs.make_closer f, Exception } + }.should raise_error(Exception) + + ScratchPad.recorded.should == [:file_opened, :file_closed] + end + + it "propagates StandardErrors produced by close" do + -> { + File.open(@file, 'r') { |f| FileSpecs.make_closer f, StandardError } + }.should raise_error(StandardError) + + ScratchPad.recorded.should == [:file_opened, :file_closed] + end + + it "does not propagate IOError with 'closed stream' message produced by close" do + File.open(@file, 'r') { |f| FileSpecs.make_closer f, IOError.new('closed stream') } + + ScratchPad.recorded.should == [:file_opened, :file_closed] + end + end + + it "opens the file (basic case)" do + @fh = File.open(@file) + @fh.should be_kind_of(File) + File.should.exist?(@file) + end + + it "opens the file with unicode characters" do + @fh = File.open(@unicode_path, "w") + @fh.should be_kind_of(File) + File.should.exist?(@unicode_path) + end + + it "opens a file when called with a block" do + File.open(@file) { |fh| } + File.should.exist?(@file) + end + + it "opens with mode string" do + @fh = File.open(@file, 'w') + @fh.should be_kind_of(File) + File.should.exist?(@file) + end + + it "opens a file with mode string and block" do + File.open(@file, 'w') { |fh| } + File.should.exist?(@file) + end + + it "opens a file with mode num" do + @fh = File.open(@file, @flags) + @fh.should be_kind_of(File) + File.should.exist?(@file) + end + + it "opens a file with mode num and block" do + File.open(@file, 'w') { |fh| } + File.should.exist?(@file) + end + + it "opens a file with mode and permission as nil" do + @fh = File.open(@file, nil, nil) + @fh.should be_kind_of(File) + end + + # For this test we delete the file first to reset the perms + it "opens the file when passed mode, num and permissions" do + rm_r @file + File.umask(0011) + @fh = File.open(@file, @flags, 0755) + @fh.should be_kind_of(File) + platform_is_not :windows do + @fh.lstat.mode.to_s(8).should == "100744" + end + File.should.exist?(@file) + end + + # For this test we delete the file first to reset the perms + it "opens the file when passed mode, num, permissions and block" do + rm_r @file + File.umask(0022) + File.open(@file, "w", 0755){ |fh| } + platform_is_not :windows do + File.stat(@file).mode.to_s(8).should == "100755" + end + File.should.exist?(@file) + end + + it "creates the file and returns writable descriptor when called with 'w' mode and r-o permissions" do + # it should be possible to write to such a file via returned descriptor, + # even though the file permissions are r-r-r. + + File.open(@file, "w", 0444) { |f| f.write("test") } + File.read(@file).should == "test" + end + + platform_is_not :windows do + it "opens the existing file, does not change permissions even when they are specified" do + File.chmod(0664, @file) + orig_perms = File.stat(@file).mode.to_s(8) + File.open(@file, "w", 0444) { |f| f.write("test") } + + File.stat(@file).mode.to_s(8).should == orig_perms + File.read(@file).should == "test" + end + end + + platform_is_not :windows do + as_user do + it "creates a new write-only file when invoked with 'w' and '0222'" do + rm_r @file + File.open(@file, 'w', 0222) {} + File.readable?(@file).should == false + File.writable?(@file).should == true + end + end + end + + it "opens the file when call with fd" do + @fh = File.open(@file) + fh_copy = File.open(@fh.fileno) + fh_copy.autoclose = false + fh_copy.should be_kind_of(File) + File.should.exist?(@file) + end + + it "opens a file that no exists when use File::WRONLY mode" do + -> { File.open(@nonexistent, File::WRONLY) }.should raise_error(Errno::ENOENT) + end + + it "opens a file that no exists when use File::RDONLY mode" do + -> { File.open(@nonexistent, File::RDONLY) }.should raise_error(Errno::ENOENT) + end + + it "opens a file that no exists when use 'r' mode" do + -> { File.open(@nonexistent, 'r') }.should raise_error(Errno::ENOENT) + end + + it "opens a file that no exists when use File::EXCL mode" do + -> { File.open(@nonexistent, File::EXCL) }.should raise_error(Errno::ENOENT) + end + + it "opens a file that no exists when use File::NONBLOCK mode" do + -> { File.open(@nonexistent, File::NONBLOCK) }.should raise_error(Errno::ENOENT) + end + + platform_is_not :openbsd, :windows do + it "opens a file that no exists when use File::TRUNC mode" do + -> { File.open(@nonexistent, File::TRUNC) }.should raise_error(Errno::ENOENT) + end + end + + platform_is :openbsd, :windows do + it "does not open a file that does no exists when using File::TRUNC mode" do + -> { File.open(@nonexistent, File::TRUNC) }.should raise_error(Errno::EINVAL) + end + end + + platform_is_not :windows do + it "opens a file that no exists when use File::NOCTTY mode" do + -> { File.open(@nonexistent, File::NOCTTY) }.should raise_error(Errno::ENOENT) + end + end + + it "opens a file that no exists when use File::CREAT mode" do + @fh = File.open(@nonexistent, File::CREAT) { |f| f } + @fh.should be_kind_of(File) + File.should.exist?(@file) + end + + it "opens a file that no exists when use 'a' mode" do + @fh = File.open(@nonexistent, 'a') { |f| f } + @fh.should be_kind_of(File) + File.should.exist?(@file) + end + + it "opens a file that no exists when use 'w' mode" do + @fh = File.open(@nonexistent, 'w') { |f| f } + @fh.should be_kind_of(File) + File.should.exist?(@file) + end + + # Check the grants associated to the different open modes combinations. + it "raises an ArgumentError exception when call with an unknown mode" do + -> { File.open(@file, "q") }.should raise_error(ArgumentError) + end + + it "can read in a block when call open with RDONLY mode" do + File.open(@file, File::RDONLY) do |f| + f.gets.should == nil + end + end + + it "can read in a block when call open with 'r' mode" do + File.open(@file, "r") do |f| + f.gets.should == nil + end + end + + it "raises an IO exception when write in a block opened with RDONLY mode" do + File.open(@file, File::RDONLY) do |f| + -> { f.puts "writing ..." }.should raise_error(IOError) + end + end + + it "raises an IO exception when write in a block opened with 'r' mode" do + File.open(@file, "r") do |f| + -> { f.puts "writing ..." }.should raise_error(IOError) + end + end + + it "can't write in a block when call open with File::WRONLY||File::RDONLY mode" do + File.open(@file, File::WRONLY|File::RDONLY ) do |f| + f.puts("writing").should == nil + end + end + + it "can't read in a block when call open with File::WRONLY||File::RDONLY mode" do + -> { + File.open(@file, File::WRONLY|File::RDONLY ) do |f| + f.gets.should == nil + end + }.should raise_error(IOError) + end + + it "can write in a block when call open with WRONLY mode" do + File.open(@file, File::WRONLY) do |f| + f.puts("writing").should == nil + end + end + + it "can write in a block when call open with 'w' mode" do + File.open(@file, "w") do |f| + f.puts("writing").should == nil + end + end + + it "raises an IOError when read in a block opened with WRONLY mode" do + File.open(@file, File::WRONLY) do |f| + -> { f.gets }.should raise_error(IOError) + end + end + + it "raises an IOError when read in a block opened with 'w' mode" do + File.open(@file, "w") do |f| + -> { f.gets }.should raise_error(IOError) + end + end + + it "raises an IOError when read in a block opened with 'a' mode" do + File.open(@file, "a") do |f| + -> { f.gets }.should raise_error(IOError) + end + end + + it "raises an IOError when read in a block opened with 'a' mode" do + File.open(@file, "a") do |f| + f.puts("writing").should == nil + -> { f.gets }.should raise_error(IOError) + end + end + + it "raises an IOError when read in a block opened with 'a' mode" do + File.open(@file, File::WRONLY|File::APPEND ) do |f| + -> { f.gets }.should raise_error(IOError) + end + end + + it "raises an IOError when read in a block opened with File::WRONLY|File::APPEND mode" do + File.open(@file, File::WRONLY|File::APPEND ) do |f| + f.puts("writing").should == nil + -> { f.gets }.should raise_error(IOError) + end + end + + it "raises an IOError when write in a block opened with File::RDONLY|File::APPEND mode" do + -> { + File.open(@file, File::RDONLY|File::APPEND ) do |f| + f.puts("writing") + end + }.should raise_error(IOError) + end + + it "can read and write in a block when call open with RDWR mode" do + File.open(@file, File::RDWR) do |f| + f.gets.should == nil + f.puts("writing").should == nil + f.rewind + f.gets.should == "writing\n" + end + end + + it "can't read in a block when call open with File::EXCL mode" do + -> { + File.open(@file, File::EXCL) do |f| + f.puts("writing").should == nil + end + }.should raise_error(IOError) + end + + it "can read in a block when call open with File::EXCL mode" do + File.open(@file, File::EXCL) do |f| + f.gets.should == nil + end + end + + it "can read and write in a block when call open with File::RDWR|File::EXCL mode" do + File.open(@file, File::RDWR|File::EXCL) do |f| + f.gets.should == nil + f.puts("writing").should == nil + f.rewind + f.gets.should == "writing\n" + end + end + + it "raises an Errno::EEXIST if the file exists when open with File::CREAT|File::EXCL" do + -> { + File.open(@file, File::CREAT|File::EXCL) do |f| + f.puts("writing") + end + }.should raise_error(Errno::EEXIST) + end + + it "creates a new file when use File::WRONLY|File::APPEND mode" do + @fh = File.open(@file, File::WRONLY|File::APPEND) + @fh.should be_kind_of(File) + File.should.exist?(@file) + end + + it "opens a file when use File::WRONLY|File::APPEND mode" do + File.open(@file, File::WRONLY) do |f| + f.puts("hello file") + end + File.open(@file, File::RDWR|File::APPEND) do |f| + f.puts("bye file") + f.rewind + f.gets.should == "hello file\n" + f.gets.should == "bye file\n" + f.gets.should == nil + end + end + + it "raises an IOError if the file exists when open with File::RDONLY|File::APPEND" do + -> { + File.open(@file, File::RDONLY|File::APPEND) do |f| + f.puts("writing").should == nil + end + }.should raise_error(IOError) + end + + platform_is_not :openbsd, :windows do + it "truncates the file when passed File::TRUNC mode" do + File.open(@file, File::RDWR) { |f| f.puts "hello file" } + @fh = File.open(@file, File::TRUNC) + @fh.gets.should == nil + end + + it "can't read in a block when call open with File::TRUNC mode" do + File.open(@file, File::TRUNC) do |f| + f.gets.should == nil + end + end + end + + it "opens a file when use File::WRONLY|File::TRUNC mode" do + fh1 = File.open(@file, "w") + begin + @fh = File.open(@file, File::WRONLY|File::TRUNC) + @fh.should be_kind_of(File) + File.should.exist?(@file) + ensure + fh1.close + end + end + + platform_is_not :openbsd, :windows do + it "can't write in a block when call open with File::TRUNC mode" do + -> { + File.open(@file, File::TRUNC) do |f| + f.puts("writing") + end + }.should raise_error(IOError) + end + + it "raises an Errno::EEXIST if the file exists when open with File::RDONLY|File::TRUNC" do + -> { + File.open(@file, File::RDONLY|File::TRUNC) do |f| + f.puts("writing").should == nil + end + }.should raise_error(IOError) + end + end + + platform_is :openbsd, :windows do + it "can't write in a block when call open with File::TRUNC mode" do + -> { + File.open(@file, File::TRUNC) do |f| + f.puts("writing") + end + }.should raise_error(Errno::EINVAL) + end + + it "raises an Errno::EEXIST if the file exists when open with File::RDONLY|File::TRUNC" do + -> { + File.open(@file, File::RDONLY|File::TRUNC) do |f| + f.puts("writing").should == nil + end + }.should raise_error(Errno::EINVAL) + end + end + + platform_is_not :windows do + as_user do + it "raises an Errno::EACCES when opening non-permitted file" do + @fh = File.open(@file, "w") + @fh.chmod(000) + -> { fh1 = File.open(@file); fh1.close }.should raise_error(Errno::EACCES) + end + end + end + + as_user do + it "raises an Errno::EACCES when opening read-only file" do + @fh = File.open(@file, "w") + @fh.chmod(0444) + -> { File.open(@file, "w") }.should raise_error(Errno::EACCES) + end + end + + it "opens a file for binary read" do + @fh = File.open(@file, "rb") + @fh.should be_kind_of(File) + File.should.exist?(@file) + end + + it "opens a file for binary write" do + @fh = File.open(@file, "wb") + @fh.should be_kind_of(File) + File.should.exist?(@file) + end + + it "opens a file for read-write and truncate the file" do + File.open(@file, "w") { |f| f.puts "testing" } + File.size(@file).should > 0 + File.open(@file, "w+") do |f| + f.pos.should == 0 + f.should.eof? + end + File.size(@file).should == 0 + end + + it "opens a file for binary read-write starting at the beginning of the file" do + File.open(@file, "w") { |f| f.puts "testing" } + File.size(@file).should > 0 + File.open(@file, "rb+") do |f| + f.binmode?.should == true + f.external_encoding.should == Encoding::ASCII_8BIT + f.pos.should == 0 + f.should_not.eof? + end + File.open(@file, "r+b") do |f| + f.binmode?.should == true + f.external_encoding.should == Encoding::ASCII_8BIT + f.pos.should == 0 + f.should_not.eof? + end + end + + it "opens a file for binary read-write and truncate the file" do + File.open(@file, "w") { |f| f.puts "testing" } + File.size(@file).should > 0 + File.open(@file, "wb+") do |f| + f.pos.should == 0 + f.should.eof? + end + File.size(@file).should == 0 + end + + platform_is :linux do + guard -> { defined?(File::TMPFILE) } do + it "creates an unnamed temporary file with File::TMPFILE" do + dir = tmp("tmpfilespec") + mkdir_p dir + begin + Dir["#{dir}/*"].should == [] + File.open(dir, "r+", flags: File::TMPFILE) do |io| + io.write("ruby") + io.flush + io.rewind + io.read.should == "ruby" + Dir["#{dir}/*"].should == [] + end + rescue Errno::EOPNOTSUPP + skip "no support from the filesystem" + rescue Errno::EINVAL, Errno::EISDIR + skip "presumably bug in glibc" + ensure + rm_r dir + end + end + end + end + + it "raises a TypeError if passed a filename that is not a String or Integer type" do + -> { File.open(true) }.should raise_error(TypeError) + -> { File.open(false) }.should raise_error(TypeError) + -> { File.open(nil) }.should raise_error(TypeError) + end + + it "raises a SystemCallError if passed an invalid Integer type" do + -> { File.open(-1) }.should raise_error(SystemCallError) + end + + it "raises an ArgumentError if passed the wrong number of arguments" do + -> { File.open(@file, File::CREAT, 0755, 'test') }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if passed an invalid string for mode" do + -> { File.open(@file, 'fake') }.should raise_error(ArgumentError) + end + + it "defaults external_encoding to BINARY for binary modes" do + File.open(@file, 'rb') {|f| f.external_encoding.should == Encoding::BINARY} + File.open(@file, 'wb+') {|f| f.external_encoding.should == Encoding::BINARY} + end + + it "accepts options as a keyword argument" do + @fh = File.open(@file, 'w', 0755, flags: File::CREAT) + @fh.should be_an_instance_of(File) + + -> { + File.open(@file, 'w', 0755, {flags: File::CREAT}) + }.should raise_error(ArgumentError, "wrong number of arguments (given 4, expected 1..3)") + end + + it "uses the second argument as an options Hash" do + @fh = File.open(@file, mode: "r") + @fh.should be_an_instance_of(File) + end + + it "calls #to_hash to convert the second argument to a Hash" do + options = mock("file open options") + options.should_receive(:to_hash).and_return({ mode: "r" }) + + @fh = File.open(@file, **options) + end + + it "accepts extra flags as a keyword argument and combine with a string mode" do + -> { + File.open(@file, "w", flags: File::EXCL) { } + }.should raise_error(Errno::EEXIST) + + -> { + File.open(@file, mode: "w", flags: File::EXCL) { } + }.should raise_error(Errno::EEXIST) + end + + it "accepts extra flags as a keyword argument and combine with an integer mode" do + -> { + File.open(@file, File::WRONLY | File::CREAT, flags: File::EXCL) { } + }.should raise_error(Errno::EEXIST) + end + + platform_is_not :windows do + describe "on a FIFO" do + before :each do + @fifo = tmp("File_open_fifo") + File.mkfifo(@fifo) + end + + after :each do + rm_r @fifo + end + + it "opens it as a normal file" do + file_w, file_r, read_bytes, written_length = nil + + # open in threads, due to blocking open and writes + writer = Thread.new do + file_w = File.open(@fifo, 'w') + written_length = file_w.syswrite('hello') + end + reader = Thread.new do + file_r = File.open(@fifo, 'r') + read_bytes = file_r.sysread(5) + end + + begin + writer.join + reader.join + + written_length.should == 5 + read_bytes.should == 'hello' + ensure + file_w.close if file_w + file_r.close if file_r + end + end + end + end + + it "raises ArgumentError if mixing :newline and binary mode" do + -> { + File.open(@file, "rb", newline: :universal) {} + }.should raise_error(ArgumentError, "newline decorator with binary mode") + end + + context "'x' flag" do + before :each do + @xfile = tmp("x-flag") + rm_r @xfile + end + + after :each do + rm_r @xfile + end + + it "does nothing if the file doesn't exist" do + File.open(@xfile, "wx") { |f| f.write("content") } + File.read(@xfile).should == "content" + end + + it "throws a Errno::EEXIST error if the file exists" do + touch @xfile + -> { File.open(@xfile, "wx") }.should raise_error(Errno::EEXIST) + end + + it "can't be used with 'r' and 'a' flags" do + -> { File.open(@xfile, "rx") }.should raise_error(ArgumentError, 'invalid access mode rx') + -> { File.open(@xfile, "ax") }.should raise_error(ArgumentError, 'invalid access mode ax') + end + end +end + +describe "File.open when passed a file descriptor" do + before do + @content = "File#open when passed a file descriptor" + @name = tmp("file_open_with_fd.txt") + @fd = new_fd @name, "w:utf-8" + @file = nil + end + + after do + @file.close if @file and not @file.closed? + rm_r @name + end + + it "opens a file" do + @file = File.open(@fd, "w") + @file.should be_an_instance_of(File) + @file.fileno.should equal(@fd) + @file.write @content + @file.flush + File.read(@name).should == @content + end + + it "opens a file when passed a block" do + @file = File.open(@fd, "w") do |f| + f.should be_an_instance_of(File) + f.fileno.should equal(@fd) + f.write @content + f + end + File.read(@name).should == @content + end +end + +platform_is_not :windows do + describe "File.open" do + it_behaves_like :open_directory, :open + end +end diff --git a/spec/ruby/core/file/owned_spec.rb b/spec/ruby/core/file/owned_spec.rb new file mode 100644 index 0000000000..06d6796da9 --- /dev/null +++ b/spec/ruby/core/file/owned_spec.rb @@ -0,0 +1,35 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/owned' + +describe "File.owned?" do + it_behaves_like :file_owned, :owned?, File +end + +describe "File.owned?" do + before :each do + @filename = tmp("i_exist") + touch(@filename) + end + + after :each do + rm_r @filename + end + + it "returns false if file does not exist" do + File.owned?("I_am_a_bogus_file").should == false + end + + it "returns true if the file exist and is owned by the user" do + File.owned?(@filename).should == true + end + + platform_is_not :windows do + as_user do + it "returns false when the file is not owned by the user" do + system_file = '/etc/passwd' + File.owned?(system_file).should == false + end + end + end + +end diff --git a/spec/ruby/core/file/path_spec.rb b/spec/ruby/core/file/path_spec.rb new file mode 100644 index 0000000000..726febcc2b --- /dev/null +++ b/spec/ruby/core/file/path_spec.rb @@ -0,0 +1,81 @@ +require_relative '../../spec_helper' +require_relative 'shared/path' + +describe "File#path" do + it_behaves_like :file_path, :path +end + +describe "File.path" do + before :each do + @name = tmp("file_path") + end + + after :each do + rm_r @name + end + + it "returns the string argument without any change" do + File.path("abc").should == "abc" + File.path("./abc").should == "./abc" + File.path("../abc").should == "../abc" + File.path("/./a/../bc").should == "/./a/../bc" + end + + it "returns path for File argument" do + File.open(@name, "w") do |f| + File.path(f).should == @name + end + end + + it "returns path for Pathname argument" do + require "pathname" + File.path(Pathname.new(@name)).should == @name + end + + it "calls #to_path for non-string argument and returns result" do + path = mock("path") + path.should_receive(:to_path).and_return("abc") + File.path(path).should == "abc" + end + + it "raises TypeError when #to_path result is not a string" do + path = mock("path") + path.should_receive(:to_path).and_return(nil) + -> { File.path(path) }.should raise_error TypeError + + path = mock("path") + path.should_receive(:to_path).and_return(42) + -> { File.path(path) }.should raise_error TypeError + end + + it "raises ArgumentError for string argument contains NUL character" do + -> { File.path("\0") }.should raise_error ArgumentError + -> { File.path("a\0") }.should raise_error ArgumentError + -> { File.path("a\0c") }.should raise_error ArgumentError + end + + it "raises ArgumentError when #to_path result contains NUL character" do + path = mock("path") + path.should_receive(:to_path).and_return("\0") + -> { File.path(path) }.should raise_error ArgumentError + + path = mock("path") + path.should_receive(:to_path).and_return("a\0") + -> { File.path(path) }.should raise_error ArgumentError + + path = mock("path") + path.should_receive(:to_path).and_return("a\0c") + -> { File.path(path) }.should raise_error ArgumentError + end + + it "raises Encoding::CompatibilityError for ASCII-incompatible string argument" do + path = "abc".encode(Encoding::UTF_32BE) + -> { File.path(path) }.should raise_error Encoding::CompatibilityError + end + + it "raises Encoding::CompatibilityError when #to_path result is ASCII-incompatible" do + path = mock("path") + path.should_receive(:to_path).and_return("abc".encode(Encoding::UTF_32BE)) + -> { File.path(path) }.should raise_error Encoding::CompatibilityError + end +end diff --git a/spec/ruby/core/file/pipe_spec.rb b/spec/ruby/core/file/pipe_spec.rb new file mode 100644 index 0000000000..01d72dbe85 --- /dev/null +++ b/spec/ruby/core/file/pipe_spec.rb @@ -0,0 +1,32 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/pipe' + +describe "File.pipe?" do + it_behaves_like :file_pipe, :pipe?, File +end + +describe "File.pipe?" do + it "returns false if file does not exist" do + File.pipe?("I_am_a_bogus_file").should == false + end + + it "returns false if the file is not a pipe" do + filename = tmp("i_exist") + touch(filename) + + File.pipe?(filename).should == false + + rm_r filename + end + + platform_is_not :windows do + it "returns true if the file is a pipe" do + filename = tmp("i_am_a_pipe") + File.mkfifo(filename) + + File.pipe?(filename).should == true + + rm_r filename + end + end +end diff --git a/spec/ruby/core/file/printf_spec.rb b/spec/ruby/core/file/printf_spec.rb new file mode 100644 index 0000000000..2530419fc7 --- /dev/null +++ b/spec/ruby/core/file/printf_spec.rb @@ -0,0 +1,18 @@ +require_relative '../../spec_helper' +require_relative '../kernel/shared/sprintf' + +describe "File#printf" do + it_behaves_like :kernel_sprintf, -> format, *args { + begin + @filename = tmp("printf.txt") + + File.open(@filename, "w", encoding: "utf-8") do |f| + f.printf(format, *args) + end + + File.read(@filename, encoding: "utf-8") + ensure + rm_r @filename + end + } +end diff --git a/spec/ruby/core/file/read_spec.rb b/spec/ruby/core/file/read_spec.rb new file mode 100644 index 0000000000..67a3325cbd --- /dev/null +++ b/spec/ruby/core/file/read_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/read' + +describe "File.read" do + it_behaves_like :file_read_directory, :read, File +end diff --git a/spec/ruby/core/file/readable_real_spec.rb b/spec/ruby/core/file/readable_real_spec.rb new file mode 100644 index 0000000000..524466cd96 --- /dev/null +++ b/spec/ruby/core/file/readable_real_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/readable_real' + +describe "File.readable_real?" do + it_behaves_like :file_readable_real, :readable_real?, File + it_behaves_like :file_readable_real_missing, :readable_real?, File +end diff --git a/spec/ruby/core/file/readable_spec.rb b/spec/ruby/core/file/readable_spec.rb new file mode 100644 index 0000000000..ed75a23f39 --- /dev/null +++ b/spec/ruby/core/file/readable_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/readable' + +describe "File.readable?" do + it_behaves_like :file_readable, :readable?, File + it_behaves_like :file_readable_missing, :readable?, File +end diff --git a/spec/ruby/core/file/readlink_spec.rb b/spec/ruby/core/file/readlink_spec.rb new file mode 100644 index 0000000000..20741ba121 --- /dev/null +++ b/spec/ruby/core/file/readlink_spec.rb @@ -0,0 +1,86 @@ +require_relative '../../spec_helper' + +describe "File.readlink" do + # symlink/readlink are not supported on Windows + platform_is_not :windows do + describe "with absolute paths" do + before :each do + @file = tmp('file_readlink.txt') + @link = tmp('file_readlink.lnk') + + File.symlink(@file, @link) + end + + after :each do + rm_r @file, @link + end + + it "returns the name of the file referenced by the given link" do + touch @file + File.readlink(@link).should == @file + end + + it "returns the name of the file referenced by the given link when the file does not exist" do + File.readlink(@link).should == @file + end + + it "raises an Errno::ENOENT if there is no such file" do + # TODO: missing_file + -> { File.readlink("/this/surely/does/not/exist") }.should raise_error(Errno::ENOENT) + end + + it "raises an Errno::EINVAL if called with a normal file" do + touch @file + -> { File.readlink(@file) }.should raise_error(Errno::EINVAL) + end + end + + describe "with paths containing unicode characters" do + before :each do + @file = tmp('tà rget.txt') + @link = tmp('lïnk.lnk') + File.symlink(@file, @link) + end + + after :each do + rm_r @file, @link + end + + it "returns the name of the file referenced by the given link" do + touch @file + result = File.readlink(@link) + result.encoding.should equal Encoding.find('filesystem') + result.should == @file.dup.force_encoding(Encoding.find('filesystem')) + end + end + + describe "when changing the working directory" do + before :each do + @cwd = Dir.pwd + @tmpdir = tmp("/readlink") + Dir.mkdir @tmpdir + Dir.chdir @tmpdir + + @link = 'readlink_link' + @file = 'readlink_file' + + File.symlink(@file, @link) + end + + after :each do + rm_r @file, @link + Dir.chdir @cwd + Dir.rmdir @tmpdir + end + + it "returns the name of the file referenced by the given link" do + touch @file + File.readlink(@link).should == @file + end + + it "returns the name of the file referenced by the given link when the file does not exist" do + File.readlink(@link).should == @file + end + end + end +end diff --git a/spec/ruby/core/file/realdirpath_spec.rb b/spec/ruby/core/file/realdirpath_spec.rb new file mode 100644 index 0000000000..74053afce3 --- /dev/null +++ b/spec/ruby/core/file/realdirpath_spec.rb @@ -0,0 +1,104 @@ +require_relative '../../spec_helper' + +platform_is_not :windows do + describe "File.realdirpath" do + before :each do + @real_dir = tmp('dir_realdirpath_real') + @fake_dir = tmp('dir_realdirpath_fake') + @link_dir = tmp('dir_realdirpath_link') + + mkdir_p @real_dir + File.symlink(@real_dir, @link_dir) + + @file = File.join(@real_dir, 'file') + @link = File.join(@link_dir, 'link') + + touch @file + File.symlink(@file, @link) + + @fake_file_in_real_dir = File.join(@real_dir, 'fake_file_in_real_dir') + @fake_file_in_fake_dir = File.join(@fake_dir, 'fake_file_in_fake_dir') + @fake_link_to_real_dir = File.join(@link_dir, 'fake_link_to_real_dir') + @fake_link_to_fake_dir = File.join(@link_dir, 'fake_link_to_fake_dir') + + File.symlink(@fake_file_in_real_dir, @fake_link_to_real_dir) + File.symlink(@fake_file_in_fake_dir, @fake_link_to_fake_dir) + + @dir_for_relative_link = File.join(@real_dir, 'dir1') + mkdir_p @dir_for_relative_link + + @relative_path_to_file = File.join('..', 'file') + @relative_symlink = File.join(@dir_for_relative_link, 'link') + File.symlink(@relative_path_to_file, @relative_symlink) + end + + after :each do + rm_r @file, @link, @fake_link_to_real_dir, @fake_link_to_fake_dir, @real_dir, @link_dir + end + + it "returns '/' when passed '/'" do + File.realdirpath('/').should == '/' + end + + it "returns the real (absolute) pathname not containing symlinks" do + File.realdirpath(@link).should == @file + end + + it "uses base directory for interpreting relative pathname" do + File.realdirpath(File.basename(@link), @link_dir).should == @file + end + + it "uses current directory for interpreting relative pathname" do + Dir.chdir @link_dir do + File.realdirpath(File.basename(@link)).should == @file + end + end + + it "uses link directory for expanding relative links" do + File.realdirpath(@relative_symlink).should == @file + end + + it "raises an Errno::ELOOP if the symlink points to itself" do + File.unlink @link + File.symlink(@link, @link) + -> { File.realdirpath(@link) }.should raise_error(Errno::ELOOP) + end + + it "returns the real (absolute) pathname if the file is absent" do + File.realdirpath(@fake_file_in_real_dir).should == @fake_file_in_real_dir + end + + it "raises Errno::ENOENT if the directory is absent" do + -> { File.realdirpath(@fake_file_in_fake_dir) }.should raise_error(Errno::ENOENT) + end + + it "returns the real (absolute) pathname if the symlink points to an absent file" do + File.realdirpath(@fake_link_to_real_dir).should == @fake_file_in_real_dir + end + + it "raises Errno::ENOENT if the symlink points to an absent directory" do + -> { File.realdirpath(@fake_link_to_fake_dir) }.should raise_error(Errno::ENOENT) + end + end +end + +platform_is :windows do + describe "File.realdirpath" do + before :each do + @file = tmp("realdirpath") + end + + after :each do + rm_r @file + end + + it "returns the same path" do + touch @file + File.realdirpath(@file).should == @file + end + + it "returns the same path even if the last component does not exist" do + File.realdirpath(@file).should == @file + end + end +end diff --git a/spec/ruby/core/file/realpath_spec.rb b/spec/ruby/core/file/realpath_spec.rb new file mode 100644 index 0000000000..bd25bfdecf --- /dev/null +++ b/spec/ruby/core/file/realpath_spec.rb @@ -0,0 +1,98 @@ +require_relative '../../spec_helper' + +platform_is_not :windows do + describe "File.realpath" do + before :each do + @real_dir = tmp('dir_realpath_real') + @link_dir = tmp('dir_realpath_link') + + mkdir_p @real_dir + File.symlink(@real_dir, @link_dir) + + @file = File.join(@real_dir, 'file') + @link = File.join(@link_dir, 'link') + + touch @file + File.symlink(@file, @link) + + @fake_file = File.join(@real_dir, 'fake_file') + @fake_link = File.join(@link_dir, 'fake_link') + + File.symlink(@fake_file, @fake_link) + + @dir_for_relative_link = File.join(@real_dir, 'dir1') + mkdir_p @dir_for_relative_link + + @relative_path_to_file = File.join('..', 'file') + @relative_symlink = File.join(@dir_for_relative_link, 'link') + File.symlink(@relative_path_to_file, @relative_symlink) + end + + after :each do + rm_r @file, @link, @fake_link, @real_dir, @link_dir + end + + it "returns '/' when passed '/'" do + File.realpath('/').should == '/' + end + + it "returns the real (absolute) pathname not containing symlinks" do + File.realpath(@link).should == @file + end + + it "uses base directory for interpreting relative pathname" do + File.realpath(File.basename(@link), @link_dir).should == @file + end + + it "uses current directory for interpreting relative pathname" do + Dir.chdir @link_dir do + File.realpath(File.basename(@link)).should == @file + end + end + + it "uses link directory for expanding relative links" do + File.realpath(@relative_symlink).should == @file + end + + it "removes the file element when going one level up" do + File.realpath('../', @file).should == @real_dir + end + + it "raises an Errno::ELOOP if the symlink points to itself" do + File.unlink @link + File.symlink(@link, @link) + -> { File.realpath(@link) }.should raise_error(Errno::ELOOP) + end + + it "raises Errno::ENOENT if the file is absent" do + -> { File.realpath(@fake_file) }.should raise_error(Errno::ENOENT) + end + + it "raises Errno::ENOENT if the symlink points to an absent file" do + -> { File.realpath(@fake_link) }.should raise_error(Errno::ENOENT) + end + + it "converts the argument with #to_path" do + path = mock("path") + path.should_receive(:to_path).and_return(__FILE__) + File.realpath(path).should == File.realpath(__FILE__ ) + end + end +end + +platform_is :windows do + describe "File.realpath" do + before :each do + @file = tmp("realpath") + touch @file + end + + after :each do + rm_r @file + end + + it "returns the same path" do + File.realpath(@file).should == @file + end + end +end diff --git a/spec/ruby/core/file/rename_spec.rb b/spec/ruby/core/file/rename_spec.rb new file mode 100644 index 0000000000..f2c18d4905 --- /dev/null +++ b/spec/ruby/core/file/rename_spec.rb @@ -0,0 +1,37 @@ +require_relative '../../spec_helper' + +describe "File.rename" do + before :each do + @old = tmp("file_rename.txt") + @new = tmp("file_rename.new") + + rm_r @new + touch(@old) { |f| f.puts "hello" } + end + + after :each do + rm_r @old, @new + end + + it "renames a file" do + File.should.exist?(@old) + File.should_not.exist?(@new) + File.rename(@old, @new) + File.should_not.exist?(@old) + File.should.exist?(@new) + end + + it "raises an Errno::ENOENT if the source does not exist" do + rm_r @old + -> { File.rename(@old, @new) }.should raise_error(Errno::ENOENT) + end + + it "raises an ArgumentError if not passed two arguments" do + -> { File.rename }.should raise_error(ArgumentError) + -> { File.rename(@file) }.should raise_error(ArgumentError) + end + + it "raises a TypeError if not passed String types" do + -> { File.rename(1, 2) }.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/file/reopen_spec.rb b/spec/ruby/core/file/reopen_spec.rb new file mode 100644 index 0000000000..858d424c67 --- /dev/null +++ b/spec/ruby/core/file/reopen_spec.rb @@ -0,0 +1,32 @@ +require_relative '../../spec_helper' + +describe "File#reopen" do + before :each do + @name_a = tmp("file_reopen_a.txt") + @name_b = tmp("file_reopen_b.txt") + @content_a = "File#reopen a" + @content_b = "File#reopen b" + + touch(@name_a) { |f| f.write @content_a } + touch(@name_b) { |f| f.write @content_b } + + @file = nil + end + + after :each do + @file.close if @file and not @file.closed? + rm_r @name_a, @name_b + end + + it "resets the stream to a new file path" do + file = File.new @name_a, "r" + file.read.should == @content_a + @file = file.reopen(@name_b, "r") + @file.read.should == @content_b + end + + it "calls #to_path to convert an Object" do + @file = File.new(@name_a).reopen(mock_to_path(@name_b), "r") + @file.read.should == @content_b + end +end diff --git a/spec/ruby/core/file/setgid_spec.rb b/spec/ruby/core/file/setgid_spec.rb new file mode 100644 index 0000000000..f5df5390f5 --- /dev/null +++ b/spec/ruby/core/file/setgid_spec.rb @@ -0,0 +1,36 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/setgid' + +describe "File.setgid?" do + it_behaves_like :file_setgid, :setgid?, File +end + +describe "File.setgid?" do + before :each do + @name = tmp('test.txt') + touch @name + end + + after :each do + rm_r @name + end + + it "returns false if the file was just made" do + File.setgid?(@name).should == false + end + + it "returns false if the file does not exist" do + rm_r @name # delete it prematurely, just for this part + File.setgid?(@name).should == false + end + + as_superuser do + platform_is_not :windows do + it "returns true when the gid bit is set" do + system "chmod g+s #{@name}" + + File.setgid?(@name).should == true + end + end + end +end diff --git a/spec/ruby/core/file/setuid_spec.rb b/spec/ruby/core/file/setuid_spec.rb new file mode 100644 index 0000000000..9e5e86df61 --- /dev/null +++ b/spec/ruby/core/file/setuid_spec.rb @@ -0,0 +1,34 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/setuid' + +describe "File.setuid?" do + it_behaves_like :file_setuid, :setuid?, File +end + +describe "File.setuid?" do + before :each do + @name = tmp('test.txt') + touch @name + end + + after :each do + rm_r @name + end + + it "returns false if the file was just made" do + File.setuid?(@name).should == false + end + + it "returns false if the file does not exist" do + rm_r @name # delete it prematurely, just for this part + File.setuid?(@name).should == false + end + + platform_is_not :windows do + it "returns true when the gid bit is set" do + system "chmod u+s #{@name}" + + File.setuid?(@name).should == true + end + end +end diff --git a/spec/ruby/core/file/shared/fnmatch.rb b/spec/ruby/core/file/shared/fnmatch.rb new file mode 100644 index 0000000000..db4b5c5d8c --- /dev/null +++ b/spec/ruby/core/file/shared/fnmatch.rb @@ -0,0 +1,294 @@ +describe :file_fnmatch, shared: true do + it "matches entire strings" do + File.send(@method, 'cat', 'cat').should == true + end + + it "does not match partial strings" do + File.send(@method, 'cat', 'category').should == false + end + + it "does not support { } patterns by default" do + File.send(@method, 'c{at,ub}s', 'cats').should == false + File.send(@method, 'c{at,ub}s', 'c{at,ub}s').should == true + end + + it "supports some { } patterns when File::FNM_EXTGLOB is passed" do + File.send(@method, "{a,b}", "a", File::FNM_EXTGLOB).should == true + File.send(@method, "{a,b}", "b", File::FNM_EXTGLOB).should == true + File.send(@method, "c{at,ub}s", "cats", File::FNM_EXTGLOB).should == true + File.send(@method, "c{at,ub}s", "cubs", File::FNM_EXTGLOB).should == true + File.send(@method, "-c{at,ub}s-", "-cats-", File::FNM_EXTGLOB).should == true + File.send(@method, "-c{at,ub}s-", "-cubs-", File::FNM_EXTGLOB).should == true + File.send(@method, "{a,b,c}{d,e,f}{g,h}", "adg", File::FNM_EXTGLOB).should == true + File.send(@method, "{a,b,c}{d,e,f}{g,h}", "bdg", File::FNM_EXTGLOB).should == true + File.send(@method, "{a,b,c}{d,e,f}{g,h}", "ceh", File::FNM_EXTGLOB).should == true + File.send(@method, "{aa,bb,cc,dd}", "aa", File::FNM_EXTGLOB).should == true + File.send(@method, "{aa,bb,cc,dd}", "bb", File::FNM_EXTGLOB).should == true + File.send(@method, "{aa,bb,cc,dd}", "cc", File::FNM_EXTGLOB).should == true + File.send(@method, "{aa,bb,cc,dd}", "dd", File::FNM_EXTGLOB).should == true + File.send(@method, "{1,5{a,b{c,d}}}", "1", File::FNM_EXTGLOB).should == true + File.send(@method, "{1,5{a,b{c,d}}}", "5a", File::FNM_EXTGLOB).should == true + File.send(@method, "{1,5{a,b{c,d}}}", "5bc", File::FNM_EXTGLOB).should == true + File.send(@method, "{1,5{a,b{c,d}}}", "5bd", File::FNM_EXTGLOB).should == true + File.send(@method, "\\\\{a\\,b,b\\}c}", "\\a,b", File::FNM_EXTGLOB).should == true + File.send(@method, "\\\\{a\\,b,b\\}c}", "\\b}c", File::FNM_EXTGLOB).should == true + end + + it "doesn't support some { } patterns even when File::FNM_EXTGLOB is passed" do + File.send(@method, "a{0..3}b", "a0b", File::FNM_EXTGLOB).should == false + File.send(@method, "a{0..3}b", "a1b", File::FNM_EXTGLOB).should == false + File.send(@method, "a{0..3}b", "a2b", File::FNM_EXTGLOB).should == false + File.send(@method, "a{0..3}b", "a3b", File::FNM_EXTGLOB).should == false + File.send(@method, "{0..12}", "0", File::FNM_EXTGLOB).should == false + File.send(@method, "{0..12}", "6", File::FNM_EXTGLOB).should == false + File.send(@method, "{0..12}", "12", File::FNM_EXTGLOB).should == false + File.send(@method, "{3..-2}", "3", File::FNM_EXTGLOB).should == false + File.send(@method, "{3..-2}", "0", File::FNM_EXTGLOB).should == false + File.send(@method, "{3..-2}", "-2", File::FNM_EXTGLOB).should == false + File.send(@method, "{a..g}", "a", File::FNM_EXTGLOB).should == false + File.send(@method, "{a..g}", "d", File::FNM_EXTGLOB).should == false + File.send(@method, "{a..g}", "g", File::FNM_EXTGLOB).should == false + File.send(@method, "{g..a}", "a", File::FNM_EXTGLOB).should == false + File.send(@method, "{g..a}", "d", File::FNM_EXTGLOB).should == false + File.send(@method, "{g..a}", "g", File::FNM_EXTGLOB).should == false + File.send(@method, "escaping: {{,\\,,\\},\\{}", "escaping: {", File::FNM_EXTGLOB).should == false + File.send(@method, "escaping: {{,\\,,\\},\\{}", "escaping: ,", File::FNM_EXTGLOB).should == false + File.send(@method, "escaping: {{,\\,,\\},\\{}", "escaping: }", File::FNM_EXTGLOB).should == false + File.send(@method, "escaping: {{,\\,,\\},\\{}", "escaping: {", File::FNM_EXTGLOB).should == false + end + + it "doesn't match an extra } when File::FNM_EXTGLOB is passed" do + File.send(@method, 'c{at,ub}}s', 'cats', File::FNM_EXTGLOB).should == false + end + + it "matches when both FNM_EXTGLOB and FNM_PATHNAME are passed" do + File.send(@method, "?.md", "a.md", File::FNM_EXTGLOB | File::FNM_PATHNAME).should == true + end + + it "matches a single character for each ? character" do + File.send(@method, 'c?t', 'cat').should == true + File.send(@method, 'c??t', 'cat').should == false + end + + it "matches zero or more characters for each * character" do + File.send(@method, 'c*', 'cats').should == true + File.send(@method, 'c*t', 'c/a/b/t').should == true + end + + it "does not match unterminated range of characters" do + File.send(@method, 'abc[de', 'abcd').should == false + end + + it "does not match unterminated range of characters as a literal" do + File.send(@method, 'abc[de', 'abc[de').should == false + end + + it "matches ranges of characters using bracket expression (e.g. [a-z])" do + File.send(@method, 'ca[a-z]', 'cat').should == true + end + + it "matches ranges of characters using bracket expression, taking case into account" do + File.send(@method, '[a-z]', 'D').should == false + File.send(@method, '[^a-z]', 'D').should == true + File.send(@method, '[A-Z]', 'd').should == false + File.send(@method, '[^A-Z]', 'd').should == true + File.send(@method, '[a-z]', 'D', File::FNM_CASEFOLD).should == true + end + + it "does not match characters outside of the range of the bracket expression" do + File.send(@method, 'ca[x-z]', 'cat').should == false + File.send(@method, '/ca[s][s-t]/rul[a-b]/[z]he/[x-Z]orld', '/cats/rule/the/World').should == false + end + + it "matches ranges of characters using exclusive bracket expression (e.g. [^t] or [!t])" do + File.send(@method, 'ca[^t]', 'cat').should == false + File.send(@method, 'ca[^t]', 'cas').should == true + File.send(@method, 'ca[!t]', 'cat').should == false + end + + it "matches characters with a case sensitive comparison" do + File.send(@method, 'cat', 'CAT').should == false + end + + it "matches characters with case insensitive comparison when flags includes FNM_CASEFOLD" do + File.send(@method, 'cat', 'CAT', File::FNM_CASEFOLD).should == true + end + + platform_is_not :windows do + it "doesn't match case sensitive characters on platforms with case sensitive paths, when flags include FNM_SYSCASE" do + File.send(@method, 'cat', 'CAT', File::FNM_SYSCASE).should == false + end + end + + platform_is :windows do + it "matches case sensitive characters on platforms with case insensitive paths, when flags include FNM_SYSCASE" do + File.send(@method, 'cat', 'CAT', File::FNM_SYSCASE).should == true + end + end + + it "matches wildcard with characters when flags includes FNM_PATHNAME" do + File.send(@method, '*a', 'aa', File::FNM_PATHNAME).should == true + File.send(@method, 'a*', 'aa', File::FNM_PATHNAME).should == true + File.send(@method, 'a*', 'aaa', File::FNM_PATHNAME).should == true + File.send(@method, '*a', 'aaa', File::FNM_PATHNAME).should == true + end + + it "does not match '/' characters with ? or * when flags includes FNM_PATHNAME" do + File.send(@method, '?', '/', File::FNM_PATHNAME).should == false + File.send(@method, '*', '/', File::FNM_PATHNAME).should == false + end + + it "does not match '/' characters inside bracket expressions when flags includes FNM_PATHNAME" do + File.send(@method, '[/]', '/', File::FNM_PATHNAME).should == false + end + + it "matches literal ? or * in path when pattern includes \\? or \\*" do + File.send(@method, '\?', '?').should == true + File.send(@method, '\?', 'a').should == false + + File.send(@method, '\*', '*').should == true + File.send(@method, '\*', 'a').should == false + end + + it "matches literal character (e.g. 'a') in path when pattern includes escaped character (e.g. \\a)" do + File.send(@method, '\a', 'a').should == true + File.send(@method, 'this\b', 'thisb').should == true + end + + it "matches '\\' characters in path when flags includes FNM_NOESACPE" do + File.send(@method, '\a', '\a', File::FNM_NOESCAPE).should == true + File.send(@method, '\a', 'a', File::FNM_NOESCAPE).should == false + File.send(@method, '\[foo\]\[bar\]', '[foo][bar]', File::FNM_NOESCAPE).should == false + end + + it "escapes special characters inside bracket expression" do + File.send(@method, '[\?]', '?').should == true + File.send(@method, '[\*]', '*').should == true + end + + it "does not match leading periods in filenames with wildcards by default" do + File.should_not.send(@method, '*', '.profile') + File.should.send(@method, '*', 'home/.profile') + File.should.send(@method, '*/*', 'home/.profile') + File.should_not.send(@method, '*/*', 'dave/.profile', File::FNM_PATHNAME) + end + + it "matches patterns with leading periods to dotfiles" do + File.send(@method, '.*', '.profile').should == true + File.send(@method, '.*', '.profile', File::FNM_PATHNAME).should == true + File.send(@method, ".*file", "nondotfile").should == false + File.send(@method, ".*file", "nondotfile", File::FNM_PATHNAME).should == false + end + + it "does not match directories with leading periods by default with FNM_PATHNAME" do + File.send(@method, '.*', '.directory/nondotfile', File::FNM_PATHNAME).should == false + File.send(@method, '.*', '.directory/.profile', File::FNM_PATHNAME).should == false + File.send(@method, '.*', 'foo/.directory/nondotfile', File::FNM_PATHNAME).should == false + File.send(@method, '.*', 'foo/.directory/.profile', File::FNM_PATHNAME).should == false + File.send(@method, '**/.dotfile', '.dotsubdir/.dotfile', File::FNM_PATHNAME).should == false + end + + it "matches leading periods in filenames when flags includes FNM_DOTMATCH" do + File.send(@method, '*', '.profile', File::FNM_DOTMATCH).should == true + File.send(@method, '*', 'home/.profile', File::FNM_DOTMATCH).should == true + end + + it "matches multiple directories with ** and *" do + files = '**/*.rb' + File.send(@method, files, 'main.rb').should == false + File.send(@method, files, './main.rb').should == false + File.send(@method, files, 'lib/song.rb').should == true + File.send(@method, '**.rb', 'main.rb').should == true + File.send(@method, '**.rb', './main.rb').should == false + File.send(@method, '**.rb', 'lib/song.rb').should == true + File.send(@method, '*', 'dave/.profile').should == true + end + + it "matches multiple directories with ** when flags includes File::FNM_PATHNAME" do + files = '**/*.rb' + flags = File::FNM_PATHNAME + + File.send(@method, files, 'main.rb', flags).should == true + File.send(@method, files, 'one/two/three/main.rb', flags).should == true + File.send(@method, files, './main.rb', flags).should == false + + flags = File::FNM_PATHNAME | File::FNM_DOTMATCH + + File.send(@method, files, './main.rb', flags).should == true + File.send(@method, files, 'one/two/.main.rb', flags).should == true + + File.send(@method, "**/best/*", 'lib/my/best/song.rb').should == true + end + + it "returns false if '/' in pattern do not match '/' in path when flags includes FNM_PATHNAME" do + pattern = '*/*' + File.send(@method, pattern, 'dave/.profile', File::FNM_PATHNAME).should be_false + + pattern = '**/foo' + File.send(@method, pattern, 'a/.b/c/foo', File::FNM_PATHNAME).should be_false + end + + it "returns true if '/' in pattern match '/' in path when flags includes FNM_PATHNAME" do + pattern = '*/*' + File.send(@method, pattern, 'dave/.profile', File::FNM_PATHNAME | File::FNM_DOTMATCH).should be_true + + pattern = '**/foo' + File.send(@method, pattern, 'a/b/c/foo', File::FNM_PATHNAME).should be_true + File.send(@method, pattern, '/a/b/c/foo', File::FNM_PATHNAME).should be_true + File.send(@method, pattern, 'c:/a/b/c/foo', File::FNM_PATHNAME).should be_true + File.send(@method, pattern, 'a/.b/c/foo', File::FNM_PATHNAME | File::FNM_DOTMATCH).should be_true + end + + it "has special handling for ./ when using * and FNM_PATHNAME" do + File.send(@method, './*', '.', File::FNM_PATHNAME).should be_false + File.send(@method, './*', './', File::FNM_PATHNAME).should be_true + File.send(@method, './*/', './', File::FNM_PATHNAME).should be_false + File.send(@method, './**', './', File::FNM_PATHNAME).should be_true + File.send(@method, './**/', './', File::FNM_PATHNAME).should be_true + File.send(@method, './*', '.', File::FNM_PATHNAME | File::FNM_DOTMATCH).should be_false + File.send(@method, './*', './', File::FNM_PATHNAME | File::FNM_DOTMATCH).should be_true + File.send(@method, './*/', './', File::FNM_PATHNAME | File::FNM_DOTMATCH).should be_false + File.send(@method, './**', './', File::FNM_PATHNAME | File::FNM_DOTMATCH).should be_true + File.send(@method, './**/', './', File::FNM_PATHNAME | File::FNM_DOTMATCH).should be_true + end + + it "matches **/* with FNM_PATHNAME to recurse directories" do + File.send(@method, 'nested/**/*', 'nested/subdir', File::FNM_PATHNAME).should be_true + File.send(@method, 'nested/**/*', 'nested/subdir/file', File::FNM_PATHNAME).should be_true + File.send(@method, 'nested/**/*', 'nested/.dotsubdir', File::FNM_PATHNAME | File::FNM_DOTMATCH).should be_true + File.send(@method, 'nested/**/*', 'nested/.dotsubir/.dotfile', File::FNM_PATHNAME | File::FNM_DOTMATCH).should be_true + end + + it "matches ** with FNM_PATHNAME only in current directory" do + File.send(@method, 'nested/**', 'nested/subdir', File::FNM_PATHNAME).should be_true + File.send(@method, 'nested/**', 'nested/subdir/file', File::FNM_PATHNAME).should be_false + File.send(@method, 'nested/**', 'nested/.dotsubdir', File::FNM_PATHNAME | File::FNM_DOTMATCH).should be_true + File.send(@method, 'nested/**', 'nested/.dotsubir/.dotfile', File::FNM_PATHNAME | File::FNM_DOTMATCH).should be_false + end + + it "accepts an object that has a #to_path method" do + File.send(@method, '\*', mock_to_path('a')).should == false + end + + it "raises a TypeError if the first and second arguments are not string-like" do + -> { File.send(@method, nil, nil, 0, 0) }.should raise_error(ArgumentError) + -> { File.send(@method, 1, 'some/thing') }.should raise_error(TypeError) + -> { File.send(@method, 'some/thing', 1) }.should raise_error(TypeError) + -> { File.send(@method, 1, 1) }.should raise_error(TypeError) + end + + it "raises a TypeError if the third argument is not an Integer" do + -> { File.send(@method, "*/place", "path/to/file", "flags") }.should raise_error(TypeError) + -> { File.send(@method, "*/place", "path/to/file", nil) }.should raise_error(TypeError) + end + + it "does not raise a TypeError if the third argument can be coerced to an Integer" do + flags = mock("flags") + flags.should_receive(:to_int).and_return(10) + -> { File.send(@method, "*/place", "path/to/file", flags) }.should_not raise_error + end + + it "matches multibyte characters" do + File.fnmatch("*/ä/ø/ñ", "a/ä/ø/ñ").should == true + end +end diff --git a/spec/ruby/core/file/shared/open.rb b/spec/ruby/core/file/shared/open.rb new file mode 100644 index 0000000000..677a82a351 --- /dev/null +++ b/spec/ruby/core/file/shared/open.rb @@ -0,0 +1,12 @@ +require_relative '../../dir/fixtures/common' + +describe :open_directory, shared: true do + it "opens directories" do + file = File.send(@method, tmp("")) + begin + file.should be_kind_of(File) + ensure + file.close + end + end +end diff --git a/spec/ruby/core/file/shared/path.rb b/spec/ruby/core/file/shared/path.rb new file mode 100644 index 0000000000..5a9fe1b0c5 --- /dev/null +++ b/spec/ruby/core/file/shared/path.rb @@ -0,0 +1,82 @@ +describe :file_path, shared: true do + before :each do + @path = tmp("file_to_path") + @name = File.basename(@path) + touch @path + end + + after :each do + @file.close if @file and !@file.closed? + rm_r @path + end + + it "returns a String" do + @file = File.new @path + @file.send(@method).should be_an_instance_of(String) + end + + it "returns a different String on every call" do + @file = File.new @path + path1 = @file.send(@method) + path2 = @file.send(@method) + path1.should == path2 + path1.should_not.equal?(path2) + end + + it "returns a mutable String" do + @file = File.new @path.dup.freeze + path = @file.send(@method) + path.should == @path + path.should_not.frozen? + path << "test" + @file.send(@method).should == @path + end + + it "calls to_str on argument and returns exact value" do + path = mock('path') + path.should_receive(:to_str).and_return(@path) + @file = File.new path + @file.send(@method).should == @path + end + + it "does not normalise the path it returns" do + Dir.chdir(tmp("")) do + unorm = "./#{@name}" + @file = File.new unorm + @file.send(@method).should == unorm + end + end + + it "does not canonicalize the path it returns" do + dir = File.basename tmp("") + path = "#{tmp("")}../#{dir}/#{@name}" + @file = File.new path + @file.send(@method).should == path + end + + it "does not absolute-ise the path it returns" do + Dir.chdir(tmp("")) do + @file = File.new @name + @file.send(@method).should == @name + end + end + + it "preserves the encoding of the path" do + path = @path.force_encoding("euc-jp") + @file = File.new path + @file.send(@method).encoding.should == Encoding.find("euc-jp") + end + + platform_is :linux do + guard -> { defined?(File::TMPFILE) } do + before :each do + @dir = tmp("tmpfilespec") + mkdir_p @dir + end + + after :each do + rm_r @dir + end + end + end +end diff --git a/spec/ruby/core/file/shared/read.rb b/spec/ruby/core/file/shared/read.rb new file mode 100644 index 0000000000..f232235298 --- /dev/null +++ b/spec/ruby/core/file/shared/read.rb @@ -0,0 +1,15 @@ +require_relative '../../dir/fixtures/common' + +describe :file_read_directory, shared: true do + platform_is :darwin, :linux, :freebsd, :openbsd, :windows do + it "raises an Errno::EISDIR when passed a path that is a directory" do + -> { @object.send(@method, ".") }.should raise_error(Errno::EISDIR) + end + end + + platform_is :netbsd do + it "does not raises any exception when passed a path that is a directory" do + -> { @object.send(@method, ".") }.should_not raise_error + end + end +end diff --git a/spec/ruby/core/file/shared/stat.rb b/spec/ruby/core/file/shared/stat.rb new file mode 100644 index 0000000000..fdaf97ea61 --- /dev/null +++ b/spec/ruby/core/file/shared/stat.rb @@ -0,0 +1,32 @@ +describe :file_stat, shared: true do + before :each do + @file = tmp('i_exist') + touch(@file) + end + + after :each do + rm_r @file + end + + it "returns a File::Stat object if the given file exists" do + st = File.send(@method, @file) + st.should be_an_instance_of(File::Stat) + end + + it "returns a File::Stat object when called on an instance of File" do + File.open(@file) do |f| + st = f.send(@method) + st.should be_an_instance_of(File::Stat) + end + end + + it "accepts an object that has a #to_path method" do + File.send(@method, mock_to_path(@file)) + end + + it "raises an Errno::ENOENT if the file does not exist" do + -> { + File.send(@method, "fake_file") + }.should raise_error(Errno::ENOENT) + end +end diff --git a/spec/ruby/core/file/shared/unlink.rb b/spec/ruby/core/file/shared/unlink.rb new file mode 100644 index 0000000000..e339e93271 --- /dev/null +++ b/spec/ruby/core/file/shared/unlink.rb @@ -0,0 +1,61 @@ +describe :file_unlink, shared: true do + before :each do + @file1 = tmp('test.txt') + @file2 = tmp('test2.txt') + + touch @file1 + touch @file2 + end + + after :each do + File.send(@method, @file1) if File.exist?(@file1) + File.send(@method, @file2) if File.exist?(@file2) + + @file1 = nil + @file2 = nil + end + + it "returns 0 when called without arguments" do + File.send(@method).should == 0 + end + + it "deletes a single file" do + File.send(@method, @file1).should == 1 + File.should_not.exist?(@file1) + end + + it "deletes multiple files" do + File.send(@method, @file1, @file2).should == 2 + File.should_not.exist?(@file1) + File.should_not.exist?(@file2) + end + + it "raises a TypeError if not passed a String type" do + -> { File.send(@method, 1) }.should raise_error(TypeError) + end + + it "raises an Errno::ENOENT when the given file doesn't exist" do + -> { File.send(@method, 'bogus') }.should raise_error(Errno::ENOENT) + end + + it "coerces a given parameter into a string if possible" do + mock = mock("to_str") + mock.should_receive(:to_str).and_return(@file1) + File.send(@method, mock).should == 1 + end + + it "accepts an object that has a #to_path method" do + File.send(@method, mock_to_path(@file1)).should == 1 + end + + platform_is :windows do + it "allows deleting an open file with File::SHARE_DELETE" do + path = tmp("share_delete.txt") + File.open(path, mode: File::CREAT | File::WRONLY | File::BINARY | File::SHARE_DELETE) do |f| + File.should.exist?(path) + File.send(@method, path) + end + File.should_not.exist?(path) + end + end +end diff --git a/spec/ruby/core/file/shared/update_time.rb b/spec/ruby/core/file/shared/update_time.rb new file mode 100644 index 0000000000..3fe7266a00 --- /dev/null +++ b/spec/ruby/core/file/shared/update_time.rb @@ -0,0 +1,105 @@ +describe :update_time, shared: true do + before :all do + @time_is_float = platform_is :windows + end + + before :each do + @atime = Time.now + @mtime = Time.now + @file1 = tmp("specs_file_utime1") + @file2 = tmp("specs_file_utime2") + touch @file1 + touch @file2 + end + + after :each do + rm_r @file1, @file2 + end + + it "sets the access and modification time of each file" do + File.send(@method, @atime, @mtime, @file1, @file2) + + if @time_is_float + File.atime(@file1).should be_close(@atime, 0.0001) + File.mtime(@file1).should be_close(@mtime, 0.0001) + File.atime(@file2).should be_close(@atime, 0.0001) + File.mtime(@file2).should be_close(@mtime, 0.0001) + else + File.atime(@file1).to_i.should be_close(@atime.to_i, TIME_TOLERANCE) + File.mtime(@file1).to_i.should be_close(@mtime.to_i, TIME_TOLERANCE) + File.atime(@file2).to_i.should be_close(@atime.to_i, TIME_TOLERANCE) + File.mtime(@file2).to_i.should be_close(@mtime.to_i, TIME_TOLERANCE) + end + end + + it "uses the current times if two nil values are passed" do + tn = Time.now + File.send(@method, nil, nil, @file1, @file2) + + if @time_is_float + File.atime(@file1).should be_close(tn, 0.050) + File.mtime(@file1).should be_close(tn, 0.050) + File.atime(@file2).should be_close(tn, 0.050) + File.mtime(@file2).should be_close(tn, 0.050) + else + File.atime(@file1).to_i.should be_close(Time.now.to_i, TIME_TOLERANCE) + File.mtime(@file1).to_i.should be_close(Time.now.to_i, TIME_TOLERANCE) + File.atime(@file2).to_i.should be_close(Time.now.to_i, TIME_TOLERANCE) + File.mtime(@file2).to_i.should be_close(Time.now.to_i, TIME_TOLERANCE) + end + end + + it "accepts an object that has a #to_path method" do + File.send(@method, @atime, @mtime, mock_to_path(@file1), mock_to_path(@file2)) + end + + it "accepts numeric atime and mtime arguments" do + if @time_is_float + File.send(@method, @atime.to_f, @mtime.to_f, @file1, @file2) + + File.atime(@file1).should be_close(@atime, 0.0001) + File.mtime(@file1).should be_close(@mtime, 0.0001) + File.atime(@file2).should be_close(@atime, 0.0001) + File.mtime(@file2).should be_close(@mtime, 0.0001) + else + File.send(@method, @atime.to_i, @mtime.to_i, @file1, @file2) + + File.atime(@file1).to_i.should be_close(@atime.to_i, TIME_TOLERANCE) + File.mtime(@file1).to_i.should be_close(@mtime.to_i, TIME_TOLERANCE) + File.atime(@file2).to_i.should be_close(@atime.to_i, TIME_TOLERANCE) + File.mtime(@file2).to_i.should be_close(@mtime.to_i, TIME_TOLERANCE) + end + end + + it "may set nanosecond precision" do + t = Time.utc(2007, 11, 1, 15, 25, 0, 123456.789r) + File.send(@method, t, t, @file1) + + File.atime(@file1).nsec.should.between?(0, 123500000) + File.mtime(@file1).nsec.should.between?(0, 123500000) + end + + it "returns the number of filenames in the arguments" do + File.send(@method, @atime.to_f, @mtime.to_f, @file1, @file2).should == 2 + end + + platform_is :linux do + platform_is pointer_size: 64 do + it "allows Time instances in the far future to set mtime and atime (but some filesystems limit it up to 2446-05-10 or 2038-01-19 or 2486-07-02)" do + # https://ext4.wiki.kernel.org/index.php/Ext4_Disk_Layout#Inode_Timestamps + # "Therefore, timestamps should not overflow until May 2446." + # https://lwn.net/Articles/804382/ + # "On-disk timestamps hitting the y2038 limit..." + # The problem seems to be being improved, but currently it actually fails on XFS on RHEL8 + # https://rubyci.org/logs/rubyci.s3.amazonaws.com/rhel8/ruby-master/log/20201112T123004Z.fail.html.gz + # Amazon Linux 2023 returns 2486-07-02 in this example + # http://rubyci.s3.amazonaws.com/amazon2023/ruby-master/log/20230322T063004Z.fail.html.gz + time = Time.at(1<<44) + File.send(@method, time, time, @file1) + + [559444, 2486, 2446, 2038].should.include? File.atime(@file1).year + [559444, 2486, 2446, 2038].should.include? File.mtime(@file1).year + end + end + end +end diff --git a/spec/ruby/core/file/size_spec.rb b/spec/ruby/core/file/size_spec.rb new file mode 100644 index 0000000000..11d20cbacb --- /dev/null +++ b/spec/ruby/core/file/size_spec.rb @@ -0,0 +1,119 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/size' + +describe "File.size?" do + it_behaves_like :file_size, :size?, File +end + +describe "File.size?" do + it_behaves_like :file_size_to_io, :size?, File +end + +describe "File.size?" do + it_behaves_like :file_size_nil_when_missing, :size?, File +end + +describe "File.size?" do + it_behaves_like :file_size_nil_when_empty, :size?, File +end + +describe "File.size?" do + it_behaves_like :file_size_with_file_argument, :size?, File +end + +describe "File.size" do + it_behaves_like :file_size, :size, File +end + +describe "File.size" do + it_behaves_like :file_size_to_io, :size, File +end + +describe "File.size" do + it_behaves_like :file_size_raise_when_missing, :size, File +end + +describe "File.size" do + it_behaves_like :file_size_0_when_empty, :size, File +end + +describe "File.size" do + it_behaves_like :file_size_with_file_argument, :size, File +end + +describe "File#size" do + + before :each do + @name = tmp('i_exist') + touch(@name) { |f| f.write 'rubinius' } + @file = File.new @name + @file_org = @file + end + + after :each do + @file_org.close unless @file_org.closed? + rm_r @name + end + + it "is an instance method" do + @file.respond_to?(:size).should be_true + end + + it "returns the file's size as an Integer" do + @file.size.should be_an_instance_of(Integer) + end + + it "returns the file's size in bytes" do + @file.size.should == 8 + end + + platform_is_not :windows do # impossible to remove opened file on Windows + it "returns the cached size of the file if subsequently deleted" do + rm_r @file.path + @file.size.should == 8 + end + end + + it "returns the file's current size even if modified" do + File.open(@file.path,'a') {|f| f.write '!'} + @file.size.should == 9 + end + + it "raises an IOError on a closed file" do + @file.close + -> { @file.size }.should raise_error(IOError) + end + + platform_is_not :windows do + it "follows symlinks if necessary" do + ln_file = tmp('i_exist_ln') + rm_r ln_file + + begin + File.symlink(@file.path, ln_file).should == 0 + file = File.new(ln_file) + file.size.should == 8 + ensure + file.close if file && !file.closed? + rm_r ln_file + end + end + end +end + +describe "File#size for an empty file" do + before :each do + @name = tmp('empty') + touch(@name) + @file = File.new @name + end + + after :each do + @file.close unless @file.closed? + rm_r @name + end + + it "returns 0" do + @file.size.should == 0 + end +end diff --git a/spec/ruby/core/file/socket_spec.rb b/spec/ruby/core/file/socket_spec.rb new file mode 100644 index 0000000000..d3f4eb013a --- /dev/null +++ b/spec/ruby/core/file/socket_spec.rb @@ -0,0 +1,10 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/socket' + +describe "File.socket?" do + it_behaves_like :file_socket, :socket?, File + + it "returns false if file does not exist" do + File.socket?("I_am_a_bogus_file").should == false + end +end diff --git a/spec/ruby/core/file/split_spec.rb b/spec/ruby/core/file/split_spec.rb new file mode 100644 index 0000000000..7b958621b9 --- /dev/null +++ b/spec/ruby/core/file/split_spec.rb @@ -0,0 +1,64 @@ +require_relative '../../spec_helper' + +describe "File.split" do + before :each do + @backslash_ext = "C:\\foo\\bar\\baz.rb" + @backslash = "C:\\foo\\bar\\baz" + end + + it "splits the string at the last '/' when the last component does not have an extension" do + File.split("/foo/bar/baz").should == ["/foo/bar", "baz"] + File.split("C:/foo/bar/baz").should == ["C:/foo/bar", "baz"] + end + + it "splits the string at the last '/' when the last component has an extension" do + File.split("/foo/bar/baz.rb").should == ["/foo/bar", "baz.rb"] + File.split("C:/foo/bar/baz.rb").should == ["C:/foo/bar", "baz.rb"] + end + + it "splits an empty string into a '.' and an empty string" do + File.split("").should == [".", ""] + end + + platform_is_not :windows do + it "collapses multiple '/' characters and strips trailing ones" do + File.split("//foo////").should == ["/", "foo"] + end + end + + platform_is_not :windows do + it "does not split a string that contains '\\'" do + File.split(@backslash).should == [".", "C:\\foo\\bar\\baz"] + File.split(@backslash_ext).should == [".", "C:\\foo\\bar\\baz.rb"] + end + end + + platform_is :windows do + it "splits the string at the last '\\' when the last component does not have an extension" do + File.split(@backslash).should == ["C:\\foo\\bar", "baz"] + end + + it "splits the string at the last '\\' when the last component has an extension" do + File.split(@backslash_ext).should == ["C:\\foo\\bar", "baz.rb"] + end + end + + it "raises an ArgumentError when not passed a single argument" do + -> { File.split }.should raise_error(ArgumentError) + -> { File.split('string', 'another string') }.should raise_error(ArgumentError) + end + + it "raises a TypeError if the argument is not a String type" do + -> { File.split(1) }.should raise_error(TypeError) + end + + it "coerces the argument with to_str if it is not a String type" do + obj = mock("str") + obj.should_receive(:to_str).and_return("/one/two/three") + File.split(obj).should == ["/one/two", "three"] + end + + it "accepts an object that has a #to_path method" do + File.split(mock_to_path("")).should == [".", ""] + end +end diff --git a/spec/ruby/core/file/stat/atime_spec.rb b/spec/ruby/core/file/stat/atime_spec.rb new file mode 100644 index 0000000000..9f1111ced1 --- /dev/null +++ b/spec/ruby/core/file/stat/atime_spec.rb @@ -0,0 +1,18 @@ +require_relative '../../../spec_helper' + +describe "File::Stat#atime" do + before :each do + @file = tmp('i_exist') + touch(@file) { |f| f.write "rubinius" } + end + + after :each do + rm_r @file + end + + it "returns the atime of a File::Stat object" do + st = File.stat(@file) + st.atime.should be_kind_of(Time) + st.atime.should <= Time.now + end +end diff --git a/spec/ruby/core/file/stat/birthtime_spec.rb b/spec/ruby/core/file/stat/birthtime_spec.rb new file mode 100644 index 0000000000..9aa39297b2 --- /dev/null +++ b/spec/ruby/core/file/stat/birthtime_spec.rb @@ -0,0 +1,29 @@ +require_relative '../../../spec_helper' + +platform_is(:windows, :darwin, :freebsd, :netbsd, + *ruby_version_is("4.0") { :linux }, + ) do + not_implemented_messages = [ + "birthtime() function is unimplemented", # unsupported OS/version + "birthtime is unimplemented", # unsupported filesystem + ] + + describe "File::Stat#birthtime" do + before :each do + @file = tmp('i_exist') + touch(@file) { |f| f.write "rubinius" } + end + + after :each do + rm_r @file + end + + it "returns the birthtime of a File::Stat object" do + st = File.stat(@file) + st.birthtime.should be_kind_of(Time) + st.birthtime.should <= Time.now + rescue NotImplementedError => e + e.message.should.start_with?(*not_implemented_messages) + end + end +end diff --git a/spec/ruby/core/file/stat/blksize_spec.rb b/spec/ruby/core/file/stat/blksize_spec.rb new file mode 100644 index 0000000000..4d85b05e4d --- /dev/null +++ b/spec/ruby/core/file/stat/blksize_spec.rb @@ -0,0 +1,27 @@ +require_relative '../../../spec_helper' + +describe "File::Stat#blksize" do + before :each do + @file = tmp('i_exist') + touch(@file) { |f| f.write "rubinius" } + end + + after :each do + rm_r @file + end + + platform_is_not :windows do + it "returns the blksize of a File::Stat object" do + st = File.stat(@file) + st.blksize.is_a?(Integer).should == true + st.blksize.should > 0 + end + end + + platform_is :windows do + it "returns nil" do + st = File.stat(@file) + st.blksize.should == nil + end + end +end diff --git a/spec/ruby/core/file/stat/blockdev_spec.rb b/spec/ruby/core/file/stat/blockdev_spec.rb new file mode 100644 index 0000000000..f986c18125 --- /dev/null +++ b/spec/ruby/core/file/stat/blockdev_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../../spec_helper' +require_relative '../../../shared/file/blockdev' +require_relative 'fixtures/classes' + +describe "File::Stat#blockdev?" do + it_behaves_like :file_blockdev, :blockdev?, FileStat +end diff --git a/spec/ruby/core/file/stat/blocks_spec.rb b/spec/ruby/core/file/stat/blocks_spec.rb new file mode 100644 index 0000000000..f3f903d0f7 --- /dev/null +++ b/spec/ruby/core/file/stat/blocks_spec.rb @@ -0,0 +1,27 @@ +require_relative '../../../spec_helper' + +describe "File::Stat#blocks" do + before :each do + @file = tmp('i_exist') + touch(@file) { |f| f.write "rubinius" } + end + + after :each do + rm_r @file + end + + platform_is_not :windows do + it "returns a non-negative integer" do + st = File.stat(@file) + st.blocks.is_a?(Integer).should == true + st.blocks.should >= 0 + end + end + + platform_is :windows do + it "returns nil" do + st = File.stat(@file) + st.blocks.should be_nil + end + end +end diff --git a/spec/ruby/core/file/stat/chardev_spec.rb b/spec/ruby/core/file/stat/chardev_spec.rb new file mode 100644 index 0000000000..622fb2052d --- /dev/null +++ b/spec/ruby/core/file/stat/chardev_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../../spec_helper' +require_relative '../../../shared/file/chardev' +require_relative 'fixtures/classes' + +describe "File::Stat#chardev?" do + it_behaves_like :file_chardev, :chardev?, FileStat +end diff --git a/spec/ruby/core/file/stat/comparison_spec.rb b/spec/ruby/core/file/stat/comparison_spec.rb new file mode 100644 index 0000000000..faa3b6bf62 --- /dev/null +++ b/spec/ruby/core/file/stat/comparison_spec.rb @@ -0,0 +1,66 @@ +require_relative '../../../spec_helper' + +describe "File::Stat#<=>" do + before :each do + @name1 = tmp("i_exist") + @name2 = tmp("i_exist_too") + touch @name1 + touch @name2 + end + + after :each do + rm_r @name1, @name2 + end + + it "is able to compare files by the same modification times" do + now = Time.now - 1 # 1 second ago to avoid NFS cache issue + File.utime(now, now, @name1) + File.utime(now, now, @name2) + + File.open(@name1) { |file1| + File.open(@name2) { |file2| + (file1.stat <=> file2.stat).should == 0 + } + } + end + + it "is able to compare files by different modification times" do + now = Time.now + File.utime(now, now + 100, @name2) + + File.open(@name1) { |file1| + File.open(@name2) { |file2| + (file1.stat <=> file2.stat).should == -1 + } + } + + File.utime(now, now - 100, @name2) + + File.open(@name1) { |file1| + File.open(@name2) { |file2| + (file1.stat <=> file2.stat).should == 1 + } + } + end + + # TODO: Fix + it "includes Comparable and #== shows mtime equality between two File::Stat objects" do + File.open(@name1) { |file1| + File.open(@name2) { |file2| + (file1.stat == file1.stat).should == true + (file2.stat == file2.stat).should == true + } + } + + now = Time.now + File.utime(now, now + 100, @name2) + + File.open(@name1) { |file1| + File.open(@name2) { |file2| + (file1.stat == file2.stat).should == false + (file1.stat == file1.stat).should == true + (file2.stat == file2.stat).should == true + } + } + end +end diff --git a/spec/ruby/core/file/stat/ctime_spec.rb b/spec/ruby/core/file/stat/ctime_spec.rb new file mode 100644 index 0000000000..fd50487a0a --- /dev/null +++ b/spec/ruby/core/file/stat/ctime_spec.rb @@ -0,0 +1,18 @@ +require_relative '../../../spec_helper' + +describe "File::Stat#ctime" do + before :each do + @file = tmp('i_exist') + touch(@file) { |f| f.write "rubinius" } + end + + after :each do + rm_r @file + end + + it "returns the ctime of a File::Stat object" do + st = File.stat(@file) + st.ctime.should be_kind_of(Time) + st.ctime.should <= Time.now + end +end diff --git a/spec/ruby/core/file/stat/dev_major_spec.rb b/spec/ruby/core/file/stat/dev_major_spec.rb new file mode 100644 index 0000000000..4966d609e2 --- /dev/null +++ b/spec/ruby/core/file/stat/dev_major_spec.rb @@ -0,0 +1,23 @@ +require_relative '../../../spec_helper' + +describe "File::Stat#dev_major" do + before :each do + @name = tmp("file.txt") + touch(@name) + end + after :each do + rm_r @name + end + + platform_is_not :windows do + it "returns the major part of File::Stat#dev" do + File.stat(@name).dev_major.should be_kind_of(Integer) + end + end + + platform_is :windows do + it "returns nil" do + File.stat(@name).dev_major.should be_nil + end + end +end diff --git a/spec/ruby/core/file/stat/dev_minor_spec.rb b/spec/ruby/core/file/stat/dev_minor_spec.rb new file mode 100644 index 0000000000..ea79c12b99 --- /dev/null +++ b/spec/ruby/core/file/stat/dev_minor_spec.rb @@ -0,0 +1,23 @@ +require_relative '../../../spec_helper' + +describe "File::Stat#dev_minor" do + before :each do + @name = tmp("file.txt") + touch(@name) + end + after :each do + rm_r @name + end + + platform_is_not :windows do + it "returns the minor part of File::Stat#dev" do + File.stat(@name).dev_minor.should be_kind_of(Integer) + end + end + + platform_is :windows do + it "returns nil" do + File.stat(@name).dev_minor.should be_nil + end + end +end diff --git a/spec/ruby/core/file/stat/dev_spec.rb b/spec/ruby/core/file/stat/dev_spec.rb new file mode 100644 index 0000000000..e953fcaa58 --- /dev/null +++ b/spec/ruby/core/file/stat/dev_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../../spec_helper' + +describe "File::Stat#dev" do + before :each do + @name = tmp("file.txt") + touch(@name) + end + after :each do + rm_r @name + end + + it "returns the number of the device on which the file exists" do + File.stat(@name).dev.should be_kind_of(Integer) + end +end diff --git a/spec/ruby/core/file/stat/directory_spec.rb b/spec/ruby/core/file/stat/directory_spec.rb new file mode 100644 index 0000000000..c03610388b --- /dev/null +++ b/spec/ruby/core/file/stat/directory_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../../spec_helper' +require_relative '../../../shared/file/directory' +require_relative 'fixtures/classes' + +describe "File::Stat#directory?" do + it_behaves_like :file_directory, :directory?, FileStat +end diff --git a/spec/ruby/core/file/stat/executable_real_spec.rb b/spec/ruby/core/file/stat/executable_real_spec.rb new file mode 100644 index 0000000000..23bffe89c5 --- /dev/null +++ b/spec/ruby/core/file/stat/executable_real_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../../spec_helper' +require_relative '../../../shared/file/executable_real' +require_relative 'fixtures/classes' + +describe "File::Stat#executable_real?" do + it_behaves_like :file_executable_real, :executable_real?, FileStat +end diff --git a/spec/ruby/core/file/stat/executable_spec.rb b/spec/ruby/core/file/stat/executable_spec.rb new file mode 100644 index 0000000000..422975d14b --- /dev/null +++ b/spec/ruby/core/file/stat/executable_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../../spec_helper' +require_relative '../../../shared/file/executable' +require_relative 'fixtures/classes' + +describe "File::Stat#executable?" do + it_behaves_like :file_executable, :executable?, FileStat +end diff --git a/spec/ruby/core/file/stat/file_spec.rb b/spec/ruby/core/file/stat/file_spec.rb new file mode 100644 index 0000000000..d141536b4b --- /dev/null +++ b/spec/ruby/core/file/stat/file_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../../spec_helper' +require_relative '../../../shared/file/file' +require_relative 'fixtures/classes' + +describe "File::Stat#file?" do + it_behaves_like :file_file, :file?, FileStat +end diff --git a/spec/ruby/core/file/stat/fixtures/classes.rb b/spec/ruby/core/file/stat/fixtures/classes.rb new file mode 100644 index 0000000000..4fe9a2a30f --- /dev/null +++ b/spec/ruby/core/file/stat/fixtures/classes.rb @@ -0,0 +1,5 @@ +class FileStat + def self.method_missing(meth, file) + File.lstat(file).send(meth) + end +end diff --git a/spec/ruby/core/file/stat/ftype_spec.rb b/spec/ruby/core/file/stat/ftype_spec.rb new file mode 100644 index 0000000000..eb892eae5f --- /dev/null +++ b/spec/ruby/core/file/stat/ftype_spec.rb @@ -0,0 +1,64 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/file_types' + +describe "File::Stat#ftype" do + before :all do + FileSpecs.configure_types + end + + it "returns a String" do + FileSpecs.normal_file do |file| + File.lstat(file).ftype.should be_kind_of(String) + end + end + + it "returns 'file' when the file is a file" do + FileSpecs.normal_file do |file| + File.lstat(file).ftype.should == 'file' + end + end + + it "returns 'directory' when the file is a dir" do + FileSpecs.directory do |dir| + File.lstat(dir).ftype.should == 'directory' + end + end + + platform_is_not :windows do + it "returns 'characterSpecial' when the file is a char" do + FileSpecs.character_device do |char| + File.lstat(char).ftype.should == 'characterSpecial' + end + end + end + + platform_is_not :freebsd do # FreeBSD does not have block devices + with_block_device do + it "returns 'blockSpecial' when the file is a block" do + FileSpecs.block_device do |block| + File.lstat(block).ftype.should == 'blockSpecial' + end + end + end + end + + platform_is_not :windows do + it "returns 'link' when the file is a link" do + FileSpecs.symlink do |link| + File.lstat(link).ftype.should == 'link' + end + end + + it "returns fifo when the file is a fifo" do + FileSpecs.fifo do |fifo| + File.lstat(fifo).ftype.should == 'fifo' + end + end + + it "returns 'socket' when the file is a socket" do + FileSpecs.socket do |socket| + File.lstat(socket).ftype.should == 'socket' + end + end + end +end diff --git a/spec/ruby/core/file/stat/gid_spec.rb b/spec/ruby/core/file/stat/gid_spec.rb new file mode 100644 index 0000000000..3bba65bc82 --- /dev/null +++ b/spec/ruby/core/file/stat/gid_spec.rb @@ -0,0 +1,19 @@ +require_relative '../../../spec_helper' + +describe "File::Stat#gid" do + before :each do + @file = tmp('i_exist') + touch(@file) { |f| f.write "rubinius" } + File.chown(nil, Process.gid, @file) + end + + after :each do + rm_r @file + end + + it "returns the group owner attribute of a File::Stat object" do + st = File.stat(@file) + st.gid.is_a?(Integer).should == true + st.gid.should == Process.gid + end +end diff --git a/spec/ruby/core/file/stat/grpowned_spec.rb b/spec/ruby/core/file/stat/grpowned_spec.rb new file mode 100644 index 0000000000..e7278e229b --- /dev/null +++ b/spec/ruby/core/file/stat/grpowned_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../../spec_helper' +require_relative '../../../shared/file/grpowned' +require_relative 'fixtures/classes' + +describe "File::Stat#grpowned?" do + it_behaves_like :file_grpowned, :grpowned?, FileStat +end diff --git a/spec/ruby/core/file/stat/ino_spec.rb b/spec/ruby/core/file/stat/ino_spec.rb new file mode 100644 index 0000000000..42370aecb7 --- /dev/null +++ b/spec/ruby/core/file/stat/ino_spec.rb @@ -0,0 +1,28 @@ +require_relative '../../../spec_helper' + +describe "File::Stat#ino" do + before :each do + @file = tmp('i_exist') + touch(@file) { |f| f.write "rubinius" } + end + + after :each do + rm_r @file + end + + platform_is_not :windows do + it "returns the ino of a File::Stat object" do + st = File.stat(@file) + st.ino.should be_kind_of(Integer) + st.ino.should > 0 + end + end + + platform_is :windows do + it "returns BY_HANDLE_FILE_INFORMATION.nFileIndexHigh/Low of a File::Stat object" do + st = File.stat(@file) + st.ino.should be_kind_of(Integer) + st.ino.should > 0 + end + end +end diff --git a/spec/ruby/core/file/stat/inspect_spec.rb b/spec/ruby/core/file/stat/inspect_spec.rb new file mode 100644 index 0000000000..1613b427d0 --- /dev/null +++ b/spec/ruby/core/file/stat/inspect_spec.rb @@ -0,0 +1,26 @@ +require_relative '../../../spec_helper' + +describe "File::Stat#inspect" do + + before :each do + @file = tmp('i_exist') + touch(@file) { |f| f.write "rubinius" } + end + + after :each do + rm_r @file + end + + it "produces a nicely formatted description of a File::Stat object" do + st = File.stat(@file) + expected = "#<File::Stat dev=0x#{st.dev.to_s(16)}, ino=#{st.ino}, mode=#{sprintf("%07o", st.mode)}, nlink=#{st.nlink}" + expected << ", uid=#{st.uid}, gid=#{st.gid}, rdev=0x#{st.rdev.to_s(16)}, size=#{st.size}, blksize=#{st.blksize.inspect}" + expected << ", blocks=#{st.blocks.inspect}, atime=#{st.atime.inspect}, mtime=#{st.mtime.inspect}, ctime=#{st.ctime.inspect}" + platform_is :netbsd, :freebsd, :darwin do + # Windows has File.birthtime but it's not here since already shown by ctime. + expected << ", birthtime=#{st.birthtime.inspect}" + end + expected << ">" + st.inspect.should == expected + end +end diff --git a/spec/ruby/core/file/stat/mode_spec.rb b/spec/ruby/core/file/stat/mode_spec.rb new file mode 100644 index 0000000000..c85fb85a58 --- /dev/null +++ b/spec/ruby/core/file/stat/mode_spec.rb @@ -0,0 +1,19 @@ +require_relative '../../../spec_helper' + +describe "File::Stat#mode" do + before :each do + @file = tmp('i_exist') + touch(@file) { |f| f.write "rubinius" } + File.chmod(0644, @file) + end + + after :each do + rm_r @file + end + + it "returns the mode of a File::Stat object" do + st = File.stat(@file) + st.mode.is_a?(Integer).should == true + (st.mode & 0777).should == 0644 + end +end diff --git a/spec/ruby/core/file/stat/mtime_spec.rb b/spec/ruby/core/file/stat/mtime_spec.rb new file mode 100644 index 0000000000..08a2b83463 --- /dev/null +++ b/spec/ruby/core/file/stat/mtime_spec.rb @@ -0,0 +1,18 @@ +require_relative '../../../spec_helper' + +describe "File::Stat#mtime" do + before :each do + @file = tmp('i_exist') + touch(@file) { |f| f.write "rubinius" } + end + + after :each do + rm_r @file + end + + it "returns the mtime of a File::Stat object" do + st = File.stat(@file) + st.mtime.should be_kind_of(Time) + st.mtime.should <= Time.now + end +end diff --git a/spec/ruby/core/file/stat/new_spec.rb b/spec/ruby/core/file/stat/new_spec.rb new file mode 100644 index 0000000000..c0d9432ac8 --- /dev/null +++ b/spec/ruby/core/file/stat/new_spec.rb @@ -0,0 +1,32 @@ +require_relative '../../../spec_helper' + +describe "File::Stat#initialize" do + + before :each do + @file = tmp('i_exist') + touch(@file) { |f| f.write "rubinius" } + File.chmod(0755, @file) + end + + after :each do + rm_r @file + end + + it "raises an exception if the file doesn't exist" do + -> { + File::Stat.new(tmp("i_am_a_dummy_file_that_doesnt_exist")) + }.should raise_error(Errno::ENOENT) + end + + it "creates a File::Stat object for the given file" do + st = File::Stat.new(@file) + st.should be_kind_of(File::Stat) + st.ftype.should == 'file' + end + + it "calls #to_path on non-String arguments" do + p = mock('path') + p.should_receive(:to_path).and_return @file + File::Stat.new p + end +end diff --git a/spec/ruby/core/file/stat/nlink_spec.rb b/spec/ruby/core/file/stat/nlink_spec.rb new file mode 100644 index 0000000000..7143923cfc --- /dev/null +++ b/spec/ruby/core/file/stat/nlink_spec.rb @@ -0,0 +1,21 @@ +require_relative '../../../spec_helper' + +describe "File::Stat#nlink" do + before :each do + @file = tmp("stat_nlink") + @link = @file + ".lnk" + touch @file + end + + after :each do + rm_r @link, @file + end + + platform_is_not :windows, :android do + it "returns the number of links to a file" do + File::Stat.new(@file).nlink.should == 1 + File.link(@file, @link) + File::Stat.new(@file).nlink.should == 2 + end + end +end diff --git a/spec/ruby/core/file/stat/owned_spec.rb b/spec/ruby/core/file/stat/owned_spec.rb new file mode 100644 index 0000000000..a23ad850c5 --- /dev/null +++ b/spec/ruby/core/file/stat/owned_spec.rb @@ -0,0 +1,33 @@ +require_relative '../../../spec_helper' +require_relative '../../../shared/file/owned' +require_relative 'fixtures/classes' + +describe "File::Stat#owned?" do + it_behaves_like :file_owned, :owned?, FileStat +end + +describe "File::Stat#owned?" do + before :each do + @file = tmp("i_exist") + touch(@file) + end + + after :each do + rm_r @file + end + + it "returns true if the file is owned by the user" do + st = File.stat(@file) + st.should.owned? + end + + platform_is_not :windows, :android do + as_user do + it "returns false if the file is not owned by the user" do + system_file = '/etc/passwd' + st = File.stat(system_file) + st.should_not.owned? + end + end + end +end diff --git a/spec/ruby/core/file/stat/pipe_spec.rb b/spec/ruby/core/file/stat/pipe_spec.rb new file mode 100644 index 0000000000..692dfbf42a --- /dev/null +++ b/spec/ruby/core/file/stat/pipe_spec.rb @@ -0,0 +1,32 @@ +require_relative '../../../spec_helper' +require_relative '../../../shared/file/pipe' +require_relative 'fixtures/classes' + +describe "File::Stat#pipe?" do + it_behaves_like :file_pipe, :pipe?, FileStat +end + +describe "File::Stat#pipe?" do + it "returns false if the file is not a pipe" do + filename = tmp("i_exist") + touch(filename) + + st = File.stat(filename) + st.should_not.pipe? + + rm_r filename + end + + platform_is_not :windows do + it "returns true if the file is a pipe" do + filename = tmp("i_am_a_pipe") + File.mkfifo(filename) + + st = File.stat(filename) + st.should.pipe? + + rm_r filename + end + end + +end diff --git a/spec/ruby/core/file/stat/rdev_major_spec.rb b/spec/ruby/core/file/stat/rdev_major_spec.rb new file mode 100644 index 0000000000..e08d19c03a --- /dev/null +++ b/spec/ruby/core/file/stat/rdev_major_spec.rb @@ -0,0 +1,24 @@ +require_relative '../../../spec_helper' + +describe "File::Stat#rdev_major" do + before :each do + @name = tmp("file.txt") + touch(@name) + end + + after :each do + rm_r @name + end + + platform_is_not :windows do + it "returns the major part of File::Stat#rdev" do + File.stat(@name).rdev_major.should be_kind_of(Integer) + end + end + + platform_is :windows do + it "returns nil" do + File.stat(@name).rdev_major.should be_nil + end + end +end diff --git a/spec/ruby/core/file/stat/rdev_minor_spec.rb b/spec/ruby/core/file/stat/rdev_minor_spec.rb new file mode 100644 index 0000000000..ace5b8a732 --- /dev/null +++ b/spec/ruby/core/file/stat/rdev_minor_spec.rb @@ -0,0 +1,24 @@ +require_relative '../../../spec_helper' + +describe "File::Stat#rdev_minor" do + before :each do + @name = tmp("file.txt") + touch(@name) + end + + after :each do + rm_r @name + end + + platform_is_not :windows do + it "returns the minor part of File::Stat#rdev" do + File.stat(@name).rdev_minor.should be_kind_of(Integer) + end + end + + platform_is :windows do + it "returns nil" do + File.stat(@name).rdev_minor.should be_nil + end + end +end diff --git a/spec/ruby/core/file/stat/rdev_spec.rb b/spec/ruby/core/file/stat/rdev_spec.rb new file mode 100644 index 0000000000..9e1aee692d --- /dev/null +++ b/spec/ruby/core/file/stat/rdev_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../../spec_helper' + +describe "File::Stat#rdev" do + before :each do + @name = tmp("file.txt") + touch(@name) + end + after :each do + rm_r @name + end + + it "returns the number of the device this file represents which the file exists" do + File.stat(@name).rdev.should be_kind_of(Integer) + end +end diff --git a/spec/ruby/core/file/stat/readable_real_spec.rb b/spec/ruby/core/file/stat/readable_real_spec.rb new file mode 100644 index 0000000000..f138fd7b00 --- /dev/null +++ b/spec/ruby/core/file/stat/readable_real_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../../spec_helper' +require_relative '../../../shared/file/readable_real' +require_relative 'fixtures/classes' + +describe "File::Stat#readable_real?" do + it_behaves_like :file_readable_real, :readable_real?, FileStat +end diff --git a/spec/ruby/core/file/stat/readable_spec.rb b/spec/ruby/core/file/stat/readable_spec.rb new file mode 100644 index 0000000000..e99e48feed --- /dev/null +++ b/spec/ruby/core/file/stat/readable_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../../spec_helper' +require_relative '../../../shared/file/readable' +require_relative 'fixtures/classes' + +describe "File::Stat#readable?" do + it_behaves_like :file_readable, :readable?, FileStat +end diff --git a/spec/ruby/core/file/stat/setgid_spec.rb b/spec/ruby/core/file/stat/setgid_spec.rb new file mode 100644 index 0000000000..c0748ede57 --- /dev/null +++ b/spec/ruby/core/file/stat/setgid_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../../spec_helper' +require_relative '../../../shared/file/setgid' +require_relative 'fixtures/classes' + +describe "File::Stat#setgid?" do + it_behaves_like :file_setgid, :setgid?, FileStat +end diff --git a/spec/ruby/core/file/stat/setuid_spec.rb b/spec/ruby/core/file/stat/setuid_spec.rb new file mode 100644 index 0000000000..6408120fc4 --- /dev/null +++ b/spec/ruby/core/file/stat/setuid_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../../spec_helper' +require_relative '../../../shared/file/setuid' +require_relative 'fixtures/classes' + +describe "File::Stat#setuid?" do + it_behaves_like :file_setuid, :setuid?, FileStat +end diff --git a/spec/ruby/core/file/stat/size_spec.rb b/spec/ruby/core/file/stat/size_spec.rb new file mode 100644 index 0000000000..4b4f57f8c8 --- /dev/null +++ b/spec/ruby/core/file/stat/size_spec.rb @@ -0,0 +1,21 @@ +require_relative '../../../spec_helper' +require_relative '../../../shared/file/size' +require_relative 'fixtures/classes' + +describe "File::Stat.size?" do + it_behaves_like :file_size, :size?, FileStat + it_behaves_like :file_size_nil_when_empty, :size?, FileStat +end + +describe "File::Stat.size" do + it_behaves_like :file_size, :size, FileStat + it_behaves_like :file_size_0_when_empty, :size, FileStat +end + +describe "File::Stat#size" do + it "needs to be reviewed for spec completeness" +end + +describe "File::Stat#size?" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/file/stat/socket_spec.rb b/spec/ruby/core/file/stat/socket_spec.rb new file mode 100644 index 0000000000..09740be110 --- /dev/null +++ b/spec/ruby/core/file/stat/socket_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../../spec_helper' +require_relative '../../../shared/file/socket' +require_relative 'fixtures/classes' + +describe "File::Stat#socket?" do + it_behaves_like :file_socket, :socket?, FileStat +end diff --git a/spec/ruby/core/file/stat/sticky_spec.rb b/spec/ruby/core/file/stat/sticky_spec.rb new file mode 100644 index 0000000000..7083e644e9 --- /dev/null +++ b/spec/ruby/core/file/stat/sticky_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../../spec_helper' +require_relative '../../../shared/file/sticky' +require_relative 'fixtures/classes' + +describe "File::Stat#sticky?" do + it_behaves_like :file_sticky, :sticky?, FileStat +end diff --git a/spec/ruby/core/file/stat/symlink_spec.rb b/spec/ruby/core/file/stat/symlink_spec.rb new file mode 100644 index 0000000000..0def832a4c --- /dev/null +++ b/spec/ruby/core/file/stat/symlink_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../../spec_helper' +require_relative '../../../shared/file/symlink' +require_relative 'fixtures/classes' + +describe "File::Stat#symlink?" do + it_behaves_like :file_symlink, :symlink?, FileStat +end diff --git a/spec/ruby/core/file/stat/uid_spec.rb b/spec/ruby/core/file/stat/uid_spec.rb new file mode 100644 index 0000000000..b97147db21 --- /dev/null +++ b/spec/ruby/core/file/stat/uid_spec.rb @@ -0,0 +1,18 @@ +require_relative '../../../spec_helper' + +describe "File::Stat#uid" do + before :each do + @file = tmp('i_exist') + touch(@file) { |f| f.write "rubinius" } + end + + after :each do + rm_r @file + end + + it "returns the owner attribute of a File::Stat object" do + st = File.stat(@file) + st.uid.is_a?(Integer).should == true + st.uid.should == Process.uid + end +end diff --git a/spec/ruby/core/file/stat/world_readable_spec.rb b/spec/ruby/core/file/stat/world_readable_spec.rb new file mode 100644 index 0000000000..d94a02205e --- /dev/null +++ b/spec/ruby/core/file/stat/world_readable_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../../spec_helper' +require_relative '../../../shared/file/world_readable' +require_relative 'fixtures/classes' + +describe "File::Stat.world_readable?" do + it_behaves_like :file_world_readable, :world_readable?, FileStat +end + +describe "File::Stat#world_readable?" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/file/stat/world_writable_spec.rb b/spec/ruby/core/file/stat/world_writable_spec.rb new file mode 100644 index 0000000000..8100008344 --- /dev/null +++ b/spec/ruby/core/file/stat/world_writable_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../../spec_helper' +require_relative '../../../shared/file/world_writable' +require_relative 'fixtures/classes' + +describe "File::Stat.world_writable?" do + it_behaves_like :file_world_writable, :world_writable?, FileStat +end + +describe "File::Stat#world_writable?" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/file/stat/writable_real_spec.rb b/spec/ruby/core/file/stat/writable_real_spec.rb new file mode 100644 index 0000000000..4c9e78eb70 --- /dev/null +++ b/spec/ruby/core/file/stat/writable_real_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../../spec_helper' +require_relative '../../../shared/file/writable_real' +require_relative 'fixtures/classes' + +describe "File::Stat#writable_real?" do + it_behaves_like :file_writable_real, :writable_real?, FileStat +end diff --git a/spec/ruby/core/file/stat/writable_spec.rb b/spec/ruby/core/file/stat/writable_spec.rb new file mode 100644 index 0000000000..551268751f --- /dev/null +++ b/spec/ruby/core/file/stat/writable_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../../spec_helper' +require_relative '../../../shared/file/writable' +require_relative 'fixtures/classes' + +describe "File::Stat#writable?" do + it_behaves_like :file_writable, :writable?, FileStat +end diff --git a/spec/ruby/core/file/stat/zero_spec.rb b/spec/ruby/core/file/stat/zero_spec.rb new file mode 100644 index 0000000000..74facac66a --- /dev/null +++ b/spec/ruby/core/file/stat/zero_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../../spec_helper' +require_relative '../../../shared/file/zero' +require_relative 'fixtures/classes' + +describe "File::Stat#zero?" do + it_behaves_like :file_zero, :zero?, FileStat +end diff --git a/spec/ruby/core/file/stat_spec.rb b/spec/ruby/core/file/stat_spec.rb new file mode 100644 index 0000000000..6365500057 --- /dev/null +++ b/spec/ruby/core/file/stat_spec.rb @@ -0,0 +1,55 @@ +require_relative '../../spec_helper' +require_relative 'shared/stat' + +describe "File.stat" do + it_behaves_like :file_stat, :stat +end + +platform_is_not :windows do + describe "File.stat" do + before :each do + @file = tmp('i_exist') + @link = tmp('i_am_a_symlink') + touch(@file) { |f| f.write "rubinius" } + end + + after :each do + rm_r @link, @file + end + + it "returns information for a file that has been deleted but is still open" do + File.open(@file) do |f| + rm_r @file + + st = f.stat + + st.should.file? + st.should_not.zero? + st.size.should == 8 + st.size?.should == 8 + st.blksize.should >= 0 + st.atime.should be_kind_of(Time) + st.ctime.should be_kind_of(Time) + st.mtime.should be_kind_of(Time) + end + end + + it "returns a File::Stat object with file properties for a symlink" do + File.symlink(@file, @link) + st = File.stat(@link) + + st.should.file? + st.should_not.symlink? + end + + it "returns an error when given missing non-ASCII path" do + missing_path = "/missingfilepath\xE3E4".b + -> { + File.stat(missing_path) + }.should raise_error(SystemCallError) { |e| + [Errno::ENOENT, Errno::EILSEQ].should include(e.class) + e.message.should include(missing_path) + } + end + end +end diff --git a/spec/ruby/core/file/sticky_spec.rb b/spec/ruby/core/file/sticky_spec.rb new file mode 100644 index 0000000000..5f7b2d93eb --- /dev/null +++ b/spec/ruby/core/file/sticky_spec.rb @@ -0,0 +1,50 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/sticky' + +describe "File.sticky?" do + it_behaves_like :file_sticky, :sticky?, File + it_behaves_like :file_sticky_missing, :sticky?, File +end + +describe "File.sticky?" do + platform_is_not :windows do + it "returns false if file does not exist" do + File.sticky?("I_am_a_bogus_file").should == false + end + + it "returns false if the file has not sticky bit set" do + filename = tmp("i_exist") + touch(filename) + + File.sticky?(filename).should == false + + rm_r filename + end + end + + platform_is :linux, :darwin do + it "returns true if the file has sticky bit set" do + filename = tmp("i_exist") + touch(filename) + system "chmod +t #{filename}" + + File.sticky?(filename).should == true + + rm_r filename + end + end + + platform_is :bsd do + # FreeBSD and NetBSD can't set sticky bit to a normal file + it "cannot set sticky bit to a normal file" do + filename = tmp("i_exist") + touch(filename) + stat = File.stat(filename) + mode = stat.mode + raise_error(Errno::EFTYPE){File.chmod(mode|01000, filename)} + File.sticky?(filename).should == false + + rm_r filename + end + end +end diff --git a/spec/ruby/core/file/symlink_spec.rb b/spec/ruby/core/file/symlink_spec.rb new file mode 100644 index 0000000000..0e8b0a5a20 --- /dev/null +++ b/spec/ruby/core/file/symlink_spec.rb @@ -0,0 +1,57 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/symlink' + +describe "File.symlink" do + before :each do + @file = tmp("file_symlink.txt") + @link = tmp("file_symlink.lnk") + + rm_r @link + touch @file + end + + after :each do + rm_r @link, @file + end + + platform_is_not :windows do + it "creates a symlink between a source and target file" do + File.symlink(@file, @link).should == 0 + File.identical?(@file, @link).should == true + end + + it "creates a symbolic link" do + File.symlink(@file, @link) + File.symlink?(@link).should == true + end + + it "accepts args that have #to_path methods" do + File.symlink(mock_to_path(@file), mock_to_path(@link)) + File.symlink?(@link).should == true + end + + it "raises an Errno::EEXIST if the target already exists" do + File.symlink(@file, @link) + -> { File.symlink(@file, @link) }.should raise_error(Errno::EEXIST) + end + + it "raises an ArgumentError if not called with two arguments" do + -> { File.symlink }.should raise_error(ArgumentError) + -> { File.symlink(@file) }.should raise_error(ArgumentError) + end + + it "raises a TypeError if not called with String types" do + -> { File.symlink(@file, nil) }.should raise_error(TypeError) + -> { File.symlink(@file, 1) }.should raise_error(TypeError) + -> { File.symlink(1, 1) }.should raise_error(TypeError) + end + end +end + +describe "File.symlink?" do + it_behaves_like :file_symlink, :symlink?, File +end + +describe "File.symlink?" do + it_behaves_like :file_symlink_nonexistent, :symlink?, File +end diff --git a/spec/ruby/core/file/to_path_spec.rb b/spec/ruby/core/file/to_path_spec.rb new file mode 100644 index 0000000000..6d168a065c --- /dev/null +++ b/spec/ruby/core/file/to_path_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/path' + +describe "File#to_path" do + it_behaves_like :file_path, :to_path +end diff --git a/spec/ruby/core/file/truncate_spec.rb b/spec/ruby/core/file/truncate_spec.rb new file mode 100644 index 0000000000..b4a2e3e577 --- /dev/null +++ b/spec/ruby/core/file/truncate_spec.rb @@ -0,0 +1,177 @@ +require_relative '../../spec_helper' + +describe "File.truncate" do + before :each do + @name = tmp("test.txt") + touch(@name) { |f| f.write("1234567890") } + end + + after :each do + rm_r @name + end + + it "truncates a file" do + File.size(@name).should == 10 + + File.truncate(@name, 5) + File.size(@name).should == 5 + + File.open(@name, "r") do |f| + f.read(99).should == "12345" + f.should.eof? + end + end + + it "truncate a file size to 0" do + File.truncate(@name, 0).should == 0 + IO.read(@name).should == "" + end + + it "truncate a file size to 5" do + File.size(@name).should == 10 + File.truncate(@name, 5) + File.size(@name).should == 5 + IO.read(@name).should == "12345" + end + + it "truncates to a larger file size than the original file" do + File.truncate(@name, 12) + File.size(@name).should == 12 + IO.read(@name).should == "1234567890\000\000" + end + + it "truncates to the same size as the original file" do + File.truncate(@name, File.size(@name)) + File.size(@name).should == 10 + IO.read(@name).should == "1234567890" + end + + it "raises an Errno::ENOENT if the file does not exist" do + # TODO: missing_file + not_existing_file = tmp("file-does-not-exist-for-sure.txt") + + # make sure it doesn't exist for real + rm_r not_existing_file + + begin + -> { File.truncate(not_existing_file, 5) }.should raise_error(Errno::ENOENT) + ensure + rm_r not_existing_file + end + end + + it "raises an ArgumentError if not passed two arguments" do + -> { File.truncate }.should raise_error(ArgumentError) + -> { File.truncate(@name) }.should raise_error(ArgumentError) + end + + platform_is_not :netbsd, :openbsd do + it "raises an Errno::EINVAL if the length argument is not valid" do + -> { File.truncate(@name, -1) }.should raise_error(Errno::EINVAL) # May fail + end + end + + it "raises a TypeError if not passed a String type for the first argument" do + -> { File.truncate(1, 1) }.should raise_error(TypeError) + end + + it "raises a TypeError if not passed an Integer type for the second argument" do + -> { File.truncate(@name, nil) }.should raise_error(TypeError) + end + + it "accepts an object that has a #to_path method" do + File.truncate(mock_to_path(@name), 0).should == 0 + end +end + + +describe "File#truncate" do + before :each do + @name = tmp("test.txt") + @file = File.open @name, 'w' + @file.write "1234567890" + @file.flush + end + + after :each do + @file.close unless @file.closed? + rm_r @name + end + + it "does not move the file write pointer to the specified byte offset" do + @file.truncate(3) + @file.write "abc" + @file.close + File.read(@name).should == "123\x00\x00\x00\x00\x00\x00\x00abc" + end + + it "does not move the file read pointer to the specified byte offset" do + File.open(@name, "r+") do |f| + f.read(1).should == "1" + f.truncate(0) + f.read(1).should == nil + end + end + + it "truncates a file" do + File.size(@name).should == 10 + + @file.truncate(5) + File.size(@name).should == 5 + File.open(@name, "r") do |f| + f.read(99).should == "12345" + f.should.eof? + end + end + + it "truncates a file size to 0" do + @file.truncate(0).should == 0 + IO.read(@name).should == "" + end + + it "truncates a file size to 5" do + File.size(@name).should == 10 + @file.truncate(5) + File.size(@name).should == 5 + IO.read(@name).should == "12345" + end + + it "truncates a file to a larger size than the original file" do + @file.truncate(12) + File.size(@name).should == 12 + IO.read(@name).should == "1234567890\000\000" + end + + it "truncates a file to the same size as the original file" do + @file.truncate(File.size(@name)) + File.size(@name).should == 10 + IO.read(@name).should == "1234567890" + end + + it "raises an ArgumentError if not passed one argument" do + -> { @file.truncate }.should raise_error(ArgumentError) + -> { @file.truncate(1) }.should_not raise_error(ArgumentError) + end + + platform_is_not :netbsd do + it "raises an Errno::EINVAL if the length argument is not valid" do + -> { @file.truncate(-1) }.should raise_error(Errno::EINVAL) # May fail + end + end + + it "raises an IOError if file is closed" do + @file.close + @file.should.closed? + -> { @file.truncate(42) }.should raise_error(IOError) + end + + it "raises an IOError if file is not opened for writing" do + File.open(@name, 'r') do |file| + -> { file.truncate(42) }.should raise_error(IOError) + end + end + + it "raises a TypeError if not passed an Integer type for the for the argument" do + -> { @file.truncate(nil) }.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/file/umask_spec.rb b/spec/ruby/core/file/umask_spec.rb new file mode 100644 index 0000000000..7f7e40abbc --- /dev/null +++ b/spec/ruby/core/file/umask_spec.rb @@ -0,0 +1,57 @@ +require_relative '../../spec_helper' + +describe "File.umask" do + before :each do + @orig_umask = File.umask + @file = tmp('test.txt') + touch @file + end + + after :each do + rm_r @file + File.umask(@orig_umask) + end + + it "returns an Integer" do + File.umask.should be_kind_of(Integer) + end + + platform_is_not :windows do + it "returns the current umask value for the process" do + File.umask(022) + File.umask(006).should == 022 + File.umask.should == 006 + end + + it "invokes to_int on non-integer argument" do + (obj = mock(022)).should_receive(:to_int).any_number_of_times.and_return(022) + File.umask(obj) + File.umask(obj).should == 022 + end + end + + platform_is :windows do + it "returns the current umask value for this process (basic)" do + File.umask.should == 0 + File.umask(022).should == 0 + File.umask(044).should == 0 + end + + # The value used here is the value of _S_IWRITE. + it "returns the current umask value for this process" do + File.umask(0000200) + File.umask.should == 0000200 + File.umask(0006) + File.umask.should == 0 + end + end + + it "raises RangeError with too large values" do + -> { File.umask(2**64) }.should raise_error(RangeError) + -> { File.umask(-2**63 - 1) }.should raise_error(RangeError) + end + + it "raises ArgumentError when more than one argument is provided" do + -> { File.umask(022, 022) }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/file/unlink_spec.rb b/spec/ruby/core/file/unlink_spec.rb new file mode 100644 index 0000000000..28872d55ed --- /dev/null +++ b/spec/ruby/core/file/unlink_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/unlink' + +describe "File.unlink" do + it_behaves_like :file_unlink, :unlink +end diff --git a/spec/ruby/core/file/utime_spec.rb b/spec/ruby/core/file/utime_spec.rb new file mode 100644 index 0000000000..d87626be50 --- /dev/null +++ b/spec/ruby/core/file/utime_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/update_time' + +describe "File.utime" do + it_behaves_like :update_time, :utime +end diff --git a/spec/ruby/core/file/world_readable_spec.rb b/spec/ruby/core/file/world_readable_spec.rb new file mode 100644 index 0000000000..11b8e67d0b --- /dev/null +++ b/spec/ruby/core/file/world_readable_spec.rb @@ -0,0 +1,12 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/world_readable' + +describe "File.world_readable?" do + it_behaves_like :file_world_readable, :world_readable?, File + + it "returns nil if the file does not exist" do + file = rand.to_s + $$.to_s + File.should_not.exist?(file) + File.world_readable?(file).should be_nil + end +end diff --git a/spec/ruby/core/file/world_writable_spec.rb b/spec/ruby/core/file/world_writable_spec.rb new file mode 100644 index 0000000000..d378cf2eb9 --- /dev/null +++ b/spec/ruby/core/file/world_writable_spec.rb @@ -0,0 +1,12 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/world_writable' + +describe "File.world_writable?" do + it_behaves_like :file_world_writable, :world_writable?, File + + it "returns nil if the file does not exist" do + file = rand.to_s + $$.to_s + File.should_not.exist?(file) + File.world_writable?(file).should be_nil + end +end diff --git a/spec/ruby/core/file/writable_real_spec.rb b/spec/ruby/core/file/writable_real_spec.rb new file mode 100644 index 0000000000..bea4c4c262 --- /dev/null +++ b/spec/ruby/core/file/writable_real_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/writable_real' + +describe "File.writable_real?" do + it_behaves_like :file_writable_real, :writable_real?, File + it_behaves_like :file_writable_real_missing, :writable_real?, File +end diff --git a/spec/ruby/core/file/writable_spec.rb b/spec/ruby/core/file/writable_spec.rb new file mode 100644 index 0000000000..519837b0d1 --- /dev/null +++ b/spec/ruby/core/file/writable_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/writable' + +describe "File.writable?" do + it_behaves_like :file_writable, :writable?, File + it_behaves_like :file_writable_missing, :writable?, File +end diff --git a/spec/ruby/core/file/zero_spec.rb b/spec/ruby/core/file/zero_spec.rb new file mode 100644 index 0000000000..01c7505ef2 --- /dev/null +++ b/spec/ruby/core/file/zero_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/zero' + +describe "File.zero?" do + it_behaves_like :file_zero, :zero?, File + it_behaves_like :file_zero_missing, :zero?, File +end diff --git a/spec/ruby/core/filetest/blockdev_spec.rb b/spec/ruby/core/filetest/blockdev_spec.rb new file mode 100644 index 0000000000..4f32991c4a --- /dev/null +++ b/spec/ruby/core/filetest/blockdev_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/blockdev' + +describe "FileTest.blockdev?" do + it_behaves_like :file_blockdev, :blockdev?, FileTest +end diff --git a/spec/ruby/core/filetest/chardev_spec.rb b/spec/ruby/core/filetest/chardev_spec.rb new file mode 100644 index 0000000000..59c48bb2d5 --- /dev/null +++ b/spec/ruby/core/filetest/chardev_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/chardev' + +describe "FileTest.chardev?" do + it_behaves_like :file_chardev, :chardev?, FileTest +end diff --git a/spec/ruby/core/filetest/directory_spec.rb b/spec/ruby/core/filetest/directory_spec.rb new file mode 100644 index 0000000000..8f9d0e3901 --- /dev/null +++ b/spec/ruby/core/filetest/directory_spec.rb @@ -0,0 +1,10 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/directory' + +describe "FileTest.directory?" do + it_behaves_like :file_directory, :directory?, FileTest +end + +describe "FileTest.directory?" do + it_behaves_like :file_directory_io, :directory?, FileTest +end diff --git a/spec/ruby/core/filetest/executable_real_spec.rb b/spec/ruby/core/filetest/executable_real_spec.rb new file mode 100644 index 0000000000..da65245785 --- /dev/null +++ b/spec/ruby/core/filetest/executable_real_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/executable_real' + +describe "FileTest.executable_real?" do + it_behaves_like :file_executable_real, :executable_real?, FileTest + it_behaves_like :file_executable_real_missing, :executable_real?, FileTest +end diff --git a/spec/ruby/core/filetest/executable_spec.rb b/spec/ruby/core/filetest/executable_spec.rb new file mode 100644 index 0000000000..03056669f6 --- /dev/null +++ b/spec/ruby/core/filetest/executable_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/executable' + +describe "FileTest.executable?" do + it_behaves_like :file_executable, :executable?, FileTest + it_behaves_like :file_executable_missing, :executable?, FileTest +end diff --git a/spec/ruby/core/filetest/exist_spec.rb b/spec/ruby/core/filetest/exist_spec.rb new file mode 100644 index 0000000000..612ffa9fcb --- /dev/null +++ b/spec/ruby/core/filetest/exist_spec.rb @@ -0,0 +1,12 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/exist' + +describe "FileTest.exist?" do + it_behaves_like :file_exist, :exist?, FileTest +end + +describe "FileTest.exists?" do + it "has been removed" do + FileTest.should_not.respond_to?(:exists?) + end +end diff --git a/spec/ruby/core/filetest/file_spec.rb b/spec/ruby/core/filetest/file_spec.rb new file mode 100644 index 0000000000..0c0cb82f96 --- /dev/null +++ b/spec/ruby/core/filetest/file_spec.rb @@ -0,0 +1,10 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/file' + +describe "File.file?" do + it_behaves_like :file_file, :file?, File +end + +describe "FileTest.file?" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/filetest/grpowned_spec.rb b/spec/ruby/core/filetest/grpowned_spec.rb new file mode 100644 index 0000000000..d073cb9770 --- /dev/null +++ b/spec/ruby/core/filetest/grpowned_spec.rb @@ -0,0 +1,10 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/grpowned' + +describe "FileTest.grpowned?" do + it_behaves_like :file_grpowned, :grpowned?, FileTest + + it "returns false if the file doesn't exist" do + FileTest.grpowned?("xxx-tmp-doesnt_exist-blah").should be_false + end +end diff --git a/spec/ruby/core/filetest/identical_spec.rb b/spec/ruby/core/filetest/identical_spec.rb new file mode 100644 index 0000000000..b00c5b75e8 --- /dev/null +++ b/spec/ruby/core/filetest/identical_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/identical' + +describe "FileTest.identical?" do + it_behaves_like :file_identical, :identical?, FileTest +end diff --git a/spec/ruby/core/filetest/owned_spec.rb b/spec/ruby/core/filetest/owned_spec.rb new file mode 100644 index 0000000000..b26165f98d --- /dev/null +++ b/spec/ruby/core/filetest/owned_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/owned' + +describe "FileTest.owned?" do + it_behaves_like :file_owned, :owned?, FileTest +end diff --git a/spec/ruby/core/filetest/pipe_spec.rb b/spec/ruby/core/filetest/pipe_spec.rb new file mode 100644 index 0000000000..8ce67568fb --- /dev/null +++ b/spec/ruby/core/filetest/pipe_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/pipe' + +describe "FileTest.pipe?" do + it_behaves_like :file_pipe, :pipe?, FileTest +end diff --git a/spec/ruby/core/filetest/readable_real_spec.rb b/spec/ruby/core/filetest/readable_real_spec.rb new file mode 100644 index 0000000000..82c62fe8f0 --- /dev/null +++ b/spec/ruby/core/filetest/readable_real_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/readable_real' + +describe "FileTest.readable_real?" do + it_behaves_like :file_readable_real, :readable_real?, FileTest + it_behaves_like :file_readable_real_missing, :readable_real?, FileTest +end diff --git a/spec/ruby/core/filetest/readable_spec.rb b/spec/ruby/core/filetest/readable_spec.rb new file mode 100644 index 0000000000..039ca56ca3 --- /dev/null +++ b/spec/ruby/core/filetest/readable_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/readable' + +describe "FileTest.readable?" do + it_behaves_like :file_readable, :readable?, FileTest + it_behaves_like :file_readable_missing, :readable?, FileTest +end diff --git a/spec/ruby/core/filetest/setgid_spec.rb b/spec/ruby/core/filetest/setgid_spec.rb new file mode 100644 index 0000000000..c83ffccb2a --- /dev/null +++ b/spec/ruby/core/filetest/setgid_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/setgid' + +describe "FileTest.setgid?" do + it_behaves_like :file_setgid, :setgid?, FileTest +end diff --git a/spec/ruby/core/filetest/setuid_spec.rb b/spec/ruby/core/filetest/setuid_spec.rb new file mode 100644 index 0000000000..1c1fd2efe4 --- /dev/null +++ b/spec/ruby/core/filetest/setuid_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/setuid' + +describe "FileTest.setuid?" do + it_behaves_like :file_setuid, :setuid?, FileTest +end diff --git a/spec/ruby/core/filetest/size_spec.rb b/spec/ruby/core/filetest/size_spec.rb new file mode 100644 index 0000000000..dc3ddb127f --- /dev/null +++ b/spec/ruby/core/filetest/size_spec.rb @@ -0,0 +1,34 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/size' + +describe "FileTest.size?" do + it_behaves_like :file_size, :size?, FileTest +end + +describe "FileTest.size?" do + it_behaves_like :file_size_nil_when_missing, :size?, FileTest +end + +describe "FileTest.size?" do + it_behaves_like :file_size_nil_when_empty, :size?, FileTest +end + +describe "FileTest.size?" do + it_behaves_like :file_size_with_file_argument, :size?, FileTest +end + +describe "FileTest.size" do + it_behaves_like :file_size, :size, FileTest +end + +describe "FileTest.size" do + it_behaves_like :file_size_raise_when_missing, :size, FileTest +end + +describe "FileTest.size" do + it_behaves_like :file_size_0_when_empty, :size, FileTest +end + +describe "FileTest.size" do + it_behaves_like :file_size_with_file_argument, :size, FileTest +end diff --git a/spec/ruby/core/filetest/socket_spec.rb b/spec/ruby/core/filetest/socket_spec.rb new file mode 100644 index 0000000000..f274be6318 --- /dev/null +++ b/spec/ruby/core/filetest/socket_spec.rb @@ -0,0 +1,10 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/socket' + +describe "FileTest.socket?" do + it_behaves_like :file_socket, :socket?, FileTest + + it "returns false if file does not exist" do + FileTest.socket?("I_am_a_bogus_file").should == false + end +end diff --git a/spec/ruby/core/filetest/sticky_spec.rb b/spec/ruby/core/filetest/sticky_spec.rb new file mode 100644 index 0000000000..8b776b6672 --- /dev/null +++ b/spec/ruby/core/filetest/sticky_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/sticky' + +describe "FileTest.sticky?" do + it_behaves_like :file_sticky, :sticky?, FileTest + it_behaves_like :file_sticky_missing, :sticky?, FileTest +end diff --git a/spec/ruby/core/filetest/symlink_spec.rb b/spec/ruby/core/filetest/symlink_spec.rb new file mode 100644 index 0000000000..41c924dc1a --- /dev/null +++ b/spec/ruby/core/filetest/symlink_spec.rb @@ -0,0 +1,10 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/symlink' + +describe "FileTest.symlink?" do + it_behaves_like :file_symlink, :symlink?, FileTest +end + +describe "FileTest.symlink?" do + it_behaves_like :file_symlink_nonexistent, :symlink?, File +end diff --git a/spec/ruby/core/filetest/world_readable_spec.rb b/spec/ruby/core/filetest/world_readable_spec.rb new file mode 100644 index 0000000000..72abdd9e03 --- /dev/null +++ b/spec/ruby/core/filetest/world_readable_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../spec_helper' + +describe "FileTest.world_readable?" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/filetest/world_writable_spec.rb b/spec/ruby/core/filetest/world_writable_spec.rb new file mode 100644 index 0000000000..533f698fd3 --- /dev/null +++ b/spec/ruby/core/filetest/world_writable_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../spec_helper' + +describe "FileTest.world_writable?" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/filetest/writable_real_spec.rb b/spec/ruby/core/filetest/writable_real_spec.rb new file mode 100644 index 0000000000..64abe4cd3f --- /dev/null +++ b/spec/ruby/core/filetest/writable_real_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/writable_real' + +describe "FileTest.writable_real?" do + it_behaves_like :file_writable_real, :writable_real?, FileTest + it_behaves_like :file_writable_real_missing, :writable_real?, FileTest +end diff --git a/spec/ruby/core/filetest/writable_spec.rb b/spec/ruby/core/filetest/writable_spec.rb new file mode 100644 index 0000000000..e921a5887b --- /dev/null +++ b/spec/ruby/core/filetest/writable_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/writable' + +describe "FileTest.writable?" do + it_behaves_like :file_writable, :writable?, FileTest + it_behaves_like :file_writable_missing, :writable?, FileTest +end diff --git a/spec/ruby/core/filetest/zero_spec.rb b/spec/ruby/core/filetest/zero_spec.rb new file mode 100644 index 0000000000..92cab67f1b --- /dev/null +++ b/spec/ruby/core/filetest/zero_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/zero' + +describe "FileTest.zero?" do + it_behaves_like :file_zero, :zero?, FileTest + it_behaves_like :file_zero_missing, :zero?, FileTest +end diff --git a/spec/ruby/core/float/abs_spec.rb b/spec/ruby/core/float/abs_spec.rb new file mode 100644 index 0000000000..a08601926d --- /dev/null +++ b/spec/ruby/core/float/abs_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/abs' + +describe "Float#abs" do + it_behaves_like :float_abs, :abs +end diff --git a/spec/ruby/core/float/angle_spec.rb b/spec/ruby/core/float/angle_spec.rb new file mode 100644 index 0000000000..c07249aa97 --- /dev/null +++ b/spec/ruby/core/float/angle_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/arg' + +describe "Float#angle" do + it_behaves_like :float_arg, :angle +end diff --git a/spec/ruby/core/float/arg_spec.rb b/spec/ruby/core/float/arg_spec.rb new file mode 100644 index 0000000000..d3a50668f8 --- /dev/null +++ b/spec/ruby/core/float/arg_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/arg' + +describe "Float#arg" do + it_behaves_like :float_arg, :arg +end diff --git a/spec/ruby/core/float/case_compare_spec.rb b/spec/ruby/core/float/case_compare_spec.rb new file mode 100644 index 0000000000..b902fbea18 --- /dev/null +++ b/spec/ruby/core/float/case_compare_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/equal' + +describe "Float#===" do + it_behaves_like :float_equal, :=== +end diff --git a/spec/ruby/core/float/ceil_spec.rb b/spec/ruby/core/float/ceil_spec.rb new file mode 100644 index 0000000000..75f5610292 --- /dev/null +++ b/spec/ruby/core/float/ceil_spec.rb @@ -0,0 +1,26 @@ +require_relative '../../spec_helper' +require_relative '../integer/shared/integer_ceil_precision' + +describe "Float#ceil" do + context "with precision" do + it_behaves_like :integer_ceil_precision, :Float + end + + it "returns the smallest Integer greater than or equal to self" do + -1.2.ceil.should eql( -1) + -1.0.ceil.should eql( -1) + 0.0.ceil.should eql( 0 ) + 1.3.ceil.should eql( 2 ) + 3.0.ceil.should eql( 3 ) + -9223372036854775808.1.ceil.should eql(-9223372036854775808) + +9223372036854775808.1.ceil.should eql(+9223372036854775808) + end + + it "returns the smallest number greater than or equal to self with an optionally given precision" do + 2.1679.ceil(0).should eql(3) + 214.94.ceil(-1).should eql(220) + 7.0.ceil(1).should eql(7.0) + -1.234.ceil(2).should eql(-1.23) + 5.123812.ceil(4).should eql(5.1239) + end +end diff --git a/spec/ruby/core/float/coerce_spec.rb b/spec/ruby/core/float/coerce_spec.rb new file mode 100644 index 0000000000..baa831dcf6 --- /dev/null +++ b/spec/ruby/core/float/coerce_spec.rb @@ -0,0 +1,18 @@ +require_relative '../../spec_helper' + +describe "Float#coerce" do + it "returns [other, self] both as Floats" do + 1.2.coerce(1).should == [1.0, 1.2] + 5.28.coerce(1.0).should == [1.0, 5.28] + 1.0.coerce(1).should == [1.0, 1.0] + 1.0.coerce("2.5").should == [2.5, 1.0] + 1.0.coerce(3.14).should == [3.14, 1.0] + + a, b = -0.0.coerce(bignum_value) + a.should be_close(18446744073709551616.0, TOLERANCE) + b.should be_close(-0.0, TOLERANCE) + a, b = 1.0.coerce(bignum_value) + a.should be_close(18446744073709551616.0, TOLERANCE) + b.should be_close(1.0, TOLERANCE) + end +end diff --git a/spec/ruby/core/float/comparison_spec.rb b/spec/ruby/core/float/comparison_spec.rb new file mode 100644 index 0000000000..0cd290f4ad --- /dev/null +++ b/spec/ruby/core/float/comparison_spec.rb @@ -0,0 +1,113 @@ +require_relative '../../spec_helper' + +describe "Float#<=>" do + it "returns -1, 0, 1 when self is less than, equal, or greater than other" do + (1.5 <=> 5).should == -1 + (2.45 <=> 2.45).should == 0 + ((bignum_value*1.1) <=> bignum_value).should == 1 + end + + it "returns nil if one side is NaN" do + [1.0, 42, bignum_value].each { |n| + (nan_value <=> n).should == nil + (n <=> nan_value).should == nil + } + end + + it "handles positive infinity" do + [1.0, 42, bignum_value].each { |n| + (infinity_value <=> n).should == 1 + (n <=> infinity_value).should == -1 + } + end + + it "handles negative infinity" do + [1.0, 42, bignum_value].each { |n| + (-infinity_value <=> n).should == -1 + (n <=> -infinity_value).should == 1 + } + end + + it "returns nil when the given argument is not a Float" do + (1.0 <=> "1").should be_nil + (1.0 <=> "1".freeze).should be_nil + (1.0 <=> :one).should be_nil + (1.0 <=> true).should be_nil + end + + it "compares using #coerce when argument is not a Float" do + klass = Class.new do + attr_reader :call_count + def coerce(other) + @call_count ||= 0 + @call_count += 1 + [other, 42.0] + end + end + + coercible = klass.new + (2.33 <=> coercible).should == -1 + (42.0 <=> coercible).should == 0 + (43.0 <=> coercible).should == 1 + coercible.call_count.should == 3 + end + + it "raises TypeError when #coerce misbehaves" do + klass = Class.new do + def coerce(other) + :incorrect + end + end + + bad_coercible = klass.new + -> { + 4.2 <=> bad_coercible + }.should raise_error(TypeError, "coerce must return [x, y]") + end + + it "returns the correct result when one side is infinite" do + (infinity_value <=> Float::MAX.to_i*2).should == 1 + (-Float::MAX.to_i*2 <=> infinity_value).should == -1 + (-infinity_value <=> -Float::MAX.to_i*2).should == -1 + (-Float::MAX.to_i*2 <=> -infinity_value).should == 1 + end + + it "returns 0 when self is Infinity and other is infinite?=1" do + obj = Object.new + def obj.infinite? + 1 + end + (infinity_value <=> obj).should == 0 + end + + it "returns 1 when self is Infinity and other is infinite?=-1" do + obj = Object.new + def obj.infinite? + -1 + end + (infinity_value <=> obj).should == 1 + end + + it "returns 1 when self is Infinity and other is infinite?=nil (which means finite)" do + obj = Object.new + def obj.infinite? + nil + end + (infinity_value <=> obj).should == 1 + end + + it "returns 0 for -0.0 and 0.0" do + (-0.0 <=> 0.0).should == 0 + (0.0 <=> -0.0).should == 0 + end + + it "returns 0 for -0.0 and 0" do + (-0.0 <=> 0).should == 0 + (0 <=> -0.0).should == 0 + end + + it "returns 0 for 0.0 and 0" do + (0.0 <=> 0).should == 0 + (0 <=> 0.0).should == 0 + end +end diff --git a/spec/ruby/core/float/constants_spec.rb b/spec/ruby/core/float/constants_spec.rb new file mode 100644 index 0000000000..497e0ae188 --- /dev/null +++ b/spec/ruby/core/float/constants_spec.rb @@ -0,0 +1,55 @@ +require_relative '../../spec_helper' + +describe "Float constant" do + it "DIG is 15" do + Float::DIG.should == 15 + end + + it "EPSILON is 2.220446049250313e-16" do + Float::EPSILON.should == 2.0 ** -52 + Float::EPSILON.should == 2.220446049250313e-16 + end + + it "MANT_DIG is 53" do + Float::MANT_DIG.should == 53 + end + + it "MAX_10_EXP is 308" do + Float::MAX_10_EXP.should == 308 + end + + it "MIN_10_EXP is -308" do + Float::MIN_10_EXP.should == -307 + end + + it "MAX_EXP is 1024" do + Float::MAX_EXP.should == 1024 + end + + it "MIN_EXP is -1021" do + Float::MIN_EXP.should == -1021 + end + + it "MAX is 1.7976931348623157e+308" do + # See https://en.wikipedia.org/wiki/Double-precision_floating-point_format#Double-precision_examples + Float::MAX.should == (1 + (1 - (2 ** -52))) * (2.0 ** 1023) + Float::MAX.should == 1.7976931348623157e+308 + end + + it "MIN is 2.2250738585072014e-308" do + Float::MIN.should == (2.0 ** -1022) + Float::MIN.should == 2.2250738585072014e-308 + end + + it "RADIX is 2" do + Float::RADIX.should == 2 + end + + it "INFINITY is the positive infinity" do + Float::INFINITY.infinite?.should == 1 + end + + it "NAN is 'not a number'" do + Float::NAN.nan?.should be_true + end +end diff --git a/spec/ruby/core/float/denominator_spec.rb b/spec/ruby/core/float/denominator_spec.rb new file mode 100644 index 0000000000..6f4fcfcf23 --- /dev/null +++ b/spec/ruby/core/float/denominator_spec.rb @@ -0,0 +1,29 @@ +require_relative '../../spec_helper' + +describe "Float#denominator" do + before :each do + @numbers = [ + 0.0, + 29871.22736282, + 7772222663.0, + 1.4592, + ].map {|n| [0-n, n]}.flatten + end + + it "returns an Integer" do + @numbers.each do |number| + number.denominator.should be_kind_of(Integer) + end + end + + it "converts self to a Rational and returns the denominator" do + @numbers.each do |number| + number.denominator.should == Rational(number).denominator + end + end + + it "returns 1 for NaN and Infinity" do + nan_value.denominator.should == 1 + infinity_value.denominator.should == 1 + end +end diff --git a/spec/ruby/core/float/divide_spec.rb b/spec/ruby/core/float/divide_spec.rb new file mode 100644 index 0000000000..72ab7527bd --- /dev/null +++ b/spec/ruby/core/float/divide_spec.rb @@ -0,0 +1,43 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/coerce' +require_relative 'shared/arithmetic_exception_in_coerce' + +describe "Float#/" do + it_behaves_like :float_arithmetic_exception_in_coerce, :/ + + it "returns self divided by other" do + (5.75 / -2).should be_close(-2.875,TOLERANCE) + (451.0 / 9.3).should be_close(48.494623655914,TOLERANCE) + (91.1 / -0xffffffff).should be_close(-2.12108716418061e-08, TOLERANCE) + end + + it "properly coerces objects" do + (5.0 / FloatSpecs::CanCoerce.new(5)).should be_close(0, TOLERANCE) + end + + it "returns +Infinity when dividing non-zero by zero of the same sign" do + (1.0 / 0.0).should be_positive_infinity + (-1.0 / -0.0).should be_positive_infinity + end + + it "returns -Infinity when dividing non-zero by zero of opposite sign" do + (-1.0 / 0.0).should be_negative_infinity + (1.0 / -0.0).should be_negative_infinity + end + + it "returns NaN when dividing zero by zero" do + (0.0 / 0.0).should be_nan + (-0.0 / 0.0).should be_nan + (0.0 / -0.0).should be_nan + (-0.0 / -0.0).should be_nan + end + + it "raises a TypeError when given a non-Numeric" do + -> { 13.0 / "10" }.should raise_error(TypeError) + -> { 13.0 / :symbol }.should raise_error(TypeError) + end + + it "divides correctly by Rational numbers" do + (1.2345678901234567 / Rational(1, 10000000000000000000)).should == 1.2345678901234567e+19 + end +end diff --git a/spec/ruby/core/float/divmod_spec.rb b/spec/ruby/core/float/divmod_spec.rb new file mode 100644 index 0000000000..dad45a9b89 --- /dev/null +++ b/spec/ruby/core/float/divmod_spec.rb @@ -0,0 +1,43 @@ +require_relative '../../spec_helper' + +describe "Float#divmod" do + it "returns an [quotient, modulus] from dividing self by other" do + values = 3.14.divmod(2) + values[0].should eql(1) + values[1].should be_close(1.14, TOLERANCE) + values = 2.8284.divmod(3.1415) + values[0].should eql(0) + values[1].should be_close(2.8284, TOLERANCE) + values = -1.0.divmod(bignum_value) + values[0].should eql(-1) + values[1].should be_close(18446744073709551616.0, TOLERANCE) + values = -1.0.divmod(1) + values[0].should eql(-1) + values[1].should eql(0.0) + end + + # Behaviour established as correct in r23953 + it "raises a FloatDomainError if self is NaN" do + -> { nan_value.divmod(1) }.should raise_error(FloatDomainError) + end + + # Behaviour established as correct in r23953 + it "raises a FloatDomainError if other is NaN" do + -> { 1.0.divmod(nan_value) }.should raise_error(FloatDomainError) + end + + # Behaviour established as correct in r23953 + it "raises a FloatDomainError if self is Infinity" do + -> { infinity_value.divmod(1) }.should raise_error(FloatDomainError) + end + + it "raises a ZeroDivisionError if other is zero" do + -> { 1.0.divmod(0) }.should raise_error(ZeroDivisionError) + -> { 1.0.divmod(0.0) }.should raise_error(ZeroDivisionError) + end + + # redmine #5276" + it "returns the correct [quotient, modulus] even for large quotient" do + 0.59.divmod(7.761021455128987e-11).first.should eql(7602092113) + end +end diff --git a/spec/ruby/core/float/dup_spec.rb b/spec/ruby/core/float/dup_spec.rb new file mode 100644 index 0000000000..294da8e2bc --- /dev/null +++ b/spec/ruby/core/float/dup_spec.rb @@ -0,0 +1,8 @@ +require_relative '../../spec_helper' + +describe "Float#dup" do + it "returns self" do + float = 2.4 + float.dup.should equal(float) + end +end diff --git a/spec/ruby/core/float/eql_spec.rb b/spec/ruby/core/float/eql_spec.rb new file mode 100644 index 0000000000..6b5f91db33 --- /dev/null +++ b/spec/ruby/core/float/eql_spec.rb @@ -0,0 +1,16 @@ +require_relative '../../spec_helper' + +describe "Float#eql?" do + it "returns true if other is a Float equal to self" do + 0.0.eql?(0.0).should be_true + end + + it "returns false if other is a Float not equal to self" do + 1.0.eql?(1.1).should be_false + end + + it "returns false if other is not a Float" do + 1.0.eql?(1).should be_false + 1.0.eql?(:one).should be_false + end +end diff --git a/spec/ruby/core/float/equal_value_spec.rb b/spec/ruby/core/float/equal_value_spec.rb new file mode 100644 index 0000000000..03eea5108e --- /dev/null +++ b/spec/ruby/core/float/equal_value_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/equal' + +describe "Float#==" do + it_behaves_like :float_equal, :== +end diff --git a/spec/ruby/core/float/exponent_spec.rb b/spec/ruby/core/float/exponent_spec.rb new file mode 100644 index 0000000000..a4c03469a7 --- /dev/null +++ b/spec/ruby/core/float/exponent_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../spec_helper' + +describe "Float#**" do + it "returns self raise to the other power" do + (2.3 ** 3).should be_close(12.167,TOLERANCE) + (5.2 ** -1).should be_close(0.192307692307692,TOLERANCE) + (9.5 ** 0.5).should be_close(3.08220700148449, TOLERANCE) + (9.5 ** 0xffffffff).to_s.should == 'Infinity' + end + + it "returns a complex number when negative and raised to a fractional power" do + ((-8.0) ** (1.0/3)) .should be_close(Complex(1, 1.73205), TOLERANCE) + ((-8.0) ** Rational(1,3)).should be_close(Complex(1, 1.73205), TOLERANCE) + end +end diff --git a/spec/ruby/core/float/fdiv_spec.rb b/spec/ruby/core/float/fdiv_spec.rb new file mode 100644 index 0000000000..be25ee283b --- /dev/null +++ b/spec/ruby/core/float/fdiv_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/quo' + +describe "Float#fdiv" do + it_behaves_like :float_quo, :fdiv +end diff --git a/spec/ruby/core/float/finite_spec.rb b/spec/ruby/core/float/finite_spec.rb new file mode 100644 index 0000000000..d839b30e32 --- /dev/null +++ b/spec/ruby/core/float/finite_spec.rb @@ -0,0 +1,19 @@ +require_relative '../../spec_helper' + +describe "Float#finite?" do + it "returns true for finite values" do + 3.14159.should.finite? + end + + it "returns false for positive infinity" do + infinity_value.should_not.finite? + end + + it "returns false for negative infinity" do + (-infinity_value).should_not.finite? + end + + it "returns false for NaN" do + nan_value.should_not.finite? + end +end diff --git a/spec/ruby/core/float/fixtures/classes.rb b/spec/ruby/core/float/fixtures/classes.rb new file mode 100644 index 0000000000..2d80184e7d --- /dev/null +++ b/spec/ruby/core/float/fixtures/classes.rb @@ -0,0 +1,4 @@ +module FloatSpecs + class CoerceError < StandardError + end +end diff --git a/spec/ruby/core/float/fixtures/coerce.rb b/spec/ruby/core/float/fixtures/coerce.rb new file mode 100644 index 0000000000..2cf155be95 --- /dev/null +++ b/spec/ruby/core/float/fixtures/coerce.rb @@ -0,0 +1,15 @@ +module FloatSpecs + class CanCoerce + def initialize(a) + @a = a + end + + def coerce(b) + [self.class.new(b), @a] + end + + def /(b) + @a.to_i % b.to_i + end + end +end diff --git a/spec/ruby/core/float/float_spec.rb b/spec/ruby/core/float/float_spec.rb new file mode 100644 index 0000000000..263ae82079 --- /dev/null +++ b/spec/ruby/core/float/float_spec.rb @@ -0,0 +1,19 @@ +require_relative '../../spec_helper' + +describe "Float" do + it "includes Comparable" do + Float.include?(Comparable).should == true + end + + it ".allocate raises a TypeError" do + -> do + Float.allocate + end.should raise_error(TypeError) + end + + it ".new is undefined" do + -> do + Float.new + end.should raise_error(NoMethodError) + end +end diff --git a/spec/ruby/core/float/floor_spec.rb b/spec/ruby/core/float/floor_spec.rb new file mode 100644 index 0000000000..8b492ef473 --- /dev/null +++ b/spec/ruby/core/float/floor_spec.rb @@ -0,0 +1,26 @@ +require_relative '../../spec_helper' +require_relative '../integer/shared/integer_floor_precision' + +describe "Float#floor" do + context "with precision" do + it_behaves_like :integer_floor_precision, :Float + end + + it "returns the largest Integer less than or equal to self" do + -1.2.floor.should eql( -2) + -1.0.floor.should eql( -1) + 0.0.floor.should eql( 0 ) + 1.0.floor.should eql( 1 ) + 5.9.floor.should eql( 5 ) + -9223372036854775808.1.floor.should eql(-9223372036854775808) + +9223372036854775808.1.floor.should eql(+9223372036854775808) + end + + it "returns the largest number less than or equal to self with an optionally given precision" do + 2.1679.floor(0).should eql(2) + 214.94.floor(-1).should eql(210) + 7.0.floor(1).should eql(7.0) + -1.234.floor(2).should eql(-1.24) + 5.123812.floor(4).should eql(5.1238) + end +end diff --git a/spec/ruby/core/float/gt_spec.rb b/spec/ruby/core/float/gt_spec.rb new file mode 100644 index 0000000000..33078e07ce --- /dev/null +++ b/spec/ruby/core/float/gt_spec.rb @@ -0,0 +1,38 @@ +require_relative '../../spec_helper' +require_relative 'shared/comparison_exception_in_coerce' + +describe "Float#>" do + it_behaves_like :float_comparison_exception_in_coerce, :> + + it "returns true if self is greater than other" do + (1.5 > 1).should == true + (2.5 > 3).should == false + (45.91 > bignum_value).should == false + end + + it "raises an ArgumentError when given a non-Numeric" do + -> { 5.0 > "4" }.should raise_error(ArgumentError) + -> { 5.0 > mock('x') }.should raise_error(ArgumentError) + end + + it "returns false if one side is NaN" do + [1.0, 42, bignum_value].each { |n| + (nan_value > n).should == false + (n > nan_value).should == false + } + end + + it "handles positive infinity" do + [1.0, 42, bignum_value].each { |n| + (infinity_value > n).should == true + (n > infinity_value).should == false + } + end + + it "handles negative infinity" do + [1.0, 42, bignum_value].each { |n| + (-infinity_value > n).should == false + (n > -infinity_value).should == true + } + end +end diff --git a/spec/ruby/core/float/gte_spec.rb b/spec/ruby/core/float/gte_spec.rb new file mode 100644 index 0000000000..44c0a81b43 --- /dev/null +++ b/spec/ruby/core/float/gte_spec.rb @@ -0,0 +1,38 @@ +require_relative '../../spec_helper' +require_relative 'shared/comparison_exception_in_coerce' + +describe "Float#>=" do + it_behaves_like :float_comparison_exception_in_coerce, :>= + + it "returns true if self is greater than or equal to other" do + (5.2 >= 5.2).should == true + (9.71 >= 1).should == true + (5.55382 >= 0xfabdafbafcab).should == false + end + + it "raises an ArgumentError when given a non-Numeric" do + -> { 5.0 >= "4" }.should raise_error(ArgumentError) + -> { 5.0 >= mock('x') }.should raise_error(ArgumentError) + end + + it "returns false if one side is NaN" do + [1.0, 42, bignum_value].each { |n| + (nan_value >= n).should == false + (n >= nan_value).should == false + } + end + + it "handles positive infinity" do + [1.0, 42, bignum_value].each { |n| + (infinity_value >= n).should == true + (n >= infinity_value).should == false + } + end + + it "handles negative infinity" do + [1.0, 42, bignum_value].each { |n| + (-infinity_value >= n).should == false + (n >= -infinity_value).should == true + } + end +end diff --git a/spec/ruby/core/float/hash_spec.rb b/spec/ruby/core/float/hash_spec.rb new file mode 100644 index 0000000000..5f77e3b4a1 --- /dev/null +++ b/spec/ruby/core/float/hash_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' + +describe "Float#hash" do + it "is provided" do + 0.0.respond_to?(:hash).should == true + end + + it "is stable" do + 1.0.hash.should == 1.0.hash + end +end diff --git a/spec/ruby/core/float/infinite_spec.rb b/spec/ruby/core/float/infinite_spec.rb new file mode 100644 index 0000000000..901c2738aa --- /dev/null +++ b/spec/ruby/core/float/infinite_spec.rb @@ -0,0 +1,19 @@ +require_relative '../../spec_helper' + +describe "Float#infinite?" do + it "returns nil for finite values" do + 1.0.infinite?.should == nil + end + + it "returns 1 for positive infinity" do + infinity_value.infinite?.should == 1 + end + + it "returns -1 for negative infinity" do + (-infinity_value).infinite?.should == -1 + end + + it "returns nil for NaN" do + nan_value.infinite?.should == nil + end +end diff --git a/spec/ruby/core/float/inspect_spec.rb b/spec/ruby/core/float/inspect_spec.rb new file mode 100644 index 0000000000..4be1927d84 --- /dev/null +++ b/spec/ruby/core/float/inspect_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/to_s' + +describe "Float#inspect" do + it_behaves_like :float_to_s, :inspect +end diff --git a/spec/ruby/core/float/lt_spec.rb b/spec/ruby/core/float/lt_spec.rb new file mode 100644 index 0000000000..94dcfc42f8 --- /dev/null +++ b/spec/ruby/core/float/lt_spec.rb @@ -0,0 +1,38 @@ +require_relative '../../spec_helper' +require_relative 'shared/comparison_exception_in_coerce' + +describe "Float#<" do + it_behaves_like :float_comparison_exception_in_coerce, :< + + it "returns true if self is less than other" do + (71.3 < 91.8).should == true + (192.6 < -500).should == false + (-0.12 < 0x4fffffff).should == true + end + + it "raises an ArgumentError when given a non-Numeric" do + -> { 5.0 < "4" }.should raise_error(ArgumentError) + -> { 5.0 < mock('x') }.should raise_error(ArgumentError) + end + + it "returns false if one side is NaN" do + [1.0, 42, bignum_value].each { |n| + (nan_value < n).should == false + (n < nan_value).should == false + } + end + + it "handles positive infinity" do + [1.0, 42, bignum_value].each { |n| + (infinity_value < n).should == false + (n < infinity_value).should == true + } + end + + it "handles negative infinity" do + [1.0, 42, bignum_value].each { |n| + (-infinity_value < n).should == true + (n < -infinity_value).should == false + } + end +end diff --git a/spec/ruby/core/float/lte_spec.rb b/spec/ruby/core/float/lte_spec.rb new file mode 100644 index 0000000000..7b5a86ee76 --- /dev/null +++ b/spec/ruby/core/float/lte_spec.rb @@ -0,0 +1,39 @@ +require_relative '../../spec_helper' +require_relative 'shared/comparison_exception_in_coerce' + +describe "Float#<=" do + it_behaves_like :float_comparison_exception_in_coerce, :>= + + it "returns true if self is less than or equal to other" do + (2.0 <= 3.14159).should == true + (-2.7183 <= -24).should == false + (0.0 <= 0.0).should == true + (9_235.9 <= bignum_value).should == true + end + + it "raises an ArgumentError when given a non-Numeric" do + -> { 5.0 <= "4" }.should raise_error(ArgumentError) + -> { 5.0 <= mock('x') }.should raise_error(ArgumentError) + end + + it "returns false if one side is NaN" do + [1.0, 42, bignum_value].each { |n| + (nan_value <= n).should == false + (n <= nan_value).should == false + } + end + + it "handles positive infinity" do + [1.0, 42, bignum_value].each { |n| + (infinity_value <= n).should == false + (n <= infinity_value).should == true + } + end + + it "handles negative infinity" do + [1.0, 42, bignum_value].each { |n| + (-infinity_value <= n).should == true + (n <= -infinity_value).should == false + } + end +end diff --git a/spec/ruby/core/float/magnitude_spec.rb b/spec/ruby/core/float/magnitude_spec.rb new file mode 100644 index 0000000000..7cdd8ef28a --- /dev/null +++ b/spec/ruby/core/float/magnitude_spec.rb @@ -0,0 +1,6 @@ +require_relative "../../spec_helper" +require_relative 'shared/abs' + +describe "Float#magnitude" do + it_behaves_like :float_abs, :magnitude +end diff --git a/spec/ruby/core/float/minus_spec.rb b/spec/ruby/core/float/minus_spec.rb new file mode 100644 index 0000000000..a4281a397b --- /dev/null +++ b/spec/ruby/core/float/minus_spec.rb @@ -0,0 +1,12 @@ +require_relative '../../spec_helper' +require_relative 'shared/arithmetic_exception_in_coerce' + +describe "Float#-" do + it_behaves_like :float_arithmetic_exception_in_coerce, :- + + it "returns self minus other" do + (9_237_212.5280 - 5_280).should be_close(9231932.528, TOLERANCE) + (2_560_496.1691 - bignum_value).should be_close(-18446744073706991616.0, TOLERANCE) + (5.5 - 5.5).should be_close(0.0,TOLERANCE) + end +end diff --git a/spec/ruby/core/float/modulo_spec.rb b/spec/ruby/core/float/modulo_spec.rb new file mode 100644 index 0000000000..8ae80a0b05 --- /dev/null +++ b/spec/ruby/core/float/modulo_spec.rb @@ -0,0 +1,10 @@ +require_relative '../../spec_helper' +require_relative 'shared/modulo' + +describe "Float#%" do + it_behaves_like :float_modulo, :% +end + +describe "Float#modulo" do + it_behaves_like :float_modulo, :modulo +end diff --git a/spec/ruby/core/float/multiply_spec.rb b/spec/ruby/core/float/multiply_spec.rb new file mode 100644 index 0000000000..2adb8796cd --- /dev/null +++ b/spec/ruby/core/float/multiply_spec.rb @@ -0,0 +1,17 @@ +require_relative '../../spec_helper' +require_relative 'shared/arithmetic_exception_in_coerce' + +describe "Float#*" do + it_behaves_like :float_arithmetic_exception_in_coerce, :* + + it "returns self multiplied by other" do + (4923.98221 * 2).should be_close(9847.96442, TOLERANCE) + (6712.5 * 0.25).should be_close(1678.125, TOLERANCE) + (256.4096 * bignum_value).should be_close(4729922269242236862464.0, TOLERANCE) + end + + it "raises a TypeError when given a non-Numeric" do + -> { 13.0 * "10" }.should raise_error(TypeError) + -> { 13.0 * :symbol }.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/float/nan_spec.rb b/spec/ruby/core/float/nan_spec.rb new file mode 100644 index 0000000000..c1043ef21b --- /dev/null +++ b/spec/ruby/core/float/nan_spec.rb @@ -0,0 +1,9 @@ +require_relative '../../spec_helper' + +describe "Float#nan?" do + it "returns true if self is not a valid IEEE floating-point number" do + 0.0.should_not.nan? + -1.5.should_not.nan? + nan_value.should.nan? + end +end diff --git a/spec/ruby/core/float/negative_spec.rb b/spec/ruby/core/float/negative_spec.rb new file mode 100644 index 0000000000..511d92ade6 --- /dev/null +++ b/spec/ruby/core/float/negative_spec.rb @@ -0,0 +1,33 @@ +require_relative '../../spec_helper' + +describe "Float#negative?" do + describe "on positive numbers" do + it "returns false" do + 0.1.negative?.should be_false + end + end + + describe "on zero" do + it "returns false" do + 0.0.negative?.should be_false + end + end + + describe "on negative zero" do + it "returns false" do + -0.0.negative?.should be_false + end + end + + describe "on negative numbers" do + it "returns true" do + -0.1.negative?.should be_true + end + end + + describe "on NaN" do + it "returns false" do + nan_value.negative?.should be_false + end + end +end diff --git a/spec/ruby/core/float/next_float_spec.rb b/spec/ruby/core/float/next_float_spec.rb new file mode 100644 index 0000000000..29e2d31146 --- /dev/null +++ b/spec/ruby/core/float/next_float_spec.rb @@ -0,0 +1,49 @@ +require_relative '../../spec_helper' + +describe "Float#next_float" do + it "returns a float the smallest possible step greater than the receiver" do + barely_positive = 0.0.next_float + barely_positive.should == 0.0.next_float + + barely_positive.should > 0.0 + barely_positive.should < barely_positive.next_float + + midpoint = barely_positive / 2 + [0.0, barely_positive].should include midpoint + end + + it "returns Float::INFINITY for Float::INFINITY" do + Float::INFINITY.next_float.should == Float::INFINITY + end + + it "steps directly between MAX and INFINITY" do + (-Float::INFINITY).next_float.should == -Float::MAX + Float::MAX.next_float.should == Float::INFINITY + end + + it "steps directly between 1.0 and 1.0 + EPSILON" do + 1.0.next_float.should == 1.0 + Float::EPSILON + end + + it "steps directly between -1.0 and -1.0 + EPSILON/2" do + (-1.0).next_float.should == -1.0 + Float::EPSILON/2 + end + + it "reverses the effect of prev_float for all Floats except INFINITY and +0.0" do + num = -rand + num.prev_float.next_float.should == num + end + + it "returns negative zero when stepping upward from just below zero" do + x = (-0.0).prev_float.next_float + (1/x).should == -Float::INFINITY + end + + it "gives the same result for -0.0 as for +0.0" do + (-0.0).next_float.should == (0.0).next_float + end + + it "returns NAN if NAN was the receiver" do + Float::NAN.next_float.should.nan? + end +end diff --git a/spec/ruby/core/float/numerator_spec.rb b/spec/ruby/core/float/numerator_spec.rb new file mode 100644 index 0000000000..7832e8f056 --- /dev/null +++ b/spec/ruby/core/float/numerator_spec.rb @@ -0,0 +1,39 @@ +require_relative '../../spec_helper' + +describe "Float#numerator" do + before :all do + @numbers = [ + 29871.2722891, + 999.1**99.928888, + -72628191273.22, + 29282.2827, + -2927.00091, + 12.0, + Float::MAX, + ] + end + + it "converts self to a Rational object then returns its numerator" do + @numbers.each do |number| + number.infinite?.should be_nil + number.numerator.should == Rational(number).numerator + end + end + + it "returns 0 for 0.0" do + 0.0.numerator.should == 0 + end + + it "returns NaN for NaN" do + nan_value.numerator.nan?.should be_true + end + + it "returns Infinity for Infinity" do + infinity_value.numerator.infinite?.should == 1 + end + + it "returns -Infinity for -Infinity" do + (-infinity_value).numerator.infinite?.should == -1 + end + +end diff --git a/spec/ruby/core/float/phase_spec.rb b/spec/ruby/core/float/phase_spec.rb new file mode 100644 index 0000000000..2aa84024b4 --- /dev/null +++ b/spec/ruby/core/float/phase_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/arg' + +describe "Float#phase" do + it_behaves_like :float_arg, :phase +end diff --git a/spec/ruby/core/float/plus_spec.rb b/spec/ruby/core/float/plus_spec.rb new file mode 100644 index 0000000000..e3e19d7f39 --- /dev/null +++ b/spec/ruby/core/float/plus_spec.rb @@ -0,0 +1,12 @@ +require_relative '../../spec_helper' +require_relative 'shared/arithmetic_exception_in_coerce' + +describe "Float#+" do + it_behaves_like :float_arithmetic_exception_in_coerce, :+ + + it "returns self plus other" do + (491.213 + 2).should be_close(493.213, TOLERANCE) + (9.99 + bignum_value).should be_close(18446744073709551616.0, TOLERANCE) + (1001.99 + 5.219).should be_close(1007.209, TOLERANCE) + end +end diff --git a/spec/ruby/core/float/positive_spec.rb b/spec/ruby/core/float/positive_spec.rb new file mode 100644 index 0000000000..575f92a720 --- /dev/null +++ b/spec/ruby/core/float/positive_spec.rb @@ -0,0 +1,33 @@ +require_relative '../../spec_helper' + +describe "Float#positive?" do + describe "on positive numbers" do + it "returns true" do + 0.1.positive?.should be_true + end + end + + describe "on zero" do + it "returns false" do + 0.0.positive?.should be_false + end + end + + describe "on negative zero" do + it "returns false" do + -0.0.positive?.should be_false + end + end + + describe "on negative numbers" do + it "returns false" do + -0.1.positive?.should be_false + end + end + + describe "on NaN" do + it "returns false" do + nan_value.positive?.should be_false + end + end +end diff --git a/spec/ruby/core/float/prev_float_spec.rb b/spec/ruby/core/float/prev_float_spec.rb new file mode 100644 index 0000000000..5e50269da7 --- /dev/null +++ b/spec/ruby/core/float/prev_float_spec.rb @@ -0,0 +1,49 @@ +require_relative '../../spec_helper' + +describe "Float#prev_float" do + it "returns a float the smallest possible step smaller than the receiver" do + barely_negative = 0.0.prev_float + barely_negative.should == 0.0.prev_float + + barely_negative.should < 0.0 + barely_negative.should > barely_negative.prev_float + + midpoint = barely_negative / 2 + [0.0, barely_negative].should include midpoint + end + + it "returns -Float::INFINITY for -Float::INFINITY" do + (-Float::INFINITY).prev_float.should == -Float::INFINITY + end + + it "steps directly between MAX and INFINITY" do + Float::INFINITY.prev_float.should == Float::MAX + (-Float::MAX).prev_float.should == -Float::INFINITY + end + + it "steps directly between 1.0 and 1.0 - EPSILON/2" do + 1.0.prev_float.should == 1.0 - Float::EPSILON/2 + end + + it "steps directly between -1.0 and -1.0 - EPSILON" do + (-1.0).prev_float.should == -1.0 - Float::EPSILON + end + + it "reverses the effect of next_float for all Floats except -INFINITY and -0.0" do + num = rand + num.next_float.prev_float.should == num + end + + it "returns positive zero when stepping downward from just above zero" do + x = 0.0.next_float.prev_float + (1/x).should == Float::INFINITY + end + + it "gives the same result for -0.0 as for +0.0" do + (0.0).prev_float.should == (-0.0).prev_float + end + + it "returns NAN if NAN was the receiver" do + Float::NAN.prev_float.should.nan? + end +end diff --git a/spec/ruby/core/float/quo_spec.rb b/spec/ruby/core/float/quo_spec.rb new file mode 100644 index 0000000000..b5c64f9d87 --- /dev/null +++ b/spec/ruby/core/float/quo_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/quo' + +describe "Float#quo" do + it_behaves_like :float_quo, :quo +end diff --git a/spec/ruby/core/float/rationalize_spec.rb b/spec/ruby/core/float/rationalize_spec.rb new file mode 100644 index 0000000000..0c5bef7ac4 --- /dev/null +++ b/spec/ruby/core/float/rationalize_spec.rb @@ -0,0 +1,43 @@ +require_relative '../../spec_helper' + +describe "Float#rationalize" do + it "returns self as a simplified Rational with no argument" do + (3382729202.92822).rationalize.should == Rational(4806858197361, 1421) + end + + # FIXME: These specs need reviewing by somebody familiar with the + # algorithm used by #rationalize + it "simplifies self to the degree specified by a Rational argument" do + f = 0.3 + f.rationalize(Rational(1,10)).should == Rational(1,3) + f.rationalize(Rational(-1,10)).should == Rational(1,3) + + f = -f + f.rationalize(Rational(1,10)).should == Rational(-1,3) + f.rationalize(Rational(-1,10)).should == Rational(-1,3) + + end + + it "simplifies self to the degree specified by a Float argument" do + f = 0.3 + f.rationalize(0.05).should == Rational(1,3) + f.rationalize(0.001).should == Rational(3, 10) + + f = -f + f.rationalize(0.05).should == Rational(-1,3) + f.rationalize(0.001).should == Rational(-3,10) + end + + it "raises a FloatDomainError for Infinity" do + -> {infinity_value.rationalize}.should raise_error(FloatDomainError) + end + + it "raises a FloatDomainError for NaN" do + -> { nan_value.rationalize }.should raise_error(FloatDomainError) + end + + it "raises ArgumentError when passed more than one argument" do + -> { 0.3.rationalize(0.1, 0.1) }.should raise_error(ArgumentError) + -> { 0.3.rationalize(0.1, 0.1, 2) }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/float/round_spec.rb b/spec/ruby/core/float/round_spec.rb new file mode 100644 index 0000000000..7e8c792051 --- /dev/null +++ b/spec/ruby/core/float/round_spec.rb @@ -0,0 +1,203 @@ +require_relative '../../spec_helper' + +describe "Float#round" do + it "returns the nearest Integer" do + 5.5.round.should == 6 + 0.4.round.should == 0 + 0.6.round.should == 1 + -1.4.round.should == -1 + -2.8.round.should == -3 + 0.0.round.should == 0 + end + + it "returns the nearest Integer for Float near the limit" do + 0.49999999999999994.round.should == 0 + -0.49999999999999994.round.should == 0 + end + + it "raises FloatDomainError for exceptional values" do + -> { (+infinity_value).round }.should raise_error(FloatDomainError) + -> { (-infinity_value).round }.should raise_error(FloatDomainError) + -> { nan_value.round }.should raise_error(FloatDomainError) + end + + it "rounds self to an optionally given precision" do + 5.5.round(0).should eql(6) + 5.7.round(1).should eql(5.7) + 1.2345678.round(2).should == 1.23 + 123456.78.round(-2).should eql(123500) # rounded up + -123456.78.round(-2).should eql(-123500) + 12.345678.round(3.999).should == 12.346 + end + + it "correctly rounds exact floats with a numerous digits in a fraction part" do + 0.8241000000000004.round(10).should == 0.8241 + 0.8241000000000002.round(10).should == 0.8241 + end + + it "returns zero when passed a negative argument with magnitude greater than magnitude of the whole number portion of the Float" do + 0.8346268.round(-1).should eql(0) + end + + it "raises a TypeError when its argument can not be converted to an Integer" do + -> { 1.0.round("4") }.should raise_error(TypeError) + -> { 1.0.round(nil) }.should raise_error(TypeError) + end + + it "raises FloatDomainError for exceptional values when passed a non-positive precision" do + -> { Float::INFINITY.round( 0) }.should raise_error(FloatDomainError) + -> { Float::INFINITY.round(-2) }.should raise_error(FloatDomainError) + -> { (-Float::INFINITY).round( 0) }.should raise_error(FloatDomainError) + -> { (-Float::INFINITY).round(-2) }.should raise_error(FloatDomainError) + end + + it "raises RangeError for NAN when passed a non-positive precision" do + -> { Float::NAN.round(0) }.should raise_error(RangeError) + -> { Float::NAN.round(-2) }.should raise_error(RangeError) + end + + it "returns self for exceptional values when passed a non-negative precision" do + Float::INFINITY.round(2).should == Float::INFINITY + (-Float::INFINITY).round(2).should == -Float::INFINITY + Float::NAN.round(2).should be_nan + end + + # redmine:5227 + it "works for corner cases" do + 42.0.round(308).should eql(42.0) + 1.0e307.round(2).should eql(1.0e307) + end + + # redmine:5271 + it "returns rounded values for big argument" do + 0.42.round(2.0**30).should == 0.42 + end + + it "returns rounded values for not so big argument" do + 0.42.round(2.0**23).should == 0.42 + end + + it "returns big values rounded to nearest" do + +2.5e20.round(-20).should eql( +3 * 10 ** 20 ) + -2.5e20.round(-20).should eql( -3 * 10 ** 20 ) + end + + # redmine #5272 + it "returns rounded values for big values" do + +2.4e20.round(-20).should eql( +2 * 10 ** 20 ) + -2.4e20.round(-20).should eql( -2 * 10 ** 20 ) + +2.5e200.round(-200).should eql( +3 * 10 ** 200 ) + +2.4e200.round(-200).should eql( +2 * 10 ** 200 ) + -2.5e200.round(-200).should eql( -3 * 10 ** 200 ) + -2.4e200.round(-200).should eql( -2 * 10 ** 200 ) + end + + it "returns different rounded values depending on the half option" do + 2.5.round(half: nil).should eql(3) + 2.5.round(half: :up).should eql(3) + 2.5.round(half: :down).should eql(2) + 2.5.round(half: :even).should eql(2) + 3.5.round(half: nil).should eql(4) + 3.5.round(half: :up).should eql(4) + 3.5.round(half: :down).should eql(3) + 3.5.round(half: :even).should eql(4) + (-2.5).round(half: nil).should eql(-3) + (-2.5).round(half: :up).should eql(-3) + (-2.5).round(half: :down).should eql(-2) + (-2.5).round(half: :even).should eql(-2) + end + + it "rounds self to an optionally given precision with a half option" do + 5.55.round(1, half: nil).should eql(5.6) + 5.55.round(1, half: :up).should eql(5.6) + 5.55.round(1, half: :down).should eql(5.5) + 5.55.round(1, half: :even).should eql(5.6) + -5.55.round(1, half: nil).should eql(-5.6) + -5.55.round(1, half: :up).should eql(-5.6) + -5.55.round(1, half: :down).should eql(-5.5) + -5.55.round(1, half: :even).should eql(-5.6) + end + + it "preserves cases where neighbouring floating pointer number increase the decimal places" do + 4.8100000000000005.round(5, half: nil).should eql(4.81) + 4.8100000000000005.round(5, half: :up).should eql(4.81) + 4.8100000000000005.round(5, half: :down).should eql(4.81) + 4.8100000000000005.round(5, half: :even).should eql(4.81) + -4.8100000000000005.round(5, half: nil).should eql(-4.81) + -4.8100000000000005.round(5, half: :up).should eql(-4.81) + -4.8100000000000005.round(5, half: :down).should eql(-4.81) + -4.8100000000000005.round(5, half: :even).should eql(-4.81) + 4.81.round(5, half: nil).should eql(4.81) + 4.81.round(5, half: :up).should eql(4.81) + 4.81.round(5, half: :down).should eql(4.81) + 4.81.round(5, half: :even).should eql(4.81) + -4.81.round(5, half: nil).should eql(-4.81) + -4.81.round(5, half: :up).should eql(-4.81) + -4.81.round(5, half: :down).should eql(-4.81) + -4.81.round(5, half: :even).should eql(-4.81) + 4.809999999999999.round(5, half: nil).should eql(4.81) + 4.809999999999999.round(5, half: :up).should eql(4.81) + 4.809999999999999.round(5, half: :down).should eql(4.81) + 4.809999999999999.round(5, half: :even).should eql(4.81) + -4.809999999999999.round(5, half: nil).should eql(-4.81) + -4.809999999999999.round(5, half: :up).should eql(-4.81) + -4.809999999999999.round(5, half: :down).should eql(-4.81) + -4.809999999999999.round(5, half: :even).should eql(-4.81) + end + + ruby_bug "#19318", ""..."3.3" do + # These numbers are neighbouring floating point numbers round a + # precise value. They test that the rounding modes work correctly + # round that value and precision is not lost which might cause + # incorrect results. + it "does not lose precision during the rounding process" do + 767573.1875850001.round(5, half: nil).should eql(767573.18759) + 767573.1875850001.round(5, half: :up).should eql(767573.18759) + 767573.1875850001.round(5, half: :down).should eql(767573.18759) + 767573.1875850001.round(5, half: :even).should eql(767573.18759) + -767573.1875850001.round(5, half: nil).should eql(-767573.18759) + -767573.1875850001.round(5, half: :up).should eql(-767573.18759) + -767573.1875850001.round(5, half: :down).should eql(-767573.18759) + -767573.1875850001.round(5, half: :even).should eql(-767573.18759) + 767573.187585.round(5, half: nil).should eql(767573.18759) + 767573.187585.round(5, half: :up).should eql(767573.18759) + 767573.187585.round(5, half: :down).should eql(767573.18758) + 767573.187585.round(5, half: :even).should eql(767573.18758) + -767573.187585.round(5, half: nil).should eql(-767573.18759) + -767573.187585.round(5, half: :up).should eql(-767573.18759) + -767573.187585.round(5, half: :down).should eql(-767573.18758) + -767573.187585.round(5, half: :even).should eql(-767573.18758) + 767573.1875849998.round(5, half: nil).should eql(767573.18758) + 767573.1875849998.round(5, half: :up).should eql(767573.18758) + 767573.1875849998.round(5, half: :down).should eql(767573.18758) + 767573.1875849998.round(5, half: :even).should eql(767573.18758) + -767573.1875849998.round(5, half: nil).should eql(-767573.18758) + -767573.1875849998.round(5, half: :up).should eql(-767573.18758) + -767573.1875849998.round(5, half: :down).should eql(-767573.18758) + -767573.1875849998.round(5, half: :even).should eql(-767573.18758) + end + end + + it "raises FloatDomainError for exceptional values with a half option" do + -> { (+infinity_value).round(half: :up) }.should raise_error(FloatDomainError) + -> { (-infinity_value).round(half: :down) }.should raise_error(FloatDomainError) + -> { nan_value.round(half: :even) }.should raise_error(FloatDomainError) + end + + it "raise for a non-existent round mode" do + -> { 14.2.round(half: :nonsense) }.should raise_error(ArgumentError, "invalid rounding mode: nonsense") + end + + describe "when 0.0 is given" do + it "returns self for positive ndigits" do + (0.0).round(5).inspect.should == "0.0" + (-0.0).round(1).inspect.should == "-0.0" + end + + it "returns 0 for 0 or undefined ndigits" do + (0.0).round.should == 0 + (-0.0).round(0).should == 0 + (0.0).round(half: :up) == 0 + end + end +end diff --git a/spec/ruby/core/float/shared/abs.rb b/spec/ruby/core/float/shared/abs.rb new file mode 100644 index 0000000000..607983322d --- /dev/null +++ b/spec/ruby/core/float/shared/abs.rb @@ -0,0 +1,21 @@ +require_relative '../../../spec_helper' + +describe :float_abs, shared: true do + it "returns the absolute value" do + -99.1.send(@method).should be_close(99.1, TOLERANCE) + 4.5.send(@method).should be_close(4.5, TOLERANCE) + 0.0.send(@method).should be_close(0.0, TOLERANCE) + end + + it "returns 0.0 if -0.0" do + (-0.0).send(@method).should be_positive_zero + end + + it "returns Infinity if -Infinity" do + (-infinity_value).send(@method).infinite?.should == 1 + end + + it "returns NaN if NaN" do + nan_value.send(@method).nan?.should be_true + end +end diff --git a/spec/ruby/core/float/shared/arg.rb b/spec/ruby/core/float/shared/arg.rb new file mode 100644 index 0000000000..136cf19ec8 --- /dev/null +++ b/spec/ruby/core/float/shared/arg.rb @@ -0,0 +1,36 @@ +describe :float_arg, shared: true do + it "returns NaN if NaN" do + f = nan_value + f.send(@method).nan?.should be_true + end + + it "returns self if NaN" do + f = nan_value + f.send(@method).should equal(f) + end + + it "returns 0 if positive" do + 1.0.send(@method).should == 0 + end + + it "returns 0 if +0.0" do + 0.0.send(@method).should == 0 + end + + it "returns 0 if +Infinity" do + infinity_value.send(@method).should == 0 + end + + it "returns Pi if negative" do + (-1.0).send(@method).should == Math::PI + end + + # This was established in r23960 + it "returns Pi if -0.0" do + (-0.0).send(@method).should == Math::PI + end + + it "returns Pi if -Infinity" do + (-infinity_value).send(@method).should == Math::PI + end +end diff --git a/spec/ruby/core/float/shared/arithmetic_exception_in_coerce.rb b/spec/ruby/core/float/shared/arithmetic_exception_in_coerce.rb new file mode 100644 index 0000000000..eec92d858f --- /dev/null +++ b/spec/ruby/core/float/shared/arithmetic_exception_in_coerce.rb @@ -0,0 +1,11 @@ +require_relative '../fixtures/classes' + +describe :float_arithmetic_exception_in_coerce, shared: true do + it "does not rescue exception raised in other#coerce" do + b = mock("numeric with failed #coerce") + b.should_receive(:coerce).and_raise(FloatSpecs::CoerceError) + + # e.g. 1.0 > b + -> { 1.0.send(@method, b) }.should raise_error(FloatSpecs::CoerceError) + end +end diff --git a/spec/ruby/core/float/shared/comparison_exception_in_coerce.rb b/spec/ruby/core/float/shared/comparison_exception_in_coerce.rb new file mode 100644 index 0000000000..3e2c1e28dd --- /dev/null +++ b/spec/ruby/core/float/shared/comparison_exception_in_coerce.rb @@ -0,0 +1,11 @@ +require_relative '../fixtures/classes' + +describe :float_comparison_exception_in_coerce, shared: true do + it "does not rescue exception raised in other#coerce" do + b = mock("numeric with failed #coerce") + b.should_receive(:coerce).and_raise(FloatSpecs::CoerceError) + + # e.g. 1.0 > b + -> { 1.0.send(@method, b) }.should raise_error(FloatSpecs::CoerceError) + end +end diff --git a/spec/ruby/core/float/shared/equal.rb b/spec/ruby/core/float/shared/equal.rb new file mode 100644 index 0000000000..4d524e1cf2 --- /dev/null +++ b/spec/ruby/core/float/shared/equal.rb @@ -0,0 +1,38 @@ +describe :float_equal, shared: true do + it "returns true if self has the same value as other" do + 1.0.send(@method, 1).should == true + 2.71828.send(@method, 1.428).should == false + -4.2.send(@method, 4.2).should == false + end + + it "calls 'other == self' if coercion fails" do + x = mock('other') + def x.==(other) + 2.0 == other + end + + 1.0.send(@method, x).should == false + 2.0.send(@method, x).should == true + end + + it "returns false if one side is NaN" do + [1.0, 42, bignum_value].each { |n| + (nan_value.send(@method, n)).should == false + (n.send(@method, nan_value)).should == false + } + end + + it "handles positive infinity" do + [1.0, 42, bignum_value].each { |n| + (infinity_value.send(@method, n)).should == false + (n.send(@method, infinity_value)).should == false + } + end + + it "handles negative infinity" do + [1.0, 42, bignum_value].each { |n| + ((-infinity_value).send(@method, n)).should == false + (n.send(@method, -infinity_value)).should == false + } + end +end diff --git a/spec/ruby/core/float/shared/modulo.rb b/spec/ruby/core/float/shared/modulo.rb new file mode 100644 index 0000000000..6700bd4f4e --- /dev/null +++ b/spec/ruby/core/float/shared/modulo.rb @@ -0,0 +1,48 @@ +describe :float_modulo, shared: true do + it "returns self modulo other" do + 6543.21.send(@method, 137).should be_close(104.21, TOLERANCE) + 5667.19.send(@method, bignum_value).should be_close(5667.19, TOLERANCE) + 6543.21.send(@method, 137.24).should be_close(92.9299999999996, TOLERANCE) + + -1.0.send(@method, 1).should == 0 + end + + it "returns self when modulus is +Infinity" do + 4.2.send(@method, Float::INFINITY).should == 4.2 + end + + it "returns -Infinity when modulus is -Infinity" do + 4.2.send(@method, -Float::INFINITY).should == -Float::INFINITY + end + + it "returns NaN when called on NaN or Infinities" do + Float::NAN.send(@method, 42).should be_nan + Float::INFINITY.send(@method, 42).should be_nan + (-Float::INFINITY).send(@method, 42).should be_nan + end + + it "returns NaN when modulus is NaN" do + 4.2.send(@method, Float::NAN).should be_nan + end + + it "returns -0.0 when called on -0.0 with a non zero modulus" do + r = (-0.0).send(@method, 42) + r.should == 0 + (1/r).should < 0 + + r = (-0.0).send(@method, Float::INFINITY) + r.should == 0 + (1/r).should < 0 + end + + it "tries to coerce the modulus" do + obj = mock("modulus") + obj.should_receive(:coerce).with(1.25).and_return([1.25, 0.5]) + (1.25 % obj).should == 0.25 + end + + it "raises a ZeroDivisionError if other is zero" do + -> { 1.0.send(@method, 0) }.should raise_error(ZeroDivisionError) + -> { 1.0.send(@method, 0.0) }.should raise_error(ZeroDivisionError) + end +end diff --git a/spec/ruby/core/float/shared/quo.rb b/spec/ruby/core/float/shared/quo.rb new file mode 100644 index 0000000000..0c90171b87 --- /dev/null +++ b/spec/ruby/core/float/shared/quo.rb @@ -0,0 +1,59 @@ +describe :float_quo, shared: true do + it "performs floating-point division between self and an Integer" do + 8.9.send(@method, 7).should == 1.2714285714285716 + end + + it "performs floating-point division between self and an Integer" do + 8.9.send(@method, 9999999999999**9).should == 8.900000000008011e-117 + end + + it "performs floating-point division between self and a Float" do + 2827.22.send(@method, 872.111111).should == 3.2418116961704433 + end + + it "returns NaN when the argument is NaN" do + -1819.999999.send(@method, nan_value).nan?.should be_true + 11109.1981271.send(@method, nan_value).nan?.should be_true + end + + it "returns Infinity when the argument is 0.0" do + 2827.22.send(@method, 0.0).infinite?.should == 1 + end + + it "returns -Infinity when the argument is 0.0 and self is negative" do + -48229.282.send(@method, 0.0).infinite?.should == -1 + end + + it "returns Infinity when the argument is 0" do + 2827.22.send(@method, 0).infinite?.should == 1 + end + + it "returns -Infinity when the argument is 0 and self is negative" do + -48229.282.send(@method, 0).infinite?.should == -1 + end + + it "returns 0.0 when the argument is Infinity" do + 47292.2821.send(@method, infinity_value).should == 0.0 + end + + it "returns -0.0 when the argument is -Infinity" do + 1.9999918.send(@method, -infinity_value).should == -0.0 + end + + it "performs floating-point division between self and a Rational" do + 74620.09.send(@method, Rational(2,3)).should == 111930.135 + end + + it "performs floating-point division between self and a Complex" do + 74620.09.send(@method, Complex(8,2)).should == Complex( + 8778.834117647059, -2194.7085294117646) + end + + it "raises a TypeError when argument isn't numeric" do + -> { 27292.2.send(@method, mock('non-numeric')) }.should raise_error(TypeError) + end + + it "raises an ArgumentError when passed multiple arguments" do + -> { 272.221.send(@method, 6,0.2) }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/float/shared/to_i.rb b/spec/ruby/core/float/shared/to_i.rb new file mode 100644 index 0000000000..33b32ca533 --- /dev/null +++ b/spec/ruby/core/float/shared/to_i.rb @@ -0,0 +1,14 @@ +describe :float_to_i, shared: true do + it "returns self truncated to an Integer" do + 899.2.send(@method).should eql(899) + -1.122256e-45.send(@method).should eql(0) + 5_213_451.9201.send(@method).should eql(5213451) + 1.233450999123389e+12.send(@method).should eql(1233450999123) + -9223372036854775808.1.send(@method).should eql(-9223372036854775808) + 9223372036854775808.1.send(@method).should eql(9223372036854775808) + end + + it "raises a FloatDomainError for NaN" do + -> { nan_value.send(@method) }.should raise_error(FloatDomainError) + end +end diff --git a/spec/ruby/core/float/shared/to_s.rb b/spec/ruby/core/float/shared/to_s.rb new file mode 100644 index 0000000000..0925efc0f7 --- /dev/null +++ b/spec/ruby/core/float/shared/to_s.rb @@ -0,0 +1,308 @@ +describe :float_to_s, shared: true do + it "returns 'NaN' for NaN" do + nan_value().send(@method).should == 'NaN' + end + + it "returns 'Infinity' for positive infinity" do + infinity_value().send(@method).should == 'Infinity' + end + + it "returns '-Infinity' for negative infinity" do + (-infinity_value()).send(@method).should == '-Infinity' + end + + it "returns '0.0' for 0.0" do + 0.0.send(@method).should == "0.0" + end + + platform_is_not :openbsd do + it "emits '-' for -0.0" do + -0.0.send(@method).should == "-0.0" + end + end + + it "emits a '-' for negative values" do + -3.14.send(@method).should == "-3.14" + end + + it "emits a trailing '.0' for a whole number" do + 50.0.send(@method).should == "50.0" + end + + it "emits a trailing '.0' for the mantissa in e format" do + 1.0e20.send(@method).should == "1.0e+20" + end + + it "uses non-e format for a positive value with fractional part having 5 significant figures" do + 0.0001.send(@method).should == "0.0001" + end + + it "uses non-e format for a negative value with fractional part having 5 significant figures" do + -0.0001.send(@method).should == "-0.0001" + end + + it "uses e format for a positive value with fractional part having 6 significant figures" do + 0.00001.send(@method).should == "1.0e-05" + end + + it "uses e format for a negative value with fractional part having 6 significant figures" do + -0.00001.send(@method).should == "-1.0e-05" + end + + it "uses non-e format for a positive value with whole part having 15 significant figures" do + 10000000000000.0.send(@method).should == "10000000000000.0" + end + + it "uses non-e format for a negative value with whole part having 15 significant figures" do + -10000000000000.0.send(@method).should == "-10000000000000.0" + end + + it "uses non-e format for a positive value with whole part having 16 significant figures" do + 100000000000000.0.send(@method).should == "100000000000000.0" + end + + it "uses non-e format for a negative value with whole part having 16 significant figures" do + -100000000000000.0.send(@method).should == "-100000000000000.0" + end + + it "uses e format for a positive value with whole part having 18 significant figures" do + 10000000000000000.0.send(@method).should == "1.0e+16" + end + + it "uses e format for a negative value with whole part having 18 significant figures" do + -10000000000000000.0.send(@method).should == "-1.0e+16" + end + + it "uses e format for a positive value with whole part having 17 significant figures" do + 1000000000000000.0.send(@method).should == "1.0e+15" + end + + it "uses e format for a negative value with whole part having 17 significant figures" do + -1000000000000000.0.send(@method).should == "-1.0e+15" + end + + # #3273 + it "outputs the minimal, unique form necessary to recreate the value" do + value = 0.21611564636388508 + string = "0.21611564636388508" + + value.send(@method).should == string + string.to_f.should == value + end + + it "outputs the minimal, unique form to represent the value" do + 0.56.send(@method).should == "0.56" + end + + describe "matches" do + it "random examples in all ranges" do + # 50.times do + # bytes = (0...8).map { rand(256) } + # string = bytes.pack('C8') + # float = string.unpack('D').first + # puts "#{'%.20g' % float}.send(@method).should == #{float.send(@method).inspect}" + # end + + 2.5540217314354050325e+163.send(@method).should == "2.554021731435405e+163" + 2.5492588360356597544e-172.send(@method).should == "2.5492588360356598e-172" + 1.742770260934704852e-82.send(@method).should == "1.7427702609347049e-82" + 6.2108093676180883209e-104.send(@method).should == "6.210809367618088e-104" + -3.3448803488331067402e-143.send(@method).should == "-3.3448803488331067e-143" + -2.2740074343500832557e-168.send(@method).should == "-2.2740074343500833e-168" + 7.0587971678048535732e+191.send(@method).should == "7.058797167804854e+191" + -284438.88327586348169.send(@method).should == "-284438.8832758635" + 3.953272468476091301e+105.send(@method).should == "3.9532724684760913e+105" + -3.6361359552959847853e+100.send(@method).should == "-3.636135955295985e+100" + -1.3222325865575206185e-31.send(@method).should == "-1.3222325865575206e-31" + 1.1440138916932761366e+130.send(@method).should == "1.1440138916932761e+130" + 4.8750891560387561157e-286.send(@method).should == "4.875089156038756e-286" + 5.6101113356591453525e-257.send(@method).should == "5.610111335659145e-257" + -3.829644279545809575e-100.send(@method).should == "-3.8296442795458096e-100" + 1.5342839401396406117e-194.send(@method).should == "1.5342839401396406e-194" + 2.2284972755169921402e-144.send(@method).should == "2.228497275516992e-144" + 2.1825655917065601737e-61.send(@method).should == "2.1825655917065602e-61" + -2.6672271363524338322e-62.send(@method).should == "-2.667227136352434e-62" + -1.9257995160119059415e+21.send(@method).should == "-1.925799516011906e+21" + -8.9096732962887121718e-198.send(@method).should == "-8.909673296288712e-198" + 2.0202075376548644959e-90.send(@method).should == "2.0202075376548645e-90" + -7.7341602581786258961e-266.send(@method).should == "-7.734160258178626e-266" + 3.5134482598733635046e+98.send(@method).should == "3.5134482598733635e+98" + -2.124411722371029134e+154.send(@method).should == "-2.124411722371029e+154" + -4.573908787355718687e+110.send(@method).should == "-4.573908787355719e+110" + -1.9344425934170969879e-232.send(@method).should == "-1.934442593417097e-232" + -1.3274227399979271095e+171.send(@method).should == "-1.3274227399979271e+171" + 9.3495270482104442383e-283.send(@method).should == "9.349527048210444e-283" + -4.2046059371986483233e+307.send(@method).should == "-4.2046059371986483e+307" + 3.6133547278583543004e-117.send(@method).should == "3.613354727858354e-117" + 4.9247416523566613499e-08.send(@method).should == "4.9247416523566613e-08" + 1.6936145488250064007e-71.send(@method).should == "1.6936145488250064e-71" + 2.4455483206829433098e+96.send(@method).should == "2.4455483206829433e+96" + 7.9797449851436455384e+124.send(@method).should == "7.979744985143646e+124" + -1.3873689634457876774e-129.send(@method).should == "-1.3873689634457877e-129" + 3.9761102037533483075e+284.send(@method).should == "3.976110203753348e+284" + -4.2819791952139402486e-303.send(@method).should == "-4.28197919521394e-303" + -5.7981017546689831298e-116.send(@method).should == "-5.798101754668983e-116" + -3.953266497860534199e-28.send(@method).should == "-3.953266497860534e-28" + -2.0659852720290440959e-243.send(@method).should == "-2.065985272029044e-243" + 8.9670488995878688018e-05.send(@method).should == "8.967048899587869e-05" + -1.2317943708113061768e-98.send(@method).should == "-1.2317943708113062e-98" + -3.8930768307633080463e+248.send(@method).should == "-3.893076830763308e+248" + 6.5854032671803925627e-239.send(@method).should == "6.5854032671803926e-239" + 4.6257022188980878952e+177.send(@method).should == "4.625702218898088e+177" + -1.9397155125507235603e-187.send(@method).should == "-1.9397155125507236e-187" + 8.5752156951245705056e+117.send(@method).should == "8.57521569512457e+117" + -2.4784875958162501671e-132.send(@method).should == "-2.4784875958162502e-132" + -4.4125691841230058457e-203.send(@method).should == "-4.412569184123006e-203" + end + + it "random examples in human ranges" do + # 50.times do + # formatted = '' + # rand(1..3).times do + # formatted << rand(10).to_s + # end + # formatted << '.' + # rand(1..9).times do + # formatted << rand(10).to_s + # end + # float = formatted.to_f + # puts "#{'%.20f' % float}.send(@method).should == #{float.send(@method).inspect}" + # end + + 5.17869899999999994122.send(@method).should == "5.178699" + 905.62695729999995819526.send(@method).should == "905.6269573" + 62.75999999999999801048.send(@method).should == "62.76" + 6.93856795800000014651.send(@method).should == "6.938567958" + 4.95999999999999996447.send(@method).should == "4.96" + 32.77993899999999882766.send(@method).should == "32.779939" + 544.12756779999995160324.send(@method).should == "544.1275678" + 66.25801119999999855281.send(@method).should == "66.2580112" + 7.90000000000000035527.send(@method).should == "7.9" + 5.93100000000000004974.send(@method).should == "5.931" + 5.21229313600000043749.send(@method).should == "5.212293136" + 503.44173809000000119340.send(@method).should == "503.44173809" + 79.26000000000000511591.send(@method).should == "79.26" + 8.51524999999999998579.send(@method).should == "8.51525" + 174.00000000000000000000.send(@method).should == "174.0" + 50.39580000000000126192.send(@method).should == "50.3958" + 35.28999999999999914735.send(@method).should == "35.29" + 5.43136675399999990788.send(@method).should == "5.431366754" + 654.07680000000004838512.send(@method).should == "654.0768" + 6.07423700000000010846.send(@method).should == "6.074237" + 102.25779799999999397642.send(@method).should == "102.257798" + 5.08129999999999970584.send(@method).should == "5.0813" + 6.00000000000000000000.send(@method).should == "6.0" + 8.30000000000000071054.send(@method).should == "8.3" + 32.68345999999999662577.send(@method).should == "32.68346" + 581.11170000000004165486.send(@method).should == "581.1117" + 76.31342999999999676675.send(@method).should == "76.31343" + 438.30826000000001840817.send(@method).should == "438.30826" + 482.06631994000002805478.send(@method).should == "482.06631994" + 55.92721026899999969828.send(@method).should == "55.927210269" + 4.00000000000000000000.send(@method).should == "4.0" + 55.86693999999999959982.send(@method).should == "55.86694" + 787.98299999999994724931.send(@method).should == "787.983" + 5.73810511000000023074.send(@method).should == "5.73810511" + 74.51926810000000500622.send(@method).should == "74.5192681" + 892.89999999999997726263.send(@method).should == "892.9" + 68.27299999999999613465.send(@method).should == "68.273" + 904.10000000000002273737.send(@method).should == "904.1" + 5.23200000000000020606.send(@method).should == "5.232" + 4.09628000000000014325.send(@method).should == "4.09628" + 46.05152633699999853434.send(@method).should == "46.051526337" + 142.12884990599999923688.send(@method).should == "142.128849906" + 3.83057023500000015659.send(@method).should == "3.830570235" + 11.81684594699999912848.send(@method).should == "11.816845947" + 80.50000000000000000000.send(@method).should == "80.5" + 382.18215010000000120272.send(@method).should == "382.1821501" + 55.38444606899999911320.send(@method).should == "55.384446069" + 5.78000000000000024869.send(@method).should == "5.78" + 2.88244999999999995666.send(@method).should == "2.88245" + 43.27709999999999723741.send(@method).should == "43.2771" + end + + it "random values from divisions" do + (1.0 / 7).send(@method).should == "0.14285714285714285" + + # 50.times do + # a = rand(10) + # b = rand(10) + # c = rand(10) + # d = rand(10) + # expression = "#{a}.#{b} / #{c}.#{d}" + # puts " (#{expression}).send(@method).should == #{eval(expression).send(@method).inspect}" + # end + + (1.1 / 7.1).send(@method).should == "0.15492957746478875" + (6.5 / 8.8).send(@method).should == "0.7386363636363635" + (4.8 / 4.3).send(@method).should == "1.1162790697674418" + (4.0 / 1.9).send(@method).should == "2.1052631578947367" + (9.1 / 0.8).send(@method).should == "11.374999999999998" + (5.3 / 7.5).send(@method).should == "0.7066666666666667" + (2.8 / 1.8).send(@method).should == "1.5555555555555554" + (2.1 / 2.5).send(@method).should == "0.8400000000000001" + (3.5 / 6.0).send(@method).should == "0.5833333333333334" + (4.6 / 0.3).send(@method).should == "15.333333333333332" + (0.6 / 2.4).send(@method).should == "0.25" + (1.3 / 9.1).send(@method).should == "0.14285714285714288" + (0.3 / 5.0).send(@method).should == "0.06" + (5.0 / 4.2).send(@method).should == "1.1904761904761905" + (3.0 / 2.0).send(@method).should == "1.5" + (6.3 / 2.0).send(@method).should == "3.15" + (5.4 / 6.0).send(@method).should == "0.9" + (9.6 / 8.1).send(@method).should == "1.1851851851851851" + (8.7 / 1.6).send(@method).should == "5.437499999999999" + (1.9 / 7.8).send(@method).should == "0.24358974358974358" + (0.5 / 2.1).send(@method).should == "0.23809523809523808" + (9.3 / 5.8).send(@method).should == "1.6034482758620692" + (2.7 / 8.0).send(@method).should == "0.3375" + (9.7 / 7.8).send(@method).should == "1.2435897435897436" + (8.1 / 2.4).send(@method).should == "3.375" + (7.7 / 2.7).send(@method).should == "2.8518518518518516" + (7.9 / 1.7).send(@method).should == "4.647058823529412" + (6.5 / 8.2).send(@method).should == "0.7926829268292683" + (7.8 / 9.6).send(@method).should == "0.8125" + (2.2 / 4.6).send(@method).should == "0.47826086956521746" + (0.0 / 1.0).send(@method).should == "0.0" + (8.3 / 2.9).send(@method).should == "2.8620689655172415" + (3.1 / 6.1).send(@method).should == "0.5081967213114754" + (2.8 / 7.8).send(@method).should == "0.358974358974359" + (8.0 / 0.1).send(@method).should == "80.0" + (1.7 / 6.4).send(@method).should == "0.265625" + (1.8 / 5.4).send(@method).should == "0.3333333333333333" + (8.0 / 5.8).send(@method).should == "1.3793103448275863" + (5.2 / 4.1).send(@method).should == "1.2682926829268295" + (9.8 / 5.8).send(@method).should == "1.6896551724137934" + (5.4 / 9.5).send(@method).should == "0.5684210526315789" + (8.4 / 4.9).send(@method).should == "1.7142857142857142" + (1.7 / 3.5).send(@method).should == "0.4857142857142857" + (1.2 / 5.1).send(@method).should == "0.23529411764705882" + (1.4 / 2.0).send(@method).should == "0.7" + (4.8 / 8.0).send(@method).should == "0.6" + (9.0 / 2.5).send(@method).should == "3.6" + (0.2 / 0.6).send(@method).should == "0.33333333333333337" + (7.8 / 5.2).send(@method).should == "1.5" + (9.5 / 5.5).send(@method).should == "1.7272727272727273" + end + end + + describe 'encoding' do + before :each do + @internal = Encoding.default_internal + end + + after :each do + Encoding.default_internal = @internal + end + + it "returns a String in US-ASCII encoding when Encoding.default_internal is nil" do + Encoding.default_internal = nil + 1.23.send(@method).encoding.should equal(Encoding::US_ASCII) + end + + it "returns a String in US-ASCII encoding when Encoding.default_internal is not nil" do + Encoding.default_internal = Encoding::IBM437 + 5.47.send(@method).encoding.should equal(Encoding::US_ASCII) + end + end +end diff --git a/spec/ruby/core/float/to_f_spec.rb b/spec/ruby/core/float/to_f_spec.rb new file mode 100644 index 0000000000..6677556cd9 --- /dev/null +++ b/spec/ruby/core/float/to_f_spec.rb @@ -0,0 +1,9 @@ +require_relative '../../spec_helper' + +describe "Float#to_f" do + it "returns self" do + -500.3.to_f.should == -500.3 + 267.51.to_f.should == 267.51 + 1.1412.to_f.should == 1.1412 + end +end diff --git a/spec/ruby/core/float/to_i_spec.rb b/spec/ruby/core/float/to_i_spec.rb new file mode 100644 index 0000000000..91d84c5fa3 --- /dev/null +++ b/spec/ruby/core/float/to_i_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/to_i' + +describe "Float#to_i" do + it_behaves_like :float_to_i, :to_i +end diff --git a/spec/ruby/core/float/to_int_spec.rb b/spec/ruby/core/float/to_int_spec.rb new file mode 100644 index 0000000000..084a58b431 --- /dev/null +++ b/spec/ruby/core/float/to_int_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/to_i' + +describe "Float#to_int" do + it_behaves_like :float_to_i, :to_int +end diff --git a/spec/ruby/core/float/to_r_spec.rb b/spec/ruby/core/float/to_r_spec.rb new file mode 100644 index 0000000000..907ff08f27 --- /dev/null +++ b/spec/ruby/core/float/to_r_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../spec_helper' + +describe "Float#to_r" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/float/to_s_spec.rb b/spec/ruby/core/float/to_s_spec.rb new file mode 100644 index 0000000000..6727a883f8 --- /dev/null +++ b/spec/ruby/core/float/to_s_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/to_s' + +describe "Float#to_s" do + it_behaves_like :float_to_s, :to_s +end diff --git a/spec/ruby/core/float/truncate_spec.rb b/spec/ruby/core/float/truncate_spec.rb new file mode 100644 index 0000000000..2c80145f9f --- /dev/null +++ b/spec/ruby/core/float/truncate_spec.rb @@ -0,0 +1,14 @@ +require_relative '../../spec_helper' +require_relative 'shared/to_i' + +describe "Float#truncate" do + it_behaves_like :float_to_i, :truncate + + it "returns self truncated to an optionally given precision" do + 2.1679.truncate(0).should eql(2) + 7.1.truncate(1).should eql(7.1) + 214.94.truncate(-1).should eql(210) + -1.234.truncate(2).should eql(-1.23) + 5.123812.truncate(4).should eql(5.1238) + end +end diff --git a/spec/ruby/core/float/uminus_spec.rb b/spec/ruby/core/float/uminus_spec.rb new file mode 100644 index 0000000000..57bae0fb4b --- /dev/null +++ b/spec/ruby/core/float/uminus_spec.rb @@ -0,0 +1,28 @@ +require_relative '../../spec_helper' + +describe "Float#-@" do + it "negates self" do + (2.221.send(:-@)).should be_close(-2.221, TOLERANCE) + -2.01.should be_close(-2.01,TOLERANCE) + -2_455_999_221.5512.should be_close(-2455999221.5512, TOLERANCE) + (--5.5).should be_close(5.5, TOLERANCE) + -8.551.send(:-@).should be_close(8.551, TOLERANCE) + end + + it "negates self at Float boundaries" do + Float::MAX.send(:-@).should be_close(0.0 - Float::MAX, TOLERANCE) + Float::MIN.send(:-@).should be_close(0.0 - Float::MIN, TOLERANCE) + end + + it "returns negative infinity for positive infinity" do + infinity_value.send(:-@).infinite?.should == -1 + end + + it "returns positive infinity for negative infinity" do + (-infinity_value).send(:-@).infinite?.should == 1 + end + + it "returns NaN for NaN" do + nan_value.send(:-@).should.nan? + end +end diff --git a/spec/ruby/core/float/uplus_spec.rb b/spec/ruby/core/float/uplus_spec.rb new file mode 100644 index 0000000000..936123558c --- /dev/null +++ b/spec/ruby/core/float/uplus_spec.rb @@ -0,0 +1,9 @@ +require_relative '../../spec_helper' + +describe "Float#+@" do + it "returns the same value with same sign (twos complement)" do + 34.56.send(:+@).should == 34.56 + -34.56.send(:+@).should == -34.56 + 0.0.send(:+@).should eql(0.0) + end +end diff --git a/spec/ruby/core/float/zero_spec.rb b/spec/ruby/core/float/zero_spec.rb new file mode 100644 index 0000000000..1f3de27793 --- /dev/null +++ b/spec/ruby/core/float/zero_spec.rb @@ -0,0 +1,9 @@ +require_relative '../../spec_helper' + +describe "Float#zero?" do + it "returns true if self is 0.0" do + 0.0.should.zero? + 1.0.should_not.zero? + -1.0.should_not.zero? + end +end diff --git a/spec/ruby/core/gc/auto_compact_spec.rb b/spec/ruby/core/gc/auto_compact_spec.rb new file mode 100644 index 0000000000..33ad1cb56c --- /dev/null +++ b/spec/ruby/core/gc/auto_compact_spec.rb @@ -0,0 +1,24 @@ +require_relative '../../spec_helper' + +describe "GC.auto_compact" do + it "can set and get a boolean value" do + begin + GC.auto_compact = GC.auto_compact + rescue NotImplementedError # platform does not support autocompact + skip + end + + original = GC.auto_compact + begin + GC.auto_compact = !original + rescue NotImplementedError # platform does not support autocompact + skip + end + + begin + GC.auto_compact.should == !original + ensure + GC.auto_compact = original + end + end +end diff --git a/spec/ruby/core/gc/config_spec.rb b/spec/ruby/core/gc/config_spec.rb new file mode 100644 index 0000000000..e20e8e4a16 --- /dev/null +++ b/spec/ruby/core/gc/config_spec.rb @@ -0,0 +1,83 @@ +require_relative '../../spec_helper' + +ruby_version_is "3.4" do + describe "GC.config" do + context "without arguments" do + it "returns a hash of current settings" do + GC.config.should be_kind_of(Hash) + end + + it "includes the name of currently loaded GC implementation as a global key" do + GC.config.should include(:implementation) + GC.config[:implementation].should be_kind_of(String) + end + end + + context "with a hash of options" do + it "allows to set GC implementation's options, returning the new config" do + config = GC.config({}) + # Try to find a boolean setting to reliably test changing it. + key, _value = config.find { |_k, v| v == true } + skip unless key + + GC.config(key => false).should == config.merge(key => false) + GC.config[key].should == false + GC.config(key => true).should == config + GC.config[key].should == true + ensure + GC.config(config.except(:implementation)) + end + + it "does not change settings that aren't present in the hash" do + previous = GC.config + GC.config({}) + GC.config.should == previous + end + + it "ignores unknown keys" do + previous = GC.config + GC.config(foo: "bar") + GC.config.should == previous + end + + it "raises an ArgumentError if options include global keys" do + -> { GC.config(implementation: "default") }.should raise_error(ArgumentError, 'Attempting to set read-only key "Implementation"') + end + end + + context "with a non-hash argument" do + it "returns current settings if argument is nil" do + GC.config(nil).should == GC.config + end + + it "raises ArgumentError for all other arguments" do + -> { GC.config([]) }.should raise_error(ArgumentError) + -> { GC.config("default") }.should raise_error(ArgumentError) + -> { GC.config(1) }.should raise_error(ArgumentError) + end + end + + guard -> { PlatformGuard.standard? && GC.config[:implementation] == "default" } do + context "with default GC implementation on MRI" do + before do + @default_config = GC.config({}) + end + + after do + GC.config(@default_config.except(:implementation)) + end + + it "includes :rgengc_allow_full_mark option, true by default" do + GC.config.should include(:rgengc_allow_full_mark) + GC.config[:rgengc_allow_full_mark].should be_true + end + + it "allows to set :rgengc_allow_full_mark" do + # This key maps truthy and falsey values to true and false. + GC.config(rgengc_allow_full_mark: nil).should == @default_config.merge(rgengc_allow_full_mark: false) + GC.config(rgengc_allow_full_mark: 1.23).should == @default_config.merge(rgengc_allow_full_mark: true) + end + end + end + end +end diff --git a/spec/ruby/core/gc/count_spec.rb b/spec/ruby/core/gc/count_spec.rb new file mode 100644 index 0000000000..af2fbbe713 --- /dev/null +++ b/spec/ruby/core/gc/count_spec.rb @@ -0,0 +1,17 @@ +require_relative '../../spec_helper' + +describe "GC.count" do + it "returns an integer" do + GC.count.should be_kind_of(Integer) + end + + it "increases as collections are run" do + count_before = GC.count + i = 0 + while GC.count <= count_before and i < 10 + GC.start + i += 1 + end + GC.count.should > count_before + end +end diff --git a/spec/ruby/core/gc/disable_spec.rb b/spec/ruby/core/gc/disable_spec.rb new file mode 100644 index 0000000000..f89a9d2768 --- /dev/null +++ b/spec/ruby/core/gc/disable_spec.rb @@ -0,0 +1,18 @@ +require_relative '../../spec_helper' + +describe "GC.disable" do + after :each do + GC.enable + end + + it "returns true if and only if the garbage collection was previously disabled" do + GC.enable + GC.disable.should == false + GC.disable.should == true + GC.disable.should == true + GC.enable + GC.disable.should == false + GC.disable.should == true + end + +end diff --git a/spec/ruby/core/gc/enable_spec.rb b/spec/ruby/core/gc/enable_spec.rb new file mode 100644 index 0000000000..ca4488547a --- /dev/null +++ b/spec/ruby/core/gc/enable_spec.rb @@ -0,0 +1,13 @@ +require_relative '../../spec_helper' + +describe "GC.enable" do + + it "returns true if and only if the garbage collection was already disabled" do + GC.enable + GC.enable.should == false + GC.disable + GC.enable.should == true + GC.enable.should == false + end + +end diff --git a/spec/ruby/core/gc/garbage_collect_spec.rb b/spec/ruby/core/gc/garbage_collect_spec.rb new file mode 100644 index 0000000000..f67f0486c8 --- /dev/null +++ b/spec/ruby/core/gc/garbage_collect_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../spec_helper' + +describe "GC#garbage_collect" do + + before :each do + @obj = Object.new + @obj.extend(GC) + end + + it "always returns nil" do + @obj.garbage_collect.should == nil + @obj.garbage_collect.should == nil + end + +end diff --git a/spec/ruby/core/gc/measure_total_time_spec.rb b/spec/ruby/core/gc/measure_total_time_spec.rb new file mode 100644 index 0000000000..f5377c18fd --- /dev/null +++ b/spec/ruby/core/gc/measure_total_time_spec.rb @@ -0,0 +1,17 @@ +require_relative '../../spec_helper' + +describe "GC.measure_total_time" do + before :each do + @default = GC.measure_total_time + end + + after :each do + GC.measure_total_time = @default + end + + it "can set and get a boolean value" do + original = GC.measure_total_time + GC.measure_total_time = !original + GC.measure_total_time.should == !original + end +end diff --git a/spec/ruby/core/gc/profiler/clear_spec.rb b/spec/ruby/core/gc/profiler/clear_spec.rb new file mode 100644 index 0000000000..95c3d4ed65 --- /dev/null +++ b/spec/ruby/core/gc/profiler/clear_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../../spec_helper' + +describe "GC::Profiler.clear" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/gc/profiler/disable_spec.rb b/spec/ruby/core/gc/profiler/disable_spec.rb new file mode 100644 index 0000000000..74089693eb --- /dev/null +++ b/spec/ruby/core/gc/profiler/disable_spec.rb @@ -0,0 +1,16 @@ +require_relative '../../../spec_helper' + +describe "GC::Profiler.disable" do + before do + @status = GC::Profiler.enabled? + end + + after do + @status ? GC::Profiler.enable : GC::Profiler.disable + end + + it "disables the profiler" do + GC::Profiler.disable + GC::Profiler.should_not.enabled? + end +end diff --git a/spec/ruby/core/gc/profiler/enable_spec.rb b/spec/ruby/core/gc/profiler/enable_spec.rb new file mode 100644 index 0000000000..313ca3d949 --- /dev/null +++ b/spec/ruby/core/gc/profiler/enable_spec.rb @@ -0,0 +1,17 @@ +require_relative '../../../spec_helper' + +describe "GC::Profiler.enable" do + + before do + @status = GC::Profiler.enabled? + end + + after do + @status ? GC::Profiler.enable : GC::Profiler.disable + end + + it "enables the profiler" do + GC::Profiler.enable + GC::Profiler.should.enabled? + end +end diff --git a/spec/ruby/core/gc/profiler/enabled_spec.rb b/spec/ruby/core/gc/profiler/enabled_spec.rb new file mode 100644 index 0000000000..23677be555 --- /dev/null +++ b/spec/ruby/core/gc/profiler/enabled_spec.rb @@ -0,0 +1,21 @@ +require_relative '../../../spec_helper' + +describe "GC::Profiler.enabled?" do + before do + @status = GC::Profiler.enabled? + end + + after do + @status ? GC::Profiler.enable : GC::Profiler.disable + end + + it "reports as enabled when enabled" do + GC::Profiler.enable + GC::Profiler.enabled?.should be_true + end + + it "reports as disabled when disabled" do + GC::Profiler.disable + GC::Profiler.enabled?.should be_false + end +end diff --git a/spec/ruby/core/gc/profiler/report_spec.rb b/spec/ruby/core/gc/profiler/report_spec.rb new file mode 100644 index 0000000000..732b1d0f51 --- /dev/null +++ b/spec/ruby/core/gc/profiler/report_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../../spec_helper' + +describe "GC::Profiler.report" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/gc/profiler/result_spec.rb b/spec/ruby/core/gc/profiler/result_spec.rb new file mode 100644 index 0000000000..2ab46a8371 --- /dev/null +++ b/spec/ruby/core/gc/profiler/result_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../../spec_helper' + +describe "GC::Profiler.result" do + it "returns a string" do + GC::Profiler.result.should be_kind_of(String) + end +end diff --git a/spec/ruby/core/gc/profiler/total_time_spec.rb b/spec/ruby/core/gc/profiler/total_time_spec.rb new file mode 100644 index 0000000000..7709f168dd --- /dev/null +++ b/spec/ruby/core/gc/profiler/total_time_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../../spec_helper' + +describe "GC::Profiler.total_time" do + it "returns an float" do + GC::Profiler.total_time.should be_kind_of(Float) + end +end diff --git a/spec/ruby/core/gc/start_spec.rb b/spec/ruby/core/gc/start_spec.rb new file mode 100644 index 0000000000..c941058969 --- /dev/null +++ b/spec/ruby/core/gc/start_spec.rb @@ -0,0 +1,12 @@ +require_relative '../../spec_helper' + +describe "GC.start" do + it "always returns nil" do + GC.start.should == nil + GC.start.should == nil + end + + it "accepts keyword arguments" do + GC.start(full_mark: true, immediate_sweep: true).should == nil + end +end diff --git a/spec/ruby/core/gc/stat_spec.rb b/spec/ruby/core/gc/stat_spec.rb new file mode 100644 index 0000000000..3b43b28a92 --- /dev/null +++ b/spec/ruby/core/gc/stat_spec.rb @@ -0,0 +1,62 @@ +require_relative '../../spec_helper' + +describe "GC.stat" do + it "returns hash of values" do + stat = GC.stat + stat.should be_kind_of(Hash) + stat.keys.should.include?(:count) + end + + it "updates the given hash values" do + hash = { count: "hello", __other__: "world" } + stat = GC.stat(hash) + + stat.should be_kind_of(Hash) + stat.should equal hash + stat[:count].should be_kind_of(Integer) + stat[:__other__].should == "world" + end + + it "the values are all Integer since rb_gc_stat() returns size_t" do + GC.stat.values.each { |value| value.should be_kind_of(Integer) } + end + + it "can return a single value" do + GC.stat(:count).should be_kind_of(Integer) + end + + it "increases count after GC is run" do + count = GC.stat(:count) + GC.start + GC.stat(:count).should > count + end + + it "increases major_gc_count after GC is run" do + count = GC.stat(:major_gc_count) + GC.start + GC.stat(:major_gc_count).should > count + end + + it "provides some number for count" do + GC.stat(:count).should be_kind_of(Integer) + GC.stat[:count].should be_kind_of(Integer) + end + + it "provides some number for heap_free_slots" do + GC.stat(:heap_free_slots).should be_kind_of(Integer) + GC.stat[:heap_free_slots].should be_kind_of(Integer) + end + + it "provides some number for total_allocated_objects" do + GC.stat(:total_allocated_objects).should be_kind_of(Integer) + GC.stat[:total_allocated_objects].should be_kind_of(Integer) + end + + it "raises an error if argument is not nil, a symbol, or a hash" do + -> { GC.stat(7) }.should raise_error(TypeError, "non-hash or symbol given") + end + + it "raises an error if an unknown key is given" do + -> { GC.stat(:foo) }.should raise_error(ArgumentError, "unknown key: foo") + end +end diff --git a/spec/ruby/core/gc/stress_spec.rb b/spec/ruby/core/gc/stress_spec.rb new file mode 100644 index 0000000000..ca62c0cb4e --- /dev/null +++ b/spec/ruby/core/gc/stress_spec.rb @@ -0,0 +1,27 @@ +require_relative '../../spec_helper' + +describe "GC.stress" do + after :each do + # make sure that we never leave these tests in stress enabled GC! + GC.stress = false + end + + it "returns current status of GC stress mode" do + GC.stress.should be_false + GC.stress = true + GC.stress.should be_true + GC.stress = false + GC.stress.should be_false + end +end + +describe "GC.stress=" do + after :each do + GC.stress = false + end + + it "sets the stress mode" do + GC.stress = true + GC.stress.should be_true + end +end diff --git a/spec/ruby/core/gc/total_time_spec.rb b/spec/ruby/core/gc/total_time_spec.rb new file mode 100644 index 0000000000..a8430fbe9a --- /dev/null +++ b/spec/ruby/core/gc/total_time_spec.rb @@ -0,0 +1,13 @@ +require_relative '../../spec_helper' + +describe "GC.total_time" do + it "returns an Integer" do + GC.total_time.should be_kind_of(Integer) + end + + it "increases as collections are run" do + time_before = GC.total_time + GC.start + GC.total_time.should >= time_before + end +end diff --git a/spec/ruby/core/hash/allocate_spec.rb b/spec/ruby/core/hash/allocate_spec.rb new file mode 100644 index 0000000000..93420de866 --- /dev/null +++ b/spec/ruby/core/hash/allocate_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../spec_helper' + +describe "Hash.allocate" do + it "returns an instance of Hash" do + hsh = Hash.allocate + hsh.should be_an_instance_of(Hash) + end + + it "returns a fully-formed instance of Hash" do + hsh = Hash.allocate + hsh.size.should == 0 + hsh[:a] = 1 + hsh.should == { a: 1 } + end +end diff --git a/spec/ruby/core/hash/any_spec.rb b/spec/ruby/core/hash/any_spec.rb new file mode 100644 index 0000000000..c26dfabde6 --- /dev/null +++ b/spec/ruby/core/hash/any_spec.rb @@ -0,0 +1,30 @@ +require_relative '../../spec_helper' + +describe "Hash#any?" do + describe 'with no block given' do + it "checks if there are any members of a Hash" do + empty_hash = {} + empty_hash.should_not.any? + + hash_with_members = { 'key' => 'value' } + hash_with_members.should.any? + end + end + + describe 'with a block given' do + it 'is false if the hash is empty' do + empty_hash = {} + empty_hash.any? {|k,v| 1 == 1 }.should == false + end + + it 'is true if the block returns true for any member of the hash' do + hash_with_members = { 'a' => false, 'b' => false, 'c' => true, 'd' => false } + hash_with_members.any? {|k,v| v == true}.should == true + end + + it 'is false if the block returns false for all members of the hash' do + hash_with_members = { 'a' => false, 'b' => false, 'c' => true, 'd' => false } + hash_with_members.any? {|k,v| v == 42}.should == false + end + end +end diff --git a/spec/ruby/core/hash/assoc_spec.rb b/spec/ruby/core/hash/assoc_spec.rb new file mode 100644 index 0000000000..62b2a11b30 --- /dev/null +++ b/spec/ruby/core/hash/assoc_spec.rb @@ -0,0 +1,50 @@ +require_relative '../../spec_helper' + +describe "Hash#assoc" do + before :each do + @h = {apple: :green, orange: :orange, grape: :green, banana: :yellow} + end + + it "returns an Array if the argument is == to a key of the Hash" do + @h.assoc(:apple).should be_an_instance_of(Array) + end + + it "returns a 2-element Array if the argument is == to a key of the Hash" do + @h.assoc(:grape).size.should == 2 + end + + it "sets the first element of the Array to the located key" do + @h.assoc(:banana).first.should == :banana + end + + it "sets the last element of the Array to the value of the located key" do + @h.assoc(:banana).last.should == :yellow + end + + it "only returns the first matching key-value pair for identity hashes" do + # Avoid literal String keys since string literals can be frozen and interned e.g. with --enable-frozen-string-literal + h = {}.compare_by_identity + k1 = 'pear'.dup + h[k1] = :red + k2 = 'pear'.dup + h[k2] = :green + h.size.should == 2 + h.keys.grep(/pear/).size.should == 2 + h.assoc('pear').should == ['pear', :red] + end + + it "uses #== to compare the argument to the keys" do + @h[1.0] = :value + 1.should == 1.0 + @h.assoc(1).should == [1.0, :value] + end + + it "returns nil if the argument is not a key of the Hash" do + @h.assoc(:green).should be_nil + end + + it "returns nil if the argument is not a key of the Hash even when there is a default" do + Hash.new(42).merge!( foo: :bar ).assoc(42).should be_nil + Hash.new{|h, k| h[k] = 42}.merge!( foo: :bar ).assoc(42).should be_nil + end +end diff --git a/spec/ruby/core/hash/clear_spec.rb b/spec/ruby/core/hash/clear_spec.rb new file mode 100644 index 0000000000..cf05e36ac9 --- /dev/null +++ b/spec/ruby/core/hash/clear_spec.rb @@ -0,0 +1,32 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash#clear" do + it "removes all key, value pairs" do + h = { 1 => 2, 3 => 4 } + h.clear.should equal(h) + h.should == {} + end + + it "does not remove default values" do + h = Hash.new(5) + h.clear + h.default.should == 5 + + h = { "a" => 100, "b" => 200 } + h.default = "Go fish" + h.clear + h["z"].should == "Go fish" + end + + it "does not remove default procs" do + h = Hash.new { 5 } + h.clear + h.default_proc.should_not == nil + end + + it "raises a FrozenError if called on a frozen instance" do + -> { HashSpecs.frozen_hash.clear }.should raise_error(FrozenError) + -> { HashSpecs.empty_frozen_hash.clear }.should raise_error(FrozenError) + end +end diff --git a/spec/ruby/core/hash/clone_spec.rb b/spec/ruby/core/hash/clone_spec.rb new file mode 100644 index 0000000000..6c96fc0c67 --- /dev/null +++ b/spec/ruby/core/hash/clone_spec.rb @@ -0,0 +1,12 @@ +require_relative '../../spec_helper' + +describe "Hash#clone" do + it "copies instance variable but not the objects they refer to" do + hash = { 'key' => 'value' } + + clone = hash.clone + + clone.should == hash + clone.should_not equal hash + end +end diff --git a/spec/ruby/core/hash/compact_spec.rb b/spec/ruby/core/hash/compact_spec.rb new file mode 100644 index 0000000000..13371bce43 --- /dev/null +++ b/spec/ruby/core/hash/compact_spec.rb @@ -0,0 +1,83 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash#compact" do + before :each do + @hash = { truthy: true, false: false, nil: nil, nil => true } + @initial_pairs = @hash.dup + @compact = { truthy: true, false: false, nil => true } + end + + it "returns new object that rejects pair has nil value" do + ret = @hash.compact + ret.should_not equal(@hash) + ret.should == @compact + end + + it "keeps own pairs" do + @hash.compact + @hash.should == @initial_pairs + end + + ruby_version_is '3.3' do + it "retains the default value" do + hash = Hash.new(1) + hash.compact.default.should == 1 + hash[:a] = 1 + hash.compact.default.should == 1 + end + + it "retains the default_proc" do + pr = proc { |h, k| h[k] = [] } + hash = Hash.new(&pr) + hash.compact.default_proc.should == pr + hash[:a] = 1 + hash.compact.default_proc.should == pr + end + + it "retains compare_by_identity flag" do + hash = {}.compare_by_identity + hash.compact.compare_by_identity?.should == true + hash[:a] = 1 + hash.compact.compare_by_identity?.should == true + end + end +end + +describe "Hash#compact!" do + before :each do + @hash = { truthy: true, false: false, nil: nil, nil => true } + @initial_pairs = @hash.dup + @compact = { truthy: true, false: false, nil => true } + end + + it "returns self" do + @hash.compact!.should equal(@hash) + end + + it "rejects own pair has nil value" do + @hash.compact! + @hash.should == @compact + end + + context "when each pair does not have nil value" do + before :each do + @hash.compact! + end + + it "returns nil" do + @hash.compact!.should be_nil + end + end + + describe "on frozen instance" do + before :each do + @hash.freeze + end + + it "keeps pairs and raises a FrozenError" do + ->{ @hash.compact! }.should raise_error(FrozenError) + @hash.should == @initial_pairs + end + end +end diff --git a/spec/ruby/core/hash/compare_by_identity_spec.rb b/spec/ruby/core/hash/compare_by_identity_spec.rb new file mode 100644 index 0000000000..2975526a97 --- /dev/null +++ b/spec/ruby/core/hash/compare_by_identity_spec.rb @@ -0,0 +1,147 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash#compare_by_identity" do + before :each do + @h = {} + @idh = {}.compare_by_identity + end + + it "causes future comparisons on the receiver to be made by identity" do + @h[[1]] = :a + @h[[1]].should == :a + @h.compare_by_identity + @h[[1].dup].should be_nil + end + + it "rehashes internally so that old keys can be looked up" do + h = {} + (1..10).each { |k| h[k] = k } + o = Object.new + def o.hash; 123; end + h[o] = 1 + h.compare_by_identity + h[o].should == 1 + end + + it "returns self" do + h = {} + h[:foo] = :bar + h.compare_by_identity.should equal h + end + + it "has no effect on an already compare_by_identity hash" do + @idh[:foo] = :bar + @idh.compare_by_identity.should equal @idh + @idh.should.compare_by_identity? + @idh[:foo].should == :bar + end + + it "uses the semantics of BasicObject#equal? to determine key identity" do + [1].should_not equal([1]) + @idh[[1]] = :c + @idh[[1]] = :d + :bar.should equal(:bar) + @idh[:bar] = :e + @idh[:bar] = :f + @idh.values.should == [:c, :d, :f] + end + + it "uses #equal? semantics, but doesn't actually call #equal? to determine identity" do + obj = mock('equal') + obj.should_not_receive(:equal?) + @idh[:foo] = :glark + @idh[obj] = :a + @idh[obj].should == :a + end + + it "does not call #hash on keys" do + key = HashSpecs::ByIdentityKey.new + @idh[key] = 1 + @idh[key].should == 1 + end + + it "regards #dup'd objects as having different identities" do + key = ['foo'] + @idh[key.dup] = :str + @idh[key].should be_nil + end + + it "regards #clone'd objects as having different identities" do + key = ['foo'] + @idh[key.clone] = :str + @idh[key].should be_nil + end + + it "regards references to the same object as having the same identity" do + o = Object.new + @h[o] = :o + @h[:a] = :a + @h[o].should == :o + end + + it "raises a FrozenError on frozen hashes" do + @h = @h.freeze + -> { @h.compare_by_identity }.should raise_error(FrozenError) + end + + # Behaviour confirmed in https://bugs.ruby-lang.org/issues/1871 + it "persists over #dups" do + @idh['foo'.dup] = :bar + @idh['foo'.dup] = :glark + @idh.dup.should == @idh + @idh.dup.size.should == @idh.size + @idh.dup.should.compare_by_identity? + end + + it "persists over #clones" do + @idh['foo'.dup] = :bar + @idh['foo'.dup] = :glark + @idh.clone.should == @idh + @idh.clone.size.should == @idh.size + @idh.dup.should.compare_by_identity? + end + + it "does not copy string keys" do + foo = 'foo' + @idh[foo] = true + @idh[foo] = true + @idh.size.should == 1 + @idh.keys.first.should equal foo + end + + # Check `#[]=` call with a String literal. + # Don't use `#+` because with `#+` it's no longer a String literal. + # + # See https://bugs.ruby-lang.org/issues/12855 + it "gives different identity for string literals" do + eval <<~RUBY + # frozen_string_literal: false + @idh['foo'] = 1 + @idh['foo'] = 2 + RUBY + @idh.values.should == [1, 2] + @idh.size.should == 2 + end +end + +describe "Hash#compare_by_identity?" do + it "returns false by default" do + h = {} + h.compare_by_identity?.should be_false + end + + it "returns true once #compare_by_identity has been invoked on self" do + h = {} + h.compare_by_identity + h.compare_by_identity?.should be_true + end + + it "returns true when called multiple times on the same ident hash" do + h = {} + h.compare_by_identity + h.compare_by_identity?.should be_true + h.compare_by_identity?.should be_true + h.compare_by_identity?.should be_true + end +end diff --git a/spec/ruby/core/hash/constructor_spec.rb b/spec/ruby/core/hash/constructor_spec.rb new file mode 100644 index 0000000000..0f97f7b40e --- /dev/null +++ b/spec/ruby/core/hash/constructor_spec.rb @@ -0,0 +1,129 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash.[]" do + describe "passed zero arguments" do + it "returns an empty hash" do + Hash[].should == {} + end + end + + it "creates a Hash; values can be provided as the argument list" do + Hash[:a, 1, :b, 2].should == { a: 1, b: 2 } + Hash[].should == {} + Hash[:a, 1, :b, { c: 2 }].should == { a: 1, b: { c: 2 } } + end + + it "creates a Hash; values can be provided as one single hash" do + Hash[a: 1, b: 2].should == { a: 1, b: 2 } + Hash[{1 => 2, 3 => 4}].should == {1 => 2, 3 => 4} + Hash[{}].should == {} + end + + describe "passed an array" do + it "treats elements that are 2 element arrays as key and value" do + Hash[[[:a, :b], [:c, :d]]].should == { a: :b, c: :d } + end + + it "treats elements that are 1 element arrays as keys with value nil" do + Hash[[[:a]]].should == { a: nil } + end + end + + # #1000 #1385 + it "creates a Hash; values can be provided as a list of value-pairs in an array" do + Hash[[[:a, 1], [:b, 2]]].should == { a: 1, b: 2 } + end + + it "coerces a single argument which responds to #to_ary" do + ary = mock('to_ary') + ary.should_receive(:to_ary).and_return([[:a, :b]]) + + Hash[ary].should == { a: :b } + end + + it "raises for elements that are not arrays" do + -> { + Hash[[:a]].should == {} + }.should raise_error(ArgumentError) + -> { + Hash[[:nil]].should == {} + }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError for arrays of more than 2 elements" do + ->{ Hash[[[:a, :b, :c]]].should == {} }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when passed a list of value-invalid-pairs in an array" do + -> { + -> { + Hash[[[:a, 1], [:b], 42, [:d, 2], [:e, 2, 3], []]] + }.should complain(/ignoring wrong elements/) + }.should raise_error(ArgumentError) + end + + describe "passed a single argument which responds to #to_hash" do + it "coerces it and returns a copy" do + h = { a: :b, c: :d } + to_hash = mock('to_hash') + to_hash.should_receive(:to_hash).and_return(h) + + result = Hash[to_hash] + result.should == h + result.should_not equal(h) + end + end + + it "raises an ArgumentError when passed an odd number of arguments" do + -> { Hash[1, 2, 3] }.should raise_error(ArgumentError) + -> { Hash[1, 2, { 3 => 4 }] }.should raise_error(ArgumentError) + end + + it "calls to_hash" do + obj = mock('x') + def obj.to_hash() { 1 => 2, 3 => 4 } end + Hash[obj].should == { 1 => 2, 3 => 4 } + end + + it "returns an instance of a subclass when passed an Array" do + HashSpecs::MyHash[1,2,3,4].should be_an_instance_of(HashSpecs::MyHash) + end + + it "returns instances of subclasses" do + HashSpecs::MyHash[].should be_an_instance_of(HashSpecs::MyHash) + end + + it "returns an instance of the class it's called on" do + Hash[HashSpecs::MyHash[1, 2]].class.should == Hash + HashSpecs::MyHash[Hash[1, 2]].should be_an_instance_of(HashSpecs::MyHash) + end + + it "does not call #initialize on the subclass instance" do + HashSpecs::MyInitializerHash[Hash[1, 2]].should be_an_instance_of(HashSpecs::MyInitializerHash) + end + + it "does not retain the default value" do + hash = Hash.new(1) + Hash[hash].default.should be_nil + hash[:a] = 1 + Hash[hash].default.should be_nil + end + + it "does not retain the default_proc" do + hash = Hash.new { |h, k| h[k] = [] } + Hash[hash].default_proc.should be_nil + hash[:a] = 1 + Hash[hash].default_proc.should be_nil + end + + ruby_version_is '3.3' do + it "does not retain compare_by_identity flag" do + hash = { a: 1 }.compare_by_identity + Hash[hash].compare_by_identity?.should == false + + hash = {}.compare_by_identity + Hash[hash].compare_by_identity?.should == false + end + end +end diff --git a/spec/ruby/core/hash/deconstruct_keys_spec.rb b/spec/ruby/core/hash/deconstruct_keys_spec.rb new file mode 100644 index 0000000000..bbcd8932e5 --- /dev/null +++ b/spec/ruby/core/hash/deconstruct_keys_spec.rb @@ -0,0 +1,23 @@ +require_relative '../../spec_helper' + +describe "Hash#deconstruct_keys" do + it "returns self" do + hash = {a: 1, b: 2} + + hash.deconstruct_keys([:a, :b]).should equal hash + end + + it "requires one argument" do + -> { + {a: 1}.deconstruct_keys + }.should raise_error(ArgumentError, /wrong number of arguments \(given 0, expected 1\)/) + end + + it "ignores argument" do + hash = {a: 1, b: 2} + + hash.deconstruct_keys([:a]).should == {a: 1, b: 2} + hash.deconstruct_keys(0 ).should == {a: 1, b: 2} + hash.deconstruct_keys('' ).should == {a: 1, b: 2} + end +end diff --git a/spec/ruby/core/hash/default_proc_spec.rb b/spec/ruby/core/hash/default_proc_spec.rb new file mode 100644 index 0000000000..f4e4803632 --- /dev/null +++ b/spec/ruby/core/hash/default_proc_spec.rb @@ -0,0 +1,80 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash#default_proc" do + it "returns the block passed to Hash.new" do + h = Hash.new { 'Paris' } + p = h.default_proc + p.call(1).should == 'Paris' + end + + it "returns nil if no block was passed to proc" do + {}.default_proc.should == nil + end +end + +describe "Hash#default_proc=" do + it "replaces the block passed to Hash.new" do + h = Hash.new { 'Paris' } + h.default_proc = Proc.new { 'Montreal' } + p = h.default_proc + p.call(1).should == 'Montreal' + end + + it "uses :to_proc on its argument" do + h = Hash.new { 'Paris' } + obj = mock('to_proc') + obj.should_receive(:to_proc).and_return(Proc.new { 'Montreal' }) + (h.default_proc = obj).should equal(obj) + h[:cool_city].should == 'Montreal' + end + + it "overrides the static default" do + h = Hash.new(42) + h.default_proc = Proc.new { 6 } + h.default.should be_nil + h.default_proc.call.should == 6 + end + + it "raises an error if passed stuff not convertible to procs" do + ->{{}.default_proc = 42}.should raise_error(TypeError) + end + + it "returns the passed Proc" do + new_proc = Proc.new {} + ({}.default_proc = new_proc).should equal(new_proc) + end + + it "clears the default proc if passed nil" do + h = Hash.new { 'Paris' } + h.default_proc = nil + h.default_proc.should == nil + h[:city].should == nil + end + + it "returns nil if passed nil" do + ({}.default_proc = nil).should be_nil + end + + it "accepts a lambda with an arity of 2" do + h = {} + -> do + h.default_proc = -> a, b { } + end.should_not raise_error(TypeError) + end + + it "raises a TypeError if passed a lambda with an arity other than 2" do + h = {} + -> do + h.default_proc = -> a { } + end.should raise_error(TypeError) + -> do + h.default_proc = -> a, b, c { } + end.should raise_error(TypeError) + end + + it "raises a FrozenError if self is frozen" do + -> { {}.freeze.default_proc = Proc.new {} }.should raise_error(FrozenError) + -> { {}.freeze.default_proc = nil }.should raise_error(FrozenError) + end +end diff --git a/spec/ruby/core/hash/default_spec.rb b/spec/ruby/core/hash/default_spec.rb new file mode 100644 index 0000000000..d8b62ea196 --- /dev/null +++ b/spec/ruby/core/hash/default_spec.rb @@ -0,0 +1,46 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash#default" do + it "returns the default value" do + h = Hash.new(5) + h.default.should == 5 + h.default(4).should == 5 + {}.default.should == nil + {}.default(4).should == nil + end + + it "uses the default proc to compute a default value, passing given key" do + h = Hash.new { |*args| args } + h.default(nil).should == [h, nil] + h.default(5).should == [h, 5] + end + + it "calls default proc with nil arg if passed a default proc but no arg" do + h = Hash.new { |*args| args } + h.default.should == nil + end +end + +describe "Hash#default=" do + it "sets the default value" do + h = {} + h.default = 99 + h.default.should == 99 + end + + it "unsets the default proc" do + [99, nil, -> { 6 }].each do |default| + h = Hash.new { 5 } + h.default_proc.should_not == nil + h.default = default + h.default.should == default + h.default_proc.should == nil + end + end + + it "raises a FrozenError if called on a frozen instance" do + -> { HashSpecs.frozen_hash.default = nil }.should raise_error(FrozenError) + -> { HashSpecs.empty_frozen_hash.default = nil }.should raise_error(FrozenError) + end +end diff --git a/spec/ruby/core/hash/delete_if_spec.rb b/spec/ruby/core/hash/delete_if_spec.rb new file mode 100644 index 0000000000..c9e670ffc3 --- /dev/null +++ b/spec/ruby/core/hash/delete_if_spec.rb @@ -0,0 +1,44 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/iteration' +require_relative '../enumerable/shared/enumeratorized' + +describe "Hash#delete_if" do + it "yields two arguments: key and value" do + all_args = [] + { 1 => 2, 3 => 4 }.delete_if { |*args| all_args << args } + all_args.sort.should == [[1, 2], [3, 4]] + end + + it "removes every entry for which block is true and returns self" do + h = { a: 1, b: 2, c: 3, d: 4 } + h.delete_if { |k,v| v % 2 == 1 }.should equal(h) + h.should == { b: 2, d: 4 } + end + + it "removes all entries if the block is true" do + h = { a: 1, b: 2, c: 3 } + h.delete_if { |k,v| true }.should equal(h) + h.should == {} + end + + it "processes entries with the same order as each()" do + h = { a: 1, b: 2, c: 3, d: 4 } + + each_pairs = [] + delete_pairs = [] + + h.each_pair { |k,v| each_pairs << [k, v] } + h.delete_if { |k,v| delete_pairs << [k,v] } + + each_pairs.should == delete_pairs + end + + it "raises a FrozenError if called on a frozen instance" do + -> { HashSpecs.frozen_hash.delete_if { false } }.should raise_error(FrozenError) + -> { HashSpecs.empty_frozen_hash.delete_if { true } }.should raise_error(FrozenError) + end + + it_behaves_like :hash_iteration_no_block, :delete_if + it_behaves_like :enumeratorized_with_origin_size, :delete_if, { 1 => 2, 3 => 4, 5 => 6 } +end diff --git a/spec/ruby/core/hash/delete_spec.rb b/spec/ruby/core/hash/delete_spec.rb new file mode 100644 index 0000000000..3e3479c69c --- /dev/null +++ b/spec/ruby/core/hash/delete_spec.rb @@ -0,0 +1,58 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash#delete" do + it "removes the entry and returns the deleted value" do + h = { a: 5, b: 2 } + h.delete(:b).should == 2 + h.should == { a: 5 } + end + + it "calls supplied block if the key is not found" do + { a: 1, b: 10, c: 100 }.delete(:d) { 5 }.should == 5 + Hash.new(:default).delete(:d) { 5 }.should == 5 + Hash.new { :default }.delete(:d) { 5 }.should == 5 + end + + it "returns nil if the key is not found when no block is given" do + { a: 1, b: 10, c: 100 }.delete(:d).should == nil + Hash.new(:default).delete(:d).should == nil + Hash.new { :default }.delete(:d).should == nil + end + + # MRI explicitly implements this behavior + it "allows removing a key while iterating" do + h = { a: 1, b: 2 } + visited = [] + h.each_pair { |k, v| + visited << k + h.delete(k) + } + visited.should == [:a, :b] + h.should == {} + end + + it "allows removing a key while iterating for big hashes" do + h = { a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8, i: 9, j: 10, + k: 11, l: 12, m: 13, n: 14, o: 15, p: 16, q: 17, r: 18, s: 19, t: 20, + u: 21, v: 22, w: 23, x: 24, y: 25, z: 26 } + visited = [] + h.each_pair { |k, v| + visited << k + h.delete(k) + } + visited.should == [:a, :b, :c, :d, :e, :f, :g, :h, :i, :j, :k, :l, :m, + :n, :o, :p, :q, :r, :s, :t, :u, :v, :w, :x, :y, :z] + h.should == {} + end + + it "accepts keys with private #hash method" do + key = HashSpecs::KeyWithPrivateHash.new + { key => 5 }.delete(key).should == 5 + end + + it "raises a FrozenError if called on a frozen instance" do + -> { HashSpecs.frozen_hash.delete("foo") }.should raise_error(FrozenError) + -> { HashSpecs.empty_frozen_hash.delete("foo") }.should raise_error(FrozenError) + end +end diff --git a/spec/ruby/core/hash/dig_spec.rb b/spec/ruby/core/hash/dig_spec.rb new file mode 100644 index 0000000000..aa0ecadd2f --- /dev/null +++ b/spec/ruby/core/hash/dig_spec.rb @@ -0,0 +1,66 @@ +require_relative '../../spec_helper' + +describe "Hash#dig" do + + it "returns #[] with one arg" do + h = { 0 => false, a: 1 } + h.dig(:a).should == 1 + h.dig(0).should be_false + h.dig(1).should be_nil + end + + it "returns the nested value specified by the sequence of keys" do + h = { foo: { bar: { baz: 1 } } } + h.dig(:foo, :bar, :baz).should == 1 + h.dig(:foo, :bar, :nope).should be_nil + h.dig(:foo, :baz).should be_nil + h.dig(:bar, :baz, :foo).should be_nil + end + + it "returns the nested value specified if the sequence includes an index" do + h = { foo: [1, 2, 3] } + h.dig(:foo, 2).should == 3 + end + + it "returns nil if any intermediate step is nil" do + h = { foo: { bar: { baz: 1 } } } + h.dig(:foo, :zot, :xyz).should == nil + end + + it "raises an ArgumentError if no arguments provided" do + -> { { the: 'borg' }.dig() }.should raise_error(ArgumentError) + end + + it "handles type-mixed deep digging" do + h = {} + h[:foo] = [ { bar: [ 1 ] }, [ obj = Object.new, 'str' ] ] + def obj.dig(*args); [ 42 ] end + + h.dig(:foo, 0, :bar).should == [ 1 ] + h.dig(:foo, 0, :bar, 0).should == 1 + h.dig(:foo, 1, 1).should == 'str' + # MRI does not recurse values returned from `obj.dig` + h.dig(:foo, 1, 0, 0).should == [ 42 ] + h.dig(:foo, 1, 0, 0, 10).should == [ 42 ] + end + + it "raises TypeError if an intermediate element does not respond to #dig" do + h = {} + h[:foo] = [ { bar: [ 1 ] }, [ nil, 'str' ] ] + -> { h.dig(:foo, 0, :bar, 0, 0) }.should raise_error(TypeError) + -> { h.dig(:foo, 1, 1, 0) }.should raise_error(TypeError) + end + + it "calls #dig on the result of #[] with the remaining arguments" do + h = { foo: { bar: { baz: 42 } } } + h[:foo].should_receive(:dig).with(:bar, :baz).and_return(42) + h.dig(:foo, :bar, :baz).should == 42 + end + + it "respects Hash's default" do + default = {bar: 42} + h = Hash.new(default) + h.dig(:foo).should equal default + h.dig(:foo, :bar).should == 42 + end +end diff --git a/spec/ruby/core/hash/each_key_spec.rb b/spec/ruby/core/hash/each_key_spec.rb new file mode 100644 index 0000000000..c84dd696d5 --- /dev/null +++ b/spec/ruby/core/hash/each_key_spec.rb @@ -0,0 +1,23 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/iteration' +require_relative '../enumerable/shared/enumeratorized' + +describe "Hash#each_key" do + it "calls block once for each key, passing key" do + r = {} + h = { 1 => -1, 2 => -2, 3 => -3, 4 => -4 } + h.each_key { |k| r[k] = k }.should equal(h) + r.should == { 1 => 1, 2 => 2, 3 => 3, 4 => 4 } + end + + it "processes keys in the same order as keys()" do + keys = [] + h = { 1 => -1, 2 => -2, 3 => -3, 4 => -4 } + h.each_key { |k| keys << k } + keys.should == h.keys + end + + it_behaves_like :hash_iteration_no_block, :each_key + it_behaves_like :enumeratorized_with_origin_size, :each_key, { 1 => 2, 3 => 4, 5 => 6 } +end diff --git a/spec/ruby/core/hash/each_pair_spec.rb b/spec/ruby/core/hash/each_pair_spec.rb new file mode 100644 index 0000000000..eb6656681d --- /dev/null +++ b/spec/ruby/core/hash/each_pair_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/iteration' +require_relative 'shared/each' +require_relative '../enumerable/shared/enumeratorized' + +describe "Hash#each_pair" do + it_behaves_like :hash_each, :each_pair + it_behaves_like :hash_iteration_no_block, :each_pair + it_behaves_like :enumeratorized_with_origin_size, :each_pair, { 1 => 2, 3 => 4, 5 => 6 } +end diff --git a/spec/ruby/core/hash/each_spec.rb b/spec/ruby/core/hash/each_spec.rb new file mode 100644 index 0000000000..f0de0bdee5 --- /dev/null +++ b/spec/ruby/core/hash/each_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/iteration' +require_relative 'shared/each' +require_relative '../enumerable/shared/enumeratorized' + +describe "Hash#each" do + it_behaves_like :hash_each, :each + it_behaves_like :hash_iteration_no_block, :each + it_behaves_like :enumeratorized_with_origin_size, :each, { 1 => 2, 3 => 4, 5 => 6 } +end diff --git a/spec/ruby/core/hash/each_value_spec.rb b/spec/ruby/core/hash/each_value_spec.rb new file mode 100644 index 0000000000..19b076730d --- /dev/null +++ b/spec/ruby/core/hash/each_value_spec.rb @@ -0,0 +1,23 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/iteration' +require_relative '../enumerable/shared/enumeratorized' + +describe "Hash#each_value" do + it "calls block once for each key, passing value" do + r = [] + h = { a: -5, b: -3, c: -2, d: -1, e: -1 } + h.each_value { |v| r << v }.should equal(h) + r.sort.should == [-5, -3, -2, -1, -1] + end + + it "processes values in the same order as values()" do + values = [] + h = { a: -5, b: -3, c: -2, d: -1, e: -1 } + h.each_value { |v| values << v } + values.should == h.values + end + + it_behaves_like :hash_iteration_no_block, :each_value + it_behaves_like :enumeratorized_with_origin_size, :each_value, { 1 => 2, 3 => 4, 5 => 6 } +end diff --git a/spec/ruby/core/hash/element_reference_spec.rb b/spec/ruby/core/hash/element_reference_spec.rb new file mode 100644 index 0000000000..d5859cb342 --- /dev/null +++ b/spec/ruby/core/hash/element_reference_spec.rb @@ -0,0 +1,134 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash#[]" do + it "returns the value for key" do + obj = mock('x') + h = { 1 => 2, 3 => 4, "foo" => "bar", obj => obj, [] => "baz" } + h[1].should == 2 + h[3].should == 4 + h["foo"].should == "bar" + h[obj].should == obj + h[[]].should == "baz" + end + + it "returns nil as default value" do + { 0 => 0 }[5].should == nil + end + + it "returns the default (immediate) value for missing keys" do + h = Hash.new(5) + h[:a].should == 5 + h[:a] = 0 + h[:a].should == 0 + h[:b].should == 5 + end + + it "calls subclass implementations of default" do + h = HashSpecs::DefaultHash.new + h[:nothing].should == 100 + end + + it "does not create copies of the immediate default value" do + str = +"foo" + h = Hash.new(str) + a = h[:a] + b = h[:b] + a << "bar" + + a.should equal(b) + a.should == "foobar" + b.should == "foobar" + end + + it "returns the default (dynamic) value for missing keys" do + h = Hash.new { |hsh, k| k.kind_of?(Numeric) ? hsh[k] = k + 2 : hsh[k] = k } + h[1].should == 3 + h['this'].should == 'this' + h.should == { 1 => 3, 'this' => 'this' } + + i = 0 + h = Hash.new { |hsh, key| i += 1 } + h[:foo].should == 1 + h[:foo].should == 2 + h[:bar].should == 3 + end + + it "does not return default values for keys with nil values" do + h = Hash.new(5) + h[:a] = nil + h[:a].should == nil + + h = Hash.new { 5 } + h[:a] = nil + h[:a].should == nil + end + + it "compares keys with eql? semantics" do + { 1.0 => "x" }[1].should == nil + { 1.0 => "x" }[1.0].should == "x" + { 1 => "x" }[1.0].should == nil + { 1 => "x" }[1].should == "x" + end + + it "compares key via hash" do + x = mock('0') + x.should_receive(:hash).and_return(0) + + h = {} + # 1.9 only calls #hash if the hash had at least one entry beforehand. + h[:foo] = :bar + h[x].should == nil + end + + it "does not compare keys with different #hash values via #eql?" do + x = mock('x') + x.should_not_receive(:eql?) + x.stub!(:hash).and_return(0) + + y = mock('y') + y.should_not_receive(:eql?) + y.stub!(:hash).and_return(1) + + { y => 1 }[x].should == nil + end + + it "compares keys with the same #hash value via #eql?" do + x = mock('x') + x.should_receive(:eql?).and_return(true) + x.stub!(:hash).and_return(42) + + y = mock('y') + y.should_not_receive(:eql?) + y.stub!(:hash).and_return(42) + + { y => 1 }[x].should == 1 + end + + it "finds a value via an identical key even when its #eql? isn't reflexive" do + x = mock('x') + x.should_receive(:hash).at_least(1).and_return(42) + x.stub!(:eql?).and_return(false) # Stubbed for clarity and latitude in implementation; not actually sent by MRI. + + { x => :x }[x].should == :x + end + + it "supports keys with private #hash method" do + key = HashSpecs::KeyWithPrivateHash.new + { key => 42 }[key].should == 42 + end + + it "does not dispatch to hash for Boolean, Integer, Float, String, or Symbol" do + code = <<-EOC + load '#{fixture __FILE__, "name.rb"}' + hash = { true => 42, false => 42, 1 => 42, 2.0 => 42, "hello" => 42, :ok => 42 } + [true, false, 1, 2.0, "hello", :ok].each do |value| + raise "incorrect value" unless hash[value] == 42 + end + puts "Ok." + EOC + result = ruby_exe(code, args: "2>&1") + result.should == "Ok.\n" + end + +end diff --git a/spec/ruby/core/hash/element_set_spec.rb b/spec/ruby/core/hash/element_set_spec.rb new file mode 100644 index 0000000000..67c5a04d73 --- /dev/null +++ b/spec/ruby/core/hash/element_set_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/store' + +describe "Hash#[]=" do + it_behaves_like :hash_store, :[]= +end diff --git a/spec/ruby/core/hash/empty_spec.rb b/spec/ruby/core/hash/empty_spec.rb new file mode 100644 index 0000000000..881e1cc34b --- /dev/null +++ b/spec/ruby/core/hash/empty_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash#empty?" do + it "returns true if the hash has no entries" do + {}.should.empty? + { 1 => 1 }.should_not.empty? + end + + it "returns true if the hash has no entries and has a default value" do + Hash.new(5).should.empty? + Hash.new { 5 }.should.empty? + Hash.new { |hsh, k| hsh[k] = k }.should.empty? + end +end diff --git a/spec/ruby/core/hash/eql_spec.rb b/spec/ruby/core/hash/eql_spec.rb new file mode 100644 index 0000000000..9013e12ffd --- /dev/null +++ b/spec/ruby/core/hash/eql_spec.rb @@ -0,0 +1,9 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/eql' + +describe "Hash#eql?" do + it_behaves_like :hash_eql, :eql? + it_behaves_like :hash_eql_additional, :eql? + it_behaves_like :hash_eql_additional_more, :eql? +end diff --git a/spec/ruby/core/hash/equal_value_spec.rb b/spec/ruby/core/hash/equal_value_spec.rb new file mode 100644 index 0000000000..ae13a42679 --- /dev/null +++ b/spec/ruby/core/hash/equal_value_spec.rb @@ -0,0 +1,18 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/eql' + +describe "Hash#==" do + it_behaves_like :hash_eql, :== + it_behaves_like :hash_eql_additional, :== + it_behaves_like :hash_eql_additional_more, :== + + it "compares values with == semantics" do + l_val = mock("left") + r_val = mock("right") + + l_val.should_receive(:==).with(r_val).and_return(true) + + ({ 1 => l_val } == { 1 => r_val }).should be_true + end +end diff --git a/spec/ruby/core/hash/except_spec.rb b/spec/ruby/core/hash/except_spec.rb new file mode 100644 index 0000000000..026e454b13 --- /dev/null +++ b/spec/ruby/core/hash/except_spec.rb @@ -0,0 +1,42 @@ +require_relative '../../spec_helper' + +describe "Hash#except" do + before :each do + @hash = { a: 1, b: 2, c: 3 } + end + + it "returns a new duplicate hash without arguments" do + ret = @hash.except + ret.should_not equal(@hash) + ret.should == @hash + end + + it "returns a hash without the requested subset" do + @hash.except(:c, :a).should == { b: 2 } + end + + it "ignores keys not present in the original hash" do + @hash.except(:a, :chunky_bacon).should == { b: 2, c: 3 } + end + + it "does not retain the default value" do + h = Hash.new(1) + h.except(:a).default.should be_nil + h[:a] = 1 + h.except(:a).default.should be_nil + end + + it "does not retain the default_proc" do + pr = proc { |h, k| h[k] = [] } + h = Hash.new(&pr) + h.except(:a).default_proc.should be_nil + h[:a] = 1 + h.except(:a).default_proc.should be_nil + end + + it "retains compare_by_identity flag" do + h = { a: 9, c: 4 }.compare_by_identity + h2 = h.except(:a) + h2.compare_by_identity?.should == true + end +end diff --git a/spec/ruby/core/hash/fetch_spec.rb b/spec/ruby/core/hash/fetch_spec.rb new file mode 100644 index 0000000000..6e0d207224 --- /dev/null +++ b/spec/ruby/core/hash/fetch_spec.rb @@ -0,0 +1,44 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative '../../shared/hash/key_error' + +describe "Hash#fetch" do + context "when the key is not found" do + it_behaves_like :key_error, -> obj, key { obj.fetch(key) }, Hash.new({ a: 5 }) + it_behaves_like :key_error, -> obj, key { obj.fetch(key) }, {} + it_behaves_like :key_error, -> obj, key { obj.fetch(key) }, Hash.new { 5 } + it_behaves_like :key_error, -> obj, key { obj.fetch(key) }, Hash.new(5) + + it "formats the object with #inspect in the KeyError message" do + -> { + {}.fetch('foo') + }.should raise_error(KeyError, 'key not found: "foo"') + end + end + + it "returns the value for key" do + { a: 1, b: -1 }.fetch(:b).should == -1 + end + + it "returns default if key is not found when passed a default" do + {}.fetch(:a, nil).should == nil + {}.fetch(:a, 'not here!').should == "not here!" + { a: nil }.fetch(:a, 'not here!').should == nil + end + + it "returns value of block if key is not found when passed a block" do + {}.fetch('a') { |k| k + '!' }.should == "a!" + end + + it "gives precedence to the default block over the default argument when passed both" do + -> { + @result = {}.fetch(9, :foo) { |i| i * i } + }.should complain(/block supersedes default value argument/) + @result.should == 81 + end + + it "raises an ArgumentError when not passed one or two arguments" do + -> { {}.fetch() }.should raise_error(ArgumentError) + -> { {}.fetch(1, 2, 3) }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/hash/fetch_values_spec.rb b/spec/ruby/core/hash/fetch_values_spec.rb new file mode 100644 index 0000000000..0cd48565af --- /dev/null +++ b/spec/ruby/core/hash/fetch_values_spec.rb @@ -0,0 +1,35 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative '../../shared/hash/key_error' + +describe "Hash#fetch_values" do + before :each do + @hash = { a: 1, b: 2, c: 3 } + end + + describe "with matched keys" do + it "returns the values for keys" do + @hash.fetch_values(:a).should == [1] + @hash.fetch_values(:a, :c).should == [1, 3] + end + + it "returns the values for keys ordered in the order of the requested keys" do + @hash.fetch_values(:c, :a).should == [3, 1] + end + end + + describe "with unmatched keys" do + it_behaves_like :key_error, -> obj, key { obj.fetch_values(key) }, Hash.new({ a: 5 }) + + it "returns the default value from block" do + @hash.fetch_values(:z) { |key| "`#{key}' is not found" }.should == ["`z' is not found"] + @hash.fetch_values(:a, :z) { |key| "`#{key}' is not found" }.should == [1, "`z' is not found"] + end + end + + describe "without keys" do + it "returns an empty Array" do + @hash.fetch_values.should == [] + end + end +end diff --git a/spec/ruby/core/hash/filter_spec.rb b/spec/ruby/core/hash/filter_spec.rb new file mode 100644 index 0000000000..7dabe44984 --- /dev/null +++ b/spec/ruby/core/hash/filter_spec.rb @@ -0,0 +1,10 @@ +require_relative '../../spec_helper' +require_relative 'shared/select' + +describe "Hash#filter" do + it_behaves_like :hash_select, :filter +end + +describe "Hash#filter!" do + it_behaves_like :hash_select!, :filter! +end diff --git a/spec/ruby/core/hash/fixtures/classes.rb b/spec/ruby/core/hash/fixtures/classes.rb new file mode 100644 index 0000000000..ae907aaff6 --- /dev/null +++ b/spec/ruby/core/hash/fixtures/classes.rb @@ -0,0 +1,75 @@ +module HashSpecs + class MyHash < Hash; end + + class MyInitializerHash < Hash + + def initialize + raise "Constructor called" + end + + end + + class NewHash < Hash + def initialize(*args) + args.each_with_index do |val, index| + self[index] = val + end + end + end + + class SubHashSettingInInitialize < Hash + def initialize(*args, &block) + self[:foo] = :bar + super(*args, &block) + end + end + + class DefaultHash < Hash + def default(key) + 100 + end + end + + class ToHashHash < Hash + def to_hash + { "to_hash" => "was", "called!" => "duh." } + end + end + + class KeyWithPrivateHash + private :hash + end + + class ByIdentityKey + def hash + fail("#hash should not be called on compare_by_identity Hash") + end + end + + class ByValueKey + attr_reader :n + def initialize(n) + @n = n + end + + def hash + n + end + + def eql? other + ByValueKey === other and @n == other.n + end + end + + def self.empty_frozen_hash + @empty ||= {} + @empty.freeze + @empty + end + + def self.frozen_hash + @hash ||= { 1 => 2, 3 => 4 } + @hash.freeze + @hash + end +end diff --git a/spec/ruby/core/hash/fixtures/name.rb b/spec/ruby/core/hash/fixtures/name.rb new file mode 100644 index 0000000000..b203bf6ae4 --- /dev/null +++ b/spec/ruby/core/hash/fixtures/name.rb @@ -0,0 +1,30 @@ +class TrueClass + def hash + raise "TrueClass#hash should not be called" + end +end +class FalseClass + def hash + raise "FalseClass#hash should not be called" + end +end +class Integer + def hash + raise "Integer#hash should not be called" + end +end +class Float + def hash + raise "Float#hash should not be called" + end +end +class String + def hash + raise "String#hash should not be called" + end +end +class Symbol + def hash + raise "Symbol#hash should not be called" + end +end diff --git a/spec/ruby/core/hash/flatten_spec.rb b/spec/ruby/core/hash/flatten_spec.rb new file mode 100644 index 0000000000..825da15bfc --- /dev/null +++ b/spec/ruby/core/hash/flatten_spec.rb @@ -0,0 +1,62 @@ +require_relative '../../spec_helper' + +describe "Hash#flatten" do + + before :each do + @h = {plato: :greek, + witgenstein: [:austrian, :british], + russell: :welsh} + end + + it "returns an Array" do + {}.flatten.should be_an_instance_of(Array) + end + + it "returns an empty Array for an empty Hash" do + {}.flatten.should == [] + end + + it "sets each even index of the Array to a key of the Hash" do + a = @h.flatten + a[0].should == :plato + a[2].should == :witgenstein + a[4].should == :russell + end + + it "sets each odd index of the Array to the value corresponding to the previous element" do + a = @h.flatten + a[1].should == :greek + a[3].should == [:austrian, :british] + a[5].should == :welsh + end + + it "does not recursively flatten Array values when called without arguments" do + a = @h.flatten + a[3].should == [:austrian, :british] + end + + it "does not recursively flatten Hash values when called without arguments" do + @h[:russell] = {born: :wales, influenced_by: :mill } + a = @h.flatten + a[5].should_not == {born: :wales, influenced_by: :mill }.flatten + end + + it "recursively flattens Array values when called with an argument >= 2" do + a = @h.flatten(2) + a[3].should == :austrian + a[4].should == :british + end + + it "recursively flattens Array values to the given depth" do + @h[:russell] = [[:born, :wales], [:influenced_by, :mill]] + a = @h.flatten(2) + a[6].should == [:born, :wales] + a[7].should == [:influenced_by, :mill] + end + + it "raises a TypeError if given a non-Integer argument" do + -> do + @h.flatten(Object.new) + end.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/hash/gt_spec.rb b/spec/ruby/core/hash/gt_spec.rb new file mode 100644 index 0000000000..cd541d4d83 --- /dev/null +++ b/spec/ruby/core/hash/gt_spec.rb @@ -0,0 +1,42 @@ +require_relative '../../spec_helper' +require_relative 'shared/comparison' +require_relative 'shared/greater_than' + +describe "Hash#>" do + it_behaves_like :hash_comparison, :> + it_behaves_like :hash_greater_than, :> + + it "returns false if both hashes are identical" do + h = { a: 1, b: 2 } + (h > h).should be_false + end +end + +describe "Hash#>" do + before :each do + @hash = {a:1, b:2} + @bigger = {a:1, b:2, c:3} + @unrelated = {c:3, d:4} + @similar = {a:2, b:3} + end + + it "returns false when receiver size is smaller than argument" do + (@hash > @bigger).should == false + (@unrelated > @bigger).should == false + end + + it "returns false when receiver size is the same as argument" do + (@hash > @hash).should == false + (@hash > @unrelated).should == false + (@unrelated > @hash).should == false + end + + it "returns true when argument is a subset of receiver" do + (@bigger > @hash).should == true + end + + it "returns false when keys match but values don't" do + (@hash > @similar).should == false + (@similar > @hash).should == false + end +end diff --git a/spec/ruby/core/hash/gte_spec.rb b/spec/ruby/core/hash/gte_spec.rb new file mode 100644 index 0000000000..99b89e7217 --- /dev/null +++ b/spec/ruby/core/hash/gte_spec.rb @@ -0,0 +1,42 @@ +require_relative '../../spec_helper' +require_relative 'shared/comparison' +require_relative 'shared/greater_than' + +describe "Hash#>=" do + it_behaves_like :hash_comparison, :>= + it_behaves_like :hash_greater_than, :>= + + it "returns true if both hashes are identical" do + h = { a: 1, b: 2 } + (h >= h).should be_true + end +end + +describe "Hash#>=" do + before :each do + @hash = {a:1, b:2} + @bigger = {a:1, b:2, c:3} + @unrelated = {c:3, d:4} + @similar = {a:2, b:3} + end + + it "returns false when receiver size is smaller than argument" do + (@hash >= @bigger).should == false + (@unrelated >= @bigger).should == false + end + + it "returns false when argument is not a subset or not equals to receiver" do + (@hash >= @unrelated).should == false + (@unrelated >= @hash).should == false + end + + it "returns true when argument is a subset of receiver or equals to receiver" do + (@bigger >= @hash).should == true + (@hash >= @hash).should == true + end + + it "returns false when keys match but values don't" do + (@hash >= @similar).should == false + (@similar >= @hash).should == false + end +end diff --git a/spec/ruby/core/hash/has_key_spec.rb b/spec/ruby/core/hash/has_key_spec.rb new file mode 100644 index 0000000000..4af53579e5 --- /dev/null +++ b/spec/ruby/core/hash/has_key_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/key' + +describe "Hash#has_key?" do + it_behaves_like :hash_key_p, :has_key? +end diff --git a/spec/ruby/core/hash/has_value_spec.rb b/spec/ruby/core/hash/has_value_spec.rb new file mode 100644 index 0000000000..39f1627fd3 --- /dev/null +++ b/spec/ruby/core/hash/has_value_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/value' + +describe "Hash#has_value?" do + it_behaves_like :hash_value_p, :has_value? +end diff --git a/spec/ruby/core/hash/hash_spec.rb b/spec/ruby/core/hash/hash_spec.rb new file mode 100644 index 0000000000..cd67f7a652 --- /dev/null +++ b/spec/ruby/core/hash/hash_spec.rb @@ -0,0 +1,51 @@ +require_relative '../../spec_helper' + +describe "Hash" do + it "includes Enumerable" do + Hash.include?(Enumerable).should == true + end +end + +describe "Hash#hash" do + it "returns a value which doesn't depend on the hash order" do + { 0=>2, 11=>1 }.hash.should == { 11=>1, 0=>2 }.hash + end + + it "returns a value in which element values do not cancel each other out" do + { a: 2, b: 2 }.hash.should_not == { a: 7, b: 7 }.hash + end + + it "returns a value in which element keys and values do not cancel each other out" do + { :a => :a }.hash.should_not == { :b => :b }.hash + end + + it "generates a hash for recursive hash structures" do + h = {} + h[:a] = h + (h.hash == h[:a].hash).should == true + end + + it "returns the same hash for recursive hashes" do + h = {} ; h[:x] = h + h.hash.should == {x: h}.hash + h.hash.should == {x: {x: h}}.hash + # This is because h.eql?(x: h) + # Remember that if two objects are eql? + # then the need to have the same hash. + # Check the Hash#eql? specs! + end + + it "returns the same hash for recursive hashes through arrays" do + h = {} ; rec = [h] ; h[:x] = rec + h.hash.should == {x: rec}.hash + h.hash.should == {x: [h]}.hash + # Like above, because h.eql?(x: [h]) + end + + it "allows omitting values" do + a = 1 + b = 2 + + {a:, b:}.should == { a: 1, b: 2 } + end +end diff --git a/spec/ruby/core/hash/include_spec.rb b/spec/ruby/core/hash/include_spec.rb new file mode 100644 index 0000000000..f3959dc589 --- /dev/null +++ b/spec/ruby/core/hash/include_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/key' + +describe "Hash#include?" do + it_behaves_like :hash_key_p, :include? +end diff --git a/spec/ruby/core/hash/initialize_spec.rb b/spec/ruby/core/hash/initialize_spec.rb new file mode 100644 index 0000000000..d13496ba3b --- /dev/null +++ b/spec/ruby/core/hash/initialize_spec.rb @@ -0,0 +1,61 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash#initialize" do + it "is private" do + Hash.should have_private_instance_method("initialize") + end + + it "can be used to reset default_proc" do + h = { "foo" => 1, "bar" => 2 } + h.default_proc.should == nil + h.send(:initialize) { |_, k| k * 2 } + h.default_proc.should_not == nil + h["a"].should == "aa" + end + + it "can be used to reset the default value" do + h = {} + h.default = 42 + h.default.should == 42 + h.send(:initialize, 1) + h.default.should == 1 + h.send(:initialize) + h.default.should == nil + end + + it "receives the arguments passed to Hash#new" do + HashSpecs::NewHash.new(:one, :two)[0].should == :one + HashSpecs::NewHash.new(:one, :two)[1].should == :two + end + + it "does not change the storage, only the default value or proc" do + h = HashSpecs::SubHashSettingInInitialize.new + h.to_a.should == [[:foo, :bar]] + + h = HashSpecs::SubHashSettingInInitialize.new(:default) + h.to_a.should == [[:foo, :bar]] + + h = HashSpecs::SubHashSettingInInitialize.new { :default_block } + h.to_a.should == [[:foo, :bar]] + end + + it "returns self" do + h = Hash.new + h.send(:initialize).should equal(h) + end + + it "raises a FrozenError if called on a frozen instance" do + block = -> { HashSpecs.frozen_hash.instance_eval { initialize() }} + block.should raise_error(FrozenError) + + block = -> { HashSpecs.frozen_hash.instance_eval { initialize(nil) } } + block.should raise_error(FrozenError) + + block = -> { HashSpecs.frozen_hash.instance_eval { initialize(5) } } + block.should raise_error(FrozenError) + + block = -> { HashSpecs.frozen_hash.instance_eval { initialize { 5 } } } + block.should raise_error(FrozenError) + end +end diff --git a/spec/ruby/core/hash/inspect_spec.rb b/spec/ruby/core/hash/inspect_spec.rb new file mode 100644 index 0000000000..f41ebb70a6 --- /dev/null +++ b/spec/ruby/core/hash/inspect_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/to_s' + +describe "Hash#inspect" do + it_behaves_like :hash_to_s, :inspect +end diff --git a/spec/ruby/core/hash/invert_spec.rb b/spec/ruby/core/hash/invert_spec.rb new file mode 100644 index 0000000000..c06e15ff7c --- /dev/null +++ b/spec/ruby/core/hash/invert_spec.rb @@ -0,0 +1,48 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash#invert" do + it "returns a new hash where keys are values and vice versa" do + { 1 => 'a', 2 => 'b', 3 => 'c' }.invert.should == + { 'a' => 1, 'b' => 2, 'c' => 3 } + end + + it "handles collisions by overriding with the key coming later in keys()" do + h = { a: 1, b: 1 } + override_key = h.keys.last + h.invert[1].should == override_key + end + + it "compares new keys with eql? semantics" do + h = { a: 1.0, b: 1 } + i = h.invert + i[1.0].should == :a + i[1].should == :b + end + + it "does not return subclass instances for subclasses" do + HashSpecs::MyHash[1 => 2, 3 => 4].invert.class.should == Hash + HashSpecs::MyHash[].invert.class.should == Hash + end + + it "does not retain the default value" do + h = Hash.new(1) + h.invert.default.should be_nil + h[:a] = 1 + h.invert.default.should be_nil + end + + it "does not retain the default_proc" do + pr = proc { |h, k| h[k] = [] } + h = Hash.new(&pr) + h.invert.default_proc.should be_nil + h[:a] = 1 + h.invert.default_proc.should be_nil + end + + it "does not retain compare_by_identity flag" do + h = { a: 9, c: 4 }.compare_by_identity + h2 = h.invert + h2.compare_by_identity?.should == false + end +end diff --git a/spec/ruby/core/hash/keep_if_spec.rb b/spec/ruby/core/hash/keep_if_spec.rb new file mode 100644 index 0000000000..d50d969467 --- /dev/null +++ b/spec/ruby/core/hash/keep_if_spec.rb @@ -0,0 +1,37 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/iteration' +require_relative '../enumerable/shared/enumeratorized' + +describe "Hash#keep_if" do + it "yields two arguments: key and value" do + all_args = [] + { 1 => 2, 3 => 4 }.keep_if { |*args| all_args << args } + all_args.should == [[1, 2], [3, 4]] + end + + it "keeps every entry for which block is true and returns self" do + h = { a: 1, b: 2, c: 3, d: 4 } + h.keep_if { |k,v| v % 2 == 0 }.should equal(h) + h.should == { b: 2, d: 4 } + end + + it "removes all entries if the block is false" do + h = { a: 1, b: 2, c: 3 } + h.keep_if { |k,v| false }.should equal(h) + h.should == {} + end + + it "returns self even if unmodified" do + h = { 1 => 2, 3 => 4 } + h.keep_if { true }.should equal(h) + end + + it "raises a FrozenError if called on a frozen instance" do + -> { HashSpecs.frozen_hash.keep_if { true } }.should raise_error(FrozenError) + -> { HashSpecs.empty_frozen_hash.keep_if { false } }.should raise_error(FrozenError) + end + + it_behaves_like :hash_iteration_no_block, :keep_if + it_behaves_like :enumeratorized_with_origin_size, :keep_if, { 1 => 2, 3 => 4, 5 => 6 } +end diff --git a/spec/ruby/core/hash/key_spec.rb b/spec/ruby/core/hash/key_spec.rb new file mode 100644 index 0000000000..73eecbc98e --- /dev/null +++ b/spec/ruby/core/hash/key_spec.rb @@ -0,0 +1,12 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/key' +require_relative 'shared/index' + +describe "Hash#key?" do + it_behaves_like :hash_key_p, :key? +end + +describe "Hash#key" do + it_behaves_like :hash_index, :key +end diff --git a/spec/ruby/core/hash/keys_spec.rb b/spec/ruby/core/hash/keys_spec.rb new file mode 100644 index 0000000000..9a067085e5 --- /dev/null +++ b/spec/ruby/core/hash/keys_spec.rb @@ -0,0 +1,23 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash#keys" do + + it "returns an array with the keys in the order they were inserted" do + {}.keys.should == [] + {}.keys.should be_kind_of(Array) + Hash.new(5).keys.should == [] + Hash.new { 5 }.keys.should == [] + { 1 => 2, 4 => 8, 2 => 4 }.keys.should == [1, 4, 2] + { 1 => 2, 2 => 4, 4 => 8 }.keys.should be_kind_of(Array) + { nil => nil }.keys.should == [nil] + end + + it "uses the same order as #values" do + h = { 1 => "1", 2 => "2", 3 => "3", 4 => "4" } + + h.size.times do |i| + h[h.keys[i]].should == h.values[i] + end + end +end diff --git a/spec/ruby/core/hash/length_spec.rb b/spec/ruby/core/hash/length_spec.rb new file mode 100644 index 0000000000..d0af0945df --- /dev/null +++ b/spec/ruby/core/hash/length_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/length' + +describe "Hash#length" do + it_behaves_like :hash_length, :length +end diff --git a/spec/ruby/core/hash/lt_spec.rb b/spec/ruby/core/hash/lt_spec.rb new file mode 100644 index 0000000000..2219615880 --- /dev/null +++ b/spec/ruby/core/hash/lt_spec.rb @@ -0,0 +1,42 @@ +require_relative '../../spec_helper' +require_relative 'shared/comparison' +require_relative 'shared/less_than' + +describe "Hash#<" do + it_behaves_like :hash_comparison, :< + it_behaves_like :hash_less_than, :< + + it "returns false if both hashes are identical" do + h = { a: 1, b: 2 } + (h < h).should be_false + end +end + +describe "Hash#<" do + before :each do + @hash = {a:1, b:2} + @bigger = {a:1, b:2, c:3} + @unrelated = {c:3, d:4} + @similar = {a:2, b:3} + end + + it "returns false when receiver size is larger than argument" do + (@bigger < @hash).should == false + (@bigger < @unrelated).should == false + end + + it "returns false when receiver size is the same as argument" do + (@hash < @hash).should == false + (@hash < @unrelated).should == false + (@unrelated < @hash).should == false + end + + it "returns true when receiver is a subset of argument" do + (@hash < @bigger).should == true + end + + it "returns false when keys match but values don't" do + (@hash < @similar).should == false + (@similar < @hash).should == false + end +end diff --git a/spec/ruby/core/hash/lte_spec.rb b/spec/ruby/core/hash/lte_spec.rb new file mode 100644 index 0000000000..a166e5bca4 --- /dev/null +++ b/spec/ruby/core/hash/lte_spec.rb @@ -0,0 +1,42 @@ +require_relative '../../spec_helper' +require_relative 'shared/comparison' +require_relative 'shared/less_than' + +describe "Hash#<=" do + it_behaves_like :hash_comparison, :<= + it_behaves_like :hash_less_than, :<= + + it "returns true if both hashes are identical" do + h = { a: 1, b: 2 } + (h <= h).should be_true + end +end + +describe "Hash#<=" do + before :each do + @hash = {a:1, b:2} + @bigger = {a:1, b:2, c:3} + @unrelated = {c:3, d:4} + @similar = {a:2, b:3} + end + + it "returns false when receiver size is larger than argument" do + (@bigger <= @hash).should == false + (@bigger <= @unrelated).should == false + end + + it "returns false when receiver size is the same as argument" do + (@hash <= @unrelated).should == false + (@unrelated <= @hash).should == false + end + + it "returns true when receiver is a subset of argument or equals to argument" do + (@hash <= @bigger).should == true + (@hash <= @hash).should == true + end + + it "returns false when keys match but values don't" do + (@hash <= @similar).should == false + (@similar <= @hash).should == false + end +end diff --git a/spec/ruby/core/hash/member_spec.rb b/spec/ruby/core/hash/member_spec.rb new file mode 100644 index 0000000000..37c0414559 --- /dev/null +++ b/spec/ruby/core/hash/member_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/key' + +describe "Hash#member?" do + it_behaves_like :hash_key_p, :member? +end diff --git a/spec/ruby/core/hash/merge_spec.rb b/spec/ruby/core/hash/merge_spec.rb new file mode 100644 index 0000000000..6710d121ef --- /dev/null +++ b/spec/ruby/core/hash/merge_spec.rb @@ -0,0 +1,123 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/iteration' +require_relative 'shared/update' + +describe "Hash#merge" do + it "returns a new hash by combining self with the contents of other" do + h = { 1 => :a, 2 => :b, 3 => :c }.merge(a: 1, c: 2) + h.should == { c: 2, 1 => :a, 2 => :b, a: 1, 3 => :c } + + hash = { a: 1, b: 2 } + {}.merge(hash).should == hash + hash.merge({}).should == hash + + h = { 1 => :a, 2 => :b, 3 => :c }.merge(1 => :b) + h.should == { 1 => :b, 2 => :b, 3 => :c } + + h = { 1 => :a, 2 => :b }.merge(1 => :b, 3 => :c) + h.should == { 1 => :b, 2 => :b, 3 => :c } + end + + it "sets any duplicate key to the value of block if passed a block" do + h1 = { a: 2, b: 1, d: 5 } + h2 = { a: -2, b: 4, c: -3 } + r = h1.merge(h2) { |k,x,y| nil } + r.should == { a: nil, b: nil, c: -3, d: 5 } + + r = h1.merge(h2) { |k,x,y| "#{k}:#{x+2*y}" } + r.should == { a: "a:-2", b: "b:9", c: -3, d: 5 } + + -> { + h1.merge(h2) { |k, x, y| raise(IndexError) } + }.should raise_error(IndexError) + + r = h1.merge(h1) { |k,x,y| :x } + r.should == { a: :x, b: :x, d: :x } + end + + it "tries to convert the passed argument to a hash using #to_hash" do + obj = mock('{1=>2}') + obj.should_receive(:to_hash).and_return({ 1 => 2 }) + { 3 => 4 }.merge(obj).should == { 1 => 2, 3 => 4 } + end + + it "does not call to_hash on hash subclasses" do + { 3 => 4 }.merge(HashSpecs::ToHashHash[1 => 2]).should == { 1 => 2, 3 => 4 } + end + + it "returns subclass instance for subclasses" do + HashSpecs::MyHash[1 => 2, 3 => 4].merge({ 1 => 2 }).should be_an_instance_of(HashSpecs::MyHash) + HashSpecs::MyHash[].merge({ 1 => 2 }).should be_an_instance_of(HashSpecs::MyHash) + + { 1 => 2, 3 => 4 }.merge(HashSpecs::MyHash[1 => 2]).class.should == Hash + {}.merge(HashSpecs::MyHash[1 => 2]).class.should == Hash + end + + it "processes entries with same order as each()" do + h = { 1 => 2, 3 => 4, 5 => 6, "x" => nil, nil => 5, [] => [] } + merge_pairs = [] + each_pairs = [] + h.each_pair { |k, v| each_pairs << [k, v] } + h.merge(h) { |k, v1, v2| merge_pairs << [k, v1] } + merge_pairs.should == each_pairs + end + + it "preserves the order of merged elements" do + h1 = { 1 => 2, 3 => 4, 5 => 6 } + h2 = { 1 => 7 } + merge_pairs = [] + h1.merge(h2).each_pair { |k, v| merge_pairs << [k, v] } + merge_pairs.should == [[1,7], [3, 4], [5, 6]] + end + + it "preserves the order of merged elements for large hashes" do + h1 = {} + h2 = {} + merge_pairs = [] + expected_pairs = [] + (1..100).each { |x| h1[x] = x; h2[101 - x] = x; expected_pairs << [x, 101 - x] } + h1.merge(h2).each_pair { |k, v| merge_pairs << [k, v] } + merge_pairs.should == expected_pairs + end + + it "accepts multiple hashes" do + result = { a: 1 }.merge({ b: 2 }, { c: 3 }, { d: 4 }) + result.should == { a: 1, b: 2, c: 3, d: 4 } + end + + it "accepts zero arguments and returns a copy of self" do + hash = { a: 1 } + merged = hash.merge + + merged.should eql(hash) + merged.should_not equal(hash) + end + + it "retains the default value" do + h = Hash.new(1) + h.merge(b: 1, d: 2).default.should == 1 + end + + it "retains the default_proc" do + pr = proc { |h, k| h[k] = [] } + h = Hash.new(&pr) + h.merge(b: 1, d: 2).default_proc.should == pr + end + + it "retains compare_by_identity flag" do + h = { a: 9, c: 4 }.compare_by_identity + h2 = h.merge(b: 1, d: 2) + h2.compare_by_identity?.should == true + end + + it "ignores compare_by_identity flag of an argument" do + h = { a: 9, c: 4 }.compare_by_identity + h2 = { b: 1, d: 2 }.merge(h) + h2.compare_by_identity?.should == false + end +end + +describe "Hash#merge!" do + it_behaves_like :hash_update, :merge! +end diff --git a/spec/ruby/core/hash/new_spec.rb b/spec/ruby/core/hash/new_spec.rb new file mode 100644 index 0000000000..5ae3e1f98d --- /dev/null +++ b/spec/ruby/core/hash/new_spec.rb @@ -0,0 +1,67 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash.new" do + it "creates an empty Hash if passed no arguments" do + Hash.new.should == {} + Hash.new.size.should == 0 + end + + it "creates a new Hash with default object if passed a default argument" do + Hash.new(5).default.should == 5 + Hash.new({}).default.should == {} + end + + it "does not create a copy of the default argument" do + str = "foo" + Hash.new(str).default.should equal(str) + end + + it "creates a Hash with a default_proc if passed a block" do + Hash.new.default_proc.should == nil + + h = Hash.new { |x| "Answer to #{x}" } + h.default_proc.call(5).should == "Answer to 5" + h.default_proc.call("x").should == "Answer to x" + end + + it "raises an ArgumentError if more than one argument is passed" do + -> { Hash.new(5,6) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if passed both default argument and default block" do + -> { Hash.new(5) { 0 } }.should raise_error(ArgumentError) + -> { Hash.new(nil) { 0 } }.should raise_error(ArgumentError) + end + + ruby_version_is "3.3"..."3.4" do + it "emits a deprecation warning if keyword arguments are passed" do + -> { Hash.new(unknown: true) }.should complain( + Regexp.new(Regexp.escape("Calling Hash.new with keyword arguments is deprecated and will be removed in Ruby 3.4; use Hash.new({ key: value }) instead")) + ) + + -> { Hash.new(1, unknown: true) }.should raise_error(ArgumentError) + -> { Hash.new(unknown: true) { 0 } }.should raise_error(ArgumentError) + + Hash.new({ unknown: true }).default.should == { unknown: true } + end + end + + ruby_version_is "3.4" do + it "accepts a capacity: argument" do + Hash.new(5, capacity: 42).default.should == 5 + Hash.new(capacity: 42).default.should == nil + (Hash.new(capacity: 42) { 1 }).default_proc.should_not == nil + end + + it "ignores negative capacity" do + -> { Hash.new(capacity: -42) }.should_not raise_error + end + + it "raises an error if unknown keyword arguments are passed" do + -> { Hash.new(unknown: true) }.should raise_error(ArgumentError) + -> { Hash.new(1, unknown: true) }.should raise_error(ArgumentError) + -> { Hash.new(unknown: true) { 0 } }.should raise_error(ArgumentError) + end + end +end diff --git a/spec/ruby/core/hash/rassoc_spec.rb b/spec/ruby/core/hash/rassoc_spec.rb new file mode 100644 index 0000000000..f3b2a6de20 --- /dev/null +++ b/spec/ruby/core/hash/rassoc_spec.rb @@ -0,0 +1,42 @@ +require_relative '../../spec_helper' + +describe "Hash#rassoc" do + before :each do + @h = {apple: :green, orange: :orange, grape: :green, banana: :yellow} + end + + it "returns an Array if the argument is a value of the Hash" do + @h.rassoc(:green).should be_an_instance_of(Array) + end + + it "returns a 2-element Array if the argument is a value of the Hash" do + @h.rassoc(:orange).size.should == 2 + end + + it "sets the first element of the Array to the key of the located value" do + @h.rassoc(:yellow).first.should == :banana + end + + it "sets the last element of the Array to the located value" do + @h.rassoc(:yellow).last.should == :yellow + end + + it "only returns the first matching key-value pair" do + @h.rassoc(:green).should == [:apple, :green] + end + + it "uses #== to compare the argument to the values" do + @h[:key] = 1.0 + 1.should == 1.0 + @h.rassoc(1).should eql [:key, 1.0] + end + + it "returns nil if the argument is not a value of the Hash" do + @h.rassoc(:banana).should be_nil + end + + it "returns nil if the argument is not a value of the Hash even when there is a default" do + Hash.new(42).merge!( foo: :bar ).rassoc(42).should be_nil + Hash.new{|h, k| h[k] = 42}.merge!( foo: :bar ).rassoc(42).should be_nil + end +end diff --git a/spec/ruby/core/hash/rehash_spec.rb b/spec/ruby/core/hash/rehash_spec.rb new file mode 100644 index 0000000000..db3e91b166 --- /dev/null +++ b/spec/ruby/core/hash/rehash_spec.rb @@ -0,0 +1,114 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash#rehash" do + it "reorganizes the Hash by recomputing all key hash codes" do + k1 = Object.new + k2 = Object.new + def k1.hash; 0; end + def k2.hash; 1; end + + h = {} + h[k1] = :v1 + h[k2] = :v2 + + def k1.hash; 1; end + + # The key should no longer be found as the #hash changed. + # Hash values 0 and 1 should not conflict, even with 1-bit stored hash. + h.key?(k1).should == false + + h.keys.include?(k1).should == true + + h.rehash.should equal(h) + h.key?(k1).should == true + h[k1].should == :v1 + end + + it "calls #hash for each key" do + k1 = mock('k1') + k2 = mock('k2') + v1 = mock('v1') + v2 = mock('v2') + + v1.should_not_receive(:hash) + v2.should_not_receive(:hash) + + h = { k1 => v1, k2 => v2 } + + k1.should_receive(:hash).twice.and_return(0) + k2.should_receive(:hash).twice.and_return(0) + + h.rehash + h[k1].should == v1 + h[k2].should == v2 + end + + it "removes duplicate keys" do + a = [1,2] + b = [1] + + h = {} + h[a] = true + h[b] = true + b << 2 + h.size.should == 2 + h.keys.should == [a, b] + h.rehash + h.size.should == 1 + h.keys.should == [a] + end + + it "removes duplicate keys for large hashes" do + a = [1,2] + b = [1] + + h = {} + h[a] = true + h[b] = true + 100.times { |n| h[n] = true } + b << 2 + h.size.should == 102 + h.keys.should.include? a + h.keys.should.include? b + h.rehash + h.size.should == 101 + h.keys.should.include? a + h.keys.should_not.include? [1] + end + + it "iterates keys in insertion order" do + key = Class.new do + attr_reader :name + + def initialize(name) + @name = name + end + + def hash + 123 + end + end + + a, b, c, d = key.new('a'), key.new('b'), key.new('c'), key.new('d') + h = { a => 1, b => 2, c => 3, d => 4 } + h.size.should == 4 + + key.class_exec do + def eql?(other) + true + end + end + + h.rehash + h.size.should == 1 + k, v = h.first + k.name.should == 'a' + v.should == 4 + end + + it "raises a FrozenError if called on a frozen instance" do + -> { HashSpecs.frozen_hash.rehash }.should raise_error(FrozenError) + -> { HashSpecs.empty_frozen_hash.rehash }.should raise_error(FrozenError) + end +end diff --git a/spec/ruby/core/hash/reject_spec.rb b/spec/ruby/core/hash/reject_spec.rb new file mode 100644 index 0000000000..8381fc7fc1 --- /dev/null +++ b/spec/ruby/core/hash/reject_spec.rb @@ -0,0 +1,116 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/iteration' +require_relative '../enumerable/shared/enumeratorized' + +describe "Hash#reject" do + it "returns a new hash removing keys for which the block yields true" do + h = { 1=>false, 2=>true, 3=>false, 4=>true } + h.reject { |k,v| v }.keys.sort.should == [1,3] + end + + it "is equivalent to hsh.dup.delete_if" do + h = { a: 'a', b: 'b', c: 'd' } + h.reject { |k,v| k == 'd' }.should == (h.dup.delete_if { |k, v| k == 'd' }) + + all_args_reject = [] + all_args_delete_if = [] + h = { 1 => 2, 3 => 4 } + h.reject { |*args| all_args_reject << args } + h.delete_if { |*args| all_args_delete_if << args } + all_args_reject.should == all_args_delete_if + + h = { 1 => 2 } + # dup doesn't copy singleton methods + def h.to_a() end + h.reject { false }.to_a.should == [[1, 2]] + end + + context "with extra state" do + it "returns Hash instance for subclasses" do + HashSpecs::MyHash[1 => 2, 3 => 4].reject { false }.should be_kind_of(Hash) + HashSpecs::MyHash[1 => 2, 3 => 4].reject { true }.should be_kind_of(Hash) + end + end + + it "processes entries with the same order as reject!" do + h = { a: 1, b: 2, c: 3, d: 4 } + + reject_pairs = [] + reject_bang_pairs = [] + h.dup.reject { |*pair| reject_pairs << pair } + h.reject! { |*pair| reject_bang_pairs << pair } + + reject_pairs.should == reject_bang_pairs + end + + it "does not retain the default value" do + h = Hash.new(1) + h.reject { false }.default.should be_nil + h[:a] = 1 + h.reject { false }.default.should be_nil + end + + it "does not retain the default_proc" do + pr = proc { |h, k| h[k] = [] } + h = Hash.new(&pr) + h.reject { false }.default_proc.should be_nil + h[:a] = 1 + h.reject { false }.default_proc.should be_nil + end + + it "retains compare_by_identity flag" do + h = { a: 9, c: 4 }.compare_by_identity + h2 = h.reject { |k, _| k == :a } + h2.compare_by_identity?.should == true + end + + it_behaves_like :hash_iteration_no_block, :reject + it_behaves_like :enumeratorized_with_origin_size, :reject, { 1 => 2, 3 => 4, 5 => 6 } +end + +describe "Hash#reject!" do + it "removes keys from self for which the block yields true" do + hsh = {} + (1 .. 10).each { |k| hsh[k] = (k % 2 == 0) } + hsh.reject! { |k,v| v } + hsh.keys.sort.should == [1,3,5,7,9] + end + + it "removes all entries if the block is true" do + h = { a: 1, b: 2, c: 3 } + h.reject! { |k,v| true }.should equal(h) + h.should == {} + end + + it "is equivalent to delete_if if changes are made" do + hsh = { a: 1 } + hsh.reject! { |k,v| v < 2 }.should == hsh.dup.delete_if { |k, v| v < 2 } + end + + it "returns nil if no changes were made" do + { a: 1 }.reject! { |k,v| v > 1 }.should == nil + end + + it "processes entries with the same order as delete_if" do + h = { a: 1, b: 2, c: 3, d: 4 } + + reject_bang_pairs = [] + delete_if_pairs = [] + h.dup.reject! { |*pair| reject_bang_pairs << pair } + h.dup.delete_if { |*pair| delete_if_pairs << pair } + + reject_bang_pairs.should == delete_if_pairs + end + + it "raises a FrozenError if called on a frozen instance that is modified" do + -> { HashSpecs.empty_frozen_hash.reject! { true } }.should raise_error(FrozenError) + end + + it "raises a FrozenError if called on a frozen instance that would not be modified" do + -> { HashSpecs.frozen_hash.reject! { false } }.should raise_error(FrozenError) + end + + it_behaves_like :hash_iteration_no_block, :reject! + it_behaves_like :enumeratorized_with_origin_size, :reject!, { 1 => 2, 3 => 4, 5 => 6 } +end diff --git a/spec/ruby/core/hash/replace_spec.rb b/spec/ruby/core/hash/replace_spec.rb new file mode 100644 index 0000000000..db30145e1a --- /dev/null +++ b/spec/ruby/core/hash/replace_spec.rb @@ -0,0 +1,79 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash#replace" do + it "replaces the contents of self with other" do + h = { a: 1, b: 2 } + h.replace(c: -1, d: -2).should equal(h) + h.should == { c: -1, d: -2 } + end + + it "tries to convert the passed argument to a hash using #to_hash" do + obj = mock('{1=>2,3=>4}') + obj.should_receive(:to_hash).and_return({ 1 => 2, 3 => 4 }) + + h = {} + h.replace(obj) + h.should == { 1 => 2, 3 => 4 } + end + + it "calls to_hash on hash subclasses" do + h = {} + h.replace(HashSpecs::ToHashHash[1 => 2]) + h.should == { 1 => 2 } + end + + it "does not retain the default value" do + hash = Hash.new(1) + hash.replace(b: 2).default.should be_nil + end + + it "transfers the default value of an argument" do + hash = Hash.new(1) + { a: 1 }.replace(hash).default.should == 1 + end + + it "does not retain the default_proc" do + pr = proc { |h, k| h[k] = [] } + hash = Hash.new(&pr) + hash.replace(b: 2).default_proc.should be_nil + end + + it "transfers the default_proc of an argument" do + pr = proc { |h, k| h[k] = [] } + hash = Hash.new(&pr) + { a: 1 }.replace(hash).default_proc.should == pr + end + + it "does not call the default_proc of an argument" do + hash_a = Hash.new { |h, k| k * 5 } + hash_b = Hash.new(-> { raise "Should not invoke lambda" }) + hash_a.replace(hash_b) + hash_a.default.should == hash_b.default + end + + it "transfers compare_by_identity flag of an argument" do + h = { a: 1, c: 3 } + h2 = { b: 2, d: 4 }.compare_by_identity + h.replace(h2) + h.compare_by_identity?.should == true + end + + it "does not retain compare_by_identity flag" do + h = { a: 1, c: 3 }.compare_by_identity + h.replace(b: 2, d: 4) + h.compare_by_identity?.should == false + end + + it "raises a FrozenError if called on a frozen instance that would not be modified" do + -> do + HashSpecs.frozen_hash.replace(HashSpecs.frozen_hash) + end.should raise_error(FrozenError) + end + + it "raises a FrozenError if called on a frozen instance that is modified" do + -> do + HashSpecs.frozen_hash.replace(HashSpecs.empty_frozen_hash) + end.should raise_error(FrozenError) + end +end diff --git a/spec/ruby/core/hash/ruby2_keywords_hash_spec.rb b/spec/ruby/core/hash/ruby2_keywords_hash_spec.rb new file mode 100644 index 0000000000..7dbb9c0a98 --- /dev/null +++ b/spec/ruby/core/hash/ruby2_keywords_hash_spec.rb @@ -0,0 +1,83 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash.ruby2_keywords_hash?" do + it "returns false if the Hash is not a keywords Hash" do + Hash.ruby2_keywords_hash?({}).should == false + end + + it "returns true if the Hash is a keywords Hash marked by Module#ruby2_keywords" do + obj = Class.new { + ruby2_keywords def m(*args) + args.last + end + }.new + Hash.ruby2_keywords_hash?(obj.m(a: 1)).should == true + end + + it "raises TypeError for non-Hash" do + -> { Hash.ruby2_keywords_hash?(nil) }.should raise_error(TypeError) + end +end + +describe "Hash.ruby2_keywords_hash" do + it "returns a copy of a Hash and marks the copy as a keywords Hash" do + h = {a: 1}.freeze + kw = Hash.ruby2_keywords_hash(h) + Hash.ruby2_keywords_hash?(h).should == false + Hash.ruby2_keywords_hash?(kw).should == true + kw.should == h + end + + it "returns an instance of the subclass if called on an instance of a subclass of Hash" do + h = HashSpecs::MyHash.new + h[:a] = 1 + kw = Hash.ruby2_keywords_hash(h) + kw.class.should == HashSpecs::MyHash + Hash.ruby2_keywords_hash?(h).should == false + Hash.ruby2_keywords_hash?(kw).should == true + kw.should == h + end + + it "copies instance variables" do + h = {a: 1} + h.instance_variable_set(:@foo, 42) + kw = Hash.ruby2_keywords_hash(h) + kw.instance_variable_get(:@foo).should == 42 + end + + it "copies the hash internals" do + h = {a: 1} + kw = Hash.ruby2_keywords_hash(h) + h[:a] = 2 + kw[:a].should == 1 + end + + it "raises TypeError for non-Hash" do + -> { Hash.ruby2_keywords_hash(nil) }.should raise_error(TypeError) + end + + it "retains the default value" do + hash = Hash.new(1) + Hash.ruby2_keywords_hash(hash).default.should == 1 + hash[:a] = 1 + Hash.ruby2_keywords_hash(hash).default.should == 1 + end + + it "retains the default_proc" do + pr = proc { |h, k| h[k] = [] } + hash = Hash.new(&pr) + Hash.ruby2_keywords_hash(hash).default_proc.should == pr + hash[:a] = 1 + Hash.ruby2_keywords_hash(hash).default_proc.should == pr + end + + ruby_version_is '3.3' do + it "retains compare_by_identity_flag" do + hash = {}.compare_by_identity + Hash.ruby2_keywords_hash(hash).compare_by_identity?.should == true + hash[:a] = 1 + Hash.ruby2_keywords_hash(hash).compare_by_identity?.should == true + end + end +end diff --git a/spec/ruby/core/hash/select_spec.rb b/spec/ruby/core/hash/select_spec.rb new file mode 100644 index 0000000000..38b0180b0e --- /dev/null +++ b/spec/ruby/core/hash/select_spec.rb @@ -0,0 +1,10 @@ +require_relative '../../spec_helper' +require_relative 'shared/select' + +describe "Hash#select" do + it_behaves_like :hash_select, :select +end + +describe "Hash#select!" do + it_behaves_like :hash_select!, :select! +end diff --git a/spec/ruby/core/hash/shared/comparison.rb b/spec/ruby/core/hash/shared/comparison.rb new file mode 100644 index 0000000000..07564e4cec --- /dev/null +++ b/spec/ruby/core/hash/shared/comparison.rb @@ -0,0 +1,15 @@ +describe :hash_comparison, shared: true do + it "raises a TypeError if the right operand is not a hash" do + -> { { a: 1 }.send(@method, 1) }.should raise_error(TypeError) + -> { { a: 1 }.send(@method, nil) }.should raise_error(TypeError) + -> { { a: 1 }.send(@method, []) }.should raise_error(TypeError) + end + + it "returns false if both hashes have the same keys but different values" do + h1 = { a: 1 } + h2 = { a: 2 } + + h1.send(@method, h2).should be_false + h2.send(@method, h1).should be_false + end +end diff --git a/spec/ruby/core/hash/shared/each.rb b/spec/ruby/core/hash/shared/each.rb new file mode 100644 index 0000000000..f9839ff58f --- /dev/null +++ b/spec/ruby/core/hash/shared/each.rb @@ -0,0 +1,105 @@ +describe :hash_each, shared: true do + + # This is inconsistent with below, MRI checks the block arity in rb_hash_each_pair() + it "yields a [[key, value]] Array for each pair to a block expecting |*args|" do + all_args = [] + { 1 => 2, 3 => 4 }.send(@method) { |*args| all_args << args } + all_args.sort.should == [[[1, 2]], [[3, 4]]] + end + + it "yields the key and value of each pair to a block expecting |key, value|" do + r = {} + h = { a: 1, b: 2, c: 3, d: 5 } + h.send(@method) { |k,v| r[k.to_s] = v.to_s }.should equal(h) + r.should == { "a" => "1", "b" => "2", "c" => "3", "d" => "5" } + end + + it "yields the key only to a block expecting |key,|" do + ary = [] + h = { "a" => 1, "b" => 2, "c" => 3 } + h.send(@method) { |k,| ary << k } + ary.sort.should == ["a", "b", "c"] + end + + it "always yields an Array of 2 elements, even when given a callable of arity 2" do + obj = Object.new + def obj.foo(key, value) + end + + -> { + { "a" => 1 }.send(@method, &obj.method(:foo)) + }.should raise_error(ArgumentError) + + -> { + { "a" => 1 }.send(@method, &-> key, value { }) + }.should raise_error(ArgumentError) + end + + it "yields an Array of 2 elements when given a callable of arity 1" do + obj = Object.new + def obj.foo(key_value) + ScratchPad << key_value + end + + ScratchPad.record([]) + { "a" => 1 }.send(@method, &obj.method(:foo)) + ScratchPad.recorded.should == [["a", 1]] + end + + it "raises an error for a Hash when an arity enforcing callable of arity >2 is passed in" do + obj = Object.new + def obj.foo(key, value, extra) + end + + -> { + { "a" => 1 }.send(@method, &obj.method(:foo)) + }.should raise_error(ArgumentError) + end + + it "uses the same order as keys() and values()" do + h = { a: 1, b: 2, c: 3, d: 5 } + keys = [] + values = [] + + h.send(@method) do |k, v| + keys << k + values << v + end + + keys.should == h.keys + values.should == h.values + end + + # Confirming the argument-splatting works from child class for both k, v and [k, v] + it "properly expands (or not) child class's 'each'-yielded args" do + cls1 = Class.new(Hash) do + attr_accessor :k_v + def each + super do |k, v| + @k_v = [k, v] + yield k, v + end + end + end + + cls2 = Class.new(Hash) do + attr_accessor :k_v + def each + super do |k, v| + @k_v = [k, v] + yield([k, v]) + end + end + end + + obj1 = cls1.new + obj1['a'] = 'b' + obj1.map {|k, v| [k, v]}.should == [['a', 'b']] + obj1.k_v.should == ['a', 'b'] + + obj2 = cls2.new + obj2['a'] = 'b' + obj2.map {|k, v| [k, v]}.should == [['a', 'b']] + obj2.k_v.should == ['a', 'b'] + end +end diff --git a/spec/ruby/core/hash/shared/eql.rb b/spec/ruby/core/hash/shared/eql.rb new file mode 100644 index 0000000000..68db49f76d --- /dev/null +++ b/spec/ruby/core/hash/shared/eql.rb @@ -0,0 +1,204 @@ +describe :hash_eql, shared: true do + it "does not compare values when keys don't match" do + value = mock('x') + value.should_not_receive(:==) + value.should_not_receive(:eql?) + { 1 => value }.send(@method, { 2 => value }).should be_false + end + + it "returns false when the numbers of keys differ without comparing any elements" do + obj = mock('x') + h = { obj => obj } + + obj.should_not_receive(:==) + obj.should_not_receive(:eql?) + + {}.send(@method, h).should be_false + h.send(@method, {}).should be_false + end + + it "first compares keys via hash" do + x = mock('x') + x.should_receive(:hash).any_number_of_times.and_return(0) + y = mock('y') + y.should_receive(:hash).any_number_of_times.and_return(0) + + { x => 1 }.send(@method, { y => 1 }).should be_false + end + + it "does not compare keys with different hash codes via eql?" do + x = mock('x') + y = mock('y') + x.should_not_receive(:eql?) + y.should_not_receive(:eql?) + + x.should_receive(:hash).any_number_of_times.and_return(0) + y.should_receive(:hash).any_number_of_times.and_return(1) + + { x => 1 }.send(@method, { y => 1 }).should be_false + end + + it "computes equality for recursive hashes" do + h = {} + h[:a] = h + h.send(@method, h[:a]).should be_true + (h == h[:a]).should be_true + end + + it "doesn't call to_hash on objects" do + mock_hash = mock("fake hash") + def mock_hash.to_hash() {} end + {}.send(@method, mock_hash).should be_false + end + + it "computes equality for complex recursive hashes" do + a, b = {}, {} + a.merge! self: a, other: b + b.merge! self: b, other: a + a.send(@method, b).should be_true # they both have the same structure! + + c = {} + c.merge! other: c, self: c + c.send(@method, a).should be_true # subtle, but they both have the same structure! + a[:delta] = c[:delta] = a + c.send(@method, a).should be_false # not quite the same structure, as a[:other][:delta] = nil + c[:delta] = 42 + c.send(@method, a).should be_false + a[:delta] = 42 + c.send(@method, a).should be_false + b[:delta] = 42 + c.send(@method, a).should be_true + end + + it "computes equality for recursive hashes & arrays" do + x, y, z = [], [], [] + a, b, c = {foo: x, bar: 42}, {foo: y, bar: 42}, {foo: z, bar: 42} + x << a + y << c + z << b + b.send(@method, c).should be_true # they clearly have the same structure! + y.send(@method, z).should be_true + a.send(@method, b).should be_true # subtle, but they both have the same structure! + x.send(@method, y).should be_true + y << x + y.send(@method, z).should be_false + z << x + y.send(@method, z).should be_true + + a[:foo], a[:bar] = a[:bar], a[:foo] + a.send(@method, b).should be_false + b[:bar] = b[:foo] + b.send(@method, c).should be_false + end +end + +describe :hash_eql_additional, shared: true do + it "compares values when keys match" do + x = mock('x') + y = mock('y') + def x.==(o) false end + def y.==(o) false end + def x.eql?(o) false end + def y.eql?(o) false end + { 1 => x }.send(@method, { 1 => y }).should be_false + + x = mock('x') + y = mock('y') + def x.==(o) true end + def y.==(o) true end + def x.eql?(o) true end + def y.eql?(o) true end + { 1 => x }.send(@method, { 1 => y }).should be_true + end + + it "compares keys with eql? semantics" do + { 1.0 => "x" }.send(@method, { 1.0 => "x" }).should be_true + { 1.0 => "x" }.send(@method, { 1.0 => "x" }).should be_true + { 1 => "x" }.send(@method, { 1.0 => "x" }).should be_false + { 1.0 => "x" }.send(@method, { 1 => "x" }).should be_false + end + + it "returns true if and only if other Hash has the same number of keys and each key-value pair matches" do + a = { a: 5 } + b = {} + a.send(@method, b).should be_false + + b[:a] = 5 + a.send(@method, b).should be_true + + not_supported_on :opal do + c = { "a" => 5 } + a.send(@method, c).should be_false + end + + c = { "A" => 5 } + a.send(@method, c).should be_false + + c = { a: 6 } + a.send(@method, c).should be_false + end + + it "does not call to_hash on hash subclasses" do + { 5 => 6 }.send(@method, HashSpecs::ToHashHash[5 => 6]).should be_true + end + + it "ignores hash class differences" do + h = { 1 => 2, 3 => 4 } + HashSpecs::MyHash[h].send(@method, h).should be_true + HashSpecs::MyHash[h].send(@method, HashSpecs::MyHash[h]).should be_true + h.send(@method, HashSpecs::MyHash[h]).should be_true + end + + # Why isn't this true of eql? too ? + it "compares keys with matching hash codes via eql?" do + a = Array.new(2) do + obj = mock('0') + obj.should_receive(:hash).at_least(1).and_return(0) + + def obj.eql?(o) + return true if self.equal?(o) + false + end + + obj + end + + { a[0] => 1 }.send(@method, { a[1] => 1 }).should be_false + + a = Array.new(2) do + obj = mock('0') + obj.should_receive(:hash).at_least(1).and_return(0) + + def obj.eql?(o) + true + end + + obj + end + + { a[0] => 1 }.send(@method, { a[1] => 1 }).should be_true + end + + it "compares the values in self to values in other hash" do + l_val = mock("left") + r_val = mock("right") + + l_val.should_receive(:eql?).with(r_val).and_return(true) + + { 1 => l_val }.eql?({ 1 => r_val }).should be_true + end +end + +describe :hash_eql_additional_more, shared: true do + it "returns true if other Hash has the same number of keys and each key-value pair matches, even though the default-value are not same" do + Hash.new(5).send(@method, Hash.new(1)).should be_true + Hash.new {|h, k| 1}.send(@method, Hash.new {}).should be_true + Hash.new {|h, k| 1}.send(@method, Hash.new(2)).should be_true + + d = Hash.new {|h, k| 1} + e = Hash.new {} + d[1] = 2 + e[1] = 2 + d.send(@method, e).should be_true + end +end diff --git a/spec/ruby/core/hash/shared/greater_than.rb b/spec/ruby/core/hash/shared/greater_than.rb new file mode 100644 index 0000000000..1f8b9fcfb7 --- /dev/null +++ b/spec/ruby/core/hash/shared/greater_than.rb @@ -0,0 +1,23 @@ +describe :hash_greater_than, shared: true do + before do + @h1 = { a: 1, b: 2, c: 3 } + @h2 = { a: 1, b: 2 } + end + + it "returns true if the other hash is a subset of self" do + @h1.send(@method, @h2).should be_true + end + + it "returns false if the other hash is not a subset of self" do + @h2.send(@method, @h1).should be_false + end + + it "converts the right operand to a hash before comparing" do + o = Object.new + def o.to_hash + { a: 1, b: 2 } + end + + @h1.send(@method, o).should be_true + end +end diff --git a/spec/ruby/core/hash/shared/index.rb b/spec/ruby/core/hash/shared/index.rb new file mode 100644 index 0000000000..7f6a186464 --- /dev/null +++ b/spec/ruby/core/hash/shared/index.rb @@ -0,0 +1,37 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' + +describe :hash_index, shared: true do + it "returns the corresponding key for value" do + suppress_warning do # for Hash#index + { 2 => 'a', 1 => 'b' }.send(@method, 'b').should == 1 + end + end + + it "returns nil if the value is not found" do + suppress_warning do # for Hash#index + { a: -1, b: 3.14, c: 2.718 }.send(@method, 1).should be_nil + end + end + + it "doesn't return default value if the value is not found" do + suppress_warning do # for Hash#index + Hash.new(5).send(@method, 5).should be_nil + end + end + + it "compares values using ==" do + suppress_warning do # for Hash#index + { 1 => 0 }.send(@method, 0.0).should == 1 + { 1 => 0.0 }.send(@method, 0).should == 1 + end + + needle = mock('needle') + inhash = mock('inhash') + inhash.should_receive(:==).with(needle).and_return(true) + + suppress_warning do # for Hash#index + { 1 => inhash }.send(@method, needle).should == 1 + end + end +end diff --git a/spec/ruby/core/hash/shared/iteration.rb b/spec/ruby/core/hash/shared/iteration.rb new file mode 100644 index 0000000000..d27c2443f8 --- /dev/null +++ b/spec/ruby/core/hash/shared/iteration.rb @@ -0,0 +1,19 @@ +describe :hash_iteration_no_block, shared: true do + before :each do + @hsh = { 1 => 2, 3 => 4, 5 => 6 } + @empty = {} + end + + it "returns an Enumerator if called on a non-empty hash without a block" do + @hsh.send(@method).should be_an_instance_of(Enumerator) + end + + it "returns an Enumerator if called on an empty hash without a block" do + @empty.send(@method).should be_an_instance_of(Enumerator) + end + + it "returns an Enumerator if called on a frozen instance" do + @hsh.freeze + @hsh.send(@method).should be_an_instance_of(Enumerator) + end +end diff --git a/spec/ruby/core/hash/shared/key.rb b/spec/ruby/core/hash/shared/key.rb new file mode 100644 index 0000000000..17f9f81457 --- /dev/null +++ b/spec/ruby/core/hash/shared/key.rb @@ -0,0 +1,38 @@ +describe :hash_key_p, shared: true do + it "returns true if argument is a key" do + h = { a: 1, b: 2, c: 3, 4 => 0 } + h.send(@method, :a).should == true + h.send(@method, :b).should == true + h.send(@method, 2).should == false + h.send(@method, 4).should == true + + not_supported_on :opal do + h.send(@method, 'b').should == false + h.send(@method, 4.0).should == false + end + end + + it "returns true if the key's matching value was nil" do + { xyz: nil }.send(@method, :xyz).should == true + end + + it "returns true if the key's matching value was false" do + { xyz: false }.send(@method, :xyz).should == true + end + + it "returns true if the key is nil" do + { nil => 'b' }.send(@method, nil).should == true + { nil => nil }.send(@method, nil).should == true + end + + it "compares keys with the same #hash value via #eql?" do + x = mock('x') + x.stub!(:hash).and_return(42) + + y = mock('y') + y.stub!(:hash).and_return(42) + y.should_receive(:eql?).and_return(false) + + { x => nil }.send(@method, y).should == false + end +end diff --git a/spec/ruby/core/hash/shared/length.rb b/spec/ruby/core/hash/shared/length.rb new file mode 100644 index 0000000000..24f5563759 --- /dev/null +++ b/spec/ruby/core/hash/shared/length.rb @@ -0,0 +1,12 @@ +describe :hash_length, shared: true do + it "returns the number of entries" do + { a: 1, b: 'c' }.send(@method).should == 2 + h = { a: 1, b: 2 } + h[:a] = 2 + h.send(@method).should == 2 + { a: 1, b: 1, c: 1 }.send(@method).should == 3 + {}.send(@method).should == 0 + Hash.new(5).send(@method).should == 0 + Hash.new { 5 }.send(@method).should == 0 + end +end diff --git a/spec/ruby/core/hash/shared/less_than.rb b/spec/ruby/core/hash/shared/less_than.rb new file mode 100644 index 0000000000..cdc6f14546 --- /dev/null +++ b/spec/ruby/core/hash/shared/less_than.rb @@ -0,0 +1,23 @@ +describe :hash_less_than, shared: true do + before do + @h1 = { a: 1, b: 2 } + @h2 = { a: 1, b: 2, c: 3 } + end + + it "returns true if self is a subset of the other hash" do + @h1.send(@method, @h2).should be_true + end + + it "returns false if self is not a subset of the other hash" do + @h2.send(@method, @h1).should be_false + end + + it "converts the right operand to a hash before comparing" do + o = Object.new + def o.to_hash + { a: 1, b: 2, c: 3 } + end + + @h1.send(@method, o).should be_true + end +end diff --git a/spec/ruby/core/hash/shared/select.rb b/spec/ruby/core/hash/shared/select.rb new file mode 100644 index 0000000000..fbeff07330 --- /dev/null +++ b/spec/ruby/core/hash/shared/select.rb @@ -0,0 +1,112 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative '../shared/iteration' +require_relative '../../enumerable/shared/enumeratorized' + +describe :hash_select, shared: true do + before :each do + @hsh = { 1 => 2, 3 => 4, 5 => 6 } + @empty = {} + end + + it "yields two arguments: key and value" do + all_args = [] + { 1 => 2, 3 => 4 }.send(@method) { |*args| all_args << args } + all_args.sort.should == [[1, 2], [3, 4]] + end + + it "returns a Hash of entries for which block is true" do + a_pairs = { 'a' => 9, 'c' => 4, 'b' => 5, 'd' => 2 }.send(@method) { |k,v| v % 2 == 0 } + a_pairs.should be_an_instance_of(Hash) + a_pairs.sort.should == [['c', 4], ['d', 2]] + end + + it "processes entries with the same order as reject" do + h = { a: 9, c: 4, b: 5, d: 2 } + + select_pairs = [] + reject_pairs = [] + h.dup.send(@method) { |*pair| select_pairs << pair } + h.reject { |*pair| reject_pairs << pair } + + select_pairs.should == reject_pairs + end + + it "returns an Enumerator when called on a non-empty hash without a block" do + @hsh.send(@method).should be_an_instance_of(Enumerator) + end + + it "returns an Enumerator when called on an empty hash without a block" do + @empty.send(@method).should be_an_instance_of(Enumerator) + end + + it "does not retain the default value" do + h = Hash.new(1) + h.send(@method) { true }.default.should be_nil + h[:a] = 1 + h.send(@method) { true }.default.should be_nil + end + + it "does not retain the default_proc" do + pr = proc { |h, k| h[k] = [] } + h = Hash.new(&pr) + h.send(@method) { true }.default_proc.should be_nil + h[:a] = 1 + h.send(@method) { true }.default_proc.should be_nil + end + + it "retains compare_by_identity flag" do + h = { a: 9, c: 4 }.compare_by_identity + h2 = h.send(@method) { |k, _| k == :a } + h2.compare_by_identity?.should == true + end + + it_should_behave_like :hash_iteration_no_block + + before :each do + @object = { 1 => 2, 3 => 4, 5 => 6 } + end + it_should_behave_like :enumeratorized_with_origin_size +end + +describe :hash_select!, shared: true do + before :each do + @hsh = { 1 => 2, 3 => 4, 5 => 6 } + @empty = {} + end + + it "is equivalent to keep_if if changes are made" do + h = { a: 2 } + h.send(@method) { |k,v| v <= 1 }.should equal h + + h = { 1 => 2, 3 => 4 } + all_args_select = [] + h.dup.send(@method) { |*args| all_args_select << args } + all_args_select.should == [[1, 2], [3, 4]] + end + + it "removes all entries if the block is false" do + h = { a: 1, b: 2, c: 3 } + h.send(@method) { |k,v| false }.should equal(h) + h.should == {} + end + + it "returns nil if no changes were made" do + { a: 1 }.send(@method) { |k,v| v <= 1 }.should == nil + end + + it "raises a FrozenError if called on an empty frozen instance" do + -> { HashSpecs.empty_frozen_hash.send(@method) { false } }.should raise_error(FrozenError) + end + + it "raises a FrozenError if called on a frozen instance that would not be modified" do + -> { HashSpecs.frozen_hash.send(@method) { true } }.should raise_error(FrozenError) + end + + it_should_behave_like :hash_iteration_no_block + + before :each do + @object = { 1 => 2, 3 => 4, 5 => 6 } + end + it_should_behave_like :enumeratorized_with_origin_size +end diff --git a/spec/ruby/core/hash/shared/store.rb b/spec/ruby/core/hash/shared/store.rb new file mode 100644 index 0000000000..72a462a42f --- /dev/null +++ b/spec/ruby/core/hash/shared/store.rb @@ -0,0 +1,115 @@ +require_relative '../fixtures/classes' + +describe :hash_store, shared: true do + it "associates the key with the value and return the value" do + h = { a: 1 } + h.send(@method, :b, 2).should == 2 + h.should == { b:2, a:1 } + end + + it "duplicates string keys using dup semantics" do + # dup doesn't copy singleton methods + key = +"foo" + def key.reverse() "bar" end + h = {} + h.send(@method, key, 0) + h.keys[0].reverse.should == "oof" + end + + it "stores unequal keys that hash to the same value" do + h = {} + k1 = ["x"] + k2 = ["y"] + # So they end up in the same bucket + k1.should_receive(:hash).and_return(0) + k2.should_receive(:hash).and_return(0) + + h.send(@method, k1, 1) + h.send(@method, k2, 2) + h.size.should == 2 + end + + it "accepts keys with private #hash method" do + key = HashSpecs::KeyWithPrivateHash.new + h = {} + h.send(@method, key, "foo") + h[key].should == "foo" + end + + it " accepts keys with an Integer hash" do + o = mock(hash: 1 << 100) + h = {} + h[o] = 1 + h[o].should == 1 + end + + it "duplicates and freezes string keys" do + key = +"foo" + h = {} + h.send(@method, key, 0) + key << "bar" + + h.should == { "foo" => 0 } + h.keys[0].should.frozen? + end + + it "doesn't duplicate and freeze already frozen string keys" do + key = "foo".freeze + h = {} + h.send(@method, key, 0) + h.keys[0].should equal(key) + end + + it "keeps the existing key in the hash if there is a matching one" do + h = { "a" => 1, "b" => 2, "c" => 3, "d" => 4 } + key1 = HashSpecs::ByValueKey.new(13) + key2 = HashSpecs::ByValueKey.new(13) + h[key1] = 41 + key_in_hash = h.keys.last + key_in_hash.should equal(key1) + h[key2] = 42 + last_key = h.keys.last + last_key.should equal(key_in_hash) + last_key.should_not equal(key2) + end + + it "keeps the existing String key in the hash if there is a matching one" do + h = { "a" => 1, "b" => 2, "c" => 3, "d" => 4 } + key1 = "foo".dup + key2 = "foo".dup + key1.should_not equal(key2) + h[key1] = 41 + frozen_key = h.keys.last + frozen_key.should_not equal(key1) + h[key2] = 42 + h.keys.last.should equal(frozen_key) + h.keys.last.should_not equal(key2) + end + + it "raises a FrozenError if called on a frozen instance" do + -> { HashSpecs.frozen_hash.send(@method, 1, 2) }.should raise_error(FrozenError) + end + + it "does not raise an exception if changing the value of an existing key during iteration" do + hash = {1 => 2, 3 => 4, 5 => 6} + hash.each { hash.send(@method, 1, :foo) } + hash.should == {1 => :foo, 3 => 4, 5 => 6} + end + + it "does not dispatch to hash for Boolean, Integer, Float, String, or Symbol" do + code = <<-EOC + load '#{fixture __FILE__, "name.rb"}' + hash = {} + [true, false, 1, 2.0, "hello", :ok].each do |value| + hash[value] = 42 + raise "incorrect value" unless hash[value] == 42 + hash[value] = 43 + raise "incorrect value" unless hash[value] == 43 + end + puts "OK" + puts hash.size + EOC + result = ruby_exe(code, args: "2>&1") + result.should == "OK\n6\n" + end +end diff --git a/spec/ruby/core/hash/shared/to_s.rb b/spec/ruby/core/hash/shared/to_s.rb new file mode 100644 index 0000000000..e116b8878b --- /dev/null +++ b/spec/ruby/core/hash/shared/to_s.rb @@ -0,0 +1,93 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' + +describe :hash_to_s, shared: true do + it "returns a string representation with same order as each()" do + h = { a: [1, 2], b: -2, d: -6, nil => nil } + expected = ruby_version_is("3.4") ? "{a: [1, 2], b: -2, d: -6, nil => nil}" : "{:a=>[1, 2], :b=>-2, :d=>-6, nil=>nil}" + h.send(@method).should == expected + end + + it "calls #inspect on keys and values" do + key = mock('key') + val = mock('val') + key.should_receive(:inspect).and_return('key') + val.should_receive(:inspect).and_return('val') + expected = ruby_version_is("3.4") ? "{key => val}" : "{key=>val}" + { key => val }.send(@method).should == expected + end + + it "does not call #to_s on a String returned from #inspect" do + str = +"abc" + str.should_not_receive(:to_s) + expected = ruby_version_is("3.4") ? '{a: "abc"}' : '{:a=>"abc"}' + { a: str }.send(@method).should == expected + end + + it "calls #to_s on the object returned from #inspect if the Object isn't a String" do + obj = mock("Hash#inspect/to_s calls #to_s") + obj.should_receive(:inspect).and_return(obj) + obj.should_receive(:to_s).and_return("abc") + expected = ruby_version_is("3.4") ? "{a: abc}" : "{:a=>abc}" + { a: obj }.send(@method).should == expected + end + + it "does not call #to_str on the object returned from #inspect when it is not a String" do + obj = mock("Hash#inspect/to_s does not call #to_str") + obj.should_receive(:inspect).and_return(obj) + obj.should_not_receive(:to_str) + expected_pattern = ruby_version_is("3.4") ? /^\{a: #<MockObject:0x[0-9a-f]+>\}$/ : /^\{:a=>#<MockObject:0x[0-9a-f]+>\}$/ + { a: obj }.send(@method).should =~ expected_pattern + end + + it "does not call #to_str on the object returned from #to_s when it is not a String" do + obj = mock("Hash#inspect/to_s does not call #to_str on #to_s result") + obj.should_receive(:inspect).and_return(obj) + obj.should_receive(:to_s).and_return(obj) + obj.should_not_receive(:to_str) + expected_pattern = ruby_version_is("3.4") ? /^\{a: #<MockObject:0x[0-9a-f]+>\}$/ : /^\{:a=>#<MockObject:0x[0-9a-f]+>\}$/ + { a: obj }.send(@method).should =~ expected_pattern + end + + it "does not swallow exceptions raised by #to_s" do + obj = mock("Hash#inspect/to_s does not swallow #to_s exceptions") + obj.should_receive(:inspect).and_return(obj) + obj.should_receive(:to_s).and_raise(Exception) + + -> { { a: obj }.send(@method) }.should raise_error(Exception) + end + + it "handles hashes with recursive values" do + x = {} + x[0] = x + expected = ruby_version_is("3.4") ? '{0 => {...}}' : '{0=>{...}}' + x.send(@method).should == expected + + x = {} + y = {} + x[0] = y + y[1] = x + expected_x = ruby_version_is("3.4") ? '{0 => {1 => {...}}}' : '{0=>{1=>{...}}}' + expected_y = ruby_version_is("3.4") ? '{1 => {0 => {...}}}' : '{1=>{0=>{...}}}' + x.send(@method).should == expected_x + y.send(@method).should == expected_y + end + + it "does not raise if inspected result is not default external encoding" do + utf_16be = mock("utf_16be") + utf_16be.should_receive(:inspect).and_return(%<"utf_16be \u3042">.encode(Encoding::UTF_16BE)) + expected = ruby_version_is("3.4") ? '{a: "utf_16be \u3042"}' : '{:a=>"utf_16be \u3042"}' + {a: utf_16be}.send(@method).should == expected + end + + it "works for keys and values whose #inspect return a frozen String" do + expected = ruby_version_is("3.4") ? "{true => false}" : "{true=>false}" + { true => false }.to_s.should == expected + end + + ruby_version_is "3.4" do + it "adds quotes to symbol keys that are not valid symbol literals" do + { "needs-quotes": 1 }.send(@method).should == '{"needs-quotes": 1}' + end + end +end diff --git a/spec/ruby/core/hash/shared/update.rb b/spec/ruby/core/hash/shared/update.rb new file mode 100644 index 0000000000..1b0eb809bf --- /dev/null +++ b/spec/ruby/core/hash/shared/update.rb @@ -0,0 +1,76 @@ +describe :hash_update, shared: true do + it "adds the entries from other, overwriting duplicate keys. Returns self" do + h = { _1: 'a', _2: '3' } + h.send(@method, _1: '9', _9: 2).should equal(h) + h.should == { _1: "9", _2: "3", _9: 2 } + end + + it "sets any duplicate key to the value of block if passed a block" do + h1 = { a: 2, b: -1 } + h2 = { a: -2, c: 1 } + h1.send(@method, h2) { |k,x,y| 3.14 }.should equal(h1) + h1.should == { c: 1, b: -1, a: 3.14 } + + h1.send(@method, h1) { nil } + h1.should == { a: nil, b: nil, c: nil } + end + + it "tries to convert the passed argument to a hash using #to_hash" do + obj = mock('{1=>2}') + obj.should_receive(:to_hash).and_return({ 1 => 2 }) + { 3 => 4 }.send(@method, obj).should == { 1 => 2, 3 => 4 } + end + + it "does not call to_hash on hash subclasses" do + { 3 => 4 }.send(@method, HashSpecs::ToHashHash[1 => 2]).should == { 1 => 2, 3 => 4 } + end + + it "processes entries with same order as merge()" do + h = { 1 => 2, 3 => 4, 5 => 6, "x" => nil, nil => 5, [] => [] } + merge_bang_pairs = [] + merge_pairs = [] + h.merge(h) { |*arg| merge_pairs << arg } + h.send(@method, h) { |*arg| merge_bang_pairs << arg } + merge_bang_pairs.should == merge_pairs + end + + it "raises a FrozenError on a frozen instance that is modified" do + -> do + HashSpecs.frozen_hash.send(@method, 1 => 2) + end.should raise_error(FrozenError) + end + + it "checks frozen status before coercing an object with #to_hash" do + obj = mock("to_hash frozen") + # This is necessary because mock cleanup code cannot run on the frozen + # object. + def obj.to_hash() raise Exception, "should not receive #to_hash" end + obj.freeze + + -> { HashSpecs.frozen_hash.send(@method, obj) }.should raise_error(FrozenError) + end + + # see redmine #1571 + it "raises a FrozenError on a frozen instance that would not be modified" do + -> do + HashSpecs.frozen_hash.send(@method, HashSpecs.empty_frozen_hash) + end.should raise_error(FrozenError) + end + + it "does not raise an exception if changing the value of an existing key during iteration" do + hash = {1 => 2, 3 => 4, 5 => 6} + hash2 = {1 => :foo, 3 => :bar} + hash.each { hash.send(@method, hash2) } + hash.should == {1 => :foo, 3 => :bar, 5 => 6} + end + + it "accepts multiple hashes" do + result = { a: 1 }.send(@method, { b: 2 }, { c: 3 }, { d: 4 }) + result.should == { a: 1, b: 2, c: 3, d: 4 } + end + + it "accepts zero arguments" do + hash = { a: 1 } + hash.send(@method).should eql(hash) + end +end diff --git a/spec/ruby/core/hash/shared/value.rb b/spec/ruby/core/hash/shared/value.rb new file mode 100644 index 0000000000..aac76c253e --- /dev/null +++ b/spec/ruby/core/hash/shared/value.rb @@ -0,0 +1,14 @@ +describe :hash_value_p, shared: true do + it "returns true if the value exists in the hash" do + { a: :b }.send(@method, :a).should == false + { 1 => 2 }.send(@method, 2).should == true + h = Hash.new(5) + h.send(@method, 5).should == false + h = Hash.new { 5 } + h.send(@method, 5).should == false + end + + it "uses == semantics for comparing values" do + { 5 => 2.0 }.send(@method, 2).should == true + end +end diff --git a/spec/ruby/core/hash/shared/values_at.rb b/spec/ruby/core/hash/shared/values_at.rb new file mode 100644 index 0000000000..ef3b0e8ba0 --- /dev/null +++ b/spec/ruby/core/hash/shared/values_at.rb @@ -0,0 +1,9 @@ +describe :hash_values_at, shared: true do + it "returns an array of values for the given keys" do + h = { a: 9, b: 'a', c: -10, d: nil } + h.send(@method).should be_kind_of(Array) + h.send(@method).should == [] + h.send(@method, :a, :d, :b).should be_kind_of(Array) + h.send(@method, :a, :d, :b).should == [9, nil, 'a'] + end +end diff --git a/spec/ruby/core/hash/shift_spec.rb b/spec/ruby/core/hash/shift_spec.rb new file mode 100644 index 0000000000..3f31b9864c --- /dev/null +++ b/spec/ruby/core/hash/shift_spec.rb @@ -0,0 +1,78 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash#shift" do + it "removes a pair from hash and return it" do + h = { a: 1, b: 2, "c" => 3, nil => 4, [] => 5 } + h2 = h.dup + + h.size.times do |i| + r = h.shift + r.should be_kind_of(Array) + h2[r.first].should == r.last + h.size.should == h2.size - i - 1 + end + + h.should == {} + end + + # MRI explicitly implements this behavior + it "allows shifting entries while iterating" do + h = { a: 1, b: 2, c: 3 } + visited = [] + shifted = [] + h.each_pair { |k,v| + visited << k + shifted << h.shift + } + visited.should == [:a, :b, :c] + shifted.should == [[:a, 1], [:b, 2], [:c, 3]] + h.should == {} + end + + it "returns nil if the Hash is empty" do + h = {} + def h.default(key) + raise + end + h.shift.should == nil + end + + it "returns nil from an empty hash" do + {}.shift.should == nil + end + + it "returns nil for empty hashes with defaults and default procs" do + Hash.new(5).shift.should == nil + h = Hash.new { |*args| args } + h.shift.should == nil + end + + it "preserves Hash invariants when removing the last item" do + h = { :a => 1, :b => 2 } + h.shift.should == [:a, 1] + h.shift.should == [:b, 2] + h[:c] = 3 + h.should == {:c => 3} + end + + it "raises a FrozenError if called on a frozen instance" do + -> { HashSpecs.frozen_hash.shift }.should raise_error(FrozenError) + -> { HashSpecs.empty_frozen_hash.shift }.should raise_error(FrozenError) + end + + it "works when the hash is at capacity" do + # We try a wide range of sizes in hopes that this will cover all implementations' base Hash size. + results = [] + 1.upto(100) do |n| + h = {} + n.times do |i| + h[i] = i + end + h.shift + results << h.size + end + + results.should == 0.upto(99).to_a + end +end diff --git a/spec/ruby/core/hash/size_spec.rb b/spec/ruby/core/hash/size_spec.rb new file mode 100644 index 0000000000..1e8abd8d97 --- /dev/null +++ b/spec/ruby/core/hash/size_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/length' + +describe "Hash#size" do + it_behaves_like :hash_length, :size +end diff --git a/spec/ruby/core/hash/slice_spec.rb b/spec/ruby/core/hash/slice_spec.rb new file mode 100644 index 0000000000..4fcc01f9a6 --- /dev/null +++ b/spec/ruby/core/hash/slice_spec.rb @@ -0,0 +1,74 @@ +require_relative '../../spec_helper' + +describe "Hash#slice" do + before :each do + @hash = { a: 1, b: 2, c: 3 } + end + + it "returns a new empty hash without arguments" do + ret = @hash.slice + ret.should_not equal(@hash) + ret.should be_an_instance_of(Hash) + ret.should == {} + end + + it "returns the requested subset" do + @hash.slice(:c, :a).should == { c: 3, a: 1 } + end + + it "returns a hash ordered in the order of the requested keys" do + @hash.slice(:c, :a).keys.should == [:c, :a] + end + + it "returns only the keys of the original hash" do + @hash.slice(:a, :chunky_bacon).should == { a: 1 } + end + + it "returns a Hash instance, even on subclasses" do + klass = Class.new(Hash) + h = klass.new + h[:bar] = 12 + h[:foo] = 42 + r = h.slice(:foo) + r.should == {foo: 42} + r.class.should == Hash + end + + it "uses the regular Hash#[] method, even on subclasses that override it" do + ScratchPad.record [] + klass = Class.new(Hash) do + def [](value) + ScratchPad << :used_subclassed_operator + super + end + end + + h = klass.new + h[:bar] = 12 + h[:foo] = 42 + h.slice(:foo) + + ScratchPad.recorded.should == [] + end + + it "does not retain the default value" do + h = Hash.new(1) + h.slice(:a).default.should be_nil + h[:a] = 1 + h.slice(:a).default.should be_nil + end + + it "does not retain the default_proc" do + pr = proc { |h, k| h[k] = [] } + h = Hash.new(&pr) + h.slice(:a).default_proc.should be_nil + h[:a] = 1 + h.slice(:a).default_proc.should be_nil + end + + it "retains compare_by_identity flag" do + h = { a: 9, c: 4 }.compare_by_identity + h2 = h.slice(:a) + h2.compare_by_identity?.should == true + end +end diff --git a/spec/ruby/core/hash/sort_spec.rb b/spec/ruby/core/hash/sort_spec.rb new file mode 100644 index 0000000000..26058c845e --- /dev/null +++ b/spec/ruby/core/hash/sort_spec.rb @@ -0,0 +1,17 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash#sort" do + it "converts self to a nested array of [key, value] arrays and sort with Array#sort" do + { 'a' => 'b', '1' => '2', 'b' => 'a' }.sort.should == + [["1", "2"], ["a", "b"], ["b", "a"]] + end + + it "works when some of the keys are themselves arrays" do + { [1,2] => 5, [1,1] => 5 }.sort.should == [[[1,1],5], [[1,2],5]] + end + + it "uses block to sort array if passed a block" do + { 1 => 2, 2 => 9, 3 => 4 }.sort { |a,b| b <=> a }.should == [[3, 4], [2, 9], [1, 2]] + end +end diff --git a/spec/ruby/core/hash/store_spec.rb b/spec/ruby/core/hash/store_spec.rb new file mode 100644 index 0000000000..7e975380ec --- /dev/null +++ b/spec/ruby/core/hash/store_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/store' + +describe "Hash#store" do + it_behaves_like :hash_store, :store +end diff --git a/spec/ruby/core/hash/to_a_spec.rb b/spec/ruby/core/hash/to_a_spec.rb new file mode 100644 index 0000000000..5baf677929 --- /dev/null +++ b/spec/ruby/core/hash/to_a_spec.rb @@ -0,0 +1,29 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash#to_a" do + it "returns a list of [key, value] pairs with same order as each()" do + h = { a: 1, 1 => :a, 3 => :b, b: 5 } + pairs = [] + + h.each_pair do |key, value| + pairs << [key, value] + end + + h.to_a.should be_kind_of(Array) + h.to_a.should == pairs + end + + it "is called for Enumerable#entries" do + h = { a: 1, 1 => :a, 3 => :b, b: 5 } + pairs = [] + + h.each_pair do |key, value| + pairs << [key, value] + end + + ent = h.entries + ent.should be_kind_of(Array) + ent.should == pairs + end +end diff --git a/spec/ruby/core/hash/to_h_spec.rb b/spec/ruby/core/hash/to_h_spec.rb new file mode 100644 index 0000000000..f84fd7b503 --- /dev/null +++ b/spec/ruby/core/hash/to_h_spec.rb @@ -0,0 +1,106 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash#to_h" do + it "returns self for Hash instances" do + h = {} + h.to_h.should equal(h) + end + + describe "when called on a subclass of Hash" do + before :each do + @h = HashSpecs::MyHash.new + @h[:foo] = :bar + end + + it "returns a new Hash instance" do + @h.to_h.should be_an_instance_of(Hash) + @h.to_h.should == @h + @h[:foo].should == :bar + end + + it "retains the default" do + @h.default = 42 + @h.to_h.default.should == 42 + @h[:hello].should == 42 + end + + it "retains the default_proc" do + @h.default_proc = prc = Proc.new{ |h, k| h[k] = 2 * k } + @h.to_h.default_proc.should == prc + @h[42].should == 84 + end + + it "retains compare_by_identity flag" do + @h.compare_by_identity + @h.to_h.compare_by_identity?.should == true + end + end + + context "with block" do + it "converts [key, value] pairs returned by the block to a hash" do + { a: 1, b: 2 }.to_h { |k, v| [k.to_s, v*v]}.should == { "a" => 1, "b" => 4 } + end + + it "passes to a block each pair's key and value as separate arguments" do + ScratchPad.record [] + { a: 1, b: 2 }.to_h { |k, v| ScratchPad << [k, v]; [k, v] } + ScratchPad.recorded.sort.should == [[:a, 1], [:b, 2]] + + ScratchPad.record [] + { a: 1, b: 2 }.to_h { |*args| ScratchPad << args; [args[0], args[1]] } + ScratchPad.recorded.sort.should == [[:a, 1], [:b, 2]] + end + + it "raises ArgumentError if block returns longer or shorter array" do + -> do + { a: 1, b: 2 }.to_h { |k, v| [k.to_s, v*v, 1] } + end.should raise_error(ArgumentError, /element has wrong array length/) + + -> do + { a: 1, b: 2 }.to_h { |k, v| [k] } + end.should raise_error(ArgumentError, /element has wrong array length/) + end + + it "raises TypeError if block returns something other than Array" do + -> do + { a: 1, b: 2 }.to_h { |k, v| "not-array" } + end.should raise_error(TypeError, /wrong element type String/) + end + + it "coerces returned pair to Array with #to_ary" do + x = mock('x') + x.stub!(:to_ary).and_return([:b, 'b']) + + { a: 1 }.to_h { |k| x }.should == { :b => 'b' } + end + + it "does not coerce returned pair to Array with #to_a" do + x = mock('x') + x.stub!(:to_a).and_return([:b, 'b']) + + -> do + { a: 1 }.to_h { |k| x } + end.should raise_error(TypeError, /wrong element type MockObject/) + end + + it "does not retain the default value" do + h = Hash.new(1) + h2 = h.to_h { |k, v| [k.to_s, v*v]} + h2.default.should be_nil + end + + it "does not retain the default_proc" do + pr = proc { |h, k| h[k] = [] } + h = Hash.new(&pr) + h2 = h.to_h { |k, v| [k.to_s, v*v]} + h2.default_proc.should be_nil + end + + it "does not retain compare_by_identity flag" do + h = { a: 9, c: 4 }.compare_by_identity + h2 = h.to_h { |k, v| [k.to_s, v*v]} + h2.compare_by_identity?.should == false + end + end +end diff --git a/spec/ruby/core/hash/to_hash_spec.rb b/spec/ruby/core/hash/to_hash_spec.rb new file mode 100644 index 0000000000..f479fa1fb2 --- /dev/null +++ b/spec/ruby/core/hash/to_hash_spec.rb @@ -0,0 +1,14 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash#to_hash" do + it "returns self for Hash instances" do + h = {} + h.to_hash.should equal(h) + end + + it "returns self for instances of subclasses of Hash" do + h = HashSpecs::MyHash.new + h.to_hash.should equal(h) + end +end diff --git a/spec/ruby/core/hash/to_proc_spec.rb b/spec/ruby/core/hash/to_proc_spec.rb new file mode 100644 index 0000000000..9dbc79e5eb --- /dev/null +++ b/spec/ruby/core/hash/to_proc_spec.rb @@ -0,0 +1,91 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash#to_proc" do + before :each do + @key = Object.new + @value = Object.new + @hash = { @key => @value } + @default = Object.new + @unstored = Object.new + end + + it "returns an instance of Proc" do + @hash.to_proc.should be_an_instance_of Proc + end + + describe "the returned proc" do + before :each do + @proc = @hash.to_proc + end + + it "is a lambda" do + @proc.should.lambda? + end + + it "has an arity of 1" do + @proc.arity.should == 1 + end + + it "raises ArgumentError if not passed exactly one argument" do + -> { + @proc.call + }.should raise_error(ArgumentError) + + -> { + @proc.call 1, 2 + }.should raise_error(ArgumentError) + end + + context "with a stored key" do + it "returns the paired value" do + @proc.call(@key).should equal(@value) + end + end + + context "passed as a block" do + it "retrieves the hash's values" do + [@key].map(&@proc)[0].should equal(@value) + end + + context "to instance_exec" do + it "always retrieves the original hash's values" do + hash = {foo: 1, bar: 2} + proc = hash.to_proc + + hash.instance_exec(:foo, &proc).should == 1 + + hash2 = {quux: 1} + hash2.instance_exec(:foo, &proc).should == 1 + end + end + end + + context "with no stored key" do + it "returns nil" do + @proc.call(@unstored).should be_nil + end + + context "when the hash has a default value" do + before :each do + @hash.default = @default + end + + it "returns the default value" do + @proc.call(@unstored).should equal(@default) + end + end + + context "when the hash has a default proc" do + it "returns an evaluated value from the default proc" do + @hash.default_proc = -> hash, called_with { [hash.keys, called_with] } + @proc.call(@unstored).should == [[@key], @unstored] + end + end + end + + it "raises an ArgumentError when calling #call on the Proc with no arguments" do + -> { @hash.to_proc.call }.should raise_error(ArgumentError) + end + end +end diff --git a/spec/ruby/core/hash/to_s_spec.rb b/spec/ruby/core/hash/to_s_spec.rb new file mode 100644 index 0000000000..e52b09962e --- /dev/null +++ b/spec/ruby/core/hash/to_s_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/to_s' + +describe "Hash#to_s" do + it_behaves_like :hash_to_s, :to_s +end diff --git a/spec/ruby/core/hash/transform_keys_spec.rb b/spec/ruby/core/hash/transform_keys_spec.rb new file mode 100644 index 0000000000..e2eeab1813 --- /dev/null +++ b/spec/ruby/core/hash/transform_keys_spec.rb @@ -0,0 +1,150 @@ +require_relative '../../spec_helper' + +describe "Hash#transform_keys" do + before :each do + @hash = { a: 1, b: 2, c: 3 } + end + + it "returns new hash" do + ret = @hash.transform_keys(&:succ) + ret.should_not equal(@hash) + ret.should be_an_instance_of(Hash) + end + + it "sets the result as transformed keys with the given block" do + @hash.transform_keys(&:succ).should == { b: 1, c: 2, d: 3 } + end + + it "keeps last pair if new keys conflict" do + @hash.transform_keys { |_| :a }.should == { a: 3 } + end + + it "makes both hashes to share values" do + value = [1, 2, 3] + new_hash = { a: value }.transform_keys(&:upcase) + new_hash[:A].should equal(value) + end + + context "when no block is given" do + it "returns a sized Enumerator" do + enumerator = @hash.transform_keys + enumerator.should be_an_instance_of(Enumerator) + enumerator.size.should == @hash.size + enumerator.each(&:succ).should == { b: 1, c: 2, d: 3 } + end + end + + it "returns a Hash instance, even on subclasses" do + klass = Class.new(Hash) + h = klass.new + h[:foo] = 42 + r = h.transform_keys{|v| :"x#{v}"} + r.keys.should == [:xfoo] + r.class.should == Hash + end + + it "allows a hash argument" do + @hash.transform_keys({ a: :A, b: :B, c: :C }).should == { A: 1, B: 2, C: 3 } + end + + it "allows a partial transformation of keys when using a hash argument" do + @hash.transform_keys({ a: :A, c: :C }).should == { A: 1, b: 2, C: 3 } + end + + it "allows a combination of hash and block argument" do + @hash.transform_keys({ a: :A }, &:to_s).should == { A: 1, 'b' => 2, 'c' => 3 } + end + + it "does not retain the default value" do + h = Hash.new(1) + h.transform_keys(&:succ).default.should be_nil + h[:a] = 1 + h.transform_keys(&:succ).default.should be_nil + end + + it "does not retain the default_proc" do + pr = proc { |h, k| h[k] = [] } + h = Hash.new(&pr) + h.transform_values(&:succ).default_proc.should be_nil + h[:a] = 1 + h.transform_values(&:succ).default_proc.should be_nil + end + + it "does not retain compare_by_identity flag" do + h = { a: 9, c: 4 }.compare_by_identity + h2 = h.transform_keys(&:succ) + h2.compare_by_identity?.should == false + end +end + +describe "Hash#transform_keys!" do + before :each do + @hash = { a: 1, b: 2, c: 3, d: 4 } + @initial_pairs = @hash.dup + end + + it "returns self" do + @hash.transform_keys!(&:succ).should equal(@hash) + end + + it "updates self as transformed values with the given block" do + @hash.transform_keys!(&:to_s) + @hash.should == { 'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4 } + end + + it "prevents conflicts between new keys and old ones" do + @hash.transform_keys!(&:succ) + @hash.should == { b: 1, c: 2, d: 3, e: 4 } + end + + it "returns the processed keys and non evaluated keys if we break from the block" do + @hash.transform_keys! do |v| + break if v == :c + v.succ + end + @hash.should == { b: 1, c: 2, d: 4 } + end + + it "keeps later pair if new keys conflict" do + @hash.transform_keys! { |_| :a }.should == { a: 4 } + end + + context "when no block is given" do + it "returns a sized Enumerator" do + enumerator = @hash.transform_keys! + enumerator.should be_an_instance_of(Enumerator) + enumerator.size.should == @hash.size + enumerator.each(&:upcase).should == { A: 1, B: 2, C: 3, D: 4 } + end + end + + it "allows a hash argument" do + @hash.transform_keys!({ a: :A, b: :B, c: :C, d: :D }) + @hash.should == { A: 1, B: 2, C: 3, D: 4 } + end + + describe "on frozen instance" do + before :each do + @hash.freeze + end + + it "raises a FrozenError on an empty hash" do + ->{ {}.freeze.transform_keys!(&:upcase) }.should raise_error(FrozenError) + end + + it "keeps pairs and raises a FrozenError" do + ->{ @hash.transform_keys!(&:upcase) }.should raise_error(FrozenError) + @hash.should == @initial_pairs + end + + it "raises a FrozenError on hash argument" do + ->{ @hash.transform_keys!({ a: :A, b: :B, c: :C }) }.should raise_error(FrozenError) + end + + context "when no block is given" do + it "does not raise an exception" do + @hash.transform_keys!.should be_an_instance_of(Enumerator) + end + end + end +end diff --git a/spec/ruby/core/hash/transform_values_spec.rb b/spec/ruby/core/hash/transform_values_spec.rb new file mode 100644 index 0000000000..4a0ae8a5a5 --- /dev/null +++ b/spec/ruby/core/hash/transform_values_spec.rb @@ -0,0 +1,118 @@ +require_relative '../../spec_helper' + +describe "Hash#transform_values" do + before :each do + @hash = { a: 1, b: 2, c: 3 } + end + + it "returns new hash" do + ret = @hash.transform_values(&:succ) + ret.should_not equal(@hash) + ret.should be_an_instance_of(Hash) + end + + it "sets the result as transformed values with the given block" do + @hash.transform_values(&:succ).should == { a: 2, b: 3, c: 4 } + end + + it "makes both hashes to share keys" do + key = [1, 2, 3] + new_hash = { key => 1 }.transform_values(&:succ) + new_hash[key].should == 2 + new_hash.keys[0].should equal(key) + end + + context "when no block is given" do + it "returns a sized Enumerator" do + enumerator = @hash.transform_values + enumerator.should be_an_instance_of(Enumerator) + enumerator.size.should == @hash.size + enumerator.each(&:succ).should == { a: 2, b: 3, c: 4 } + end + end + + it "returns a Hash instance, even on subclasses" do + klass = Class.new(Hash) + h = klass.new + h[:foo] = 42 + r = h.transform_values{|v| 2 * v} + r[:foo].should == 84 + r.class.should == Hash + end + + it "does not retain the default value" do + h = Hash.new(1) + h.transform_values(&:succ).default.should be_nil + h[:a] = 1 + h.transform_values(&:succ).default.should be_nil + end + + it "does not retain the default_proc" do + pr = proc { |h, k| h[k] = [] } + h = Hash.new(&pr) + h.transform_values(&:succ).default_proc.should be_nil + h[:a] = 1 + h.transform_values(&:succ).default_proc.should be_nil + end + + it "retains compare_by_identity flag" do + h = { a: 9, c: 4 }.compare_by_identity + h2 = h.transform_values(&:succ) + h2.compare_by_identity?.should == true + end +end + +describe "Hash#transform_values!" do + before :each do + @hash = { a: 1, b: 2, c: 3 } + @initial_pairs = @hash.dup + end + + it "returns self" do + @hash.transform_values!(&:succ).should equal(@hash) + end + + it "updates self as transformed values with the given block" do + @hash.transform_values!(&:succ) + @hash.should == { a: 2, b: 3, c: 4 } + end + + it "partially modifies the contents if we broke from the block" do + @hash.transform_values! do |v| + break if v == 3 + 100 + v + end + @hash.should == { a: 101, b: 102, c: 3} + end + + context "when no block is given" do + it "returns a sized Enumerator" do + enumerator = @hash.transform_values! + enumerator.should be_an_instance_of(Enumerator) + enumerator.size.should == @hash.size + enumerator.each(&:succ) + @hash.should == { a: 2, b: 3, c: 4 } + end + end + + describe "on frozen instance" do + before :each do + @hash.freeze + end + + it "raises a FrozenError on an empty hash" do + ->{ {}.freeze.transform_values!(&:succ) }.should raise_error(FrozenError) + end + + it "keeps pairs and raises a FrozenError" do + ->{ @hash.transform_values!(&:succ) }.should raise_error(FrozenError) + @hash.should == @initial_pairs + end + + context "when no block is given" do + it "does not raise an exception" do + @hash.transform_values!.should be_an_instance_of(Enumerator) + end + end + end +end diff --git a/spec/ruby/core/hash/try_convert_spec.rb b/spec/ruby/core/hash/try_convert_spec.rb new file mode 100644 index 0000000000..d359ae49d8 --- /dev/null +++ b/spec/ruby/core/hash/try_convert_spec.rb @@ -0,0 +1,50 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash.try_convert" do + it "returns the argument if it's a Hash" do + x = Hash.new + Hash.try_convert(x).should equal(x) + end + + it "returns the argument if it's a kind of Hash" do + x = HashSpecs::MyHash.new + Hash.try_convert(x).should equal(x) + end + + it "returns nil when the argument does not respond to #to_hash" do + Hash.try_convert(Object.new).should be_nil + end + + it "sends #to_hash to the argument and returns the result if it's nil" do + obj = mock("to_hash") + obj.should_receive(:to_hash).and_return(nil) + Hash.try_convert(obj).should be_nil + end + + it "sends #to_hash to the argument and returns the result if it's a Hash" do + x = Hash.new + obj = mock("to_hash") + obj.should_receive(:to_hash).and_return(x) + Hash.try_convert(obj).should equal(x) + end + + it "sends #to_hash to the argument and returns the result if it's a kind of Hash" do + x = HashSpecs::MyHash.new + obj = mock("to_hash") + obj.should_receive(:to_hash).and_return(x) + Hash.try_convert(obj).should equal(x) + end + + it "sends #to_hash to the argument and raises TypeError if it's not a kind of Hash" do + obj = mock("to_hash") + obj.should_receive(:to_hash).and_return(Object.new) + -> { Hash.try_convert obj }.should raise_error(TypeError, "can't convert MockObject to Hash (MockObject#to_hash gives Object)") + end + + it "does not rescue exceptions raised by #to_hash" do + obj = mock("to_hash") + obj.should_receive(:to_hash).and_raise(RuntimeError) + -> { Hash.try_convert obj }.should raise_error(RuntimeError) + end +end diff --git a/spec/ruby/core/hash/update_spec.rb b/spec/ruby/core/hash/update_spec.rb new file mode 100644 index 0000000000..0975045ad1 --- /dev/null +++ b/spec/ruby/core/hash/update_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/update' + +describe "Hash#update" do + it_behaves_like :hash_update, :update +end diff --git a/spec/ruby/core/hash/value_spec.rb b/spec/ruby/core/hash/value_spec.rb new file mode 100644 index 0000000000..0ab16a5d1b --- /dev/null +++ b/spec/ruby/core/hash/value_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/value' + +describe "Hash#value?" do + it_behaves_like :hash_value_p, :value? +end diff --git a/spec/ruby/core/hash/values_at_spec.rb b/spec/ruby/core/hash/values_at_spec.rb new file mode 100644 index 0000000000..b620a279ba --- /dev/null +++ b/spec/ruby/core/hash/values_at_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/values_at' + +describe "Hash#values_at" do + it_behaves_like :hash_values_at, :values_at +end diff --git a/spec/ruby/core/hash/values_spec.rb b/spec/ruby/core/hash/values_spec.rb new file mode 100644 index 0000000000..9f2a481a48 --- /dev/null +++ b/spec/ruby/core/hash/values_spec.rb @@ -0,0 +1,10 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Hash#values" do + it "returns an array of values" do + h = { 1 => :a, 'a' => :a, 'the' => 'lang' } + h.values.should be_kind_of(Array) + h.values.sort {|a, b| a.to_s <=> b.to_s}.should == [:a, :a, 'lang'] + end +end diff --git a/spec/ruby/core/integer/abs_spec.rb b/spec/ruby/core/integer/abs_spec.rb new file mode 100644 index 0000000000..c40356db12 --- /dev/null +++ b/spec/ruby/core/integer/abs_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/abs' + +describe "Integer#abs" do + it_behaves_like :integer_abs, :abs +end diff --git a/spec/ruby/core/integer/allbits_spec.rb b/spec/ruby/core/integer/allbits_spec.rb new file mode 100644 index 0000000000..edce4b15e7 --- /dev/null +++ b/spec/ruby/core/integer/allbits_spec.rb @@ -0,0 +1,37 @@ +require_relative '../../spec_helper' + +describe "Integer#allbits?" do + it "returns true if and only if all the bits of the argument are set in the receiver" do + 42.allbits?(42).should == true + 0b1010_1010.allbits?(0b1000_0010).should == true + 0b1010_1010.allbits?(0b1000_0001).should == false + 0b1000_0010.allbits?(0b1010_1010).should == false + (0b1010_1010 | bignum_value).allbits?(0b1000_0010 | bignum_value).should == true + (0b1010_1010 | bignum_value).allbits?(0b1000_0001 | bignum_value).should == false + (0b1000_0010 | bignum_value).allbits?(0b1010_1010 | bignum_value).should == false + end + + it "handles negative values using two's complement notation" do + (~0b1).allbits?(42).should == true + (-42).allbits?(-42).should == true + (~0b1010_1010).allbits?(~0b1110_1011).should == true + (~0b1010_1010).allbits?(~0b1000_0010).should == false + (~(0b1010_1010 | bignum_value)).allbits?(~(0b1110_1011 | bignum_value)).should == true + (~(0b1010_1010 | bignum_value)).allbits?(~(0b1000_0010 | bignum_value)).should == false + end + + it "coerces the rhs using to_int" do + obj = mock("the int 0b10") + obj.should_receive(:to_int).and_return(0b10) + 0b110.allbits?(obj).should == true + end + + it "raises a TypeError when given a non-Integer" do + -> { + (obj = mock('10')).should_receive(:coerce).any_number_of_times.and_return([42,10]) + 13.allbits?(obj) + }.should raise_error(TypeError) + -> { 13.allbits?("10") }.should raise_error(TypeError) + -> { 13.allbits?(:symbol) }.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/integer/anybits_spec.rb b/spec/ruby/core/integer/anybits_spec.rb new file mode 100644 index 0000000000..e0449074a2 --- /dev/null +++ b/spec/ruby/core/integer/anybits_spec.rb @@ -0,0 +1,36 @@ +require_relative '../../spec_helper' + +describe "Integer#anybits?" do + it "returns true if and only if all the bits of the argument are set in the receiver" do + 42.anybits?(42).should == true + 0b1010_1010.anybits?(0b1000_0010).should == true + 0b1010_1010.anybits?(0b1000_0001).should == true + 0b1000_0010.anybits?(0b0010_1100).should == false + different_bignum = (2 * bignum_value) & (~bignum_value) + (0b1010_1010 | different_bignum).anybits?(0b1000_0010 | bignum_value).should == true + (0b1010_1010 | different_bignum).anybits?(0b0010_1100 | bignum_value).should == true + (0b1000_0010 | different_bignum).anybits?(0b0010_1100 | bignum_value).should == false + end + + it "handles negative values using two's complement notation" do + (~42).anybits?(42).should == false + (-42).anybits?(-42).should == true + (~0b100).anybits?(~0b1).should == true + (~(0b100 | bignum_value)).anybits?(~(0b1 | bignum_value)).should == true + end + + it "coerces the rhs using to_int" do + obj = mock("the int 0b10") + obj.should_receive(:to_int).and_return(0b10) + 0b110.anybits?(obj).should == true + end + + it "raises a TypeError when given a non-Integer" do + -> { + (obj = mock('10')).should_receive(:coerce).any_number_of_times.and_return([42,10]) + 13.anybits?(obj) + }.should raise_error(TypeError) + -> { 13.anybits?("10") }.should raise_error(TypeError) + -> { 13.anybits?(:symbol) }.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/integer/bit_and_spec.rb b/spec/ruby/core/integer/bit_and_spec.rb new file mode 100644 index 0000000000..e7face39ac --- /dev/null +++ b/spec/ruby/core/integer/bit_and_spec.rb @@ -0,0 +1,97 @@ +require_relative '../../spec_helper' + +describe "Integer#&" do + context "fixnum" do + it "returns self bitwise AND other" do + (256 & 16).should == 0 + (2010 & 5).should == 0 + (65535 & 1).should == 1 + (0xffff & bignum_value + 0xffff_ffff).should == 65535 + end + + it "returns self bitwise AND other when one operand is negative" do + ((1 << 33) & -1).should == (1 << 33) + (-1 & (1 << 33)).should == (1 << 33) + + ((-(1<<33)-1) & 5).should == 5 + (5 & (-(1<<33)-1)).should == 5 + end + + it "returns self bitwise AND other when both operands are negative" do + (-5 & -1).should == -5 + (-3 & -4).should == -4 + (-12 & -13).should == -16 + (-13 & -12).should == -16 + end + + it "returns self bitwise AND a bignum" do + (-1 & 2**64).should == 18446744073709551616 + end + + it "coerces the rhs and calls #coerce" do + obj = mock("fixnum bit and") + obj.should_receive(:coerce).with(6).and_return([6, 3]) + (6 & obj).should == 2 + end + + it "raises a TypeError when passed a Float" do + -> { (3 & 3.4) }.should raise_error(TypeError) + end + + it "raises a TypeError and does not call #to_int when defined on an object" do + obj = mock("fixnum bit and") + obj.should_not_receive(:to_int) + + -> { 3 & obj }.should raise_error(TypeError) + end + end + + context "bignum" do + before :each do + @bignum = bignum_value(5) + end + + it "returns self bitwise AND other" do + @bignum = bignum_value(5) + (@bignum & 3).should == 1 + (@bignum & 52).should == 4 + (@bignum & bignum_value(9921)).should == 18446744073709551617 + + ((2*bignum_value) & 1).should == 0 + ((2*bignum_value) & (2*bignum_value)).should == 36893488147419103232 + end + + it "returns self bitwise AND other when one operand is negative" do + ((2*bignum_value) & -1).should == (2*bignum_value) + ((4*bignum_value) & -1).should == (4*bignum_value) + (@bignum & -0xffffffffffffff5).should == 18446744073709551617 + (@bignum & -@bignum).should == 1 + (@bignum & -0x8000000000000000).should == 18446744073709551616 + end + + it "returns self bitwise AND other when both operands are negative" do + (-@bignum & -0x4000000000000005).should == -23058430092136939525 + (-@bignum & -@bignum).should == -18446744073709551621 + (-@bignum & -0x4000000000000000).should == -23058430092136939520 + end + + it "returns self bitwise AND other when both are negative and a multiple in bitsize of Fixnum::MIN" do + val = - ((1 << 93) - 1) + (val & val).should == val + + val = - ((1 << 126) - 1) + (val & val).should == val + end + + it "raises a TypeError when passed a Float" do + -> { (@bignum & 3.4) }.should raise_error(TypeError) + end + + it "raises a TypeError and does not call #to_int when defined on an object" do + obj = mock("bignum bit and") + obj.should_not_receive(:to_int) + + -> { @bignum & obj }.should raise_error(TypeError) + end + end +end diff --git a/spec/ruby/core/integer/bit_length_spec.rb b/spec/ruby/core/integer/bit_length_spec.rb new file mode 100644 index 0000000000..827007b7e0 --- /dev/null +++ b/spec/ruby/core/integer/bit_length_spec.rb @@ -0,0 +1,76 @@ +require_relative '../../spec_helper' + +describe "Integer#bit_length" do + context "fixnum" do + it "returns the position of the leftmost bit of a positive number" do + 0.bit_length.should == 0 + 1.bit_length.should == 1 + 2.bit_length.should == 2 + 3.bit_length.should == 2 + 4.bit_length.should == 3 + n = fixnum_max.bit_length + fixnum_max[n].should == 0 + fixnum_max[n - 1].should == 1 + + 0.bit_length.should == 0 + 1.bit_length.should == 1 + 0xff.bit_length.should == 8 + 0x100.bit_length.should == 9 + (2**12 - 1).bit_length.should == 12 + (2**12).bit_length.should == 13 + (2**12 + 1).bit_length.should == 13 + end + + it "returns the position of the leftmost 0 bit of a negative number" do + -1.bit_length.should == 0 + -2.bit_length.should == 1 + -3.bit_length.should == 2 + -4.bit_length.should == 2 + -5.bit_length.should == 3 + n = fixnum_min.bit_length + fixnum_min[n].should == 1 + fixnum_min[n - 1].should == 0 + + (-2**12 - 1).bit_length.should == 13 + (-2**12).bit_length.should == 12 + (-2**12 + 1).bit_length.should == 12 + -0x101.bit_length.should == 9 + -0x100.bit_length.should == 8 + -0xff.bit_length.should == 8 + -2.bit_length.should == 1 + -1.bit_length.should == 0 + end + end + + context "bignum" do + it "returns the position of the leftmost bit of a positive number" do + (2**1000-1).bit_length.should == 1000 + (2**1000).bit_length.should == 1001 + (2**1000+1).bit_length.should == 1001 + + (2**10000-1).bit_length.should == 10000 + (2**10000).bit_length.should == 10001 + (2**10000+1).bit_length.should == 10001 + + (1 << 100).bit_length.should == 101 + (1 << 100).succ.bit_length.should == 101 + (1 << 100).pred.bit_length.should == 100 + (1 << 10000).bit_length.should == 10001 + end + + it "returns the position of the leftmost 0 bit of a negative number" do + (-2**10000-1).bit_length.should == 10001 + (-2**10000).bit_length.should == 10000 + (-2**10000+1).bit_length.should == 10000 + + (-2**1000-1).bit_length.should == 1001 + (-2**1000).bit_length.should == 1000 + (-2**1000+1).bit_length.should == 1000 + + ((-1 << 100)-1).bit_length.should == 101 + ((-1 << 100)-1).succ.bit_length.should == 100 + ((-1 << 100)-1).pred.bit_length.should == 101 + ((-1 << 10000)-1).bit_length.should == 10001 + end + end +end diff --git a/spec/ruby/core/integer/bit_or_spec.rb b/spec/ruby/core/integer/bit_or_spec.rb new file mode 100644 index 0000000000..fdf8a191e5 --- /dev/null +++ b/spec/ruby/core/integer/bit_or_spec.rb @@ -0,0 +1,89 @@ +require_relative '../../spec_helper' + +describe "Integer#|" do + context "fixnum" do + it "returns self bitwise OR other" do + (1 | 0).should == 1 + (5 | 4).should == 5 + (5 | 6).should == 7 + (248 | 4096).should == 4344 + (0xffff | bignum_value + 0xf0f0).should == 0x1_0000_0000_0000_ffff + end + + it "returns self bitwise OR other when one operand is negative" do + ((1 << 33) | -1).should == -1 + (-1 | (1 << 33)).should == -1 + + ((-(1<<33)-1) | 5).should == -8589934593 + (5 | (-(1<<33)-1)).should == -8589934593 + end + + it "returns self bitwise OR other when both operands are negative" do + (-5 | -1).should == -1 + (-3 | -4).should == -3 + (-12 | -13).should == -9 + (-13 | -12).should == -9 + end + + it "returns self bitwise OR a bignum" do + (-1 | 2**64).should == -1 + end + + it "coerces the rhs and calls #coerce" do + obj = mock("fixnum bit or") + obj.should_receive(:coerce).with(6).and_return([6, 3]) + (6 | obj).should == 7 + end + + it "raises a TypeError when passed a Float" do + -> { (3 | 3.4) }.should raise_error(TypeError) + end + + it "raises a TypeError and does not call #to_int when defined on an object" do + obj = mock("integer bit or") + obj.should_not_receive(:to_int) + + -> { 3 | obj }.should raise_error(TypeError) + end + end + + context "bignum" do + before :each do + @bignum = bignum_value(11) + end + + it "returns self bitwise OR other" do + (@bignum | 2).should == 18446744073709551627 + (@bignum | 9).should == 18446744073709551627 + (@bignum | bignum_value).should == 18446744073709551627 + end + + it "returns self bitwise OR other when one operand is negative" do + (@bignum | -0x40000000000000000).should == -55340232221128654837 + (@bignum | -@bignum).should == -1 + (@bignum | -0x8000000000000000).should == -9223372036854775797 + end + + it "returns self bitwise OR other when both operands are negative" do + (-@bignum | -0x4000000000000005).should == -1 + (-@bignum | -@bignum).should == -18446744073709551627 + (-@bignum | -0x4000000000000000).should == -11 + end + + it "raises a TypeError when passed a Float" do + not_supported_on :opal do + -> { + bignum_value | bignum_value(0xffff).to_f + }.should raise_error(TypeError) + end + -> { @bignum | 9.9 }.should raise_error(TypeError) + end + + it "raises a TypeError and does not call #to_int when defined on an object" do + obj = mock("bignum bit or") + obj.should_not_receive(:to_int) + + -> { @bignum | obj }.should raise_error(TypeError) + end + end +end diff --git a/spec/ruby/core/integer/bit_xor_spec.rb b/spec/ruby/core/integer/bit_xor_spec.rb new file mode 100644 index 0000000000..1f46bc52f3 --- /dev/null +++ b/spec/ruby/core/integer/bit_xor_spec.rb @@ -0,0 +1,93 @@ +require_relative '../../spec_helper' + +describe "Integer#^" do + context "fixnum" do + it "returns self bitwise EXCLUSIVE OR other" do + (3 ^ 5).should == 6 + (-2 ^ -255).should == 255 + (5 ^ bignum_value + 0xffff_ffff).should == 0x1_0000_0000_ffff_fffa + end + + it "returns self bitwise XOR other when one operand is negative" do + ((1 << 33) ^ -1).should == -8589934593 + (-1 ^ (1 << 33)).should == -8589934593 + + ((-(1<<33)-1) ^ 5).should == -8589934598 + (5 ^ (-(1<<33)-1)).should == -8589934598 + end + + it "returns self bitwise XOR other when both operands are negative" do + (-5 ^ -1).should == 4 + (-3 ^ -4).should == 1 + (-12 ^ -13).should == 7 + (-13 ^ -12).should == 7 + end + + it "returns self bitwise EXCLUSIVE OR a bignum" do + (-1 ^ 2**64).should == -18446744073709551617 + end + + it "coerces the rhs and calls #coerce" do + obj = mock("fixnum bit xor") + obj.should_receive(:coerce).with(6).and_return([6, 3]) + (6 ^ obj).should == 5 + end + + it "raises a TypeError when passed a Float" do + -> { (3 ^ 3.4) }.should raise_error(TypeError) + end + + it "raises a TypeError and does not call #to_int when defined on an object" do + obj = mock("integer bit xor") + obj.should_not_receive(:to_int) + + -> { 3 ^ obj }.should raise_error(TypeError) + end + end + + context "bignum" do + before :each do + @bignum = bignum_value(18) + end + + it "returns self bitwise EXCLUSIVE OR other" do + (@bignum ^ 2).should == 18446744073709551632 + (@bignum ^ @bignum).should == 0 + (@bignum ^ 14).should == 18446744073709551644 + end + + it "returns self bitwise EXCLUSIVE OR other when one operand is negative" do + (@bignum ^ -0x40000000000000000).should == -55340232221128654830 + (@bignum ^ -@bignum).should == -4 + (@bignum ^ -0x8000000000000000).should == -27670116110564327406 + end + + it "returns self bitwise EXCLUSIVE OR other when both operands are negative" do + (-@bignum ^ -0x40000000000000000).should == 55340232221128654830 + (-@bignum ^ -@bignum).should == 0 + (-@bignum ^ -0x4000000000000000).should == 23058430092136939502 + end + + it "returns self bitwise EXCLUSIVE OR other when all bits are 1 and other value is negative" do + (9903520314283042199192993791 ^ -1).should == -9903520314283042199192993792 + (784637716923335095479473677900958302012794430558004314111 ^ -1).should == + -784637716923335095479473677900958302012794430558004314112 + end + + it "raises a TypeError when passed a Float" do + not_supported_on :opal do + -> { + bignum_value ^ bignum_value(0xffff).to_f + }.should raise_error(TypeError) + end + -> { @bignum ^ 14.5 }.should raise_error(TypeError) + end + + it "raises a TypeError and does not call #to_int when defined on an object" do + obj = mock("bignum bit xor") + obj.should_not_receive(:to_int) + + -> { @bignum ^ obj }.should raise_error(TypeError) + end + end +end diff --git a/spec/ruby/core/integer/case_compare_spec.rb b/spec/ruby/core/integer/case_compare_spec.rb new file mode 100644 index 0000000000..e5dde2c64a --- /dev/null +++ b/spec/ruby/core/integer/case_compare_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/equal' + +describe "Integer#===" do + it_behaves_like :integer_equal, :=== +end diff --git a/spec/ruby/core/integer/ceil_spec.rb b/spec/ruby/core/integer/ceil_spec.rb new file mode 100644 index 0000000000..eb633fba78 --- /dev/null +++ b/spec/ruby/core/integer/ceil_spec.rb @@ -0,0 +1,24 @@ +require_relative '../../spec_helper' +require_relative 'shared/to_i' +require_relative 'shared/integer_rounding' +require_relative 'shared/integer_ceil_precision' + +describe "Integer#ceil" do + it_behaves_like :integer_to_i, :ceil + it_behaves_like :integer_rounding_positive_precision, :ceil + + context "with precision" do + it_behaves_like :integer_ceil_precision, :Integer + end + + context "precision argument specified as part of the ceil method is negative" do + it "returns the smallest integer greater than self with at least precision.abs trailing zeros" do + 18.ceil(-1).should eql(20) + 18.ceil(-2).should eql(100) + 18.ceil(-3).should eql(1000) + -1832.ceil(-1).should eql(-1830) + -1832.ceil(-2).should eql(-1800) + -1832.ceil(-3).should eql(-1000) + end + end +end diff --git a/spec/ruby/core/integer/ceildiv_spec.rb b/spec/ruby/core/integer/ceildiv_spec.rb new file mode 100644 index 0000000000..c6e22a457d --- /dev/null +++ b/spec/ruby/core/integer/ceildiv_spec.rb @@ -0,0 +1,20 @@ +require_relative '../../spec_helper' + +describe "Integer#ceildiv" do + it "returns a quotient of division which is rounded up to the nearest integer" do + 0.ceildiv(3).should eql(0) + 1.ceildiv(3).should eql(1) + 3.ceildiv(3).should eql(1) + 4.ceildiv(3).should eql(2) + + 4.ceildiv(-3).should eql(-1) + -4.ceildiv(3).should eql(-1) + -4.ceildiv(-3).should eql(2) + + 3.ceildiv(1.2).should eql(3) + 3.ceildiv(6/5r).should eql(3) + + (10**100-11).ceildiv(10**99-1).should eql(10) + (10**100-9).ceildiv(10**99-1).should eql(11) + end +end diff --git a/spec/ruby/core/integer/chr_spec.rb b/spec/ruby/core/integer/chr_spec.rb new file mode 100644 index 0000000000..39cafe2874 --- /dev/null +++ b/spec/ruby/core/integer/chr_spec.rb @@ -0,0 +1,257 @@ +require_relative '../../spec_helper' + +describe "Integer#chr without argument" do + it "returns a String" do + 17.chr.should be_an_instance_of(String) + end + + it "returns a new String for each call" do + 82.chr.should_not equal(82.chr) + end + + it "raises a RangeError is self is less than 0" do + -> { -1.chr }.should raise_error(RangeError, /-1 out of char range/) + -> { (-bignum_value).chr }.should raise_error(RangeError, /bignum out of char range/) + end + + it "raises a RangeError if self is too large" do + -> { 2206368128.chr(Encoding::UTF_8) }.should raise_error(RangeError, /2206368128 out of char range/) + end + + describe "when Encoding.default_internal is nil" do + describe "and self is between 0 and 127 (inclusive)" do + it "returns a US-ASCII String" do + (0..127).each do |c| + c.chr.encoding.should == Encoding::US_ASCII + end + end + + it "returns a String encoding self interpreted as a US-ASCII codepoint" do + (0..127).each do |c| + c.chr.bytes.to_a.should == [c] + end + end + end + + describe "and self is between 128 and 255 (inclusive)" do + it "returns a binary String" do + (128..255).each do |c| + c.chr.encoding.should == Encoding::BINARY + end + end + + it "returns a String containing self interpreted as a byte" do + (128..255).each do |c| + c.chr.bytes.to_a.should == [c] + end + end + end + + it "raises a RangeError is self is greater than 255" do + -> { 256.chr }.should raise_error(RangeError, /256 out of char range/) + -> { bignum_value.chr }.should raise_error(RangeError, /bignum out of char range/) + end + end + + describe "when Encoding.default_internal is not nil" do + before do + @default_internal = Encoding.default_internal + end + + after do + Encoding.default_internal = @default_internal + end + + describe "and self is between 0 and 127 (inclusive)" do + it "returns a US-ASCII String" do + (0..127).each do |c| + Encoding.default_internal = Encoding::UTF_8 + c.chr.encoding.should == Encoding::US_ASCII + + Encoding.default_internal = Encoding::SHIFT_JIS + c.chr.encoding.should == Encoding::US_ASCII + end + end + + it "returns a String encoding self interpreted as a US-ASCII codepoint" do + (0..127).each do |c| + Encoding.default_internal = Encoding::UTF_8 + c.chr.bytes.to_a.should == [c] + + Encoding.default_internal = Encoding::SHIFT_JIS + c.chr.bytes.to_a.should == [c] + end + end + end + + describe "and self is between 128 and 255 (inclusive)" do + it "returns a binary String" do + (128..255).each do |c| + Encoding.default_internal = Encoding::UTF_8 + c.chr.encoding.should == Encoding::BINARY + + Encoding.default_internal = Encoding::SHIFT_JIS + c.chr.encoding.should == Encoding::BINARY + end + end + + it "returns a String containing self interpreted as a byte" do + (128..255).each do |c| + Encoding.default_internal = Encoding::UTF_8 + c.chr.bytes.to_a.should == [c] + + Encoding.default_internal = Encoding::SHIFT_JIS + c.chr.bytes.to_a.should == [c] + end + end + end + + describe "and self is greater than 255" do + it "returns a String with the default internal encoding" do + Encoding.default_internal = Encoding::UTF_8 + 0x0100.chr.encoding.should == Encoding::UTF_8 + 0x3000.chr.encoding.should == Encoding::UTF_8 + + Encoding.default_internal = Encoding::SHIFT_JIS + 0x8140.chr.encoding.should == Encoding::SHIFT_JIS + 0xFC4B.chr.encoding.should == Encoding::SHIFT_JIS + end + + it "returns a String encoding self interpreted as a codepoint in the default internal encoding" do + Encoding.default_internal = Encoding::UTF_8 + 0x0100.chr.bytes.to_a.should == [0xC4, 0x80] + 0x3000.chr.bytes.to_a.should == [0xE3, 0x80, 0x80] + + Encoding.default_internal = Encoding::SHIFT_JIS + 0x8140.chr.bytes.to_a.should == [0x81, 0x40] # Smallest assigned CP932 codepoint greater than 255 + 0xFC4B.chr.bytes.to_a.should == [0xFC, 0x4B] # Largest assigned CP932 codepoint + end + + # #5864 + it "raises RangeError if self is invalid as a codepoint in the default internal encoding" do + [ [0x0100, "US-ASCII"], + [0x0100, "BINARY"], + [0x0100, "EUC-JP"], + [0xA1A0, "EUC-JP"], + [0x0100, "ISO-8859-9"], + [620, "TIS-620"] + ].each do |integer, encoding_name| + Encoding.default_internal = Encoding.find(encoding_name) + -> { integer.chr }.should raise_error(RangeError, /(invalid codepoint|out of char range)/) + end + end + end + end +end + +describe "Integer#chr with an encoding argument" do + it "returns a String" do + 900.chr(Encoding::UTF_8).should be_an_instance_of(String) + end + + it "returns a new String for each call" do + 8287.chr(Encoding::UTF_8).should_not equal(8287.chr(Encoding::UTF_8)) + end + + it "accepts a String as an argument" do + -> { 0xA4A2.chr('euc-jp') }.should_not raise_error + end + + it "converts a String to an Encoding as Encoding.find does" do + ['utf-8', 'UTF-8', 'Utf-8'].each do |encoding| + 7894.chr(encoding).encoding.should == Encoding::UTF_8 + end + end + + # http://redmine.ruby-lang.org/issues/4869 + it "raises a RangeError is self is less than 0" do + -> { -1.chr(Encoding::UTF_8) }.should raise_error(RangeError, /-1 out of char range/) + -> { (-bignum_value).chr(Encoding::EUC_JP) }.should raise_error(RangeError, /bignum out of char range/) + end + + it "raises a RangeError if self is too large" do + -> { 2206368128.chr(Encoding::UTF_8) }.should raise_error(RangeError, /2206368128 out of char range/) + end + + it "returns a String with the specified encoding" do + 0x0000.chr(Encoding::US_ASCII).encoding.should == Encoding::US_ASCII + 0x007F.chr(Encoding::US_ASCII).encoding.should == Encoding::US_ASCII + + 0x0000.chr(Encoding::BINARY).encoding.should == Encoding::BINARY + 0x007F.chr(Encoding::BINARY).encoding.should == Encoding::BINARY + 0x0080.chr(Encoding::BINARY).encoding.should == Encoding::BINARY + 0x00FF.chr(Encoding::BINARY).encoding.should == Encoding::BINARY + + 0x0000.chr(Encoding::UTF_8).encoding.should == Encoding::UTF_8 + 0x007F.chr(Encoding::UTF_8).encoding.should == Encoding::UTF_8 + 0x0080.chr(Encoding::UTF_8).encoding.should == Encoding::UTF_8 + 0x00FF.chr(Encoding::UTF_8).encoding.should == Encoding::UTF_8 + 0x0100.chr(Encoding::UTF_8).encoding.should == Encoding::UTF_8 + 0x3000.chr(Encoding::UTF_8).encoding.should == Encoding::UTF_8 + + 0x0000.chr(Encoding::SHIFT_JIS).encoding.should == Encoding::SHIFT_JIS + 0x007F.chr(Encoding::SHIFT_JIS).encoding.should == Encoding::SHIFT_JIS + 0x00A1.chr(Encoding::SHIFT_JIS).encoding.should == Encoding::SHIFT_JIS + 0x00DF.chr(Encoding::SHIFT_JIS).encoding.should == Encoding::SHIFT_JIS + 0x8140.chr(Encoding::SHIFT_JIS).encoding.should == Encoding::SHIFT_JIS + 0xFC4B.chr(Encoding::SHIFT_JIS).encoding.should == Encoding::SHIFT_JIS + end + + it "returns a String encoding self interpreted as a codepoint in the specified encoding" do + 0x0000.chr(Encoding::US_ASCII).bytes.to_a.should == [0x00] + 0x007F.chr(Encoding::US_ASCII).bytes.to_a.should == [0x7F] + + 0x0000.chr(Encoding::BINARY).bytes.to_a.should == [0x00] + 0x007F.chr(Encoding::BINARY).bytes.to_a.should == [0x7F] + 0x0080.chr(Encoding::BINARY).bytes.to_a.should == [0x80] + 0x00FF.chr(Encoding::BINARY).bytes.to_a.should == [0xFF] + + 0x0000.chr(Encoding::UTF_8).bytes.to_a.should == [0x00] + 0x007F.chr(Encoding::UTF_8).bytes.to_a.should == [0x7F] + 0x0080.chr(Encoding::UTF_8).bytes.to_a.should == [0xC2, 0x80] + 0x00FF.chr(Encoding::UTF_8).bytes.to_a.should == [0xC3, 0xBF] + 0x0100.chr(Encoding::UTF_8).bytes.to_a.should == [0xC4, 0x80] + 0x3000.chr(Encoding::UTF_8).bytes.to_a.should == [0xE3, 0x80, 0x80] + + 0x0000.chr(Encoding::SHIFT_JIS).bytes.to_a.should == [0x00] + 0x007F.chr(Encoding::SHIFT_JIS).bytes.to_a.should == [0x7F] + 0x00A1.chr(Encoding::SHIFT_JIS).bytes.to_a.should == [0xA1] + 0x00DF.chr(Encoding::SHIFT_JIS).bytes.to_a.should == [0xDF] + 0x8140.chr(Encoding::SHIFT_JIS).bytes.to_a.should == [0x81, 0x40] # Smallest assigned CP932 codepoint greater than 255 + 0xFC4B.chr(Encoding::SHIFT_JIS).bytes.to_a.should == [0xFC, 0x4B] # Largest assigned CP932 codepoint + end + + # #5864 + it "raises RangeError if self is invalid as a codepoint in the specified encoding" do + -> { 0x80.chr("US-ASCII") }.should raise_error(RangeError) + -> { 0x0100.chr("BINARY") }.should raise_error(RangeError) + -> { 0x0100.chr("EUC-JP") }.should raise_error(RangeError) + -> { 0xA1A0.chr("EUC-JP") }.should raise_error(RangeError) + -> { 0xA1.chr("EUC-JP") }.should raise_error(RangeError) + -> { 0x80.chr("SHIFT_JIS") }.should raise_error(RangeError) + -> { 0xE0.chr("SHIFT_JIS") }.should raise_error(RangeError) + -> { 0x0100.chr("ISO-8859-9") }.should raise_error(RangeError) + -> { 620.chr("TIS-620") }.should raise_error(RangeError) + # UTF-16 surrogate range + -> { 0xD800.chr("UTF-8") }.should raise_error(RangeError) + -> { 0xDBFF.chr("UTF-8") }.should raise_error(RangeError) + -> { 0xDC00.chr("UTF-8") }.should raise_error(RangeError) + -> { 0xDFFF.chr("UTF-8") }.should raise_error(RangeError) + # UTF-16 surrogate range + -> { 0xD800.chr("UTF-16") }.should raise_error(RangeError) + -> { 0xDBFF.chr("UTF-16") }.should raise_error(RangeError) + -> { 0xDC00.chr("UTF-16") }.should raise_error(RangeError) + -> { 0xDFFF.chr("UTF-16") }.should raise_error(RangeError) + end + + it 'returns a String encoding self interpreted as a codepoint in the CESU-8 encoding' do + # see more details here https://en.wikipedia.org/wiki/CESU-8 + # code points from U+0000 to U+FFFF is encoded in the same way as in UTF-8 + 0x0045.chr(Encoding::CESU_8).bytes.should == 0x0045.chr(Encoding::UTF_8).bytes + + # code points in range from U+10000 to U+10FFFF is CESU-8 data containing a 6-byte surrogate pair, + # which decodes to a 4-byte UTF-8 string + 0x10400.chr(Encoding::CESU_8).bytes.should != 0x10400.chr(Encoding::UTF_8).bytes + 0x10400.chr(Encoding::CESU_8).bytes.to_a.should == [0xED, 0xA0, 0x81, 0xED, 0xB0, 0x80] + end +end diff --git a/spec/ruby/core/integer/coerce_spec.rb b/spec/ruby/core/integer/coerce_spec.rb new file mode 100644 index 0000000000..1d6dc9713f --- /dev/null +++ b/spec/ruby/core/integer/coerce_spec.rb @@ -0,0 +1,91 @@ +require_relative '../../spec_helper' + +describe "Integer#coerce" do + context "fixnum" do + describe "when given a Fixnum" do + it "returns an array containing two Fixnums" do + 1.coerce(2).should == [2, 1] + 1.coerce(2).map { |i| i.class }.should == [Integer, Integer] + end + end + + describe "when given a String" do + it "raises an ArgumentError when trying to coerce with a non-number String" do + -> { 1.coerce(":)") }.should raise_error(ArgumentError) + end + + it "returns an array containing two Floats" do + 1.coerce("2").should == [2.0, 1.0] + 1.coerce("-2").should == [-2.0, 1.0] + end + end + + it "raises a TypeError when trying to coerce with nil" do + -> { 1.coerce(nil) }.should raise_error(TypeError) + end + + it "tries to convert the given Object into a Float by using #to_f" do + (obj = mock('1.0')).should_receive(:to_f).and_return(1.0) + 2.coerce(obj).should == [1.0, 2.0] + + (obj = mock('0')).should_receive(:to_f).and_return('0') + -> { 2.coerce(obj).should == [1.0, 2.0] }.should raise_error(TypeError) + end + + it "raises a TypeError when given an Object that does not respond to #to_f" do + -> { 1.coerce(mock('x')) }.should raise_error(TypeError) + -> { 1.coerce(1..4) }.should raise_error(TypeError) + -> { 1.coerce(:test) }.should raise_error(TypeError) + end + end + + context "bignum" do + it "coerces other to a Bignum and returns [other, self] when passed a Fixnum" do + a = bignum_value + ary = a.coerce(2) + + ary[0].should be_kind_of(Integer) + ary[1].should be_kind_of(Integer) + ary.should == [2, a] + end + + it "returns [other, self] when passed a Bignum" do + a = bignum_value + b = bignum_value + ary = a.coerce(b) + + ary[0].should be_kind_of(Integer) + ary[1].should be_kind_of(Integer) + ary.should == [b, a] + end + + it "raises a TypeError when not passed a Fixnum or Bignum" do + a = bignum_value + + -> { a.coerce(nil) }.should raise_error(TypeError) + -> { a.coerce(mock('str')) }.should raise_error(TypeError) + -> { a.coerce(1..4) }.should raise_error(TypeError) + -> { a.coerce(:test) }.should raise_error(TypeError) + end + + it "coerces both values to Floats and returns [other, self] when passed a Float" do + a = bignum_value + a.coerce(1.2).should == [1.2, a.to_f] + end + + it "coerces both values to Floats and returns [other, self] when passed a String" do + a = bignum_value + a.coerce("123").should == [123.0, a.to_f] + end + + it "calls #to_f to coerce other to a Float" do + b = mock("bignum value") + b.should_receive(:to_f).and_return(1.2) + + a = bignum_value + ary = a.coerce(b) + + ary.should == [1.2, a.to_f] + end + end +end diff --git a/spec/ruby/core/integer/comparison_spec.rb b/spec/ruby/core/integer/comparison_spec.rb new file mode 100644 index 0000000000..408c8e52ce --- /dev/null +++ b/spec/ruby/core/integer/comparison_spec.rb @@ -0,0 +1,177 @@ +require_relative '../../spec_helper' + +describe "Integer#<=>" do + context "fixnum" do + it "returns -1 when self is less than the given argument" do + (-3 <=> -1).should == -1 + (-5 <=> 10).should == -1 + (-5 <=> -4.5).should == -1 + end + + it "returns 0 when self is equal to the given argument" do + (0 <=> 0).should == 0 + (954 <=> 954).should == 0 + (954 <=> 954.0).should == 0 + end + + it "returns 1 when self is greater than the given argument" do + (496 <=> 5).should == 1 + (200 <=> 100).should == 1 + (51 <=> 50.5).should == 1 + end + + it "returns nil when the given argument is not an Integer" do + (3 <=> mock('x')).should == nil + (3 <=> 'test').should == nil + end + end + + context "bignum" do + describe "with a Fixnum" do + it "returns -1 when other is larger" do + (-bignum_value <=> 2).should == -1 + end + + it "returns 1 when other is smaller" do + (bignum_value <=> 2).should == 1 + end + end + + describe "with a Bignum" do + describe "when other is negative" do + it "returns -1 when self is negative and other is larger" do + (-bignum_value(42) <=> -bignum_value).should == -1 + end + + it "returns 0 when other is equal" do + (-bignum_value <=> -bignum_value).should == 0 + end + + it "returns 1 when self is negative and other is smaller" do + (-bignum_value <=> -bignum_value(94)).should == 1 + end + + it "returns 1 when self is positive" do + (bignum_value <=> -bignum_value).should == 1 + end + end + + describe "when other is positive" do + it "returns -1 when self is negative" do + (-bignum_value <=> bignum_value).should == -1 + end + + it "returns -1 when self is positive and other is larger" do + (bignum_value <=> bignum_value(38)).should == -1 + end + + it "returns 0 when other is equal" do + (bignum_value <=> bignum_value).should == 0 + end + + it "returns 1 when other is smaller" do + (bignum_value(56) <=> bignum_value).should == 1 + end + end + end + + describe "with a Float" do + describe "when other is negative" do + it "returns -1 when self is negative and other is larger" do + (-bignum_value(0xffff) <=> -bignum_value.to_f).should == -1 + end + + it "returns 0 when other is equal" do + (-bignum_value <=> -bignum_value.to_f).should == 0 + end + + it "returns 1 when self is negative and other is smaller" do + (-bignum_value <=> -bignum_value(0xffef).to_f).should == 1 + end + + it "returns 1 when self is positive" do + (bignum_value <=> -bignum_value.to_f).should == 1 + end + end + + describe "when other is positive" do + it "returns -1 when self is negative" do + (-bignum_value <=> bignum_value.to_f).should == -1 + end + + it "returns -1 when self is positive and other is larger" do + (bignum_value <=> bignum_value(0xfffe).to_f).should == -1 + end + + it "returns 0 when other is equal" do + (bignum_value <=> bignum_value.to_f).should == 0 + end + + it "returns 1 when other is smaller" do + (bignum_value(0xfeff) <=> bignum_value.to_f).should == 1 + end + end + end + + describe "with an Object" do + before :each do + @big = bignum_value + @num = mock("value for Integer#<=>") + end + + it "calls #coerce on other" do + @num.should_receive(:coerce).with(@big).and_return([@big.to_f, 2.5]) + @big <=> @num + end + + it "lets the exception go through if #coerce raises an exception" do + @num.should_receive(:coerce).with(@big).and_raise(RuntimeError.new("my error")) + -> { + @big <=> @num + }.should raise_error(RuntimeError, "my error") + end + + it "raises an exception if #coerce raises a non-StandardError exception" do + @num.should_receive(:coerce).with(@big).and_raise(Exception) + -> { @big <=> @num }.should raise_error(Exception) + end + + it "returns nil if #coerce does not return an Array" do + @num.should_receive(:coerce).with(@big).and_return(nil) + (@big <=> @num).should be_nil + end + + it "returns -1 if the coerced value is larger" do + @num.should_receive(:coerce).with(@big).and_return([@big, bignum_value(10)]) + (@big <=> @num).should == -1 + end + + it "returns 0 if the coerced value is equal" do + @num.should_receive(:coerce).with(@big).and_return([@big, bignum_value]) + (@big <=> @num).should == 0 + end + + it "returns 1 if the coerced value is smaller" do + @num.should_receive(:coerce).with(@big).and_return([@big, 22]) + (@big <=> @num).should == 1 + end + end + + # The tests below are taken from matz's revision 23730 for Ruby trunk + it "returns 1 when self is Infinity and other is a Bignum" do + (infinity_value <=> Float::MAX.to_i*2).should == 1 + end + + it "returns -1 when self is negative and other is Infinity" do + (-Float::MAX.to_i*2 <=> infinity_value).should == -1 + end + + it "returns 1 when self is negative and other is -Infinity" do + (-Float::MAX.to_i*2 <=> -infinity_value).should == 1 + end + + it "returns -1 when self is -Infinity and other is negative" do + (-infinity_value <=> -Float::MAX.to_i*2).should == -1 + end + end +end diff --git a/spec/ruby/core/integer/complement_spec.rb b/spec/ruby/core/integer/complement_spec.rb new file mode 100644 index 0000000000..baf5e6c457 --- /dev/null +++ b/spec/ruby/core/integer/complement_spec.rb @@ -0,0 +1,20 @@ +require_relative '../../spec_helper' + +describe "Integer#~" do + context "fixnum" do + it "returns self with each bit flipped" do + (~0).should == -1 + (~1221).should == -1222 + (~-2).should == 1 + (~-599).should == 598 + end + end + + context "bignum" do + it "returns self with each bit flipped" do + (~bignum_value(48)).should == -18446744073709551665 + (~(-bignum_value(21))).should == 18446744073709551636 + (~bignum_value(1)).should == -18446744073709551618 + end + end +end diff --git a/spec/ruby/core/integer/constants_spec.rb b/spec/ruby/core/integer/constants_spec.rb new file mode 100644 index 0000000000..937806c72f --- /dev/null +++ b/spec/ruby/core/integer/constants_spec.rb @@ -0,0 +1,13 @@ +require_relative '../../spec_helper' + +describe "Fixnum" do + it "is no longer defined" do + Object.should_not.const_defined?(:Fixnum) + end +end + +describe "Bignum" do + it "is no longer defined" do + Object.should_not.const_defined?(:Bignum) + end +end diff --git a/spec/ruby/core/integer/denominator_spec.rb b/spec/ruby/core/integer/denominator_spec.rb new file mode 100644 index 0000000000..c1477d0757 --- /dev/null +++ b/spec/ruby/core/integer/denominator_spec.rb @@ -0,0 +1,20 @@ +require_relative '../../spec_helper' + +describe "Integer#denominator" do + # The Numeric child classes override this method, so their behaviour is + # specified in the appropriate place + before :each do + @numbers = [ + 20, # Integer + -2709, # Negative Integer + 99999999**99, # Bignum + -99999**621, # Negative BigNum + 0, + 1 + ] + end + + it "returns 1" do + @numbers.each {|number| number.denominator.should == 1} + end +end diff --git a/spec/ruby/core/integer/digits_spec.rb b/spec/ruby/core/integer/digits_spec.rb new file mode 100644 index 0000000000..3d0a64c25f --- /dev/null +++ b/spec/ruby/core/integer/digits_spec.rb @@ -0,0 +1,41 @@ +require_relative '../../spec_helper' + +describe "Integer#digits" do + it "returns an array of place values in base-10 by default" do + 12345.digits.should == [5,4,3,2,1] + end + + it "returns digits by place value of a given radix" do + 12345.digits(7).should == [4,6,6,0,5] + end + + it "converts the radix with #to_int" do + 12345.digits(mock_int(2)).should == [1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1] + end + + it "returns [0] when called on 0, regardless of base" do + 0.digits.should == [0] + 0.digits(7).should == [0] + end + + it "raises ArgumentError when calling with a radix less than 2" do + -> { 12345.digits(1) }.should raise_error(ArgumentError) + end + + it "raises ArgumentError when calling with a negative radix" do + -> { 12345.digits(-2) }.should raise_error(ArgumentError) + end + + it "raises Math::DomainError when calling digits on a negative number" do + -> { -12345.digits(7) }.should raise_error(Math::DomainError) + end + + it "returns integer values > 9 when base is above 10" do + 1234.digits(16).should == [2, 13, 4] + end + + it "can be used with base > 37" do + 1234.digits(100).should == [34, 12] + 980099.digits(100).should == [99, 0, 98] + end +end diff --git a/spec/ruby/core/integer/div_spec.rb b/spec/ruby/core/integer/div_spec.rb new file mode 100644 index 0000000000..2eb9c0623b --- /dev/null +++ b/spec/ruby/core/integer/div_spec.rb @@ -0,0 +1,154 @@ +require_relative '../../spec_helper' + +describe "Integer#div" do + context "fixnum" do + it "returns self divided by the given argument as an Integer" do + 2.div(2).should == 1 + 1.div(2).should == 0 + 5.div(2).should == 2 + end + + it "rounds towards -inf" do + 8192.div(10).should == 819 + 8192.div(-10).should == -820 + (-8192).div(10).should == -820 + (-8192).div(-10).should == 819 + end + + it "means (x / y).floor" do + 5.div(2).should == (5 / 2).floor + 5.div(2.0).should == (5 / 2.0).floor + 5.div(-2).should == (5 / -2).floor + + 5.div(100).should == (5 / 100).floor + 5.div(100.0).should == (5 / 100.0).floor + 5.div(-100).should == (5 / -100).floor + end + + it "calls #coerce and #div if argument responds to #coerce" do + x = mock("x") + y = mock("y") + result = mock("result") + + y.should_receive(:coerce).and_return([x, y]) + x.should_receive(:div).with(y).and_return(result) + + 10.div(y).should == result + end + + it "coerces self and the given argument to Floats and returns self divided by other as Integer" do + 1.div(0.2).should == 5 + 1.div(0.16).should == 6 + 1.div(0.169).should == 5 + -1.div(50.4).should == -1 + 1.div(bignum_value).should == 0 + 1.div(Rational(1, 5)).should == 5 + end + + it "raises a ZeroDivisionError when the given argument is 0 and a Float" do + -> { 0.div(0.0) }.should raise_error(ZeroDivisionError) + -> { 10.div(0.0) }.should raise_error(ZeroDivisionError) + -> { -10.div(0.0) }.should raise_error(ZeroDivisionError) + end + + it "raises a ZeroDivisionError when the given argument is 0 and not a Float" do + -> { 13.div(0) }.should raise_error(ZeroDivisionError) + -> { 13.div(-0) }.should raise_error(ZeroDivisionError) + end + + it "raises a TypeError when given a non-numeric argument" do + -> { 13.div(mock('10')) }.should raise_error(TypeError) + -> { 5.div("2") }.should raise_error(TypeError) + -> { 5.div(:"2") }.should raise_error(TypeError) + -> { 5.div([]) }.should raise_error(TypeError) + end + end + + context "bignum" do + before :each do + @bignum = bignum_value(88) + end + + it "returns self divided by other" do + @bignum.div(4).should == 4611686018427387926 + @bignum.div(Rational(4, 1)).should == 4611686018427387926 + @bignum.div(bignum_value(2)).should == 1 + + (-(10**50)).div(-(10**40 + 1)).should == 9999999999 + (10**50).div(10**40 + 1).should == 9999999999 + + (-10**50).div(10**40 + 1).should == -10000000000 + (10**50).div(-(10**40 + 1)).should == -10000000000 + end + + it "handles fixnum_min / -1" do + (fixnum_min / -1).should == -fixnum_min + (fixnum_min / -1).should > 0 + + int_min = -2147483648 + (int_min / -1).should == 2147483648 + end + + it "calls #coerce and #div if argument responds to #coerce" do + x = mock("x") + y = mock("y") + result = mock("result") + + y.should_receive(:coerce).and_return([x, y]) + x.should_receive(:div).with(y).and_return(result) + + @bignum.div(y).should == result + end + + it "means (x / y).floor" do + @bignum.div(2).should == (@bignum / 2).floor + @bignum.div(-2).should == (@bignum / -2).floor + + @bignum.div(@bignum+1).should == (@bignum / (@bignum+1)).floor + @bignum.div(-(@bignum+1)).should == (@bignum / -(@bignum+1)).floor + + @bignum.div(2.0).should == (@bignum / 2.0).floor + @bignum.div(100.0).should == (@bignum / 100.0).floor + end + + it "looses precision if passed Float argument" do + @bignum.div(1).should_not == @bignum.div(1.0) + @bignum.div(4).should_not == @bignum.div(4.0) + @bignum.div(21).should_not == @bignum.div(21.0) + end + + it "raises a TypeError when given a non-numeric" do + -> { @bignum.div(mock("10")) }.should raise_error(TypeError) + -> { @bignum.div("2") }.should raise_error(TypeError) + -> { @bignum.div(:symbol) }.should raise_error(TypeError) + end + + it "returns a result of integer division of self by a float argument" do + @bignum.div(4294967295.5).should eql(4294967296) + not_supported_on :opal do + @bignum.div(4294967295.0).should eql(4294967297) + @bignum.div(bignum_value(88).to_f).should eql(1) + @bignum.div((-bignum_value(88)).to_f).should eql(-1) + end + end + + # #5490 + it "raises ZeroDivisionError if the argument is 0 and is a Float" do + -> { @bignum.div(0.0) }.should raise_error(ZeroDivisionError) + -> { @bignum.div(-0.0) }.should raise_error(ZeroDivisionError) + end + + it "raises ZeroDivisionError if the argument is 0 and is not a Float" do + -> { @bignum.div(0) }.should raise_error(ZeroDivisionError) + -> { @bignum.div(-0) }.should raise_error(ZeroDivisionError) + end + end + + context "rational" do + it "returns self divided by the given argument as an Integer" do + 2.div(6/5r).should == 1 + 1.div(6/5r).should == 0 + 5.div(6/5r).should == 4 + end + end +end diff --git a/spec/ruby/core/integer/divide_spec.rb b/spec/ruby/core/integer/divide_spec.rb new file mode 100644 index 0000000000..0d5e16e986 --- /dev/null +++ b/spec/ruby/core/integer/divide_spec.rb @@ -0,0 +1,126 @@ +require_relative '../../spec_helper' +require_relative 'shared/arithmetic_coerce' + +describe "Integer#/" do + it_behaves_like :integer_arithmetic_coerce_not_rescue, :/ + + context "fixnum" do + it "returns self divided by the given argument" do + (2 / 2).should == 1 + (3 / 2).should == 1 + end + + it "supports dividing negative numbers" do + (-1 / 10).should == -1 + (-1 / 10**10).should == -1 + (-1 / 10**20).should == -1 + end + + it "preservers sign correctly" do + (4 / 3).should == 1 + (4 / -3).should == -2 + (-4 / 3).should == -2 + (-4 / -3).should == 1 + (0 / -3).should == 0 + (0 / 3).should == 0 + end + + it "returns result the same class as the argument" do + (3 / 2).should == 1 + (3 / 2.0).should == 1.5 + (3 / Rational(2, 1)).should == Rational(3, 2) + end + + it "raises a ZeroDivisionError if the given argument is zero and not a Float" do + -> { 1 / 0 }.should raise_error(ZeroDivisionError) + end + + it "does NOT raise ZeroDivisionError if the given argument is zero and is a Float" do + (1 / 0.0).to_s.should == 'Infinity' + (-1 / 0.0).to_s.should == '-Infinity' + end + + it "coerces fixnum and return self divided by other" do + (-1 / 50.4).should be_close(-0.0198412698412698, TOLERANCE) + (1 / bignum_value).should == 0 + end + + it "raises a TypeError when given a non-Integer" do + -> { 13 / mock('10') }.should raise_error(TypeError) + -> { 13 / "10" }.should raise_error(TypeError) + -> { 13 / :symbol }.should raise_error(TypeError) + end + end + + context "bignum" do + before :each do + @bignum = bignum_value(88) + end + + it "returns self divided by other" do + (@bignum / 4).should == 4611686018427387926 + + (@bignum / bignum_value(2)).should == 1 + + (-(10**50) / -(10**40 + 1)).should == 9999999999 + ((10**50) / (10**40 + 1)).should == 9999999999 + + ((-10**50) / (10**40 + 1)).should == -10000000000 + ((10**50) / -(10**40 + 1)).should == -10000000000 + end + + it "preservers sign correctly" do + (4 / bignum_value).should == 0 + (4 / -bignum_value).should == -1 + (-4 / bignum_value).should == -1 + (-4 / -bignum_value).should == 0 + (0 / bignum_value).should == 0 + (0 / -bignum_value).should == 0 + end + + it "returns self divided by Float" do + not_supported_on :opal do + (bignum_value(88) / 4294967295.0).should be_close(4294967297.0, TOLERANCE) + end + (bignum_value(88) / 4294967295.5).should be_close(4294967296.5, TOLERANCE) + end + + it "returns result the same class as the argument" do + (@bignum / 4).should == 4611686018427387926 + (@bignum / 4.0).should be_close(4611686018427387926, TOLERANCE) + (@bignum / Rational(4, 1)).should == Rational(4611686018427387926, 1) + end + + it "does NOT raise ZeroDivisionError if other is zero and is a Float" do + (bignum_value / 0.0).to_s.should == 'Infinity' + (bignum_value / -0.0).to_s.should == '-Infinity' + end + + it "raises a ZeroDivisionError if other is zero and not a Float" do + -> { @bignum / 0 }.should raise_error(ZeroDivisionError) + end + + it "raises a TypeError when given a non-numeric" do + -> { @bignum / mock('10') }.should raise_error(TypeError) + -> { @bignum / "2" }.should raise_error(TypeError) + -> { @bignum / :symbol }.should raise_error(TypeError) + end + end + + it "coerces the RHS and calls #coerce" do + obj = mock("integer plus") + obj.should_receive(:coerce).with(6).and_return([6, 3]) + (6 / obj).should == 2 + end + + it "coerces the RHS and calls #coerce even if it's private" do + obj = Object.new + class << obj + private def coerce(n) + [n, 3] + end + end + + (6 / obj).should == 2 + end +end diff --git a/spec/ruby/core/integer/divmod_spec.rb b/spec/ruby/core/integer/divmod_spec.rb new file mode 100644 index 0000000000..7a489e9344 --- /dev/null +++ b/spec/ruby/core/integer/divmod_spec.rb @@ -0,0 +1,117 @@ +require_relative '../../spec_helper' + +describe "Integer#divmod" do + context "fixnum" do + it "returns an Array containing quotient and modulus obtained from dividing self by the given argument" do + 13.divmod(4).should == [3, 1] + 4.divmod(13).should == [0, 4] + + 13.divmod(4.0).should == [3, 1] + 4.divmod(13.0).should == [0, 4] + + 1.divmod(2.0).should == [0, 1.0] + 200.divmod(bignum_value).should == [0, 200] + end + + it "raises a ZeroDivisionError when the given argument is 0" do + -> { 13.divmod(0) }.should raise_error(ZeroDivisionError) + -> { 0.divmod(0) }.should raise_error(ZeroDivisionError) + -> { -10.divmod(0) }.should raise_error(ZeroDivisionError) + end + + it "raises a ZeroDivisionError when the given argument is 0 and a Float" do + -> { 0.divmod(0.0) }.should raise_error(ZeroDivisionError) + -> { 10.divmod(0.0) }.should raise_error(ZeroDivisionError) + -> { -10.divmod(0.0) }.should raise_error(ZeroDivisionError) + end + + it "raises a TypeError when given a non-Integer" do + -> { + (obj = mock('10')).should_receive(:to_int).any_number_of_times.and_return(10) + 13.divmod(obj) + }.should raise_error(TypeError) + -> { 13.divmod("10") }.should raise_error(TypeError) + -> { 13.divmod(:symbol) }.should raise_error(TypeError) + end + end + + context "bignum" do + before :each do + @bignum = bignum_value(55) + end + + # Based on MRI's test/test_integer.rb (test_divmod), + # MRI maintains the following property: + # if q, r = a.divmod(b) ==> + # assert(0 < b ? (0 <= r && r < b) : (b < r && r <= 0)) + # So, r is always between 0 and b. + it "returns an Array containing quotient and modulus obtained from dividing self by the given argument" do + @bignum.divmod(4).should == [4611686018427387917, 3] + @bignum.divmod(13).should == [1418980313362273205, 6] + + @bignum.divmod(4.5).should == [4099276460824344576, 2.5] + + not_supported_on :opal do + @bignum.divmod(4.0).should == [4611686018427387904, 0.0] + @bignum.divmod(13.0).should == [1418980313362273280, 3.0] + + @bignum.divmod(2.0).should == [9223372036854775808, 0.0] + end + + @bignum.divmod(bignum_value).should == [1, 55] + + (-(10**50)).divmod(-(10**40 + 1)).should == [9999999999, -9999999999999999999999999999990000000001] + (10**50).divmod(10**40 + 1).should == [9999999999, 9999999999999999999999999999990000000001] + + (-10**50).divmod(10**40 + 1).should == [-10000000000, 10000000000] + (10**50).divmod(-(10**40 + 1)).should == [-10000000000, -10000000000] + end + + describe "with q = floor(x/y), a = q*b + r," do + it "returns [q,r] when a < 0, b > 0 and |a| < b" do + a = -@bignum + 1 + b = @bignum + a.divmod(b).should == [-1, 1] + end + + it "returns [q,r] when a > 0, b < 0 and a > |b|" do + b = -@bignum + 1 + a = @bignum + a.divmod(b).should == [-2, -@bignum + 2] + end + + it "returns [q,r] when a > 0, b < 0 and a < |b|" do + a = @bignum - 1 + b = -@bignum + a.divmod(b).should == [-1, -1] + end + + it "returns [q,r] when a < 0, b < 0 and |a| < |b|" do + a = -@bignum + 1 + b = -@bignum + a.divmod(b).should == [0, -@bignum + 1] + end + end + + it "raises a ZeroDivisionError when the given argument is 0" do + -> { @bignum.divmod(0) }.should raise_error(ZeroDivisionError) + -> { (-@bignum).divmod(0) }.should raise_error(ZeroDivisionError) + end + + # Behaviour established as correct in r23953 + it "raises a FloatDomainError if other is NaN" do + -> { @bignum.divmod(nan_value) }.should raise_error(FloatDomainError) + end + + it "raises a ZeroDivisionError when the given argument is 0 and a Float" do + -> { @bignum.divmod(0.0) }.should raise_error(ZeroDivisionError) + -> { (-@bignum).divmod(0.0) }.should raise_error(ZeroDivisionError) + end + + it "raises a TypeError when the given argument is not an Integer" do + -> { @bignum.divmod(mock('10')) }.should raise_error(TypeError) + -> { @bignum.divmod("10") }.should raise_error(TypeError) + -> { @bignum.divmod(:symbol) }.should raise_error(TypeError) + end + end +end diff --git a/spec/ruby/core/integer/downto_spec.rb b/spec/ruby/core/integer/downto_spec.rb new file mode 100644 index 0000000000..987704b02e --- /dev/null +++ b/spec/ruby/core/integer/downto_spec.rb @@ -0,0 +1,69 @@ +require_relative '../../spec_helper' + +describe "Integer#downto [stop] when self and stop are Integers" do + it "does not yield when stop is greater than self" do + result = [] + 5.downto(6) { |x| result << x } + result.should == [] + end + + it "yields once when stop equals self" do + result = [] + 5.downto(5) { |x| result << x } + result.should == [5] + end + + it "yields while decreasing self until it is less than stop" do + result = [] + 5.downto(2) { |x| result << x } + result.should == [5, 4, 3, 2] + end + + it "yields while decreasing self until it less than ceil for a Float endpoint" do + result = [] + 9.downto(1.3) {|i| result << i} + 3.downto(-1.3) {|i| result << i} + result.should == [9, 8, 7, 6, 5, 4, 3, 2, 3, 2, 1, 0, -1] + end + + it "raises an ArgumentError for invalid endpoints" do + -> {1.downto("A") {|x| p x } }.should raise_error(ArgumentError) + -> {1.downto(nil) {|x| p x } }.should raise_error(ArgumentError) + end + + describe "when no block is given" do + it "returns an Enumerator" do + result = [] + + enum = 5.downto(2) + enum.each { |i| result << i } + + result.should == [5, 4, 3, 2] + end + + describe "returned Enumerator" do + describe "size" do + it "raises an ArgumentError for invalid endpoints" do + enum = 1.downto("A") + -> { enum.size }.should raise_error(ArgumentError) + enum = 1.downto(nil) + -> { enum.size }.should raise_error(ArgumentError) + end + + it "returns self - stop + 1" do + 10.downto(5).size.should == 6 + 10.downto(1).size.should == 10 + 10.downto(0).size.should == 11 + 0.downto(0).size.should == 1 + -3.downto(-5).size.should == 3 + end + + it "returns 0 when stop > self" do + 4.downto(5).size.should == 0 + -5.downto(0).size.should == 0 + -5.downto(-3).size.should == 0 + end + end + end + end +end diff --git a/spec/ruby/core/integer/dup_spec.rb b/spec/ruby/core/integer/dup_spec.rb new file mode 100644 index 0000000000..7f4d512465 --- /dev/null +++ b/spec/ruby/core/integer/dup_spec.rb @@ -0,0 +1,13 @@ +require_relative '../../spec_helper' + +describe "Integer#dup" do + it "returns self for small integers" do + integer = 1_000 + integer.dup.should equal(integer) + end + + it "returns self for large integers" do + integer = 4_611_686_018_427_387_905 + integer.dup.should equal(integer) + end +end diff --git a/spec/ruby/core/integer/element_reference_spec.rb b/spec/ruby/core/integer/element_reference_spec.rb new file mode 100644 index 0000000000..cb7e0dc9b0 --- /dev/null +++ b/spec/ruby/core/integer/element_reference_spec.rb @@ -0,0 +1,188 @@ +require_relative '../../spec_helper' + +describe "Integer#[]" do + context "fixnum" do + it "behaves like (n >> b) & 1" do + 0b101[1].should == 0 + 0b101[2].should == 1 + end + + it "returns 1 if the nth bit is set" do + 15[1].should == 1 + end + + it "returns 1 if the nth bit is set (in two's-complement representation)" do + (-1)[1].should == 1 + end + + it "returns 0 if the nth bit is not set" do + 8[2].should == 0 + end + + it "returns 0 if the nth bit is not set (in two's-complement representation)" do + (-2)[0].should == 0 + end + + it "returns 0 if the nth bit is greater than the most significant bit" do + 2[3].should == 0 + end + + it "returns 1 if self is negative and the nth bit is greater than the most significant bit" do + (-1)[3].should == 1 + end + + it "returns 0 when passed a negative argument" do + 3[-1].should == 0 + (-1)[-1].should == 0 + end + + it "calls #to_int to convert the argument to an Integer and returns 1 if the nth bit is set" do + obj = mock('1') + obj.should_receive(:to_int).and_return(1) + + 2[obj].should == 1 + end + + it "calls #to_int to convert the argument to an Integer and returns 0 if the nth bit is set" do + obj = mock('0') + obj.should_receive(:to_int).and_return(0) + + 2[obj].should == 0 + end + + it "accepts a Float argument and returns 0 if the bit at the truncated value is not set" do + 13[1.3].should == 0 + end + + it "accepts a Float argument and returns 1 if the bit at the truncated value is set" do + 13[2.1].should == 1 + end + + it "raises a TypeError when passed a String" do + -> { 3["3"] }.should raise_error(TypeError) + end + + it "raises a TypeError when #to_int does not return an Integer" do + obj = mock('asdf') + obj.should_receive(:to_int).and_return("asdf") + -> { 3[obj] }.should raise_error(TypeError) + end + + it "calls #to_int to coerce a String to a Bignum and returns 0" do + obj = mock('bignum value') + obj.should_receive(:to_int).and_return(bignum_value) + + 3[obj].should == 0 + end + + it "returns 0 when passed a Float in the range of a Bignum" do + 3[bignum_value.to_f].should == 0 + end + + context "when index and length passed" do + it "returns specified number of bits from specified position" do + 0b101001101[2, 4].should == 0b0011 + 0b101001101[2, 5].should == 0b10011 + 0b101001101[2, 7].should == 0b1010011 + end + + it "ensures n[i, len] equals to (n >> i) & ((1 << len) - 1)" do + n = 0b101001101; i = 2; len = 4 + n[i, len].should == (n >> i) & ((1 << len) - 1) + end + + it "moves start position to the most significant bits when negative index passed" do + 0b000001[-1, 4].should == 0b10 + 0b000001[-2, 4].should == 0b100 + 0b000001[-3, 4].should == 0b1000 + end + + it "ignores negative length" do + 0b101001101[1, -1].should == 0b10100110 + 0b101001101[2, -1].should == 0b1010011 + 0b101001101[3, -1].should == 0b101001 + + 0b101001101[3, -5].should == 0b101001 + 0b101001101[3, -15].should == 0b101001 + 0b101001101[3, -125].should == 0b101001 + end + end + + context "when range passed" do + it "returns bits specified by range" do + 0b101001101[2..5].should == 0b0011 + 0b101001101[2..6].should == 0b10011 + 0b101001101[2..8].should == 0b1010011 + end + + it "ensures n[i..j] equals to (n >> i) & ((1 << (j - i + 1)) - 1)" do + n = 0b101001101; i = 2; j = 5 + n[i..j].should == (n >> i) & ((1 << (j - i + 1)) - 1) + end + + it "ensures n[i..] equals to (n >> i)" do + eval("0b101001101[3..]").should == 0b101001101 >> 3 + end + + it "moves lower boundary to the most significant bits when negative value passed" do + 0b000001[-1, 4].should == 0b10 + 0b000001[-2, 4].should == 0b100 + 0b000001[-3, 4].should == 0b1000 + end + + it "ignores upper boundary smaller than lower boundary" do + 0b101001101[4..1].should == 0b10100 + 0b101001101[4..2].should == 0b10100 + 0b101001101[-4..-5].should == 0b1010011010000 + end + + it "raises FloatDomainError if any boundary is infinity" do + -> { 0x0001[3..Float::INFINITY] }.should raise_error(FloatDomainError, /Infinity/) + -> { 0x0001[-Float::INFINITY..3] }.should raise_error(FloatDomainError, /-Infinity/) + end + + context "when passed (..i)" do + it "returns 0 if all i bits equal 0" do + eval("0b10000[..1]").should == 0 + eval("0b10000[..2]").should == 0 + eval("0b10000[..3]").should == 0 + end + + it "raises ArgumentError if any of i bit equals 1" do + -> { + eval("0b111110[..3]") + }.should raise_error(ArgumentError, /The beginless range for Integer#\[\] results in infinity/) + end + end + end + end + + context "bignum" do + before :each do + @bignum = bignum_value(4996) + end + + it "returns the nth bit in the binary representation of self" do + @bignum[2].should == 1 + @bignum[9.2].should == 1 + @bignum[21].should == 0 + @bignum[0xffffffff].should == 0 + @bignum[-0xffffffff].should == 0 + end + + it "tries to convert the given argument to an Integer using #to_int" do + @bignum[1.3].should == @bignum[1] + + (obj = mock('2')).should_receive(:to_int).at_least(1).and_return(2) + @bignum[obj].should == 1 + end + + it "raises a TypeError when the given argument can't be converted to Integer" do + obj = mock('asdf') + -> { @bignum[obj] }.should raise_error(TypeError) + + obj.should_receive(:to_int).and_return("asdf") + -> { @bignum[obj] }.should raise_error(TypeError) + end + end +end diff --git a/spec/ruby/core/integer/equal_value_spec.rb b/spec/ruby/core/integer/equal_value_spec.rb new file mode 100644 index 0000000000..67a73713af --- /dev/null +++ b/spec/ruby/core/integer/equal_value_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/equal' + +describe "Integer#==" do + it_behaves_like :integer_equal, :== +end diff --git a/spec/ruby/core/integer/even_spec.rb b/spec/ruby/core/integer/even_spec.rb new file mode 100644 index 0000000000..a314cc6b19 --- /dev/null +++ b/spec/ruby/core/integer/even_spec.rb @@ -0,0 +1,40 @@ +require_relative '../../spec_helper' + +describe "Integer#even?" do + context "fixnum" do + it "returns true for a Fixnum when it is an even number" do + (-2).even?.should be_true + (-1).even?.should be_false + + 0.even?.should be_true + 1.even?.should be_false + 2.even?.should be_true + end + + it "returns true for a Bignum when it is an even number" do + bignum_value(0).even?.should be_true + bignum_value(1).even?.should be_false + + (-bignum_value(0)).even?.should be_true + (-bignum_value(1)).even?.should be_false + end + end + + context "bignum" do + it "returns true if self is even and positive" do + (10000**10).even?.should be_true + end + + it "returns true if self is even and negative" do + (-10000**10).even?.should be_true + end + + it "returns false if self is odd and positive" do + (9879**976).even?.should be_false + end + + it "returns false if self is odd and negative" do + (-9879**976).even?.should be_false + end + end +end diff --git a/spec/ruby/core/integer/exponent_spec.rb b/spec/ruby/core/integer/exponent_spec.rb new file mode 100644 index 0000000000..c610661aff --- /dev/null +++ b/spec/ruby/core/integer/exponent_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/exponent' + +describe "Integer#**" do + it_behaves_like :integer_exponent, :** +end diff --git a/spec/ruby/core/integer/fdiv_spec.rb b/spec/ruby/core/integer/fdiv_spec.rb new file mode 100644 index 0000000000..d9ea2fdf8d --- /dev/null +++ b/spec/ruby/core/integer/fdiv_spec.rb @@ -0,0 +1,100 @@ +require_relative '../../spec_helper' + +describe "Integer#fdiv" do + it "performs floating-point division between self and a fixnum" do + 8.fdiv(7).should be_close(1.14285714285714, TOLERANCE) + end + + it "performs floating-point division between self and a bignum" do + 8.fdiv(bignum_value).should be_close(8.673617379884035e-19, TOLERANCE) + end + + it "performs floating-point division between self bignum and a bignum" do + num = 1000000000000000000000000000000000048148248609680896326399448564623182963452541226153892315137780403285956264146010000000000000000000000000000000000048148248609680896326399448564623182963452541226153892315137780403285956264146010000000000000000000000000000000000048148248609680896326399448564623182963452541226153892315137780403285956264146009 + den = 2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 + num.fdiv(den).should == 500.0 + end + + it "rounds to the correct value for bignums" do + den = 9 * 10**342 + + num = 1 * 10**344 + num.fdiv(den).should == 11.11111111111111 + + num = 1 * 10**343 + num.fdiv(den).should == 1.1111111111111112 + + num = 1 * 10**342 + num.fdiv(den).should == 0.1111111111111111 + + num = 2 * 10**342 + num.fdiv(den).should == 0.2222222222222222 + + num = 3 * 10**342 + num.fdiv(den).should == 0.3333333333333333 + + num = 4 * 10**342 + num.fdiv(den).should == 0.4444444444444444 + + num = 5 * 10**342 + num.fdiv(den).should == 0.5555555555555556 + + num = 6 * 10**342 + num.fdiv(den).should == 0.6666666666666666 + + num = 7 * 10**342 + num.fdiv(den).should == 0.7777777777777778 + + num = 8 * 10**342 + num.fdiv(den).should == 0.8888888888888888 + + num = 9 * 10**342 + num.fdiv(den).should == 1.0 + + num = -5 * 10**342 + num.fdiv(den).should == -0.5555555555555556 + end + + it "rounds to the correct float for bignum denominators" do + 1.fdiv(10**324).should == 0.0 + 1.fdiv(10**323).should == 1.0e-323 + end + + it "performs floating-point division between self and a Float" do + 8.fdiv(9.0).should be_close(0.888888888888889, TOLERANCE) + end + + it "returns NaN when the argument is NaN" do + -1.fdiv(nan_value).nan?.should be_true + 1.fdiv(nan_value).nan?.should be_true + end + + it "returns Infinity when the argument is 0" do + 1.fdiv(0).infinite?.should == 1 + end + + it "returns -Infinity when the argument is 0 and self is negative" do + -1.fdiv(0).infinite?.should == -1 + end + + it "returns Infinity when the argument is 0.0" do + 1.fdiv(0.0).infinite?.should == 1 + end + + it "returns -Infinity when the argument is 0.0 and self is negative" do + -1.fdiv(0.0).infinite?.should == -1 + end + + it "raises a TypeError when argument isn't numeric" do + -> { 1.fdiv(mock('non-numeric')) }.should raise_error(TypeError) + end + + it "raises an ArgumentError when passed multiple arguments" do + -> { 1.fdiv(6,0.2) }.should raise_error(ArgumentError) + end + + it "follows the coercion protocol" do + (obj = mock('10')).should_receive(:coerce).with(1).and_return([1, 10]) + 1.fdiv(obj).should == 0.1 + end +end diff --git a/spec/ruby/core/integer/fixtures/classes.rb b/spec/ruby/core/integer/fixtures/classes.rb new file mode 100644 index 0000000000..6ebfbd1565 --- /dev/null +++ b/spec/ruby/core/integer/fixtures/classes.rb @@ -0,0 +1,4 @@ +module IntegerSpecs + class CoerceError < StandardError + end +end diff --git a/spec/ruby/core/integer/floor_spec.rb b/spec/ruby/core/integer/floor_spec.rb new file mode 100644 index 0000000000..8fb84d58cb --- /dev/null +++ b/spec/ruby/core/integer/floor_spec.rb @@ -0,0 +1,13 @@ +require_relative '../../spec_helper' +require_relative 'shared/to_i' +require_relative 'shared/integer_rounding' +require_relative 'shared/integer_floor_precision' + +describe "Integer#floor" do + it_behaves_like :integer_to_i, :floor + it_behaves_like :integer_rounding_positive_precision, :floor + + context "with precision" do + it_behaves_like :integer_floor_precision, :Integer + end +end diff --git a/spec/ruby/core/integer/gcd_spec.rb b/spec/ruby/core/integer/gcd_spec.rb new file mode 100644 index 0000000000..961f1c5c2e --- /dev/null +++ b/spec/ruby/core/integer/gcd_spec.rb @@ -0,0 +1,69 @@ +require_relative '../../spec_helper' + +describe "Integer#gcd" do + it "returns self if equal to the argument" do + 1.gcd(1).should == 1 + 398.gcd(398).should == 398 + end + + it "returns an Integer" do + 36.gcd(6).should be_kind_of(Integer) + 4.gcd(20981).should be_kind_of(Integer) + end + + it "returns the greatest common divisor of self and argument" do + 10.gcd(5).should == 5 + 200.gcd(20).should == 20 + end + + it "returns a positive integer even if self is negative" do + -12.gcd(6).should == 6 + -100.gcd(100).should == 100 + end + + it "returns a positive integer even if the argument is negative" do + 12.gcd(-6).should == 6 + 100.gcd(-100).should == 100 + end + + it "returns a positive integer even if both self and argument are negative" do + -12.gcd(-6).should == 6 + -100.gcd(-100).should == 100 + end + + it "accepts a Bignum argument" do + bignum = 9999**99 + bignum.should be_kind_of(Integer) + 99.gcd(bignum).should == 99 + end + + it "works if self is a Bignum" do + bignum = 9999**99 + bignum.should be_kind_of(Integer) + bignum.gcd(99).should == 99 + end + + it "doesn't cause an integer overflow" do + [2 ** (1.size * 8 - 2), 0x8000000000000000].each do |max| + [max - 1, max, max + 1].each do |num| + num.gcd(num).should == num + (-num).gcd(num).should == num + (-num).gcd(-num).should == num + num.gcd(-num).should == num + end + end + end + + it "raises an ArgumentError if not given an argument" do + -> { 12.gcd }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if given more than one argument" do + -> { 12.gcd(30, 20) }.should raise_error(ArgumentError) + end + + it "raises a TypeError unless the argument is an Integer" do + -> { 39.gcd(3.8) }.should raise_error(TypeError) + -> { 45872.gcd([]) }.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/integer/gcdlcm_spec.rb b/spec/ruby/core/integer/gcdlcm_spec.rb new file mode 100644 index 0000000000..ebf45e55a1 --- /dev/null +++ b/spec/ruby/core/integer/gcdlcm_spec.rb @@ -0,0 +1,53 @@ +require_relative '../../spec_helper' + +describe "Integer#gcdlcm" do + it "returns [self, self] if self is equal to the argument" do + 1.gcdlcm(1).should == [1, 1] + 398.gcdlcm(398).should == [398, 398] + end + + it "returns an Array" do + 36.gcdlcm(6).should be_kind_of(Array) + 4.gcdlcm(20981).should be_kind_of(Array) + end + + it "returns a two-element Array" do + 36.gcdlcm(876).size.should == 2 + 29.gcdlcm(17).size.should == 2 + end + + it "returns the greatest common divisor of self and argument as the first element" do + 10.gcdlcm(5)[0].should == 10.gcd(5) + 200.gcdlcm(20)[0].should == 200.gcd(20) + end + + it "returns the least common multiple of self and argument as the last element" do + 10.gcdlcm(5)[1].should == 10.lcm(5) + 200.gcdlcm(20)[1].should == 200.lcm(20) + end + + it "accepts a Bignum argument" do + bignum = 91999**99 + bignum.should be_kind_of(Integer) + 99.gcdlcm(bignum).should == [99.gcd(bignum), 99.lcm(bignum)] + end + + it "works if self is a Bignum" do + bignum = 9999**89 + bignum.should be_kind_of(Integer) + bignum.gcdlcm(99).should == [bignum.gcd(99), bignum.lcm(99)] + end + + it "raises an ArgumentError if not given an argument" do + -> { 12.gcdlcm }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if given more than one argument" do + -> { 12.gcdlcm(30, 20) }.should raise_error(ArgumentError) + end + + it "raises a TypeError unless the argument is an Integer" do + -> { 39.gcdlcm(3.8) }.should raise_error(TypeError) + -> { 45872.gcdlcm([]) }.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/integer/gt_spec.rb b/spec/ruby/core/integer/gt_spec.rb new file mode 100644 index 0000000000..f0179e566d --- /dev/null +++ b/spec/ruby/core/integer/gt_spec.rb @@ -0,0 +1,43 @@ +require_relative '../../spec_helper' +require_relative 'shared/comparison_coerce' + +describe "Integer#>" do + it_behaves_like :integer_comparison_coerce_not_rescue, :> + + context "fixnum" do + it "returns true if self is greater than the given argument" do + (13 > 2).should == true + (-500 > -600).should == true + + (1 > 5).should == false + (5 > 5).should == false + + (900 > bignum_value).should == false + (5 > 4.999).should == true + end + + it "raises an ArgumentError when given a non-Integer" do + -> { 5 > "4" }.should raise_error(ArgumentError) + -> { 5 > mock('x') }.should raise_error(ArgumentError) + end + end + + context "bignum" do + before :each do + @bignum = bignum_value(732) + end + + it "returns true if self is greater than the given argument" do + (@bignum > (@bignum - 1)).should == true + (@bignum > 14.6).should == true + (@bignum > 10).should == true + + (@bignum > (@bignum + 500)).should == false + end + + it "raises an ArgumentError when given a non-Integer" do + -> { @bignum > "4" }.should raise_error(ArgumentError) + -> { @bignum > mock('str') }.should raise_error(ArgumentError) + end + end +end diff --git a/spec/ruby/core/integer/gte_spec.rb b/spec/ruby/core/integer/gte_spec.rb new file mode 100644 index 0000000000..6237fc2c51 --- /dev/null +++ b/spec/ruby/core/integer/gte_spec.rb @@ -0,0 +1,43 @@ +require_relative '../../spec_helper' +require_relative 'shared/comparison_coerce' + +describe "Integer#>=" do + it_behaves_like :integer_comparison_coerce_not_rescue, :>= + + context "fixnum" do + it "returns true if self is greater than or equal to the given argument" do + (13 >= 2).should == true + (-500 >= -600).should == true + + (1 >= 5).should == false + (2 >= 2).should == true + (5 >= 5).should == true + + (900 >= bignum_value).should == false + (5 >= 4.999).should == true + end + + it "raises an ArgumentError when given a non-Integer" do + -> { 5 >= "4" }.should raise_error(ArgumentError) + -> { 5 >= mock('x') }.should raise_error(ArgumentError) + end + end + + context "bignum" do + before :each do + @bignum = bignum_value(14) + end + + it "returns true if self is greater than or equal to other" do + (@bignum >= @bignum).should == true + (@bignum >= (@bignum + 2)).should == false + (@bignum >= 5664.2).should == true + (@bignum >= 4).should == true + end + + it "raises an ArgumentError when given a non-Integer" do + -> { @bignum >= "4" }.should raise_error(ArgumentError) + -> { @bignum >= mock('str') }.should raise_error(ArgumentError) + end + end +end diff --git a/spec/ruby/core/integer/integer_spec.rb b/spec/ruby/core/integer/integer_spec.rb new file mode 100644 index 0000000000..2d5d2e3e92 --- /dev/null +++ b/spec/ruby/core/integer/integer_spec.rb @@ -0,0 +1,20 @@ +require_relative '../../spec_helper' + +describe "Integer" do + it "includes Comparable" do + Integer.include?(Comparable).should == true + end + + it "is the class of both small and large integers" do + 42.class.should equal(Integer) + bignum_value.class.should equal(Integer) + end +end + +describe "Integer#integer?" do + it "returns true for Integers" do + 0.should.integer? + -1.should.integer? + bignum_value.should.integer? + end +end diff --git a/spec/ruby/core/integer/lcm_spec.rb b/spec/ruby/core/integer/lcm_spec.rb new file mode 100644 index 0000000000..296a001c8c --- /dev/null +++ b/spec/ruby/core/integer/lcm_spec.rb @@ -0,0 +1,58 @@ +require_relative '../../spec_helper' + +describe "Integer#lcm" do + it "returns self if equal to the argument" do + 1.lcm(1).should == 1 + 398.lcm(398).should == 398 + end + + it "returns an Integer" do + 36.lcm(6).should be_kind_of(Integer) + 4.lcm(20981).should be_kind_of(Integer) + end + + it "returns the least common multiple of self and argument" do + 200.lcm(2001).should == 400200 + 99.lcm(90).should == 990 + end + + it "returns a positive integer even if self is negative" do + -12.lcm(6).should == 12 + -100.lcm(100).should == 100 + end + + it "returns a positive integer even if the argument is negative" do + 12.lcm(-6).should == 12 + 100.lcm(-100).should == 100 + end + + it "returns a positive integer even if both self and argument are negative" do + -12.lcm(-6).should == 12 + -100.lcm(-100).should == 100 + end + + it "accepts a Bignum argument" do + bignum = 9999**99 + bignum.should be_kind_of(Integer) + 99.lcm(bignum).should == bignum + end + + it "works if self is a Bignum" do + bignum = 9999**99 + bignum.should be_kind_of(Integer) + bignum.lcm(99).should == bignum + end + + it "raises an ArgumentError if not given an argument" do + -> { 12.lcm }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if given more than one argument" do + -> { 12.lcm(30, 20) }.should raise_error(ArgumentError) + end + + it "raises a TypeError unless the argument is an Integer" do + -> { 39.lcm(3.8) }.should raise_error(TypeError) + -> { 45872.lcm([]) }.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/integer/left_shift_spec.rb b/spec/ruby/core/integer/left_shift_spec.rb new file mode 100644 index 0000000000..86c2b18ae2 --- /dev/null +++ b/spec/ruby/core/integer/left_shift_spec.rb @@ -0,0 +1,211 @@ +require_relative '../../spec_helper' + +describe "Integer#<< (with n << m)" do + context "fixnum" do + it "returns n shifted left m bits when n > 0, m > 0" do + (1 << 1).should == 2 + end + + it "returns n shifted left m bits when n < 0, m > 0" do + (-1 << 1).should == -2 + (-7 << 1).should == -14 + (-42 << 2).should == -168 + end + + it "returns n shifted right m bits when n > 0, m < 0" do + (2 << -1).should == 1 + end + + it "returns n shifted right m bits when n < 0, m < 0" do + (-2 << -1).should == -1 + end + + it "returns 0 when n == 0" do + (0 << 1).should == 0 + end + + it "returns n when n > 0, m == 0" do + (1 << 0).should == 1 + end + + it "returns n when n < 0, m == 0" do + (-1 << 0).should == -1 + end + + it "returns 0 when n > 0, m < 0 and n < 2**-m" do + (3 << -2).should == 0 + (7 << -3).should == 0 + (127 << -7).should == 0 + + # To make sure the exponent is not truncated + (7 << -32).should == 0 + (7 << -64).should == 0 + end + + it "returns -1 when n < 0, m < 0 and n > -(2**-m)" do + (-3 << -2).should == -1 + (-7 << -3).should == -1 + (-127 << -7).should == -1 + + # To make sure the exponent is not truncated + (-7 << -32).should == -1 + (-7 << -64).should == -1 + end + + it "returns 0 when m < 0 and m is a Bignum" do + (3 << -bignum_value).should == 0 + end + + it "returns a Bignum == fixnum_max * 2 when fixnum_max << 1 and n > 0" do + result = fixnum_max << 1 + result.should be_an_instance_of(Integer) + result.should == fixnum_max * 2 + end + + it "returns a Bignum == fixnum_min * 2 when fixnum_min << 1 and n < 0" do + result = fixnum_min << 1 + result.should be_an_instance_of(Integer) + result.should == fixnum_min * 2 + end + + it "calls #to_int to convert the argument to an Integer" do + obj = mock("4") + obj.should_receive(:to_int).and_return(4) + (3 << obj).should == 48 + + obj = mock("to_int_neg_bignum") + obj.should_receive(:to_int).and_return(-bignum_value) + (3 << obj).should == 0 + end + + it "raises a TypeError when #to_int does not return an Integer" do + obj = mock("a string") + obj.should_receive(:to_int).and_return("asdf") + + -> { 3 << obj }.should raise_error(TypeError) + end + + it "raises a TypeError when passed nil" do + -> { 3 << nil }.should raise_error(TypeError) + end + + it "raises a TypeError when passed a String" do + -> { 3 << "4" }.should raise_error(TypeError) + end + end + + context "bignum" do + before :each do + @bignum = bignum_value * 8 # 2 ** 67 + end + + it "returns n shifted left m bits when n > 0, m > 0" do + (@bignum << 4).should == 2361183241434822606848 + end + + it "returns n shifted left m bits when n < 0, m > 0" do + (-@bignum << 9).should == -75557863725914323419136 + end + + it "returns n shifted right m bits when n > 0, m < 0" do + (@bignum << -1).should == 73786976294838206464 + end + + it "returns n shifted right m bits when n < 0, m < 0" do + (-@bignum << -2).should == -36893488147419103232 + end + + it "returns n when n > 0, m == 0" do + (@bignum << 0).should == @bignum + end + + it "returns n when n < 0, m == 0" do + (-@bignum << 0).should == -@bignum + end + + it "returns 0 when m < 0 and m == p where 2**p > n >= 2**(p-1)" do + (@bignum << -68).should == 0 + end + + it "returns a Fixnum == fixnum_max when (fixnum_max * 2) << -1 and n > 0" do + result = (fixnum_max * 2) << -1 + result.should be_an_instance_of(Integer) + result.should == fixnum_max + end + + it "returns a Fixnum == fixnum_min when (fixnum_min * 2) << -1 and n < 0" do + result = (fixnum_min * 2) << -1 + result.should be_an_instance_of(Integer) + result.should == fixnum_min + end + + it "calls #to_int to convert the argument to an Integer" do + obj = mock("4") + obj.should_receive(:to_int).and_return(4) + + (@bignum << obj).should == 2361183241434822606848 + end + + it "raises a TypeError when #to_int does not return an Integer" do + obj = mock("a string") + obj.should_receive(:to_int).and_return("asdf") + + -> { @bignum << obj }.should raise_error(TypeError) + end + + it "raises a TypeError when passed nil" do + -> { @bignum << nil }.should raise_error(TypeError) + end + + it "raises a TypeError when passed a String" do + -> { @bignum << "4" }.should raise_error(TypeError) + end + end + + context "when m is a bignum or larger than int" do + it "returns -1 when m < 0 and n < 0" do + (-1 << -bignum_value).should == -1 + (-1 << -(2**40)).should == -1 + + (-bignum_value << -bignum_value).should == -1 + (-bignum_value << -(2**40)).should == -1 + end + + it "returns 0 when m < 0 and n >= 0" do + (0 << -bignum_value).should == 0 + (1 << -bignum_value).should == 0 + (bignum_value << -bignum_value).should == 0 + + (0 << -(2**40)).should == 0 + (1 << -(2**40)).should == 0 + (bignum_value << -(2**40)).should == 0 + end + + it "returns 0 when m > 0 long and n == 0" do + (0 << (2**40)).should == 0 + end + + it "returns 0 when m > 0 bignum and n == 0" do + (0 << bignum_value).should == 0 + end + + it "raises RangeError when m > 0 and n != 0" do + # https://bugs.ruby-lang.org/issues/18518#note-9 + limit = RUBY_ENGINE == 'ruby' ? 2**67 : 2**32 + + coerce_long = mock("long") + coerce_long.stub!(:to_int).and_return(limit) + coerce_bignum = mock("bignum") + coerce_bignum.stub!(:to_int).and_return(bignum_value) + exps = [limit, coerce_long] + exps << bignum_value << coerce_bignum if bignum_value >= limit + + exps.each { |exp| + -> { (1 << exp) }.should raise_error(RangeError, 'shift width too big') + -> { (-1 << exp) }.should raise_error(RangeError, 'shift width too big') + -> { (bignum_value << exp) }.should raise_error(RangeError, 'shift width too big') + -> { (-bignum_value << exp) }.should raise_error(RangeError, 'shift width too big') + } + end + end +end diff --git a/spec/ruby/core/integer/lt_spec.rb b/spec/ruby/core/integer/lt_spec.rb new file mode 100644 index 0000000000..c182f82555 --- /dev/null +++ b/spec/ruby/core/integer/lt_spec.rb @@ -0,0 +1,45 @@ +require_relative '../../spec_helper' +require_relative 'shared/comparison_coerce' + +describe "Integer#<" do + it_behaves_like :integer_comparison_coerce_not_rescue, :< + + context "fixnum" do + it "returns true if self is less than the given argument" do + (2 < 13).should == true + (-600 < -500).should == true + + (5 < 1).should == false + (5 < 5).should == false + + (900 < bignum_value).should == true + (5 < 4.999).should == false + end + + it "raises an ArgumentError when given a non-Integer" do + -> { 5 < "4" }.should raise_error(ArgumentError) + -> { 5 < mock('x') }.should raise_error(ArgumentError) + end + end + + context "bignum" do + before :each do + @bignum = bignum_value(32) + end + + it "returns true if self is less than the given argument" do + (@bignum < @bignum + 1).should == true + (-@bignum < -(@bignum - 1)).should == true + + (@bignum < 1).should == false + (@bignum < 5).should == false + + (@bignum < 4.999).should == false + end + + it "raises an ArgumentError when given a non-Integer" do + -> { @bignum < "4" }.should raise_error(ArgumentError) + -> { @bignum < mock('str') }.should raise_error(ArgumentError) + end + end +end diff --git a/spec/ruby/core/integer/lte_spec.rb b/spec/ruby/core/integer/lte_spec.rb new file mode 100644 index 0000000000..65b71d77f2 --- /dev/null +++ b/spec/ruby/core/integer/lte_spec.rb @@ -0,0 +1,53 @@ +require_relative '../../spec_helper' +require_relative 'shared/comparison_coerce' + +describe "Integer#<=" do + it_behaves_like :integer_comparison_coerce_not_rescue, :<= + + context "fixnum" do + it "returns true if self is less than or equal to other" do + (2 <= 13).should == true + (-600 <= -500).should == true + + (5 <= 1).should == false + (5 <= 5).should == true + (-2 <= -2).should == true + + (900 <= bignum_value).should == true + (5 <= 4.999).should == false + end + + it "raises an ArgumentError when given a non-Integer" do + -> { 5 <= "4" }.should raise_error(ArgumentError) + -> { 5 <= mock('x') }.should raise_error(ArgumentError) + end + end + + context "bignum" do + before :each do + @bignum = bignum_value(39) + end + + it "returns true if self is less than or equal to other" do + (@bignum <= @bignum).should == true + (-@bignum <= -(@bignum - 1)).should == true + + (@bignum <= 4.999).should == false + end + + it "returns false if compares with near float" do + (@bignum <= (@bignum + 0.0)).should == false + (@bignum <= (@bignum + 0.5)).should == false + end + + it "returns true for bignums compare to a bigger float" do + (18446744073709551616 <= 1.8446744073709552e+19).should == true + (@bignum <= (@bignum + 9999.0)).should == true + end + + it "raises an ArgumentError when given a non-Integer" do + -> { @bignum <= "4" }.should raise_error(ArgumentError) + -> { @bignum <= mock('str') }.should raise_error(ArgumentError) + end + end +end diff --git a/spec/ruby/core/integer/magnitude_spec.rb b/spec/ruby/core/integer/magnitude_spec.rb new file mode 100644 index 0000000000..48cf1a8534 --- /dev/null +++ b/spec/ruby/core/integer/magnitude_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/abs' + +describe "Integer#magnitude" do + it_behaves_like :integer_abs, :magnitude +end diff --git a/spec/ruby/core/integer/minus_spec.rb b/spec/ruby/core/integer/minus_spec.rb new file mode 100644 index 0000000000..6072ba7c8b --- /dev/null +++ b/spec/ruby/core/integer/minus_spec.rb @@ -0,0 +1,60 @@ +require_relative '../../spec_helper' +require_relative 'shared/arithmetic_coerce' + +describe "Integer#-" do + it_behaves_like :integer_arithmetic_coerce_not_rescue, :- + + context "fixnum" do + it "returns self minus the given Integer" do + (5 - 10).should == -5 + (9237212 - 5_280).should == 9231932 + + (781 - 0.5).should == 780.5 + (2_560_496 - bignum_value).should == -18446744073706991120 + end + + it "raises a TypeError when given a non-Integer" do + -> { + (obj = mock('10')).should_receive(:to_int).any_number_of_times.and_return(10) + 13 - obj + }.should raise_error(TypeError) + -> { 13 - "10" }.should raise_error(TypeError) + -> { 13 - :symbol }.should raise_error(TypeError) + end + end + + context "bignum" do + before :each do + @bignum = bignum_value(314) + end + + it "returns self minus the given Integer" do + (@bignum - 9).should == 18446744073709551921 + (@bignum - 12.57).should be_close(18446744073709551917.43, TOLERANCE) + (@bignum - bignum_value(42)).should == 272 + end + + it "raises a TypeError when given a non-Integer" do + -> { @bignum - mock('10') }.should raise_error(TypeError) + -> { @bignum - "10" }.should raise_error(TypeError) + -> { @bignum - :symbol }.should raise_error(TypeError) + end + end + + it "coerces the RHS and calls #coerce" do + obj = mock("integer plus") + obj.should_receive(:coerce).with(5).and_return([5, 10]) + (5 - obj).should == -5 + end + + it "coerces the RHS and calls #coerce even if it's private" do + obj = Object.new + class << obj + private def coerce(n) + [n, 10] + end + end + + (5 - obj).should == -5 + end +end diff --git a/spec/ruby/core/integer/modulo_spec.rb b/spec/ruby/core/integer/modulo_spec.rb new file mode 100644 index 0000000000..e263338e38 --- /dev/null +++ b/spec/ruby/core/integer/modulo_spec.rb @@ -0,0 +1,10 @@ +require_relative '../../spec_helper' +require_relative 'shared/modulo' + +describe "Integer#%" do + it_behaves_like :integer_modulo, :% +end + +describe "Integer#modulo" do + it_behaves_like :integer_modulo, :modulo +end diff --git a/spec/ruby/core/integer/multiply_spec.rb b/spec/ruby/core/integer/multiply_spec.rb new file mode 100644 index 0000000000..5037d27458 --- /dev/null +++ b/spec/ruby/core/integer/multiply_spec.rb @@ -0,0 +1,45 @@ +require_relative '../../spec_helper' +require_relative 'shared/arithmetic_coerce' + +describe "Integer#*" do + it_behaves_like :integer_arithmetic_coerce_not_rescue, :* + + context "fixnum" do + it "returns self multiplied by the given Integer" do + (4923 * 2).should == 9846 + (1342177 * 800).should == 1073741600 + (65536 * 65536).should == 4294967296 + + (256 * bignum_value).should == 4722366482869645213696 + (6712 * 0.25).should == 1678.0 + end + + it "raises a TypeError when given a non-Integer" do + -> { + (obj = mock('10')).should_receive(:to_int).any_number_of_times.and_return(10) + 13 * obj + }.should raise_error(TypeError) + -> { 13 * "10" }.should raise_error(TypeError) + -> { 13 * :symbol }.should raise_error(TypeError) + end + end + + context "bignum" do + before :each do + @bignum = bignum_value(772) + end + + it "returns self multiplied by the given Integer" do + (@bignum * (1/bignum_value(0xffff).to_f)).should be_close(1.0, TOLERANCE) + (@bignum * (1/bignum_value(0xffff).to_f)).should be_close(1.0, TOLERANCE) + (@bignum * 10).should == 184467440737095523880 + (@bignum * (@bignum - 40)).should == 340282366920938491207277694290934407024 + end + + it "raises a TypeError when given a non-Integer" do + -> { @bignum * mock('10') }.should raise_error(TypeError) + -> { @bignum * "10" }.should raise_error(TypeError) + -> { @bignum * :symbol }.should raise_error(TypeError) + end + end +end diff --git a/spec/ruby/core/integer/next_spec.rb b/spec/ruby/core/integer/next_spec.rb new file mode 100644 index 0000000000..63c4e67893 --- /dev/null +++ b/spec/ruby/core/integer/next_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/next' + +describe "Integer#next" do + it_behaves_like :integer_next, :next +end diff --git a/spec/ruby/core/integer/nobits_spec.rb b/spec/ruby/core/integer/nobits_spec.rb new file mode 100644 index 0000000000..685759ffa3 --- /dev/null +++ b/spec/ruby/core/integer/nobits_spec.rb @@ -0,0 +1,36 @@ +require_relative '../../spec_helper' + +describe "Integer#nobits?" do + it "returns true if and only if all no bits of the argument are set in the receiver" do + 42.nobits?(42).should == false + 0b1010_1010.nobits?(0b1000_0010).should == false + 0b1010_1010.nobits?(0b1000_0001).should == false + 0b0100_0101.nobits?(0b1010_1010).should == true + different_bignum = (2 * bignum_value) & (~bignum_value) + (0b1010_1010 | different_bignum).nobits?(0b1000_0010 | bignum_value).should == false + (0b1010_1010 | different_bignum).nobits?(0b1000_0001 | bignum_value).should == false + (0b0100_0101 | different_bignum).nobits?(0b1010_1010 | bignum_value).should == true + end + + it "handles negative values using two's complement notation" do + (~0b1101).nobits?(0b1101).should == true + (-42).nobits?(-42).should == false + (~0b1101).nobits?(~0b10).should == false + (~(0b1101 | bignum_value)).nobits?(~(0b10 | bignum_value)).should == false + end + + it "coerces the rhs using to_int" do + obj = mock("the int 0b10") + obj.should_receive(:to_int).and_return(0b10) + 0b110.nobits?(obj).should == false + end + + it "raises a TypeError when given a non-Integer" do + -> { + (obj = mock('10')).should_receive(:coerce).any_number_of_times.and_return([42,10]) + 13.nobits?(obj) + }.should raise_error(TypeError) + -> { 13.nobits?("10") }.should raise_error(TypeError) + -> { 13.nobits?(:symbol) }.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/integer/numerator_spec.rb b/spec/ruby/core/integer/numerator_spec.rb new file mode 100644 index 0000000000..f4149d770e --- /dev/null +++ b/spec/ruby/core/integer/numerator_spec.rb @@ -0,0 +1,18 @@ +require_relative '../../spec_helper' + +describe "Integer#numerator" do + before :all do + @numbers = [ + 0, + 29871, + 99999999999999**99, + 72628191273, + ].map{|n| [-n, n]}.flatten + end + + it "returns self" do + @numbers.each do |number| + number.numerator.should == number + end + end +end diff --git a/spec/ruby/core/integer/odd_spec.rb b/spec/ruby/core/integer/odd_spec.rb new file mode 100644 index 0000000000..dd779fa44b --- /dev/null +++ b/spec/ruby/core/integer/odd_spec.rb @@ -0,0 +1,38 @@ +require_relative '../../spec_helper' + +describe "Integer#odd?" do + context "fixnum" do + it "returns true when self is an odd number" do + (-2).odd?.should be_false + (-1).odd?.should be_true + + 0.odd?.should be_false + 1.odd?.should be_true + 2.odd?.should be_false + + bignum_value(0).odd?.should be_false + bignum_value(1).odd?.should be_true + + (-bignum_value(0)).odd?.should be_false + (-bignum_value(1)).odd?.should be_true + end + end + + context "bignum" do + it "returns true if self is odd and positive" do + (987279**19).odd?.should be_true + end + + it "returns true if self is odd and negative" do + (-9873389**97).odd?.should be_true + end + + it "returns false if self is even and positive" do + (10000000**10).odd?.should be_false + end + + it "returns false if self is even and negative" do + (-1000000**100).odd?.should be_false + end + end +end diff --git a/spec/ruby/core/integer/ord_spec.rb b/spec/ruby/core/integer/ord_spec.rb new file mode 100644 index 0000000000..bcb57bea98 --- /dev/null +++ b/spec/ruby/core/integer/ord_spec.rb @@ -0,0 +1,17 @@ +require_relative '../../spec_helper' + +describe "Integer#ord" do + it "returns self" do + 20.ord.should eql(20) + 40.ord.should eql(40) + + 0.ord.should eql(0) + (-10).ord.should eql(-10) + + ?a.ord.should eql(97) + ?Z.ord.should eql(90) + + bignum_value.ord.should eql(bignum_value) + (-bignum_value).ord.should eql(-bignum_value) + end +end diff --git a/spec/ruby/core/integer/plus_spec.rb b/spec/ruby/core/integer/plus_spec.rb new file mode 100644 index 0000000000..38428e56c5 --- /dev/null +++ b/spec/ruby/core/integer/plus_spec.rb @@ -0,0 +1,75 @@ +require_relative '../../spec_helper' +require_relative 'shared/arithmetic_coerce' + +describe "Integer#+" do + it_behaves_like :integer_arithmetic_coerce_not_rescue, :+ + + context "fixnum" do + it "returns self plus the given Integer" do + (491 + 2).should == 493 + (90210 + 10).should == 90220 + + (9 + bignum_value).should == 18446744073709551625 + (1001 + 5.219).should == 1006.219 + end + + it "raises a TypeError when given a non-Integer" do + -> { + (obj = mock('10')).should_receive(:to_int).any_number_of_times.and_return(10) + 13 + obj + }.should raise_error(TypeError) + -> { 13 + "10" }.should raise_error(TypeError) + -> { 13 + :symbol }.should raise_error(TypeError) + end + end + + context "bignum" do + before :each do + @bignum = bignum_value(76) + end + + it "returns self plus the given Integer" do + (@bignum + 4).should == 18446744073709551696 + (@bignum + 4.2).should be_close(18446744073709551696.2, TOLERANCE) + (@bignum + bignum_value(3)).should == 36893488147419103311 + end + + it "raises a TypeError when given a non-Integer" do + -> { @bignum + mock('10') }.should raise_error(TypeError) + -> { @bignum + "10" }.should raise_error(TypeError) + -> { @bignum + :symbol}.should raise_error(TypeError) + end + end + + it "can be redefined" do + code = <<~RUBY + class Integer + alias_method :old_plus, :+ + def +(other) + self - other + end + end + result = 1 + 2 + Integer.alias_method :+, :old_plus + print result + RUBY + ruby_exe(code).should == "-1" + end + + it "coerces the RHS and calls #coerce" do + obj = mock("integer plus") + obj.should_receive(:coerce).with(6).and_return([6, 3]) + (6 + obj).should == 9 + end + + it "coerces the RHS and calls #coerce even if it's private" do + obj = Object.new + class << obj + private def coerce(n) + [n, 3] + end + end + + (6 + obj).should == 9 + end +end diff --git a/spec/ruby/core/integer/pow_spec.rb b/spec/ruby/core/integer/pow_spec.rb new file mode 100644 index 0000000000..ecaca01eff --- /dev/null +++ b/spec/ruby/core/integer/pow_spec.rb @@ -0,0 +1,51 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/exponent' + +describe "Integer#pow" do + context "one argument is passed" do + it_behaves_like :integer_exponent, :pow + end + + context "two arguments are passed" do + it "returns modulo of self raised to the given power" do + 2.pow(5, 12).should == 8 + 2.pow(6, 13).should == 12 + 2.pow(7, 14).should == 2 + 2.pow(8, 15).should == 1 + end + + it "works well with bignums" do + 2.pow(61, 5843009213693951).should eql 3697379018277258 + 2.pow(62, 5843009213693952).should eql 1551748822859776 + 2.pow(63, 5843009213693953).should eql 3103497645717974 + 2.pow(64, 5843009213693954).should eql 363986077738838 + end + + it "handles sign like #divmod does" do + 2.pow(5, 12).should == 8 + 2.pow(5, -12).should == -4 + -2.pow(5, 12).should == 4 + -2.pow(5, -12).should == -8 + end + + it "ensures all arguments are integers" do + -> { 2.pow(5, 12.0) }.should raise_error(TypeError, /2nd argument not allowed unless all arguments are integers/) + -> { 2.pow(5, Rational(12, 1)) }.should raise_error(TypeError, /2nd argument not allowed unless all arguments are integers/) + end + + it "raises TypeError for non-numeric value" do + -> { 2.pow(5, "12") }.should raise_error(TypeError) + -> { 2.pow(5, []) }.should raise_error(TypeError) + -> { 2.pow(5, nil) }.should raise_error(TypeError) + end + + it "raises a ZeroDivisionError when the given argument is 0" do + -> { 2.pow(5, 0) }.should raise_error(ZeroDivisionError) + end + + it "raises a RangeError when the first argument is negative and the second argument is present" do + -> { 2.pow(-5, 1) }.should raise_error(RangeError) + end + end +end diff --git a/spec/ruby/core/integer/pred_spec.rb b/spec/ruby/core/integer/pred_spec.rb new file mode 100644 index 0000000000..86750ebfd1 --- /dev/null +++ b/spec/ruby/core/integer/pred_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' + +describe "Integer#pred" do + it "returns the Integer equal to self - 1" do + 0.pred.should eql(-1) + -1.pred.should eql(-2) + bignum_value.pred.should eql(bignum_value(-1)) + fixnum_min.pred.should eql(fixnum_min - 1) + 20.pred.should eql(19) + end +end diff --git a/spec/ruby/core/integer/rationalize_spec.rb b/spec/ruby/core/integer/rationalize_spec.rb new file mode 100644 index 0000000000..09d741af33 --- /dev/null +++ b/spec/ruby/core/integer/rationalize_spec.rb @@ -0,0 +1,39 @@ +require_relative '../../spec_helper' + +describe "Integer#rationalize" do + before :all do + @numbers = [ + 0, + 29871, + 99999999999999**99, + -72628191273, + ] + end + + it "returns a Rational object" do + @numbers.each do |number| + number.rationalize.should be_an_instance_of(Rational) + end + end + + it "uses self as the numerator" do + @numbers.each do |number| + number.rationalize.numerator.should == number + end + end + + it "uses 1 as the denominator" do + @numbers.each do |number| + number.rationalize.denominator.should == 1 + end + end + + it "ignores a single argument" do + 1.rationalize(0.1).should == Rational(1,1) + end + + it "raises ArgumentError when passed more than one argument" do + -> { 1.rationalize(0.1, 0.1) }.should raise_error(ArgumentError) + -> { 1.rationalize(0.1, 0.1, 2) }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/integer/remainder_spec.rb b/spec/ruby/core/integer/remainder_spec.rb new file mode 100644 index 0000000000..757e42fbe8 --- /dev/null +++ b/spec/ruby/core/integer/remainder_spec.rb @@ -0,0 +1,51 @@ +require_relative '../../spec_helper' + +describe "Integer#remainder" do + context "fixnum" do + it "returns the remainder of dividing self by other" do + 5.remainder(3).should == 2 + 5.remainder(3.0).should == 2.0 + 5.remainder(Rational(3, 1)).should == Rational(2, 1) + end + + it "means x-y*(x/y).truncate" do + 5.remainder(3).should == 2 + 5.remainder(3.3).should be_close(1.7, TOLERANCE) + 5.remainder(3.7).should be_close(1.3, TOLERANCE) + end + + it "keeps sign of self" do + 5.remainder( 3).should == 2 + 5.remainder(-3).should == 2 + -5.remainder( 3).should == -2 + -5.remainder(-3).should == -2 + end + + it "raises TypeError if passed non-numeric argument" do + -> { 5.remainder("3") }.should raise_error(TypeError) + -> { 5.remainder(:"3") }.should raise_error(TypeError) + -> { 5.remainder([]) }.should raise_error(TypeError) + -> { 5.remainder(nil) }.should raise_error(TypeError) + end + end + + context "bignum" do + it "returns the remainder of dividing self by other" do + a = bignum_value(79) + a.remainder(2).should == 1 + a.remainder(97.345).should be_close(93.1349992295444, TOLERANCE) + a.remainder(bignum_value).should == 79 + end + + it "raises a ZeroDivisionError if other is zero and not a Float" do + -> { bignum_value(66).remainder(0) }.should raise_error(ZeroDivisionError) + end + + it "does raises ZeroDivisionError if other is zero and a Float" do + a = bignum_value(7) + b = bignum_value(32) + -> { a.remainder(0.0) }.should raise_error(ZeroDivisionError) + -> { b.remainder(-0.0) }.should raise_error(ZeroDivisionError) + end + end +end diff --git a/spec/ruby/core/integer/right_shift_spec.rb b/spec/ruby/core/integer/right_shift_spec.rb new file mode 100644 index 0000000000..c902674e2f --- /dev/null +++ b/spec/ruby/core/integer/right_shift_spec.rb @@ -0,0 +1,233 @@ +require_relative '../../spec_helper' + +describe "Integer#>> (with n >> m)" do + context "fixnum" do + it "returns n shifted right m bits when n > 0, m > 0" do + (2 >> 1).should == 1 + end + + it "returns n shifted right m bits when n < 0, m > 0" do + (-2 >> 1).should == -1 + (-7 >> 1).should == -4 + (-42 >> 2).should == -11 + end + + it "returns n shifted left m bits when n > 0, m < 0" do + (1 >> -1).should == 2 + end + + it "returns n shifted left m bits when n < 0, m < 0" do + (-1 >> -1).should == -2 + end + + it "returns 0 when n == 0" do + (0 >> 1).should == 0 + end + + it "returns n when n > 0, m == 0" do + (1 >> 0).should == 1 + end + + it "returns n when n < 0, m == 0" do + (-1 >> 0).should == -1 + end + + it "returns 0 when n > 0, m > 0 and n < 2**m" do + (3 >> 2).should == 0 + (7 >> 3).should == 0 + (127 >> 7).should == 0 + + # To make sure the exponent is not truncated + (7 >> 32).should == 0 + (7 >> 64).should == 0 + end + + it "returns -1 when n < 0, m > 0 and n > -(2**m)" do + (-3 >> 2).should == -1 + (-7 >> 3).should == -1 + (-127 >> 7).should == -1 + + # To make sure the exponent is not truncated + (-7 >> 32).should == -1 + (-7 >> 64).should == -1 + end + + it "returns a Bignum == fixnum_max * 2 when fixnum_max >> -1 and n > 0" do + result = fixnum_max >> -1 + result.should be_an_instance_of(Integer) + result.should == fixnum_max * 2 + end + + it "returns a Bignum == fixnum_min * 2 when fixnum_min >> -1 and n < 0" do + result = fixnum_min >> -1 + result.should be_an_instance_of(Integer) + result.should == fixnum_min * 2 + end + + it "calls #to_int to convert the argument to an Integer" do + obj = mock("2") + obj.should_receive(:to_int).and_return(2) + (8 >> obj).should == 2 + + obj = mock("to_int_bignum") + obj.should_receive(:to_int).and_return(bignum_value) + (8 >> obj).should == 0 + end + + it "raises a TypeError when #to_int does not return an Integer" do + obj = mock("a string") + obj.should_receive(:to_int).and_return("asdf") + + -> { 3 >> obj }.should raise_error(TypeError) + end + + it "raises a TypeError when passed nil" do + -> { 3 >> nil }.should raise_error(TypeError) + end + + it "raises a TypeError when passed a String" do + -> { 3 >> "4" }.should raise_error(TypeError) + end + end + + context "bignum" do + before :each do + @bignum = bignum_value * 8 # 2 ** 67 + end + + it "returns n shifted right m bits when n > 0, m > 0" do + (@bignum >> 1).should == 73786976294838206464 + end + + it "returns n shifted right m bits when n < 0, m > 0" do + (-@bignum >> 2).should == -36893488147419103232 + end + + it "respects twos complement signed shifting" do + # This explicit left hand value is important because it is the + # exact bit pattern that matters, so it's important it's right + # here to show the significance. + # + + (-42949672980000000000000 >> 14).should == -2621440001220703125 + (-42949672980000000000001 >> 14).should == -2621440001220703126 + # Note the off by one -------------------- ^^^^^^^^^^^^^^^^^^^^ + # This is because even though we discard the lowest bit, in twos + # complement it would influence the bits to the left of it. + + (-42949672980000000000000 >> 15).should == -1310720000610351563 + (-42949672980000000000001 >> 15).should == -1310720000610351563 + + (-0xfffffffffffffffff >> 32).should == -68719476736 + end + + it "respects twos complement signed shifting for very large values" do + giant = 42949672980000000000000000000000000000000000000000000000000000000000000000000000000000000000 + neg = -giant + + (giant >> 84).should == 2220446050284288846538547929770901490087453566957265138626098632812 + (neg >> 84).should == -2220446050284288846538547929770901490087453566957265138626098632813 + end + + it "returns n shifted left m bits when n > 0, m < 0" do + (@bignum >> -2).should == 590295810358705651712 + end + + it "returns n shifted left m bits when n < 0, m < 0" do + (-@bignum >> -3).should == -1180591620717411303424 + end + + it "returns n when n > 0, m == 0" do + (@bignum >> 0).should == @bignum + end + + it "returns n when n < 0, m == 0" do + (-@bignum >> 0).should == -@bignum + end + + it "returns 0 when m > 0 and m == p where 2**p > n >= 2**(p-1)" do + (@bignum >> 68).should == 0 + end + + it "returns a Fixnum == fixnum_max when (fixnum_max * 2) >> 1 and n > 0" do + result = (fixnum_max * 2) >> 1 + result.should be_an_instance_of(Integer) + result.should == fixnum_max + end + + it "returns a Fixnum == fixnum_min when (fixnum_min * 2) >> 1 and n < 0" do + result = (fixnum_min * 2) >> 1 + result.should be_an_instance_of(Integer) + result.should == fixnum_min + end + + it "calls #to_int to convert the argument to an Integer" do + obj = mock("2") + obj.should_receive(:to_int).and_return(2) + + (@bignum >> obj).should == 36893488147419103232 + end + + it "raises a TypeError when #to_int does not return an Integer" do + obj = mock("a string") + obj.should_receive(:to_int).and_return("asdf") + + -> { @bignum >> obj }.should raise_error(TypeError) + end + + it "raises a TypeError when passed nil" do + -> { @bignum >> nil }.should raise_error(TypeError) + end + + it "raises a TypeError when passed a String" do + -> { @bignum >> "4" }.should raise_error(TypeError) + end + end + + context "when m is a bignum or larger than int" do + it "returns -1 when m > 0 and n < 0" do + (-1 >> bignum_value).should == -1 + (-1 >> (2**40)).should == -1 + + (-bignum_value >> bignum_value).should == -1 + (-bignum_value >> (2**40)).should == -1 + end + + it "returns 0 when m > 0 and n >= 0" do + (0 >> bignum_value).should == 0 + (1 >> bignum_value).should == 0 + (bignum_value >> bignum_value).should == 0 + + (0 >> (2**40)).should == 0 + (1 >> (2**40)).should == 0 + (bignum_value >> (2**40)).should == 0 + end + + it "returns 0 when m < 0 long and n == 0" do + (0 >> -(2**40)).should == 0 + end + + it "returns 0 when m < 0 bignum and n == 0" do + (0 >> -bignum_value).should == 0 + end + + it "raises RangeError when m < 0 and n != 0" do + # https://bugs.ruby-lang.org/issues/18518#note-9 + limit = RUBY_ENGINE == 'ruby' ? 2**67 : 2**32 + + coerce_long = mock("long") + coerce_long.stub!(:to_int).and_return(-limit) + coerce_bignum = mock("bignum") + coerce_bignum.stub!(:to_int).and_return(-bignum_value) + exps = [-limit, coerce_long] + exps << -bignum_value << coerce_bignum if bignum_value >= limit + + exps.each { |exp| + -> { (1 >> exp) }.should raise_error(RangeError, 'shift width too big') + -> { (-1 >> exp) }.should raise_error(RangeError, 'shift width too big') + -> { (bignum_value >> exp) }.should raise_error(RangeError, 'shift width too big') + -> { (-bignum_value >> exp) }.should raise_error(RangeError, 'shift width too big') + } + end + end +end diff --git a/spec/ruby/core/integer/round_spec.rb b/spec/ruby/core/integer/round_spec.rb new file mode 100644 index 0000000000..189384f11a --- /dev/null +++ b/spec/ruby/core/integer/round_spec.rb @@ -0,0 +1,81 @@ +require_relative '../../spec_helper' +require_relative 'shared/to_i' +require_relative 'shared/integer_rounding' + +describe "Integer#round" do + it_behaves_like :integer_to_i, :round + it_behaves_like :integer_rounding_positive_precision, :round + + # redmine:5228 + it "returns itself rounded if passed a negative value" do + +249.round(-2).should eql(+200) + -249.round(-2).should eql(-200) + (+25 * 10**70 - 1).round(-71).should eql(+20 * 10**70) + (-25 * 10**70 + 1).round(-71).should eql(-20 * 10**70) + end + + it "returns itself rounded to nearest if passed a negative value" do + +250.round(-2).should eql(+300) + -250.round(-2).should eql(-300) + (+25 * 10**70).round(-71).should eql(+30 * 10**70) + (-25 * 10**70).round(-71).should eql(-30 * 10**70) + end + + it "raises a RangeError when passed a big negative value" do + -> { 42.round(min_long - 1) }.should raise_error(RangeError) + end + + it "raises a RangeError when passed Float::INFINITY" do + -> { 42.round(Float::INFINITY) }.should raise_error(RangeError) + end + + it "raises a RangeError when passed a beyond signed int" do + -> { 42.round(1<<31) }.should raise_error(RangeError) + end + + it "raises a TypeError when passed a String" do + -> { 42.round("4") }.should raise_error(TypeError) + end + + it "raises a TypeError when its argument cannot be converted to an Integer" do + -> { 42.round(nil) }.should raise_error(TypeError) + end + + it "calls #to_int on the argument to convert it to an Integer" do + obj = mock("Object") + obj.should_receive(:to_int).and_return(0) + 42.round(obj) + end + + it "raises a TypeError when #to_int does not return an Integer" do + obj = mock("Object") + obj.stub!(:to_int).and_return([]) + -> { 42.round(obj) }.should raise_error(TypeError) + end + + it "returns different rounded values depending on the half option" do + 25.round(-1, half: :up).should eql(30) + 25.round(-1, half: :down).should eql(20) + 25.round(-1, half: :even).should eql(20) + 25.round(-1, half: nil).should eql(30) + 35.round(-1, half: :up).should eql(40) + 35.round(-1, half: :down).should eql(30) + 35.round(-1, half: :even).should eql(40) + 35.round(-1, half: nil).should eql(40) + (-25).round(-1, half: :up).should eql(-30) + (-25).round(-1, half: :down).should eql(-20) + (-25).round(-1, half: :even).should eql(-20) + (-25).round(-1, half: nil).should eql(-30) + end + + it "returns itself if passed a positive precision and the half option" do + 35.round(1, half: :up).should eql(35) + 35.round(1, half: :down).should eql(35) + 35.round(1, half: :even).should eql(35) + end + + it "raises ArgumentError for an unknown rounding mode" do + -> { 42.round(-1, half: :foo) }.should raise_error(ArgumentError, /invalid rounding mode: foo/) + -> { 42.round(1, half: :foo) }.should raise_error(ArgumentError, /invalid rounding mode: foo/) + end +end diff --git a/spec/ruby/core/integer/shared/abs.rb b/spec/ruby/core/integer/shared/abs.rb new file mode 100644 index 0000000000..43844c9065 --- /dev/null +++ b/spec/ruby/core/integer/shared/abs.rb @@ -0,0 +1,18 @@ +describe :integer_abs, shared: true do + context "fixnum" do + it "returns self's absolute fixnum value" do + { 0 => [0, -0, +0], 2 => [2, -2, +2], 100 => [100, -100, +100] }.each do |key, values| + values.each do |value| + value.send(@method).should == key + end + end + end + end + + context "bignum" do + it "returns the absolute bignum value" do + bignum_value(39).send(@method).should == 18446744073709551655 + (-bignum_value(18)).send(@method).should == 18446744073709551634 + end + end +end diff --git a/spec/ruby/core/integer/shared/arithmetic_coerce.rb b/spec/ruby/core/integer/shared/arithmetic_coerce.rb new file mode 100644 index 0000000000..1260192df1 --- /dev/null +++ b/spec/ruby/core/integer/shared/arithmetic_coerce.rb @@ -0,0 +1,11 @@ +require_relative '../fixtures/classes' + +describe :integer_arithmetic_coerce_not_rescue, shared: true do + it "does not rescue exception raised in other#coerce" do + b = mock("numeric with failed #coerce") + b.should_receive(:coerce).and_raise(IntegerSpecs::CoerceError) + + # e.g. 1 + b + -> { 1.send(@method, b) }.should raise_error(IntegerSpecs::CoerceError) + end +end diff --git a/spec/ruby/core/integer/shared/comparison_coerce.rb b/spec/ruby/core/integer/shared/comparison_coerce.rb new file mode 100644 index 0000000000..af52f5e99b --- /dev/null +++ b/spec/ruby/core/integer/shared/comparison_coerce.rb @@ -0,0 +1,11 @@ +require_relative '../fixtures/classes' + +describe :integer_comparison_coerce_not_rescue, shared: true do + it "does not rescue exception raised in other#coerce" do + b = mock("numeric with failed #coerce") + b.should_receive(:coerce).and_raise(IntegerSpecs::CoerceError) + + # e.g. 1 > b + -> { 1.send(@method, b) }.should raise_error(IntegerSpecs::CoerceError) + end +end diff --git a/spec/ruby/core/integer/shared/equal.rb b/spec/ruby/core/integer/shared/equal.rb new file mode 100644 index 0000000000..ecee17831c --- /dev/null +++ b/spec/ruby/core/integer/shared/equal.rb @@ -0,0 +1,58 @@ +describe :integer_equal, shared: true do + context "fixnum" do + it "returns true if self has the same value as other" do + 1.send(@method, 1).should == true + 9.send(@method, 5).should == false + + # Actually, these call Float#==, Integer#== etc. + 9.send(@method, 9.0).should == true + 9.send(@method, 9.01).should == false + + 10.send(@method, bignum_value).should == false + end + + it "calls 'other == self' if the given argument is not an Integer" do + 1.send(@method, '*').should == false + + obj = mock('one other') + obj.should_receive(:==).any_number_of_times.and_return(false) + 1.send(@method, obj).should == false + + obj = mock('another') + obj.should_receive(:==).any_number_of_times.and_return(true) + 2.send(@method, obj).should == true + end + end + + context "bignum" do + before :each do + @bignum = bignum_value + end + + it "returns true if self has the same value as the given argument" do + @bignum.send(@method, @bignum).should == true + @bignum.send(@method, @bignum.to_f).should == true + + @bignum.send(@method, @bignum + 1).should == false + (@bignum + 1).send(@method, @bignum).should == false + + @bignum.send(@method, 9).should == false + @bignum.send(@method, 9.01).should == false + + @bignum.send(@method, bignum_value(10)).should == false + end + + it "calls 'other == self' if the given argument is not an Integer" do + obj = mock('not integer') + obj.should_receive(:==).and_return(true) + @bignum.send(@method, obj).should == true + end + + it "returns the result of 'other == self' as a boolean" do + obj = mock('not integer') + obj.should_receive(:==).exactly(2).times.and_return("woot", nil) + @bignum.send(@method, obj).should == true + @bignum.send(@method, obj).should == false + end + end +end diff --git a/spec/ruby/core/integer/shared/exponent.rb b/spec/ruby/core/integer/shared/exponent.rb new file mode 100644 index 0000000000..5ef6d686d8 --- /dev/null +++ b/spec/ruby/core/integer/shared/exponent.rb @@ -0,0 +1,144 @@ +describe :integer_exponent, shared: true do + context "fixnum" do + it "returns self raised to the given power" do + 2.send(@method, 0).should eql 1 + 2.send(@method, 1).should eql 2 + 2.send(@method, 2).should eql 4 + + 9.send(@method, 0.5).should eql 3.0 + 9.send(@method, Rational(1, 2)).should eql 3.0 + 5.send(@method, -1).to_f.to_s.should == '0.2' + + 2.send(@method, 40).should eql 1099511627776 + end + + it "overflows the answer to a bignum transparently" do + 2.send(@method, 29).should eql 536870912 + 2.send(@method, 30).should eql 1073741824 + 2.send(@method, 31).should eql 2147483648 + 2.send(@method, 32).should eql 4294967296 + + 2.send(@method, 61).should eql 2305843009213693952 + 2.send(@method, 62).should eql 4611686018427387904 + 2.send(@method, 63).should eql 9223372036854775808 + 2.send(@method, 64).should eql 18446744073709551616 + 8.send(@method, 23).should eql 590295810358705651712 + end + + it "raises negative numbers to the given power" do + (-2).send(@method, 29).should eql(-536870912) + (-2).send(@method, 30).should eql(1073741824) + (-2).send(@method, 31).should eql(-2147483648) + (-2).send(@method, 32).should eql(4294967296) + (-2).send(@method, 33).should eql(-8589934592) + + (-2).send(@method, 61).should eql(-2305843009213693952) + (-2).send(@method, 62).should eql(4611686018427387904) + (-2).send(@method, 63).should eql(-9223372036854775808) + (-2).send(@method, 64).should eql(18446744073709551616) + (-2).send(@method, 65).should eql(-36893488147419103232) + end + + it "can raise 1 to a bignum safely" do + 1.send(@method, 4611686018427387904).should eql 1 + end + + it "can raise -1 to a bignum safely" do + (-1).send(@method, 4611686018427387904).should eql(1) + (-1).send(@method, 4611686018427387905).should eql(-1) + end + + ruby_version_is ""..."3.4" do + it "returns Float::INFINITY when the number is too big" do + -> { + 2.send(@method, 427387904).should == Float::INFINITY + }.should complain(/warning: in a\*\*b, b may be too big/) + end + end + + ruby_version_is "3.4" do + it "raises an ArgumentError when the number is too big" do + -> { 100000000.send(@method, 1000000000) }.should raise_error(ArgumentError) + end + end + + it "raises a ZeroDivisionError for 0 ** -1" do + -> { 0.send(@method, -1) }.should raise_error(ZeroDivisionError) + -> { 0.send(@method, Rational(-1, 1)) }.should raise_error(ZeroDivisionError) + end + + it "returns Float::INFINITY for 0 ** -1.0" do + 0.send(@method, -1.0).should == Float::INFINITY + end + + it "raises a TypeError when given a non-numeric power" do + -> { 13.send(@method, "10") }.should raise_error(TypeError) + -> { 13.send(@method, :symbol) }.should raise_error(TypeError) + -> { 13.send(@method, nil) }.should raise_error(TypeError) + end + + it "coerces power and calls #**" do + num_2 = mock("2") + num_13 = mock("13") + num_2.should_receive(:coerce).with(13).and_return([num_13, num_2]) + num_13.should_receive(:**).with(num_2).and_return(169) + + 13.send(@method, num_2).should == 169 + end + + it "returns Float when power is Float" do + 2.send(@method, 2.0).should == 4.0 + end + + it "returns Rational when power is Rational" do + 2.send(@method, Rational(2, 1)).should == Rational(4, 1) + end + + it "returns a complex number when negative and raised to a fractional power" do + (-8).send(@method, 1.0/3) .should be_close(Complex(1, 1.73205), TOLERANCE) + (-8).send(@method, Rational(1, 3)).should be_close(Complex(1, 1.73205), TOLERANCE) + end + end + + context "bignum" do + before :each do + @bignum = bignum_value(47) + end + + it "returns self raised to other power" do + (@bignum.send(@method, 4)).should == 115792089237316196603666111261383895964500887398800252671052694326794607293761 + (@bignum.send(@method, 1.3)).should be_close(11109528802438156839288832.0, TOLERANCE) + end + + it "raises a TypeError when given a non-Integer" do + -> { @bignum.send(@method, mock('10')) }.should raise_error(TypeError) + -> { @bignum.send(@method, "10") }.should raise_error(TypeError) + -> { @bignum.send(@method, :symbol) }.should raise_error(TypeError) + end + + ruby_version_is ""..."3.4" do + it "switch to a Float when the values is too big" do + flt = nil + -> { + flt = @bignum.send(@method, @bignum) + }.should complain(/warning: in a\*\*b, b may be too big/) + flt.should be_kind_of(Float) + flt.infinite?.should == 1 + end + end + + ruby_version_is "3.4" do + it "does not switch to a Float when the values is too big" do + -> { + @bignum.send(@method, @bignum) + }.should raise_error(ArgumentError) + end + end + + it "returns a complex number when negative and raised to a fractional power" do + (-bignum_value).send(@method, (1.0/2)).should be_close(Complex(0.0, 4294967296.0), TOLERANCE) + (-@bignum).send(@method, (1.0/3)) .should be_close(Complex(1321122.9748145656, 2288252.1154253655), TOLERANCE) + (-@bignum).send(@method, Rational(1,3)).should be_close(Complex(1321122.9748145656, 2288252.1154253655), TOLERANCE) + end + end +end diff --git a/spec/ruby/core/integer/shared/integer_ceil_precision.rb b/spec/ruby/core/integer/shared/integer_ceil_precision.rb new file mode 100644 index 0000000000..9f31c2cf61 --- /dev/null +++ b/spec/ruby/core/integer/shared/integer_ceil_precision.rb @@ -0,0 +1,43 @@ +describe :integer_ceil_precision, shared: true do + context "precision is zero" do + it "returns integer self" do + send(@method, 0).ceil(0).should.eql?(0) + send(@method, 123).ceil(0).should.eql?(123) + send(@method, -123).ceil(0).should.eql?(-123) + end + end + + context "precision is positive" do + it "returns self" do + send(@method, 0).ceil(1).should.eql?(send(@method, 0)) + send(@method, 0).ceil(10).should.eql?(send(@method, 0)) + + send(@method, 123).ceil(10).should.eql?(send(@method, 123)) + send(@method, -123).ceil(10).should.eql?(send(@method, -123)) + end + end + + context "precision is negative" do + it "always returns 0 when self is 0" do + send(@method, 0).ceil(-1).should.eql?(0) + send(@method, 0).ceil(-10).should.eql?(0) + end + + it "returns largest integer less than self with at least precision.abs trailing zeros" do + send(@method, 123).ceil(-1).should.eql?(130) + send(@method, 123).ceil(-2).should.eql?(200) + send(@method, 123).ceil(-3).should.eql?(1000) + + send(@method, -123).ceil(-1).should.eql?(-120) + send(@method, -123).ceil(-2).should.eql?(-100) + send(@method, -123).ceil(-3).should.eql?(0) + end + + ruby_bug "#20654", ""..."3.4" do + it "returns 10**precision.abs when precision.abs is larger than the number digits of self" do + send(@method, 123).ceil(-20).should.eql?(100000000000000000000) + send(@method, 123).ceil(-50).should.eql?(100000000000000000000000000000000000000000000000000) + end + end + end +end diff --git a/spec/ruby/core/integer/shared/integer_floor_precision.rb b/spec/ruby/core/integer/shared/integer_floor_precision.rb new file mode 100644 index 0000000000..4c5888c6c4 --- /dev/null +++ b/spec/ruby/core/integer/shared/integer_floor_precision.rb @@ -0,0 +1,43 @@ +describe :integer_floor_precision, shared: true do + context "precision is zero" do + it "returns integer self" do + send(@method, 0).floor(0).should.eql?(0) + send(@method, 123).floor(0).should.eql?(123) + send(@method, -123).floor(0).should.eql?(-123) + end + end + + context "precision is positive" do + it "returns self" do + send(@method, 0).floor(1).should.eql?(send(@method, 0)) + send(@method, 0).floor(10).should.eql?(send(@method, 0)) + + send(@method, 123).floor(10).should.eql?(send(@method, 123)) + send(@method, -123).floor(10).should.eql?(send(@method, -123)) + end + end + + context "precision is negative" do + it "always returns 0 when self is 0" do + send(@method, 0).floor(-1).should.eql?(0) + send(@method, 0).floor(-10).should.eql?(0) + end + + it "returns largest integer less than self with at least precision.abs trailing zeros" do + send(@method, 123).floor(-1).should.eql?(120) + send(@method, 123).floor(-2).should.eql?(100) + send(@method, 123).floor(-3).should.eql?(0) + + send(@method, -123).floor(-1).should.eql?(-130) + send(@method, -123).floor(-2).should.eql?(-200) + send(@method, -123).floor(-3).should.eql?(-1000) + end + + ruby_bug "#20654", ""..."3.4" do + it "returns -(10**precision.abs) when self is negative and precision.abs is larger than the number digits of self" do + send(@method, -123).floor(-20).should.eql?(-100000000000000000000) + send(@method, -123).floor(-50).should.eql?(-100000000000000000000000000000000000000000000000000) + end + end + end +end diff --git a/spec/ruby/core/integer/shared/integer_rounding.rb b/spec/ruby/core/integer/shared/integer_rounding.rb new file mode 100644 index 0000000000..56d1819f84 --- /dev/null +++ b/spec/ruby/core/integer/shared/integer_rounding.rb @@ -0,0 +1,19 @@ +describe :integer_rounding_positive_precision, shared: true do + it "returns self if not passed a precision" do + [2, -4, 10**70, -10**100].each do |v| + v.send(@method).should eql(v) + end + end + + it "returns self if passed a precision of zero" do + [2, -4, 10**70, -10**100].each do |v| + v.send(@method, 0).should eql(v) + end + end + + it "returns itself if passed a positive precision" do + [2, -4, 10**70, -10**100].each do |v| + v.send(@method, 42).should eql(v) + end + end +end diff --git a/spec/ruby/core/integer/shared/modulo.rb b/spec/ruby/core/integer/shared/modulo.rb new file mode 100644 index 0000000000..d91af1e924 --- /dev/null +++ b/spec/ruby/core/integer/shared/modulo.rb @@ -0,0 +1,114 @@ +describe :integer_modulo, shared: true do + context "fixnum" do + it "returns the modulus obtained from dividing self by the given argument" do + # test all possible combinations: + # - integer/double/bignum argument + # - positive/negative argument + # - positive/negative self + # - self greater/smaller than argument + + 13.send(@method, 4).should == 1 + 4.send(@method, 13).should == 4 + + 13.send(@method, 4.0).should == 1 + 4.send(@method, 13.0).should == 4 + + (-200).send(@method, 256).should == 56 + (-1000).send(@method, 512).should == 24 + + (-200).send(@method, -256).should == -200 + (-1000).send(@method, -512).should == -488 + + (200).send(@method, -256).should == -56 + (1000).send(@method, -512).should == -24 + + 13.send(@method, -4.0).should == -3.0 + 4.send(@method, -13.0).should == -9.0 + + -13.send(@method, -4.0).should == -1.0 + -4.send(@method, -13.0).should == -4.0 + + -13.send(@method, 4.0).should == 3.0 + -4.send(@method, 13.0).should == 9.0 + + 1.send(@method, 2.0).should == 1.0 + 200.send(@method, bignum_value).should == 200 + + 4.send(@method, bignum_value(10)).should == 4 + 4.send(@method, -bignum_value(10)).should == -18446744073709551622 + -4.send(@method, bignum_value(10)).should == 18446744073709551622 + -4.send(@method, -bignum_value(10)).should == -4 + end + + it "raises a ZeroDivisionError when the given argument is 0" do + -> { 13.send(@method, 0) }.should raise_error(ZeroDivisionError) + -> { 0.send(@method, 0) }.should raise_error(ZeroDivisionError) + -> { -10.send(@method, 0) }.should raise_error(ZeroDivisionError) + end + + it "raises a ZeroDivisionError when the given argument is 0 and a Float" do + -> { 0.send(@method, 0.0) }.should raise_error(ZeroDivisionError) + -> { 10.send(@method, 0.0) }.should raise_error(ZeroDivisionError) + -> { -10.send(@method, 0.0) }.should raise_error(ZeroDivisionError) + end + + it "raises a TypeError when given a non-Integer" do + -> { + (obj = mock('10')).should_receive(:to_int).any_number_of_times.and_return(10) + 13.send(@method, obj) + }.should raise_error(TypeError) + -> { 13.send(@method, "10") }.should raise_error(TypeError) + -> { 13.send(@method, :symbol) }.should raise_error(TypeError) + end + end + + context "bignum" do + before :each do + @bignum = bignum_value(10) + end + + it "returns the modulus obtained from dividing self by the given argument" do + # test all possible combinations: + # - integer/double/bignum argument + # - positive/negative argument + # - positive/negative self + # - self greater/smaller than argument + + @bignum.send(@method, 5).should == 1 + @bignum.send(@method, -5).should == -4 + (-@bignum).send(@method, 5).should == 4 + (-@bignum).send(@method, -5).should == -1 + + @bignum.send(@method, 2.22).should be_close(1.5603603603605034, TOLERANCE) + @bignum.send(@method, -2.22).should be_close(-0.6596396396394968, TOLERANCE) + (-@bignum).send(@method, 2.22).should be_close(0.6596396396394968, TOLERANCE) + (-@bignum).send(@method, -2.22).should be_close(-1.5603603603605034, TOLERANCE) + + @bignum.send(@method, @bignum + 10).should == 18446744073709551626 + @bignum.send(@method, -(@bignum + 10)).should == -10 + (-@bignum).send(@method, @bignum + 10).should == 10 + (-@bignum).send(@method, -(@bignum + 10)).should == -18446744073709551626 + + (@bignum + 10).send(@method, @bignum).should == 10 + (@bignum + 10).send(@method, -@bignum).should == -18446744073709551616 + (-(@bignum + 10)).send(@method, @bignum).should == 18446744073709551616 + (-(@bignum + 10)).send(@method, -@bignum).should == -10 + end + + it "raises a ZeroDivisionError when the given argument is 0" do + -> { @bignum.send(@method, 0) }.should raise_error(ZeroDivisionError) + -> { (-@bignum).send(@method, 0) }.should raise_error(ZeroDivisionError) + end + + it "raises a ZeroDivisionError when the given argument is 0 and a Float" do + -> { @bignum.send(@method, 0.0) }.should raise_error(ZeroDivisionError) + -> { -@bignum.send(@method, 0.0) }.should raise_error(ZeroDivisionError) + end + + it "raises a TypeError when given a non-Integer" do + -> { @bignum.send(@method, mock('10')) }.should raise_error(TypeError) + -> { @bignum.send(@method, "10") }.should raise_error(TypeError) + -> { @bignum.send(@method, :symbol) }.should raise_error(TypeError) + end + end +end diff --git a/spec/ruby/core/integer/shared/next.rb b/spec/ruby/core/integer/shared/next.rb new file mode 100644 index 0000000000..85b83d6965 --- /dev/null +++ b/spec/ruby/core/integer/shared/next.rb @@ -0,0 +1,25 @@ +describe :integer_next, shared: true do + it "returns the next larger positive Fixnum" do + 2.send(@method).should == 3 + end + + it "returns the next larger negative Fixnum" do + (-2).send(@method).should == -1 + end + + it "returns the next larger positive Bignum" do + bignum_value.send(@method).should == bignum_value(1) + end + + it "returns the next larger negative Bignum" do + (-bignum_value(1)).send(@method).should == -bignum_value + end + + it "overflows a Fixnum to a Bignum" do + fixnum_max.send(@method).should == fixnum_max + 1 + end + + it "underflows a Bignum to a Fixnum" do + (fixnum_min - 1).send(@method).should == fixnum_min + end +end diff --git a/spec/ruby/core/integer/shared/to_i.rb b/spec/ruby/core/integer/shared/to_i.rb new file mode 100644 index 0000000000..7b974cd3a7 --- /dev/null +++ b/spec/ruby/core/integer/shared/to_i.rb @@ -0,0 +1,8 @@ +describe :integer_to_i, shared: true do + it "returns self" do + 10.send(@method).should eql(10) + (-15).send(@method).should eql(-15) + bignum_value.send(@method).should eql(bignum_value) + (-bignum_value).send(@method).should eql(-bignum_value) + end +end diff --git a/spec/ruby/core/integer/size_spec.rb b/spec/ruby/core/integer/size_spec.rb new file mode 100644 index 0000000000..725e9eb062 --- /dev/null +++ b/spec/ruby/core/integer/size_spec.rb @@ -0,0 +1,34 @@ +require_relative '../../spec_helper' + +describe "Integer#size" do + platform_is c_long_size: 32 do + it "returns the number of bytes in the machine representation of self" do + -1.size.should == 4 + 0.size.should == 4 + 4091.size.should == 4 + end + end + + platform_is c_long_size: 64 do + it "returns the number of bytes in the machine representation of self" do + -1.size.should == 8 + 0.size.should == 8 + 4091.size.should == 8 + end + end + + context "bignum" do + it "returns the number of bytes required to hold the unsigned bignum data" do + # that is, n such that 256 * n <= val.abs < 256 * (n+1) + (256**7).size.should == 8 + (256**8).size.should == 9 + (256**9).size.should == 10 + (256**10).size.should == 11 + (256**10-1).size.should == 10 + (256**11).size.should == 12 + (256**12).size.should == 13 + (256**20-1).size.should == 20 + (256**40-1).size.should == 40 + end + end +end diff --git a/spec/ruby/core/integer/sqrt_spec.rb b/spec/ruby/core/integer/sqrt_spec.rb new file mode 100644 index 0000000000..4149ca2cc9 --- /dev/null +++ b/spec/ruby/core/integer/sqrt_spec.rb @@ -0,0 +1,31 @@ +require_relative '../../spec_helper' + +describe "Integer.sqrt" do + it "returns an integer" do + Integer.sqrt(10).should be_kind_of(Integer) + end + + it "returns the integer square root of the argument" do + Integer.sqrt(0).should == 0 + Integer.sqrt(1).should == 1 + Integer.sqrt(24).should == 4 + Integer.sqrt(25).should == 5 + Integer.sqrt(10**400).should == 10**200 + end + + it "raises a Math::DomainError if the argument is negative" do + -> { Integer.sqrt(-4) }.should raise_error(Math::DomainError) + end + + it "accepts any argument that can be coerced to Integer" do + Integer.sqrt(10.0).should == 3 + end + + it "converts the argument with #to_int" do + Integer.sqrt(mock_int(10)).should == 3 + end + + it "raises a TypeError if the argument cannot be coerced to Integer" do + -> { Integer.sqrt("test") }.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/integer/succ_spec.rb b/spec/ruby/core/integer/succ_spec.rb new file mode 100644 index 0000000000..9ae9a14fe7 --- /dev/null +++ b/spec/ruby/core/integer/succ_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/next' + +describe "Integer#succ" do + it_behaves_like :integer_next, :succ +end diff --git a/spec/ruby/core/integer/times_spec.rb b/spec/ruby/core/integer/times_spec.rb new file mode 100644 index 0000000000..356340592b --- /dev/null +++ b/spec/ruby/core/integer/times_spec.rb @@ -0,0 +1,79 @@ +require_relative '../../spec_helper' + +describe "Integer#times" do + it "returns self" do + 5.times {}.should == 5 + 9.times {}.should == 9 + 9.times { |n| n - 2 }.should == 9 + end + + it "yields each value from 0 to self - 1" do + a = [] + 9.times { |i| a << i } + -2.times { |i| a << i } + a.should == [0, 1, 2, 3, 4, 5, 6, 7, 8] + end + + it "skips the current iteration when encountering 'next'" do + a = [] + 3.times do |i| + next if i == 1 + a << i + end + a.should == [0, 2] + end + + it "skips all iterations when encountering 'break'" do + a = [] + 5.times do |i| + break if i == 3 + a << i + end + a.should == [0, 1, 2] + end + + it "skips all iterations when encountering break with an argument and returns that argument" do + 9.times { break 2 }.should == 2 + end + + it "executes a nested while loop containing a break expression" do + a = [false] + b = 1.times do |i| + while true + a.shift or break + end + end + a.should == [] + b.should == 1 + end + + it "executes a nested #times" do + a = 0 + b = 3.times do |i| + 2.times { a += 1 } + end + a.should == 6 + b.should == 3 + end + + it "returns an Enumerator" do + result = [] + + enum = 3.times + enum.each { |i| result << i } + + result.should == [0, 1, 2] + end + + describe "when no block is given" do + describe "returned Enumerator" do + describe "size" do + it "returns self" do + 5.times.size.should == 5 + 10.times.size.should == 10 + 0.times.size.should == 0 + end + end + end + end +end diff --git a/spec/ruby/core/integer/to_f_spec.rb b/spec/ruby/core/integer/to_f_spec.rb new file mode 100644 index 0000000000..9f1df9ada9 --- /dev/null +++ b/spec/ruby/core/integer/to_f_spec.rb @@ -0,0 +1,23 @@ +require_relative '../../spec_helper' + +describe "Integer#to_f" do + context "fixnum" do + it "returns self converted to a Float" do + 0.to_f.should == 0.0 + -500.to_f.should == -500.0 + 9_641_278.to_f.should == 9641278.0 + end + end + + context "bignum" do + it "returns self converted to a Float" do + bignum_value(0x4000_0aa0_0bb0_0000).to_f.should eql(23_058_441_774_644_068_352.0) + bignum_value(0x8000_0000_0000_0ccc).to_f.should eql(27_670_116_110_564_330_700.0) + (-bignum_value(99)).to_f.should eql(-18_446_744_073_709_551_715.0) + end + + it "converts number close to Float::MAX without exceeding MAX or producing NaN" do + (10**308).to_f.should == 10.0 ** 308 + end + end +end diff --git a/spec/ruby/core/integer/to_i_spec.rb b/spec/ruby/core/integer/to_i_spec.rb new file mode 100644 index 0000000000..1a8e477b3c --- /dev/null +++ b/spec/ruby/core/integer/to_i_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/to_i' + +describe "Integer#to_i" do + it_behaves_like :integer_to_i, :to_i +end diff --git a/spec/ruby/core/integer/to_int_spec.rb b/spec/ruby/core/integer/to_int_spec.rb new file mode 100644 index 0000000000..4b7eccad3a --- /dev/null +++ b/spec/ruby/core/integer/to_int_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/to_i' + +describe "Integer#to_int" do + it_behaves_like :integer_to_i, :to_int +end diff --git a/spec/ruby/core/integer/to_r_spec.rb b/spec/ruby/core/integer/to_r_spec.rb new file mode 100644 index 0000000000..7732bedca1 --- /dev/null +++ b/spec/ruby/core/integer/to_r_spec.rb @@ -0,0 +1,26 @@ +require_relative '../../spec_helper' + +describe "Integer#to_r" do + it "returns a Rational object" do + 309.to_r.should be_an_instance_of(Rational) + end + + it "constructs a rational number with self as the numerator" do + 34.to_r.numerator.should == 34 + end + + it "constructs a rational number with 1 as the denominator" do + 298.to_r.denominator.should == 1 + end + + it "works even if self is a Bignum" do + bignum = 99999**999 + bignum.should be_an_instance_of(Integer) + bignum.to_r.should == Rational(bignum, 1) + end + + it "raises an ArgumentError if given any arguments" do + -> { 287.to_r(2) }.should raise_error(ArgumentError) + -> { 9102826.to_r(309, [], 71) }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/integer/to_s_spec.rb b/spec/ruby/core/integer/to_s_spec.rb new file mode 100644 index 0000000000..ca08dad95c --- /dev/null +++ b/spec/ruby/core/integer/to_s_spec.rb @@ -0,0 +1,95 @@ +require_relative '../../spec_helper' + +describe "Integer#to_s" do + context "fixnum" do + context "when given a base" do + it "returns self converted to a String in the given base" do + 12345.to_s(2).should == "11000000111001" + 12345.to_s(8).should == "30071" + 12345.to_s(10).should == "12345" + 12345.to_s(16).should == "3039" + 95.to_s(16).should == "5f" + 12345.to_s(36).should == "9ix" + end + + it "raises an ArgumentError if the base is less than 2 or higher than 36" do + -> { 123.to_s(-1) }.should raise_error(ArgumentError) + -> { 123.to_s(0) }.should raise_error(ArgumentError) + -> { 123.to_s(1) }.should raise_error(ArgumentError) + -> { 123.to_s(37) }.should raise_error(ArgumentError) + end + end + + context "when no base given" do + it "returns self converted to a String using base 10" do + 255.to_s.should == '255' + 3.to_s.should == '3' + 0.to_s.should == '0' + -9002.to_s.should == '-9002' + end + end + + before :each do + @internal = Encoding.default_internal + end + + after :each do + Encoding.default_internal = @internal + end + + it "returns a String in US-ASCII encoding when Encoding.default_internal is nil" do + Encoding.default_internal = nil + 1.to_s.encoding.should equal(Encoding::US_ASCII) + end + + it "returns a String in US-ASCII encoding when Encoding.default_internal is not nil" do + Encoding.default_internal = Encoding::IBM437 + 1.to_s.encoding.should equal(Encoding::US_ASCII) + end + end + + context "bignum" do + describe "when given a base" do + it "returns self converted to a String using the given base" do + a = 2**64 + a.to_s(2).should == "10000000000000000000000000000000000000000000000000000000000000000" + a.to_s(8).should == "2000000000000000000000" + a.to_s(16).should == "10000000000000000" + a.to_s(32).should == "g000000000000" + end + + it "raises an ArgumentError if the base is less than 2 or higher than 36" do + -> { 123.to_s(-1) }.should raise_error(ArgumentError) + -> { 123.to_s(0) }.should raise_error(ArgumentError) + -> { 123.to_s(1) }.should raise_error(ArgumentError) + -> { 123.to_s(37) }.should raise_error(ArgumentError) + end + end + + describe "when given no base" do + it "returns self converted to a String using base 10" do + bignum_value(9).to_s.should == "18446744073709551625" + bignum_value.to_s.should == "18446744073709551616" + (-bignum_value(675)).to_s.should == "-18446744073709552291" + end + end + + before :each do + @internal = Encoding.default_internal + end + + after :each do + Encoding.default_internal = @internal + end + + it "returns a String in US-ASCII encoding when Encoding.default_internal is nil" do + Encoding.default_internal = nil + bignum_value.to_s.encoding.should equal(Encoding::US_ASCII) + end + + it "returns a String in US-ASCII encoding when Encoding.default_internal is not nil" do + Encoding.default_internal = Encoding::IBM437 + bignum_value.to_s.encoding.should equal(Encoding::US_ASCII) + end + end +end diff --git a/spec/ruby/core/integer/truncate_spec.rb b/spec/ruby/core/integer/truncate_spec.rb new file mode 100644 index 0000000000..db16e74be4 --- /dev/null +++ b/spec/ruby/core/integer/truncate_spec.rb @@ -0,0 +1,19 @@ +require_relative '../../spec_helper' +require_relative 'shared/to_i' +require_relative 'shared/integer_rounding' + +describe "Integer#truncate" do + it_behaves_like :integer_to_i, :truncate + it_behaves_like :integer_rounding_positive_precision, :truncate + + context "precision argument specified as part of the truncate method is negative" do + it "returns an integer with at least precision.abs trailing zeros" do + 1832.truncate(-1).should eql(1830) + 1832.truncate(-2).should eql(1800) + 1832.truncate(-3).should eql(1000) + -1832.truncate(-1).should eql(-1830) + -1832.truncate(-2).should eql(-1800) + -1832.truncate(-3).should eql(-1000) + end + end +end diff --git a/spec/ruby/core/integer/try_convert_spec.rb b/spec/ruby/core/integer/try_convert_spec.rb new file mode 100644 index 0000000000..8a0ca671a9 --- /dev/null +++ b/spec/ruby/core/integer/try_convert_spec.rb @@ -0,0 +1,48 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Integer.try_convert" do + it "returns the argument if it's an Integer" do + x = 42 + Integer.try_convert(x).should equal(x) + end + + it "returns nil when the argument does not respond to #to_int" do + Integer.try_convert(Object.new).should be_nil + end + + it "sends #to_int to the argument and returns the result if it's nil" do + obj = mock("to_int") + obj.should_receive(:to_int).and_return(nil) + Integer.try_convert(obj).should be_nil + end + + it "sends #to_int to the argument and returns the result if it's an Integer" do + x = 234 + obj = mock("to_int") + obj.should_receive(:to_int).and_return(x) + Integer.try_convert(obj).should equal(x) + end + + it "sends #to_int to the argument and raises TypeError if it's not a kind of Integer" do + obj = mock("to_int") + obj.should_receive(:to_int).and_return(Object.new) + -> { + Integer.try_convert obj + }.should raise_error(TypeError, "can't convert MockObject to Integer (MockObject#to_int gives Object)") + end + + it "responds with a different error message when it raises a TypeError, depending on the type of the non-Integer object :to_int returns" do + obj = mock("to_int") + obj.should_receive(:to_int).and_return("A String") + -> { + Integer.try_convert obj + }.should raise_error(TypeError, "can't convert MockObject to Integer (MockObject#to_int gives String)") + end + + it "does not rescue exceptions raised by #to_int" do + obj = mock("to_int") + obj.should_receive(:to_int).and_raise(RuntimeError) + -> { Integer.try_convert obj }.should raise_error(RuntimeError) + end +end diff --git a/spec/ruby/core/integer/uminus_spec.rb b/spec/ruby/core/integer/uminus_spec.rb new file mode 100644 index 0000000000..7a9cfe89bf --- /dev/null +++ b/spec/ruby/core/integer/uminus_spec.rb @@ -0,0 +1,30 @@ +require_relative '../../spec_helper' + +describe "Integer#-@" do + context "fixnum" do + it "returns self as a negative value" do + 2.send(:-@).should == -2 + -2.should == -2 + -268435455.should == -268435455 + (--5).should == 5 + -8.send(:-@).should == 8 + end + + it "negates self at Fixnum/Bignum boundaries" do + (-fixnum_max).should == (0 - fixnum_max) + (-fixnum_max).should < 0 + (-fixnum_min).should == (0 - fixnum_min) + (-fixnum_min).should > 0 + end + end + + context "bignum" do + it "returns self as a negative value" do + bignum_value.send(:-@).should == -18446744073709551616 + (-bignum_value).send(:-@).should == 18446744073709551616 + + bignum_value(921).send(:-@).should == -18446744073709552537 + (-bignum_value(921).send(:-@)).should == 18446744073709552537 + end + end +end diff --git a/spec/ruby/core/integer/upto_spec.rb b/spec/ruby/core/integer/upto_spec.rb new file mode 100644 index 0000000000..dd6995e94b --- /dev/null +++ b/spec/ruby/core/integer/upto_spec.rb @@ -0,0 +1,69 @@ +require_relative '../../spec_helper' + +describe "Integer#upto [stop] when self and stop are Integers" do + it "does not yield when stop is less than self" do + result = [] + 5.upto(4) { |x| result << x } + result.should == [] + end + + it "yields once when stop equals self" do + result = [] + 5.upto(5) { |x| result << x } + result.should == [5] + end + + it "yields while increasing self until it is less than stop" do + result = [] + 2.upto(5) { |x| result << x } + result.should == [2, 3, 4, 5] + end + + it "yields while increasing self until it is greater than floor of a Float endpoint" do + result = [] + 9.upto(13.3) {|i| result << i} + -5.upto(-1.3) {|i| result << i} + result.should == [9,10,11,12,13,-5,-4,-3,-2] + end + + it "raises an ArgumentError for non-numeric endpoints" do + -> { 1.upto("A") {|x| p x} }.should raise_error(ArgumentError) + -> { 1.upto(nil) {|x| p x} }.should raise_error(ArgumentError) + end + + describe "when no block is given" do + it "returns an Enumerator" do + result = [] + + enum = 2.upto(5) + enum.each { |i| result << i } + + result.should == [2, 3, 4, 5] + end + + describe "returned Enumerator" do + describe "size" do + it "raises an ArgumentError for non-numeric endpoints" do + enum = 1.upto("A") + -> { enum.size }.should raise_error(ArgumentError) + enum = 1.upto(nil) + -> { enum.size }.should raise_error(ArgumentError) + end + + it "returns stop - self + 1" do + 5.upto(10).size.should == 6 + 1.upto(10).size.should == 10 + 0.upto(10).size.should == 11 + 0.upto(0).size.should == 1 + -5.upto(-3).size.should == 3 + end + + it "returns 0 when stop < self" do + 5.upto(4).size.should == 0 + 0.upto(-5).size.should == 0 + -3.upto(-5).size.should == 0 + end + end + end + end +end diff --git a/spec/ruby/core/integer/zero_spec.rb b/spec/ruby/core/integer/zero_spec.rb new file mode 100644 index 0000000000..bd362c4181 --- /dev/null +++ b/spec/ruby/core/integer/zero_spec.rb @@ -0,0 +1,13 @@ +require_relative '../../spec_helper' + +describe "Integer#zero?" do + it "returns true if self is 0" do + 0.should.zero? + 1.should_not.zero? + -1.should_not.zero? + end + + it "Integer#zero? overrides Numeric#zero?" do + 42.method(:zero?).owner.should == Integer + end +end diff --git a/spec/ruby/core/io/advise_spec.rb b/spec/ruby/core/io/advise_spec.rb new file mode 100644 index 0000000000..651fc52378 --- /dev/null +++ b/spec/ruby/core/io/advise_spec.rb @@ -0,0 +1,86 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#advise" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.close unless @io.closed? + end + + it "raises a TypeError if advise is not a Symbol" do + -> { + @io.advise("normal") + }.should raise_error(TypeError) + end + + it "raises a TypeError if offset cannot be coerced to an Integer" do + -> { + @io.advise(:normal, "wat") + }.should raise_error(TypeError) + end + + it "raises a TypeError if len cannot be coerced to an Integer" do + -> { + @io.advise(:normal, 0, "wat") + }.should raise_error(TypeError) + end + + it "raises a RangeError if offset is too big" do + -> { + @io.advise(:normal, 10 ** 32) + }.should raise_error(RangeError) + end + + it "raises a RangeError if len is too big" do + -> { + @io.advise(:normal, 0, 10 ** 32) + }.should raise_error(RangeError) + end + + it "raises a NotImplementedError if advise is not recognized" do + ->{ + @io.advise(:foo) + }.should raise_error(NotImplementedError) + end + + it "supports the normal advice type" do + @io.advise(:normal).should be_nil + end + + it "supports the sequential advice type" do + @io.advise(:sequential).should be_nil + end + + it "supports the random advice type" do + @io.advise(:random).should be_nil + end + + it "supports the dontneed advice type" do + @io.advise(:dontneed).should be_nil + end + + it "supports the noreuse advice type" do + @io.advise(:noreuse).should be_nil + end + + platform_is_not :linux do + it "supports the willneed advice type" do + @io.advise(:willneed).should be_nil + end + end + + guard -> { platform_is :linux and kernel_version_is '3.6' } do # [ruby-core:65355] tmpfs is not supported + it "supports the willneed advice type" do + @io.advise(:willneed).should be_nil + end + end + + it "raises an IOError if the stream is closed" do + @io.close + -> { @io.advise(:normal) }.should raise_error(IOError) + end +end diff --git a/spec/ruby/core/io/autoclose_spec.rb b/spec/ruby/core/io/autoclose_spec.rb new file mode 100644 index 0000000000..715ada7c93 --- /dev/null +++ b/spec/ruby/core/io/autoclose_spec.rb @@ -0,0 +1,77 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#autoclose?" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.autoclose = true unless @io.closed? + @io.close unless @io.closed? + end + + it "is set to true by default" do + @io.should.autoclose? + end + + it "cannot be queried on a closed IO object" do + @io.close + -> { @io.autoclose? }.should raise_error(IOError, /closed stream/) + end +end + +describe "IO#autoclose=" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.autoclose = true unless @io.closed? + @io.close unless @io.closed? + end + + it "can be set to true" do + @io.autoclose = false + @io.autoclose = true + @io.should.autoclose? + end + + it "can be set to false" do + @io.autoclose = true + @io.autoclose = false + @io.should_not.autoclose? + end + + it "can be set to any truthy value" do + @io.autoclose = false + @io.autoclose = 42 + @io.should.autoclose? + + @io.autoclose = false + @io.autoclose = Object.new + @io.should.autoclose? + end + + it "can be set to any falsy value" do + @io.autoclose = true + @io.autoclose = nil + @io.should_not.autoclose? + end + + it "can be set multiple times" do + @io.autoclose = true + @io.should.autoclose? + + @io.autoclose = false + @io.should_not.autoclose? + + @io.autoclose = true + @io.should.autoclose? + end + + it "cannot be set on a closed IO object" do + @io.close + -> { @io.autoclose = false }.should raise_error(IOError, /closed stream/) + end +end diff --git a/spec/ruby/core/io/binmode_spec.rb b/spec/ruby/core/io/binmode_spec.rb new file mode 100644 index 0000000000..342cac2a9b --- /dev/null +++ b/spec/ruby/core/io/binmode_spec.rb @@ -0,0 +1,64 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#binmode" do + before :each do + @name = tmp("io_binmode.txt") + end + + after :each do + @io.close if @io and !@io.closed? + rm_r @name + end + + it "returns self" do + @io = new_io(@name) + @io.binmode.should equal(@io) + end + + it "raises an IOError on closed stream" do + -> { IOSpecs.closed_io.binmode }.should raise_error(IOError) + end + + it "sets external encoding to binary" do + @io = new_io(@name, "w:utf-8") + @io.binmode + @io.external_encoding.should == Encoding::BINARY + end + + it "sets internal encoding to nil" do + @io = new_io(@name, "w:utf-8:ISO-8859-1") + @io.binmode + @io.internal_encoding.should == nil + end +end + +describe "IO#binmode?" do + before :each do + @filename = tmp("IO_binmode_file") + @file = File.open(@filename, "w") + @duped = nil + end + + after :each do + @duped.close if @duped + @file.close + rm_r @filename + end + + it "is true after a call to IO#binmode" do + @file.binmode?.should be_false + @file.binmode + @file.binmode?.should be_true + end + + it "propagates to dup'ed IO objects" do + @file.binmode + @duped = @file.dup + @duped.binmode?.should == @file.binmode? + end + + it "raises an IOError on closed stream" do + -> { IOSpecs.closed_io.binmode? }.should raise_error(IOError) + end +end diff --git a/spec/ruby/core/io/binread_spec.rb b/spec/ruby/core/io/binread_spec.rb new file mode 100644 index 0000000000..9e36b84da9 --- /dev/null +++ b/spec/ruby/core/io/binread_spec.rb @@ -0,0 +1,57 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO.binread" do + before :each do + @internal = Encoding.default_internal + + @fname = tmp('io_read.txt') + @contents = "1234567890" + touch(@fname) { |f| f.write @contents } + end + + after :each do + rm_r @fname + Encoding.default_internal = @internal + end + + it "reads the contents of a file" do + IO.binread(@fname).should == @contents + end + + it "reads the contents of a file up to a certain size when specified" do + IO.binread(@fname, 5).should == @contents.slice(0..4) + end + + it "reads the contents of a file from an offset of a specific size when specified" do + IO.binread(@fname, 5, 3).should == @contents.slice(3, 5) + end + + it "returns a String in BINARY encoding" do + IO.binread(@fname).encoding.should == Encoding::BINARY + end + + it "returns a String in BINARY encoding regardless of Encoding.default_internal" do + Encoding.default_internal = Encoding::EUC_JP + IO.binread(@fname).encoding.should == Encoding::BINARY + end + + it "raises an ArgumentError when not passed a valid length" do + -> { IO.binread @fname, -1 }.should raise_error(ArgumentError) + end + + it "raises an Errno::EINVAL when not passed a valid offset" do + -> { IO.binread @fname, 0, -1 }.should raise_error(Errno::EINVAL) + end + + ruby_version_is "3.3"..."4.0" do + # https://bugs.ruby-lang.org/issues/19630 + it "warns about deprecation given a path with a pipe" do + cmd = "|echo ok" + -> { + IO.binread(cmd) + }.should complain(/IO process creation with a leading '\|'/) + end + end +end diff --git a/spec/ruby/core/io/binwrite_spec.rb b/spec/ruby/core/io/binwrite_spec.rb new file mode 100644 index 0000000000..8ebc86a52e --- /dev/null +++ b/spec/ruby/core/io/binwrite_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/binwrite' + +describe "IO.binwrite" do + it_behaves_like :io_binwrite, :binwrite +end diff --git a/spec/ruby/core/io/buffer/empty_spec.rb b/spec/ruby/core/io/buffer/empty_spec.rb new file mode 100644 index 0000000000..e1fd4ab6a2 --- /dev/null +++ b/spec/ruby/core/io/buffer/empty_spec.rb @@ -0,0 +1,29 @@ +require_relative '../../../spec_helper' +require_relative 'shared/null_and_empty' + +describe "IO::Buffer#empty?" do + after :each do + @buffer&.free + @buffer = nil + end + + it_behaves_like :io_buffer_null_and_empty, :empty? + + it "is true for a 0-length String-backed buffer created with .for" do + @buffer = IO::Buffer.for("") + @buffer.empty?.should be_true + end + + ruby_version_is "3.3" do + it "is true for a 0-length String-backed buffer created with .string" do + IO::Buffer.string(0) do |buffer| + buffer.empty?.should be_true + end + end + end + + it "is true for a 0-length slice of a buffer with size > 0" do + @buffer = IO::Buffer.new(4) + @buffer.slice(3, 0).empty?.should be_true + end +end diff --git a/spec/ruby/core/io/buffer/external_spec.rb b/spec/ruby/core/io/buffer/external_spec.rb new file mode 100644 index 0000000000..4377a38357 --- /dev/null +++ b/spec/ruby/core/io/buffer/external_spec.rb @@ -0,0 +1,108 @@ +require_relative '../../../spec_helper' + +describe "IO::Buffer#external?" do + after :each do + @buffer&.free + @buffer = nil + end + + context "with a buffer created with .new" do + it "is false for an internal buffer" do + @buffer = IO::Buffer.new(4) + @buffer.external?.should be_false + end + + it "is false for a mapped buffer" do + @buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) + @buffer.external?.should be_false + end + end + + context "with a file-backed buffer created with .map" do + it "is true for a regular mapping" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) + @buffer.external?.should be_true + end + end + + ruby_version_is "3.3" do + it "is false for a private mapping" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY | IO::Buffer::PRIVATE) + @buffer.external?.should be_false + end + end + end + end + + context "with a String-backed buffer created with .for" do + it "is true for a buffer created without a block" do + @buffer = IO::Buffer.for("test") + @buffer.external?.should be_true + end + + it "is true for a buffer created with a block" do + IO::Buffer.for(+"test") do |buffer| + buffer.external?.should be_true + end + end + end + + ruby_version_is "3.3" do + context "with a String-backed buffer created with .string" do + it "is true" do + IO::Buffer.string(4) do |buffer| + buffer.external?.should be_true + end + end + end + end + + # Always false for slices + context "with a slice of a buffer" do + context "created with .new" do + it "is false when slicing an internal buffer" do + @buffer = IO::Buffer.new(4) + @buffer.slice.external?.should be_false + end + + it "is false when slicing a mapped buffer" do + @buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) + @buffer.slice.external?.should be_false + end + end + + context "created with .map" do + it "is false" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) + @buffer.slice.external?.should be_false + end + end + end + + context "created with .for" do + it "is false when slicing a buffer created without a block" do + @buffer = IO::Buffer.for("test") + @buffer.slice.external?.should be_false + end + + it "is false when slicing a buffer created with a block" do + IO::Buffer.for(+"test") do |buffer| + buffer.slice.external?.should be_false + end + end + end + + ruby_version_is "3.3" do + context "created with .string" do + it "is false" do + IO::Buffer.string(4) do |buffer| + buffer.slice.external?.should be_false + end + end + end + end + end +end diff --git a/spec/ruby/core/io/buffer/free_spec.rb b/spec/ruby/core/io/buffer/free_spec.rb new file mode 100644 index 0000000000..f3a4918978 --- /dev/null +++ b/spec/ruby/core/io/buffer/free_spec.rb @@ -0,0 +1,104 @@ +require_relative '../../../spec_helper' + +describe "IO::Buffer#free" do + context "with a buffer created with .new" do + it "frees internal memory and nullifies the buffer" do + buffer = IO::Buffer.new(4) + buffer.free + buffer.null?.should be_true + end + + it "frees mapped memory and nullifies the buffer" do + buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) + buffer.free + buffer.null?.should be_true + end + end + + context "with a file-backed buffer created with .map" do + it "frees mapped memory and nullifies the buffer" do + File.open(__FILE__, "r") do |file| + buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) + buffer.free + buffer.null?.should be_true + end + end + end + + context "with a String-backed buffer created with .for" do + context "without a block" do + it "disassociates the buffer from the string and nullifies the buffer" do + string = +"test" + buffer = IO::Buffer.for(string) + # Read-only buffer, can't modify the string. + buffer.free + buffer.null?.should be_true + end + end + + context "with a block" do + it "disassociates the buffer from the string and nullifies the buffer" do + string = +"test" + IO::Buffer.for(string) do |buffer| + buffer.set_string("meat") + buffer.free + buffer.null?.should be_true + end + string.should == "meat" + end + end + end + + ruby_version_is "3.3" do + context "with a String-backed buffer created with .string" do + it "disassociates the buffer from the string and nullifies the buffer" do + string = + IO::Buffer.string(4) do |buffer| + buffer.set_string("meat") + buffer.free + buffer.null?.should be_true + end + string.should == "meat" + end + end + end + + it "can be called repeatedly without an error" do + buffer = IO::Buffer.new(4) + buffer.free + buffer.null?.should be_true + buffer.free + buffer.null?.should be_true + end + + it "is disallowed while locked, raising IO::Buffer::LockedError" do + buffer = IO::Buffer.new(4) + buffer.locked do + -> { buffer.free }.should raise_error(IO::Buffer::LockedError, "Buffer is locked!") + end + buffer.free + buffer.null?.should be_true + end + + context "with a slice of a buffer" do + it "nullifies the slice, not touching the buffer" do + buffer = IO::Buffer.new(4) + slice = buffer.slice(0, 2) + + slice.free + slice.null?.should be_true + buffer.null?.should be_false + + buffer.free + end + + it "nullifies buffer, invalidating the slice" do + buffer = IO::Buffer.new(4) + slice = buffer.slice(0, 2) + + buffer.free + slice.null?.should be_false + slice.valid?.should be_false + end + end +end diff --git a/spec/ruby/core/io/buffer/initialize_spec.rb b/spec/ruby/core/io/buffer/initialize_spec.rb new file mode 100644 index 0000000000..c86d1e7f1d --- /dev/null +++ b/spec/ruby/core/io/buffer/initialize_spec.rb @@ -0,0 +1,103 @@ +require_relative '../../../spec_helper' + +describe "IO::Buffer#initialize" do + after :each do + @buffer&.free + @buffer = nil + end + + it "creates a new zero-filled buffer with default size" do + @buffer = IO::Buffer.new + @buffer.size.should == IO::Buffer::DEFAULT_SIZE + @buffer.each(:U8).should.all? { |_offset, value| value.eql?(0) } + end + + it "creates a buffer with default state" do + @buffer = IO::Buffer.new + @buffer.should_not.shared? + @buffer.should_not.readonly? + + @buffer.should_not.empty? + @buffer.should_not.null? + + # This is run-time state, set by #locked. + @buffer.should_not.locked? + end + + context "with size argument" do + it "creates a new internal buffer if size is less than IO::Buffer::PAGE_SIZE" do + size = IO::Buffer::PAGE_SIZE - 1 + @buffer = IO::Buffer.new(size) + @buffer.size.should == size + @buffer.should.internal? + @buffer.should_not.mapped? + @buffer.should_not.empty? + end + + it "creates a new mapped buffer if size is greater than or equal to IO::Buffer::PAGE_SIZE" do + size = IO::Buffer::PAGE_SIZE + @buffer = IO::Buffer.new(size) + @buffer.size.should == size + @buffer.should_not.internal? + @buffer.should.mapped? + @buffer.should_not.empty? + end + + it "creates a null buffer if size is 0" do + @buffer = IO::Buffer.new(0) + @buffer.size.should.zero? + @buffer.should_not.internal? + @buffer.should_not.mapped? + @buffer.should.null? + @buffer.should.empty? + end + + it "raises TypeError if size is not an Integer" do + -> { IO::Buffer.new(nil) }.should raise_error(TypeError, "not an Integer") + -> { IO::Buffer.new(10.0) }.should raise_error(TypeError, "not an Integer") + end + + it "raises ArgumentError if size is negative" do + -> { IO::Buffer.new(-1) }.should raise_error(ArgumentError, "Size can't be negative!") + end + end + + context "with size and flags arguments" do + it "forces mapped buffer with IO::Buffer::MAPPED flag" do + @buffer = IO::Buffer.new(IO::Buffer::PAGE_SIZE - 1, IO::Buffer::MAPPED) + @buffer.should.mapped? + @buffer.should_not.internal? + @buffer.should_not.empty? + end + + it "forces internal buffer with IO::Buffer::INTERNAL flag" do + @buffer = IO::Buffer.new(IO::Buffer::PAGE_SIZE, IO::Buffer::INTERNAL) + @buffer.should.internal? + @buffer.should_not.mapped? + @buffer.should_not.empty? + end + + it "raises IO::Buffer::AllocationError if neither IO::Buffer::MAPPED nor IO::Buffer::INTERNAL is given" do + -> { IO::Buffer.new(10, IO::Buffer::READONLY) }.should raise_error(IO::Buffer::AllocationError, "Could not allocate buffer!") + -> { IO::Buffer.new(10, 0) }.should raise_error(IO::Buffer::AllocationError, "Could not allocate buffer!") + end + + ruby_version_is "3.3" do + it "raises ArgumentError if flags is negative" do + -> { IO::Buffer.new(10, -1) }.should raise_error(ArgumentError, "Flags can't be negative!") + end + end + + ruby_version_is ""..."3.3" do + it "raises IO::Buffer::AllocationError with non-Integer flags" do + -> { IO::Buffer.new(10, 0.0) }.should raise_error(IO::Buffer::AllocationError, "Could not allocate buffer!") + end + end + + ruby_version_is "3.3" do + it "raises TypeError with non-Integer flags" do + -> { IO::Buffer.new(10, 0.0) }.should raise_error(TypeError, "not an Integer") + end + end + end +end diff --git a/spec/ruby/core/io/buffer/internal_spec.rb b/spec/ruby/core/io/buffer/internal_spec.rb new file mode 100644 index 0000000000..409699cc3c --- /dev/null +++ b/spec/ruby/core/io/buffer/internal_spec.rb @@ -0,0 +1,108 @@ +require_relative '../../../spec_helper' + +describe "IO::Buffer#internal?" do + after :each do + @buffer&.free + @buffer = nil + end + + context "with a buffer created with .new" do + it "is true for an internal buffer" do + @buffer = IO::Buffer.new(4) + @buffer.internal?.should be_true + end + + it "is false for a mapped buffer" do + @buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) + @buffer.internal?.should be_false + end + end + + context "with a file-backed buffer created with .map" do + it "is false for a regular mapping" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) + @buffer.internal?.should be_false + end + end + + ruby_version_is "3.3" do + it "is false for a private mapping" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY | IO::Buffer::PRIVATE) + @buffer.internal?.should be_false + end + end + end + end + + context "with a String-backed buffer created with .for" do + it "is false for a buffer created without a block" do + @buffer = IO::Buffer.for("test") + @buffer.internal?.should be_false + end + + it "is false for a buffer created with a block" do + IO::Buffer.for(+"test") do |buffer| + buffer.internal?.should be_false + end + end + end + + ruby_version_is "3.3" do + context "with a String-backed buffer created with .string" do + it "is false" do + IO::Buffer.string(4) do |buffer| + buffer.internal?.should be_false + end + end + end + end + + # Always false for slices + context "with a slice of a buffer" do + context "created with .new" do + it "is false when slicing an internal buffer" do + @buffer = IO::Buffer.new(4) + @buffer.slice.internal?.should be_false + end + + it "is false when slicing a mapped buffer" do + @buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) + @buffer.slice.internal?.should be_false + end + end + + context "created with .map" do + it "is false" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) + @buffer.slice.internal?.should be_false + end + end + end + + context "created with .for" do + it "is false when slicing a buffer created without a block" do + @buffer = IO::Buffer.for("test") + @buffer.slice.internal?.should be_false + end + + it "is false when slicing a buffer created with a block" do + IO::Buffer.for(+"test") do |buffer| + buffer.slice.internal?.should be_false + end + end + end + + ruby_version_is "3.3" do + context "created with .string" do + it "is false" do + IO::Buffer.string(4) do |buffer| + buffer.slice.internal?.should be_false + end + end + end + end + end +end diff --git a/spec/ruby/core/io/buffer/locked_spec.rb b/spec/ruby/core/io/buffer/locked_spec.rb new file mode 100644 index 0000000000..4ffa569fd2 --- /dev/null +++ b/spec/ruby/core/io/buffer/locked_spec.rb @@ -0,0 +1,75 @@ +require_relative '../../../spec_helper' + +describe "IO::Buffer#locked" do + after :each do + @buffer&.free + @buffer = nil + end + + context "when buffer is locked" do + it "allows reading and writing operations on the buffer" do + @buffer = IO::Buffer.new(4) + @buffer.set_string("test") + @buffer.locked do + @buffer.get_string.should == "test" + @buffer.set_string("meat") + end + @buffer.get_string.should == "meat" + end + + it "disallows operations changing buffer itself, raising IO::Buffer::LockedError" do + @buffer = IO::Buffer.new(4) + @buffer.locked do + # Just an example, each method is responsible for checking the lock state. + -> { @buffer.resize(8) }.should raise_error(IO::Buffer::LockedError) + end + end + end + + it "disallows reentrant locking, raising IO::Buffer::LockedError" do + @buffer = IO::Buffer.new(4) + @buffer.locked do + -> { @buffer.locked {} }.should raise_error(IO::Buffer::LockedError, "Buffer already locked!") + end + end + + it "does not propagate to buffer's slices" do + @buffer = IO::Buffer.new(4) + slice = @buffer.slice(0, 2) + @buffer.locked do + @buffer.locked?.should be_true + slice.locked?.should be_false + slice.locked { slice.locked?.should be_true } + end + end + + it "does not propagate backwards from buffer's slices" do + @buffer = IO::Buffer.new(4) + slice = @buffer.slice(0, 2) + slice.locked do + slice.locked?.should be_true + @buffer.locked?.should be_false + @buffer.locked { @buffer.locked?.should be_true } + end + end +end + +describe "IO::Buffer#locked?" do + after :each do + @buffer&.free + @buffer = nil + end + + it "is false by default" do + @buffer = IO::Buffer.new(4) + @buffer.locked?.should be_false + end + + it "is true only inside of #locked block" do + @buffer = IO::Buffer.new(4) + @buffer.locked do + @buffer.locked?.should be_true + end + @buffer.locked?.should be_false + end +end diff --git a/spec/ruby/core/io/buffer/mapped_spec.rb b/spec/ruby/core/io/buffer/mapped_spec.rb new file mode 100644 index 0000000000..b3610207ff --- /dev/null +++ b/spec/ruby/core/io/buffer/mapped_spec.rb @@ -0,0 +1,108 @@ +require_relative '../../../spec_helper' + +describe "IO::Buffer#mapped?" do + after :each do + @buffer&.free + @buffer = nil + end + + context "with a buffer created with .new" do + it "is false for an internal buffer" do + @buffer = IO::Buffer.new(4) + @buffer.mapped?.should be_false + end + + it "is true for a mapped buffer" do + @buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) + @buffer.mapped?.should be_true + end + end + + context "with a file-backed buffer created with .map" do + it "is true for a regular mapping" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) + @buffer.mapped?.should be_true + end + end + + ruby_version_is "3.3" do + it "is true for a private mapping" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY | IO::Buffer::PRIVATE) + @buffer.mapped?.should be_true + end + end + end + end + + context "with a String-backed buffer created with .for" do + it "is false for a buffer created without a block" do + @buffer = IO::Buffer.for("test") + @buffer.mapped?.should be_false + end + + it "is false for a buffer created with a block" do + IO::Buffer.for(+"test") do |buffer| + buffer.mapped?.should be_false + end + end + end + + ruby_version_is "3.3" do + context "with a String-backed buffer created with .string" do + it "is false" do + IO::Buffer.string(4) do |buffer| + buffer.mapped?.should be_false + end + end + end + end + + # Always false for slices + context "with a slice of a buffer" do + context "created with .new" do + it "is false when slicing an internal buffer" do + @buffer = IO::Buffer.new(4) + @buffer.slice.mapped?.should be_false + end + + it "is false when slicing a mapped buffer" do + @buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) + @buffer.slice.mapped?.should be_false + end + end + + context "created with .map" do + it "is false" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) + @buffer.slice.mapped?.should be_false + end + end + end + + context "created with .for" do + it "is false when slicing a buffer created without a block" do + @buffer = IO::Buffer.for("test") + @buffer.slice.mapped?.should be_false + end + + it "is false when slicing a buffer created with a block" do + IO::Buffer.for(+"test") do |buffer| + buffer.slice.mapped?.should be_false + end + end + end + + ruby_version_is "3.3" do + context "created with .string" do + it "is false" do + IO::Buffer.string(4) do |buffer| + buffer.slice.mapped?.should be_false + end + end + end + end + end +end diff --git a/spec/ruby/core/io/buffer/null_spec.rb b/spec/ruby/core/io/buffer/null_spec.rb new file mode 100644 index 0000000000..3fb1144d0e --- /dev/null +++ b/spec/ruby/core/io/buffer/null_spec.rb @@ -0,0 +1,29 @@ +require_relative '../../../spec_helper' +require_relative 'shared/null_and_empty' + +describe "IO::Buffer#null?" do + after :each do + @buffer&.free + @buffer = nil + end + + it_behaves_like :io_buffer_null_and_empty, :null? + + it "is false for a 0-length String-backed buffer created with .for" do + @buffer = IO::Buffer.for("") + @buffer.null?.should be_false + end + + ruby_version_is "3.3" do + it "is false for a 0-length String-backed buffer created with .string" do + IO::Buffer.string(0) do |buffer| + buffer.null?.should be_false + end + end + end + + it "is false for a 0-length slice of a buffer with size > 0" do + @buffer = IO::Buffer.new(4) + @buffer.slice(3, 0).null?.should be_false + end +end diff --git a/spec/ruby/core/io/buffer/private_spec.rb b/spec/ruby/core/io/buffer/private_spec.rb new file mode 100644 index 0000000000..7aa308997b --- /dev/null +++ b/spec/ruby/core/io/buffer/private_spec.rb @@ -0,0 +1,111 @@ +require_relative '../../../spec_helper' + +ruby_version_is "3.3" do + describe "IO::Buffer#private?" do + after :each do + @buffer&.free + @buffer = nil + end + + context "with a buffer created with .new" do + it "is false for an internal buffer" do + @buffer = IO::Buffer.new(4, IO::Buffer::INTERNAL) + @buffer.private?.should be_false + end + + it "is false for a mapped buffer" do + @buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) + @buffer.private?.should be_false + end + end + + context "with a file-backed buffer created with .map" do + it "is false for a regular mapping" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) + @buffer.private?.should be_false + end + end + + it "is true for a private mapping" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY | IO::Buffer::PRIVATE) + @buffer.private?.should be_true + end + end + end + + context "with a String-backed buffer created with .for" do + it "is false for a buffer created without a block" do + @buffer = IO::Buffer.for("test") + @buffer.private?.should be_false + end + + it "is false for a buffer created with a block" do + IO::Buffer.for(+"test") do |buffer| + buffer.private?.should be_false + end + end + end + + context "with a String-backed buffer created with .string" do + it "is false" do + IO::Buffer.string(4) do |buffer| + buffer.private?.should be_false + end + end + end + + # Always false for slices + context "with a slice of a buffer" do + context "created with .new" do + it "is false when slicing an internal buffer" do + @buffer = IO::Buffer.new(4) + @buffer.slice.private?.should be_false + end + + it "is false when slicing a mapped buffer" do + @buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) + @buffer.slice.private?.should be_false + end + end + + context "created with .map" do + it "is false when slicing a regular file-backed buffer" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) + @buffer.slice.private?.should be_false + end + end + + it "is false when slicing a private file-backed buffer" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY | IO::Buffer::PRIVATE) + @buffer.slice.private?.should be_false + end + end + end + + context "created with .for" do + it "is false when slicing a buffer created without a block" do + @buffer = IO::Buffer.for("test") + @buffer.slice.private?.should be_false + end + + it "is false when slicing a buffer created with a block" do + IO::Buffer.for(+"test") do |buffer| + buffer.slice.private?.should be_false + end + end + end + + context "created with .string" do + it "is false" do + IO::Buffer.string(4) do |buffer| + buffer.slice.private?.should be_false + end + end + end + end + end +end diff --git a/spec/ruby/core/io/buffer/readonly_spec.rb b/spec/ruby/core/io/buffer/readonly_spec.rb new file mode 100644 index 0000000000..0014a876ed --- /dev/null +++ b/spec/ruby/core/io/buffer/readonly_spec.rb @@ -0,0 +1,143 @@ +require_relative '../../../spec_helper' + +describe "IO::Buffer#readonly?" do + after :each do + @buffer&.free + @buffer = nil + end + + context "with a buffer created with .new" do + it "is false for an internal buffer" do + @buffer = IO::Buffer.new(4, IO::Buffer::INTERNAL) + @buffer.readonly?.should be_false + end + + it "is false for a mapped buffer" do + @buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) + @buffer.readonly?.should be_false + end + end + + context "with a file-backed buffer created with .map" do + it "is false for a writable mapping" do + File.open(__FILE__, "r+") do |file| + @buffer = IO::Buffer.map(file) + @buffer.readonly?.should be_false + end + end + + it "is true for a readonly mapping" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) + @buffer.readonly?.should be_true + end + end + + ruby_version_is "3.3" do + it "is false for a private mapping" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::PRIVATE) + @buffer.readonly?.should be_false + end + end + end + end + + context "with a String-backed buffer created with .for" do + it "is true for a buffer created without a block" do + @buffer = IO::Buffer.for(+"test") + @buffer.readonly?.should be_true + end + + it "is false for a buffer created with a block" do + IO::Buffer.for(+"test") do |buffer| + buffer.readonly?.should be_false + end + end + + it "is true for a buffer created with a block from a frozen string" do + IO::Buffer.for(-"test") do |buffer| + buffer.readonly?.should be_true + end + end + end + + ruby_version_is "3.3" do + context "with a String-backed buffer created with .string" do + it "is false" do + IO::Buffer.string(4) do |buffer| + buffer.readonly?.should be_false + end + end + end + end + + # This seems to be the only flag propagated from the source buffer to the slice. + context "with a slice of a buffer" do + context "created with .new" do + it "is false when slicing an internal buffer" do + @buffer = IO::Buffer.new(4) + @buffer.slice.readonly?.should be_false + end + + it "is false when slicing a mapped buffer" do + @buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) + @buffer.slice.readonly?.should be_false + end + end + + context "created with .map" do + it "is false when slicing a read-write file-backed buffer" do + File.open(__FILE__, "r+") do |file| + @buffer = IO::Buffer.map(file) + @buffer.slice.readonly?.should be_false + end + end + + it "is true when slicing a readonly file-backed buffer" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) + @buffer.slice.readonly?.should be_true + end + end + + ruby_version_is "3.3" do + it "is false when slicing a private file-backed buffer" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::PRIVATE) + @buffer.slice.readonly?.should be_false + end + end + end + end + + context "created with .for" do + it "is true when slicing a buffer created without a block" do + @buffer = IO::Buffer.for(+"test") + @buffer.slice.readonly?.should be_true + end + + it "is false when slicing a buffer created with a block" do + IO::Buffer.for(+"test") do |buffer| + buffer.slice.readonly?.should be_false + end + end + + it "is true when slicing a buffer created with a block from a frozen string" do + IO::Buffer.for(-"test") do |buffer| + buffer.slice.readonly?.should be_true + end + end + end + + ruby_version_is "3.3" do + context "created with .string" do + it "is false" do + IO::Buffer.string(4) do |buffer| + buffer.slice.readonly?.should be_false + end + end + end + end + end +end diff --git a/spec/ruby/core/io/buffer/resize_spec.rb b/spec/ruby/core/io/buffer/resize_spec.rb new file mode 100644 index 0000000000..0da3a23356 --- /dev/null +++ b/spec/ruby/core/io/buffer/resize_spec.rb @@ -0,0 +1,155 @@ +require_relative '../../../spec_helper' + +describe "IO::Buffer#resize" do + after :each do + @buffer&.free + @buffer = nil + end + + context "with a buffer created with .new" do + it "resizes internal buffer, preserving type" do + @buffer = IO::Buffer.new(4) + @buffer.resize(IO::Buffer::PAGE_SIZE) + @buffer.size.should == IO::Buffer::PAGE_SIZE + @buffer.internal?.should be_true + @buffer.mapped?.should be_false + end + + platform_is :linux do + it "resizes mapped buffer, preserving type" do + @buffer = IO::Buffer.new(IO::Buffer::PAGE_SIZE, IO::Buffer::MAPPED) + @buffer.resize(4) + @buffer.size.should == 4 + @buffer.internal?.should be_false + @buffer.mapped?.should be_true + end + end + + platform_is_not :linux do + it "resizes mapped buffer, changing type to internal" do + @buffer = IO::Buffer.new(IO::Buffer::PAGE_SIZE, IO::Buffer::MAPPED) + @buffer.resize(4) + @buffer.size.should == 4 + @buffer.internal?.should be_true + @buffer.mapped?.should be_false + end + end + end + + context "with a file-backed buffer created with .map" do + it "disallows resizing shared buffer, raising IO::Buffer::AccessError" do + File.open(__FILE__, "r+") do |file| + @buffer = IO::Buffer.map(file) + -> { @buffer.resize(10) }.should raise_error(IO::Buffer::AccessError, "Cannot resize external buffer!") + end + end + + ruby_version_is "3.3" do + it "resizes private buffer, discarding excess contents" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::PRIVATE) + @buffer.resize(10) + @buffer.size.should == 10 + @buffer.get_string.should == "require_re" + @buffer.resize(12) + @buffer.size.should == 12 + @buffer.get_string.should == "require_re\0\0" + end + end + end + end + + context "with a String-backed buffer created with .for" do + context "without a block" do + it "disallows resizing, raising IO::Buffer::AccessError" do + @buffer = IO::Buffer.for(+"test") + -> { @buffer.resize(10) }.should raise_error(IO::Buffer::AccessError, "Cannot resize external buffer!") + end + end + + context "with a block" do + it "disallows resizing, raising IO::Buffer::AccessError" do + IO::Buffer.for(+'test') do |buffer| + -> { buffer.resize(10) }.should raise_error(IO::Buffer::AccessError, "Cannot resize external buffer!") + end + end + end + end + + ruby_version_is "3.3" do + context "with a String-backed buffer created with .string" do + it "disallows resizing, raising IO::Buffer::AccessError" do + IO::Buffer.string(4) do |buffer| + -> { buffer.resize(10) }.should raise_error(IO::Buffer::AccessError, "Cannot resize external buffer!") + end + end + end + end + + context "with a null buffer" do + it "allows resizing a 0-sized buffer, creating a regular buffer according to new size" do + @buffer = IO::Buffer.new(0) + @buffer.resize(IO::Buffer::PAGE_SIZE) + @buffer.size.should == IO::Buffer::PAGE_SIZE + @buffer.internal?.should be_false + @buffer.mapped?.should be_true + end + + it "allows resizing after a free, creating a regular buffer according to new size" do + @buffer = IO::Buffer.for("test") + @buffer.free + @buffer.resize(10) + @buffer.size.should == 10 + @buffer.internal?.should be_true + @buffer.mapped?.should be_false + end + end + + it "allows resizing to 0, freeing memory" do + @buffer = IO::Buffer.new(4) + @buffer.resize(0) + @buffer.null?.should be_true + end + + it "can be called repeatedly" do + @buffer = IO::Buffer.new(4) + @buffer.resize(10) + @buffer.resize(27) + @buffer.resize(1) + @buffer.size.should == 1 + end + + it "always clears extra memory" do + @buffer = IO::Buffer.new(4) + @buffer.set_string("test") + # This should not cause a re-allocation, just a technical resizing, + # even with very aggressive memory allocation. + @buffer.resize(2) + @buffer.resize(4) + @buffer.get_string.should == "te\0\0" + end + + it "is disallowed while locked, raising IO::Buffer::LockedError" do + @buffer = IO::Buffer.new(4) + @buffer.locked do + -> { @buffer.resize(10) }.should raise_error(IO::Buffer::LockedError, "Cannot resize locked buffer!") + end + end + + it "raises ArgumentError if size is negative" do + @buffer = IO::Buffer.new(4) + -> { @buffer.resize(-1) }.should raise_error(ArgumentError, "Size can't be negative!") + end + + it "raises TypeError if size is not an Integer" do + @buffer = IO::Buffer.new(4) + -> { @buffer.resize(nil) }.should raise_error(TypeError, "not an Integer") + -> { @buffer.resize(10.0) }.should raise_error(TypeError, "not an Integer") + end + + context "with a slice of a buffer" do + # Current behavior of slice resizing seems unintended (it's undocumented, too). + # It either creates a completely new buffer, or breaks the slice on size 0. + it "needs to be reviewed for spec completeness" + end +end diff --git a/spec/ruby/core/io/buffer/shared/null_and_empty.rb b/spec/ruby/core/io/buffer/shared/null_and_empty.rb new file mode 100644 index 0000000000..c8fe9e5e46 --- /dev/null +++ b/spec/ruby/core/io/buffer/shared/null_and_empty.rb @@ -0,0 +1,59 @@ +describe :io_buffer_null_and_empty, shared: true do + it "is false for a buffer with size > 0" do + @buffer = IO::Buffer.new(1) + @buffer.send(@method).should be_false + end + + it "is false for a slice with length > 0" do + @buffer = IO::Buffer.new(4) + @buffer.slice(1, 2).send(@method).should be_false + end + + it "is false for a file-mapped buffer" do + File.open(__FILE__, "rb") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) + @buffer.send(@method).should be_false + end + end + + it "is false for a non-empty String-backed buffer created with .for" do + @buffer = IO::Buffer.for("test") + @buffer.send(@method).should be_false + end + + ruby_version_is "3.3" do + it "is false for a non-empty String-backed buffer created with .string" do + IO::Buffer.string(4) do |buffer| + buffer.send(@method).should be_false + end + end + end + + it "is true for a 0-sized buffer" do + @buffer = IO::Buffer.new(0) + @buffer.send(@method).should be_true + end + + it "is true for a slice of a 0-sized buffer" do + @buffer = IO::Buffer.new(0) + @buffer.slice(0, 0).send(@method).should be_true + end + + it "is true for a freed buffer" do + @buffer = IO::Buffer.new(1) + @buffer.free + @buffer.send(@method).should be_true + end + + it "is true for a buffer resized to 0" do + @buffer = IO::Buffer.new(1) + @buffer.resize(0) + @buffer.send(@method).should be_true + end + + it "is true for a buffer whose memory was transferred" do + buffer = IO::Buffer.new(1) + @buffer = buffer.transfer + buffer.send(@method).should be_true + end +end diff --git a/spec/ruby/core/io/buffer/shared_spec.rb b/spec/ruby/core/io/buffer/shared_spec.rb new file mode 100644 index 0000000000..f2a638cf39 --- /dev/null +++ b/spec/ruby/core/io/buffer/shared_spec.rb @@ -0,0 +1,117 @@ +require_relative '../../../spec_helper' + +describe "IO::Buffer#shared?" do + after :each do + @buffer&.free + @buffer = nil + end + + context "with a buffer created with .new" do + it "is false for an internal buffer" do + @buffer = IO::Buffer.new(4, IO::Buffer::INTERNAL) + @buffer.shared?.should be_false + end + + it "is false for a mapped buffer" do + @buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) + @buffer.shared?.should be_false + end + end + + context "with a file-backed buffer created with .map" do + it "is true for a regular mapping" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) + @buffer.shared?.should be_true + end + end + + ruby_version_is "3.3" do + it "is false for a private mapping" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY | IO::Buffer::PRIVATE) + @buffer.shared?.should be_false + end + end + end + end + + context "with a String-backed buffer created with .for" do + it "is false for a buffer created without a block" do + @buffer = IO::Buffer.for("test") + @buffer.shared?.should be_false + end + + it "is false for a buffer created with a block" do + IO::Buffer.for(+"test") do |buffer| + buffer.shared?.should be_false + end + end + end + + ruby_version_is "3.3" do + context "with a String-backed buffer created with .string" do + it "is false" do + IO::Buffer.string(4) do |buffer| + buffer.shared?.should be_false + end + end + end + end + + # Always false for slices + context "with a slice of a buffer" do + context "created with .new" do + it "is false when slicing an internal buffer" do + @buffer = IO::Buffer.new(4) + @buffer.slice.shared?.should be_false + end + + it "is false when slicing a mapped buffer" do + @buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) + @buffer.slice.shared?.should be_false + end + end + + context "created with .map" do + it "is false when slicing a regular file-backed buffer" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) + @buffer.slice.shared?.should be_false + end + end + + ruby_version_is "3.3" do + it "is false when slicing a private file-backed buffer" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY | IO::Buffer::PRIVATE) + @buffer.slice.shared?.should be_false + end + end + end + end + + context "created with .for" do + it "is false when slicing a buffer created without a block" do + @buffer = IO::Buffer.for("test") + @buffer.slice.shared?.should be_false + end + + it "is false when slicing a buffer created with a block" do + IO::Buffer.for(+"test") do |buffer| + buffer.slice.shared?.should be_false + end + end + end + + ruby_version_is "3.3" do + context "created with .string" do + it "is false" do + IO::Buffer.string(4) do |buffer| + buffer.slice.shared?.should be_false + end + end + end + end + end +end diff --git a/spec/ruby/core/io/buffer/transfer_spec.rb b/spec/ruby/core/io/buffer/transfer_spec.rb new file mode 100644 index 0000000000..cb8c843ff2 --- /dev/null +++ b/spec/ruby/core/io/buffer/transfer_spec.rb @@ -0,0 +1,118 @@ +require_relative '../../../spec_helper' + +describe "IO::Buffer#transfer" do + after :each do + @buffer&.free + @buffer = nil + end + + context "with a buffer created with .new" do + it "transfers internal memory to a new buffer, nullifying the original" do + buffer = IO::Buffer.new(4) + info = buffer.to_s + @buffer = buffer.transfer + @buffer.to_s.should == info + buffer.null?.should be_true + end + + it "transfers mapped memory to a new buffer, nullifying the original" do + buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) + info = buffer.to_s + @buffer = buffer.transfer + @buffer.to_s.should == info + buffer.null?.should be_true + end + end + + context "with a file-backed buffer created with .map" do + it "transfers mapped memory to a new buffer, nullifying the original" do + File.open(__FILE__, "r") do |file| + buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) + info = buffer.to_s + @buffer = buffer.transfer + @buffer.to_s.should == info + buffer.null?.should be_true + end + end + end + + context "with a String-backed buffer created with .for" do + context "without a block" do + it "transfers memory to a new buffer, nullifying the original" do + buffer = IO::Buffer.for("test") + info = buffer.to_s + @buffer = buffer.transfer + @buffer.to_s.should == info + buffer.null?.should be_true + end + end + + context "with a block" do + it "transfers memory to a new buffer, breaking the transaction by nullifying the original" do + IO::Buffer.for(+"test") do |buffer| + info = buffer.to_s + @buffer = buffer.transfer + @buffer.to_s.should == info + buffer.null?.should be_true + end + @buffer.null?.should be_false + end + end + end + + ruby_version_is "3.3" do + context "with a String-backed buffer created with .string" do + it "transfers memory to a new buffer, breaking the transaction by nullifying the original" do + IO::Buffer.string(4) do |buffer| + info = buffer.to_s + @buffer = buffer.transfer + @buffer.to_s.should == info + buffer.null?.should be_true + end + @buffer.null?.should be_false + end + end + end + + it "allows multiple transfers" do + buffer_1 = IO::Buffer.new(4) + buffer_2 = buffer_1.transfer + @buffer = buffer_2.transfer + buffer_1.null?.should be_true + buffer_2.null?.should be_true + @buffer.null?.should be_false + end + + it "is disallowed while locked, raising IO::Buffer::LockedError" do + @buffer = IO::Buffer.new(4) + @buffer.locked do + -> { @buffer.transfer }.should raise_error(IO::Buffer::LockedError, "Cannot transfer ownership of locked buffer!") + end + end + + context "with a slice of a buffer" do + it "transfers source to a new slice, not touching the buffer" do + @buffer = IO::Buffer.new(4) + slice = @buffer.slice(0, 2) + @buffer.set_string("test") + + new_slice = slice.transfer + slice.null?.should be_true + new_slice.null?.should be_false + @buffer.null?.should be_false + + new_slice.set_string("ea") + @buffer.get_string.should == "east" + end + + it "nullifies buffer, invalidating the slice" do + buffer = IO::Buffer.new(4) + slice = buffer.slice(0, 2) + @buffer = buffer.transfer + + slice.null?.should be_false + slice.valid?.should be_false + -> { slice.get_string }.should raise_error(IO::Buffer::InvalidatedError, "Buffer has been invalidated!") + end + end +end diff --git a/spec/ruby/core/io/buffer/valid_spec.rb b/spec/ruby/core/io/buffer/valid_spec.rb new file mode 100644 index 0000000000..680a35ae9a --- /dev/null +++ b/spec/ruby/core/io/buffer/valid_spec.rb @@ -0,0 +1,110 @@ +require_relative '../../../spec_helper' + +describe "IO::Buffer#valid?" do + after :each do + @buffer&.free + @buffer = nil + end + + # Non-slices are always valid + context "with a non-slice buffer" do + it "is true for a regular buffer" do + @buffer = IO::Buffer.new(4) + @buffer.valid?.should be_true + end + + it "is true for a 0-size buffer" do + @buffer = IO::Buffer.new(0) + @buffer.valid?.should be_true + end + + it "is true for a freed buffer" do + @buffer = IO::Buffer.new(4) + @buffer.free + @buffer.valid?.should be_true + end + + it "is true for a freed file-backed buffer" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) + @buffer.valid?.should be_true + @buffer.free + @buffer.valid?.should be_true + end + end + + it "is true for a freed string-backed buffer" do + @buffer = IO::Buffer.for("hello") + @buffer.valid?.should be_true + @buffer.free + @buffer.valid?.should be_true + end + end + + # "A buffer becomes invalid if it is a slice of another buffer (or string) + # which has been freed or re-allocated at a different address." + context "with a slice" do + it "is true for a slice of a live buffer" do + @buffer = IO::Buffer.new(4) + slice = @buffer.slice(0, 2) + slice.valid?.should be_true + end + + context "when buffer is resized" do + it "is false when slice becomes outside the buffer" do + @buffer = IO::Buffer.new(4) + slice = @buffer.slice(2, 2) + @buffer.resize(3) + slice.valid?.should be_false + end + + platform_is_not :linux do + # This test does not cause a copy-resize on Linux. + # `#resize` MAY cause the buffer to move, but there is no guarantee. + it "is false when buffer is copied on resize" do + @buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) + slice = @buffer.slice(0, 2) + @buffer.resize(8) + slice.valid?.should be_false + end + end + end + + it "is false for a slice of a transferred buffer" do + buffer = IO::Buffer.new(4) + slice = buffer.slice(0, 2) + @buffer = buffer.transfer + slice.valid?.should be_false + end + + it "is false for a slice of a freed buffer" do + @buffer = IO::Buffer.new(4) + slice = @buffer.slice(0, 2) + @buffer.free + slice.valid?.should be_false + end + + it "is false for a slice of a freed file-backed buffer" do + File.open(__FILE__, "r") do |file| + @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY) + slice = @buffer.slice(0, 2) + slice.valid?.should be_true + @buffer.free + slice.valid?.should be_false + end + end + + it "is true for a slice of a freed string-backed buffer while string is alive" do + @buffer = IO::Buffer.for("alive") + slice = @buffer.slice(0, 2) + slice.valid?.should be_true + @buffer.free + slice.valid?.should be_true + end + + # There probably should be a test with a garbage-collected string, + # but it's not clear how to force that. + + it "needs to be reviewed for spec completeness" + end +end diff --git a/spec/ruby/core/io/close_on_exec_spec.rb b/spec/ruby/core/io/close_on_exec_spec.rb new file mode 100644 index 0000000000..4e89e08d61 --- /dev/null +++ b/spec/ruby/core/io/close_on_exec_spec.rb @@ -0,0 +1,76 @@ +require_relative '../../spec_helper' + +describe "IO#close_on_exec=" do + before :each do + @name = tmp('io_close_on_exec.txt') + @io = new_io @name + end + + after :each do + @io.close unless @io.closed? + rm_r @name + end + + guard -> { platform_is_not :windows } do + it "sets the close-on-exec flag if true" do + @io.close_on_exec = true + @io.should.close_on_exec? + end + + it "sets the close-on-exec flag if non-false" do + @io.close_on_exec = :true + @io.should.close_on_exec? + end + + it "unsets the close-on-exec flag if false" do + @io.close_on_exec = true + @io.close_on_exec = false + @io.should_not.close_on_exec? + end + + it "unsets the close-on-exec flag if nil" do + @io.close_on_exec = true + @io.close_on_exec = nil + @io.should_not.close_on_exec? + end + + it "ensures the IO's file descriptor is closed in exec'ed processes" do + require 'fcntl' + @io.close_on_exec = true + (@io.fcntl(Fcntl::F_GETFD) & Fcntl::FD_CLOEXEC).should == Fcntl::FD_CLOEXEC + end + + it "raises IOError if called on a closed IO" do + @io.close + -> { @io.close_on_exec = true }.should raise_error(IOError) + end + end +end + +describe "IO#close_on_exec?" do + before :each do + @name = tmp('io_is_close_on_exec.txt') + @io = new_io @name + end + + after :each do + @io.close unless @io.closed? + rm_r @name + end + + guard -> { platform_is_not :windows } do + it "returns true by default" do + @io.should.close_on_exec? + end + + it "returns true if set" do + @io.close_on_exec = true + @io.should.close_on_exec? + end + + it "raises IOError if called on a closed IO" do + @io.close + -> { @io.close_on_exec? }.should raise_error(IOError) + end + end +end diff --git a/spec/ruby/core/io/close_read_spec.rb b/spec/ruby/core/io/close_read_spec.rb new file mode 100644 index 0000000000..e700e85bd9 --- /dev/null +++ b/spec/ruby/core/io/close_read_spec.rb @@ -0,0 +1,61 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#close_read" do + + before :each do + cmd = platform_is(:windows) ? 'rem' : 'cat' + @io = IO.popen cmd, "r+" + @path = tmp('io.close.txt') + end + + after :each do + @io.close unless @io.closed? + rm_r @path + end + + it "closes the read end of a duplex I/O stream" do + @io.close_read + + -> { @io.read }.should raise_error(IOError) + end + + it "does nothing on subsequent invocations" do + @io.close_read + + @io.close_read.should be_nil + end + + it "allows subsequent invocation of close" do + @io.close_read + + -> { @io.close }.should_not raise_error + end + + it "raises an IOError if the stream is writable and not duplexed" do + io = File.open @path, 'w' + + begin + -> { io.close_read }.should raise_error(IOError) + ensure + io.close unless io.closed? + end + end + + it "closes the stream if it is neither writable nor duplexed" do + io_close_path = @path + touch io_close_path + + io = File.open io_close_path + + io.close_read + + io.should.closed? + end + + it "does nothing on closed stream" do + @io.close + + @io.close_read.should be_nil + end +end diff --git a/spec/ruby/core/io/close_spec.rb b/spec/ruby/core/io/close_spec.rb new file mode 100644 index 0000000000..3a44cc8b17 --- /dev/null +++ b/spec/ruby/core/io/close_spec.rb @@ -0,0 +1,118 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#close" do + before :each do + @name = tmp('io_close.txt') + @io = new_io @name + end + + after :each do + @io.close unless @io.closed? + rm_r @name + end + + it "closes the stream" do + @io.close + @io.should.closed? + end + + it "returns nil" do + @io.close.should == nil + end + + it "raises an IOError reading from a closed IO" do + @io.close + -> { @io.read }.should raise_error(IOError) + end + + it "raises an IOError writing to a closed IO" do + @io.close + -> { @io.write "data" }.should raise_error(IOError) + end + + it 'does not close the stream if autoclose is false' do + other_io = IO.new(@io.fileno) + other_io.autoclose = false + other_io.close + -> { @io.write "data" }.should_not raise_error(IOError) + end + + it "does nothing if already closed" do + @io.close + + @io.close.should be_nil + end + + it "does not call the #flush method but flushes the stream internally" do + @io.should_not_receive(:flush) + @io.close + @io.should.closed? + end + + it 'raises an IOError with a clear message' do + matching_exception = nil + + -> do + IOSpecs::THREAD_CLOSE_RETRIES.times do + read_io, write_io = IO.pipe + going_to_read = false + + thread = Thread.new do + begin + going_to_read = true + read_io.read + rescue IOError => ioe + if ioe.message == IOSpecs::THREAD_CLOSE_ERROR_MESSAGE + matching_exception = ioe + end + # try again + end + end + + # best attempt to ensure the thread is actually blocked on read + Thread.pass until going_to_read && thread.stop? + sleep(0.001) + + read_io.close + thread.join + write_io.close + + matching_exception&.tap {|ex| raise ex} + end + end.should raise_error(IOError, IOSpecs::THREAD_CLOSE_ERROR_MESSAGE) + end +end + +describe "IO#close on an IO.popen stream" do + + it "clears #pid" do + io = IO.popen ruby_cmd('r = loop{puts "y"; 0} rescue 1; exit r'), 'r' + + io.pid.should_not == 0 + + io.close + + -> { io.pid }.should raise_error(IOError) + end + + it "sets $?" do + io = IO.popen ruby_cmd('exit 0'), 'r' + io.close + + $?.exitstatus.should == 0 + + io = IO.popen ruby_cmd('exit 1'), 'r' + io.close + + $?.exitstatus.should == 1 + end + + it "waits for the child to exit" do + io = IO.popen ruby_cmd('r = loop{puts "y"; 0} rescue 1; exit r'), 'r' + io.close + + $?.exitstatus.should_not == 0 + end + +end diff --git a/spec/ruby/core/io/close_write_spec.rb b/spec/ruby/core/io/close_write_spec.rb new file mode 100644 index 0000000000..70610a3e9d --- /dev/null +++ b/spec/ruby/core/io/close_write_spec.rb @@ -0,0 +1,68 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#close_write" do + before :each do + cmd = platform_is(:windows) ? 'rem' : 'cat' + @io = IO.popen cmd, 'r+' + @path = tmp('io.close.txt') + end + + after :each do + @io.close unless @io.closed? + rm_r @path + end + + it "closes the write end of a duplex I/O stream" do + @io.close_write + + -> { @io.write "attempt to write" }.should raise_error(IOError) + end + + it "does nothing on subsequent invocations" do + @io.close_write + + @io.close_write.should be_nil + end + + it "allows subsequent invocation of close" do + @io.close_write + + -> { @io.close }.should_not raise_error + end + + it "raises an IOError if the stream is readable and not duplexed" do + io = File.open @path, 'w+' + + begin + -> { io.close_write }.should raise_error(IOError) + ensure + io.close unless io.closed? + end + end + + it "closes the stream if it is neither readable nor duplexed" do + io = File.open @path, 'w' + + io.close_write + + io.should.closed? + end + + # Windows didn't have command like cat + platform_is_not :windows do + it "flushes and closes the write stream" do + @io.puts '12345' + + @io.close_write + + @io.read.should == "12345\n" + end + end + + it "does nothing on closed stream" do + @io.close + + @io.close_write.should be_nil + end +end diff --git a/spec/ruby/core/io/closed_spec.rb b/spec/ruby/core/io/closed_spec.rb new file mode 100644 index 0000000000..7316546a0d --- /dev/null +++ b/spec/ruby/core/io/closed_spec.rb @@ -0,0 +1,20 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#closed?" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.close + end + + it "returns true on closed stream" do + IOSpecs.closed_io.closed?.should be_true + end + + it "returns false on open stream" do + @io.closed?.should be_false + end +end diff --git a/spec/ruby/core/io/constants_spec.rb b/spec/ruby/core/io/constants_spec.rb new file mode 100644 index 0000000000..f9dccd08b9 --- /dev/null +++ b/spec/ruby/core/io/constants_spec.rb @@ -0,0 +1,19 @@ +require_relative '../../spec_helper' + +describe "IO::SEEK_SET" do + it "is defined" do + IO.const_defined?(:SEEK_SET).should == true + end +end + +describe "IO::SEEK_CUR" do + it "is defined" do + IO.const_defined?(:SEEK_CUR).should == true + end +end + +describe "IO::SEEK_END" do + it "is defined" do + IO.const_defined?(:SEEK_END).should == true + end +end diff --git a/spec/ruby/core/io/copy_stream_spec.rb b/spec/ruby/core/io/copy_stream_spec.rb new file mode 100644 index 0000000000..ffa2ea992c --- /dev/null +++ b/spec/ruby/core/io/copy_stream_spec.rb @@ -0,0 +1,343 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe :io_copy_stream_to_file, shared: true do + it "copies the entire IO contents to the file" do + IO.copy_stream(@object.from, @to_name) + File.read(@to_name).should == @content + IO.copy_stream(@from_bigfile, @to_name) + File.read(@to_name).should == @content_bigfile + end + + it "returns the number of bytes copied" do + IO.copy_stream(@object.from, @to_name).should == @size + IO.copy_stream(@from_bigfile, @to_name).should == @size_bigfile + end + + it "copies only length bytes when specified" do + IO.copy_stream(@object.from, @to_name, 8).should == 8 + File.read(@to_name).should == "Line one" + end + + it "calls #to_path to convert on object to a file name" do + obj = mock("io_copy_stream_to") + obj.should_receive(:to_path).and_return(@to_name) + + IO.copy_stream(@object.from, obj) + File.read(@to_name).should == @content + end + + it "raises a TypeError if #to_path does not return a String" do + obj = mock("io_copy_stream_to") + obj.should_receive(:to_path).and_return(1) + + -> { IO.copy_stream(@object.from, obj) }.should raise_error(TypeError) + end +end + +describe :io_copy_stream_to_file_with_offset, shared: true do + platform_is_not :windows do + it "copies only length bytes from the offset" do + IO.copy_stream(@object.from, @to_name, 8, 4).should == 8 + File.read(@to_name).should == " one\n\nLi" + end + end +end + +describe :io_copy_stream_to_io, shared: true do + it "copies the entire IO contents to the IO" do + IO.copy_stream(@object.from, @to_io) + File.read(@to_name).should == @content + IO.copy_stream(@from_bigfile, @to_io) + File.read(@to_name).should == (@content + @content_bigfile) + end + + it "returns the number of bytes copied" do + IO.copy_stream(@object.from, @to_io).should == @size + IO.copy_stream(@from_bigfile, @to_io).should == @size_bigfile + end + + it "starts writing at the destination IO's current position" do + @to_io.write("prelude ") + IO.copy_stream(@object.from, @to_io) + File.read(@to_name).should == ("prelude " + @content) + end + + it "leaves the destination IO position at the last write" do + IO.copy_stream(@object.from, @to_io) + @to_io.pos.should == @size + end + + it "raises an IOError if the destination IO is not open for writing" do + to_io = new_io __FILE__, "r" + begin + -> { IO.copy_stream @object.from, to_io }.should raise_error(IOError) + ensure + to_io.close + end + end + + it "does not close the destination IO" do + IO.copy_stream(@object.from, @to_io) + @to_io.closed?.should be_false + end + + it "copies only length bytes when specified" do + IO.copy_stream(@object.from, @to_io, 8).should == 8 + File.read(@to_name).should == "Line one" + end +end + +describe :io_copy_stream_to_io_with_offset, shared: true do + platform_is_not :windows do + it "copies only length bytes from the offset" do + IO.copy_stream(@object.from, @to_io, 8, 4).should == 8 + File.read(@to_name).should == " one\n\nLi" + end + end +end + +describe "IO.copy_stream" do + before :each do + @from_name = fixture __FILE__, "copy_stream.txt" + @to_name = tmp("io_copy_stream_io_name") + + @content = IO.read(@from_name) + @size = @content.size + + @from_bigfile = tmp("io_copy_stream_bigfile") + @content_bigfile = "A" * 17_000 + touch(@from_bigfile){|f| f.print @content_bigfile } + @size_bigfile = @content_bigfile.size + end + + after :each do + rm_r @to_name if @to_name + rm_r @from_bigfile + end + + describe "from an IO" do + before :each do + @from_io = new_io @from_name, "rb" + IOSpecs::CopyStream.from = @from_io + end + + after :each do + @from_io.close + end + + it "raises an IOError if the source IO is not open for reading" do + @from_io.close + @from_io = new_io @from_bigfile, "a" + -> { IO.copy_stream @from_io, @to_name }.should raise_error(IOError) + end + + it "does not close the source IO" do + IO.copy_stream(@from_io, @to_name) + @from_io.closed?.should be_false + end + + platform_is_not :windows do + it "does not change the IO offset when an offset is specified" do + @from_io.pos = 10 + IO.copy_stream(@from_io, @to_name, 8, 4) + @from_io.pos.should == 10 + end + end + + it "does change the IO offset when an offset is not specified" do + @from_io.pos = 10 + IO.copy_stream(@from_io, @to_name) + @from_io.pos.should == 42 + end + + describe "to a file name" do + it_behaves_like :io_copy_stream_to_file, nil, IOSpecs::CopyStream + it_behaves_like :io_copy_stream_to_file_with_offset, nil, IOSpecs::CopyStream + end + + describe "to an IO" do + before :each do + @to_io = new_io @to_name, "wb" + end + + after :each do + @to_io.close + end + + it_behaves_like :io_copy_stream_to_io, nil, IOSpecs::CopyStream + it_behaves_like :io_copy_stream_to_io_with_offset, nil, IOSpecs::CopyStream + end + + describe "to a Tempfile" do + before :all do + require 'tempfile' + end + + before :each do + @to_io = Tempfile.new("rubyspec_copy_stream", encoding: Encoding::BINARY, mode: File::RDONLY) + @to_name = @to_io.path + end + + after :each do + @to_io.close! + @to_name = nil # do not rm_r it, already done by Tempfile#close! + end + + it_behaves_like :io_copy_stream_to_io, nil, IOSpecs::CopyStream + it_behaves_like :io_copy_stream_to_io_with_offset, nil, IOSpecs::CopyStream + end + end + + describe "from a file name" do + before :each do + IOSpecs::CopyStream.from = @from_name + end + + it "calls #to_path to convert on object to a file name" do + obj = mock("io_copy_stream_from") + obj.should_receive(:to_path).and_return(@from_name) + + IO.copy_stream(obj, @to_name) + File.read(@to_name).should == @content + end + + it "raises a TypeError if #to_path does not return a String" do + obj = mock("io_copy_stream_from") + obj.should_receive(:to_path).and_return(1) + + -> { IO.copy_stream(obj, @to_name) }.should raise_error(TypeError) + end + + describe "to a file name" do + it_behaves_like :io_copy_stream_to_file, nil, IOSpecs::CopyStream + it_behaves_like :io_copy_stream_to_file_with_offset, nil, IOSpecs::CopyStream + end + + describe "to an IO" do + before :each do + @to_io = new_io @to_name, "wb" + end + + after :each do + @to_io.close + end + + it_behaves_like :io_copy_stream_to_io, nil, IOSpecs::CopyStream + it_behaves_like :io_copy_stream_to_io_with_offset, nil, IOSpecs::CopyStream + end + end + + describe "from a pipe IO" do + before :each do + @from_io = IOSpecs.pipe_fixture(@content) + IOSpecs::CopyStream.from = @from_io + end + + after :each do + @from_io.close + end + + it "does not close the source IO" do + IO.copy_stream(@from_io, @to_name) + @from_io.closed?.should be_false + end + + platform_is_not :windows do + it "raises an error when an offset is specified" do + -> { IO.copy_stream(@from_io, @to_name, 8, 4) }.should raise_error(Errno::ESPIPE) + end + end + + describe "to a file name" do + it_behaves_like :io_copy_stream_to_file, nil, IOSpecs::CopyStream + end + + describe "to an IO" do + before :each do + @to_io = new_io @to_name, "wb" + end + + after :each do + @to_io.close + end + + it_behaves_like :io_copy_stream_to_io, nil, IOSpecs::CopyStream + end + end + + describe "with non-IO Objects" do + before do + @io = new_io @from_name, "rb" + end + + after do + @io.close unless @io.closed? + end + + it "calls #readpartial on the source Object if defined" do + from = IOSpecs::CopyStreamReadPartial.new @io + + IO.copy_stream(from, @to_name) + File.read(@to_name).should == @content + end + + it "calls #read on the source Object" do + from = IOSpecs::CopyStreamRead.new @io + + IO.copy_stream(from, @to_name) + File.read(@to_name).should == @content + end + + it "calls #write on the destination Object" do + to = mock("io_copy_stream_to_object") + to.should_receive(:write).with(@content).and_return(@content.size) + + IO.copy_stream(@from_name, to) + end + + it "does not call #pos on the source if no offset is given" do + @io.should_not_receive(:pos) + IO.copy_stream(@io, @to_name) + end + end + + describe "with a destination that does partial reads" do + before do + @from_out, @from_in = IO.pipe + @to_out, @to_in = IO.pipe + end + + after do + [@from_out, @from_in, @to_out, @to_in].each {|io| io.close rescue nil} + end + + it "calls #write repeatedly on the destination Object" do + @from_in.write "1234" + @from_in.close + + th = Thread.new do + IO.copy_stream(@from_out, @to_in) + end + + copied = "" + 4.times do + copied += @to_out.read(1) + end + + th.join + + copied.should == "1234" + end + + end +end + +describe "IO.copy_stream" do + it "does not use buffering when writing to STDOUT" do + IO.popen([*ruby_exe, fixture(__FILE__ , "copy_in_out.rb")], "r+") do |io| + io.write("bar") + io.read(3).should == "bar" + end + end +end diff --git a/spec/ruby/core/io/dup_spec.rb b/spec/ruby/core/io/dup_spec.rb new file mode 100644 index 0000000000..564e007438 --- /dev/null +++ b/spec/ruby/core/io/dup_spec.rb @@ -0,0 +1,106 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#dup" do + before :each do + @file = tmp("spec_io_dup") + @f = File.open @file, 'w+' + @i = @f.dup + + @f.sync = true + @i.sync = true + end + + after :each do + @i.close if @i && !@i.closed? + @f.close if @f && !@f.closed? + rm_r @file + end + + it "returns a new IO instance" do + @i.class.should == @f.class + end + + it "sets a new descriptor on the returned object" do + @i.fileno.should_not == @f.fileno + end + + quarantine! do # This does not appear to be consistent across platforms + it "shares the original stream between the two IOs" do + start = @f.pos + @i.pos.should == start + + s = "Hello, wo.. wait, where am I?\n" + s2 = "<evil voice> Muhahahaa!" + + @f.write s + @i.pos.should == @f.pos + + @i.rewind + @i.gets.should == s + + @i.rewind + @i.write s2 + + @f.rewind + @f.gets.should == "#{s2}\n" + end + end + + it "allows closing the new IO without affecting the original" do + @i.close + -> { @f.gets }.should_not raise_error(Exception) + + @i.should.closed? + @f.should_not.closed? + end + + it "allows closing the original IO without affecting the new one" do + @f.close + -> { @i.gets }.should_not raise_error(Exception) + + @i.should_not.closed? + @f.should.closed? + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.dup }.should raise_error(IOError) + end + + it "always sets the close-on-exec flag for the new IO object" do + @f.close_on_exec = true + dup = @f.dup + begin + dup.should.close_on_exec? + ensure + dup.close + end + + @f.close_on_exec = false + dup = @f.dup + begin + dup.should.close_on_exec? + ensure + dup.close + end + end + + it "always sets the autoclose flag for the new IO object" do + @f.autoclose = true + dup = @f.dup + begin + dup.should.autoclose? + ensure + dup.close + end + + @f.autoclose = false + dup = @f.dup + begin + dup.should.autoclose? + ensure + dup.close + @f.autoclose = true + end + end +end diff --git a/spec/ruby/core/io/each_byte_spec.rb b/spec/ruby/core/io/each_byte_spec.rb new file mode 100644 index 0000000000..ea618e8c0c --- /dev/null +++ b/spec/ruby/core/io/each_byte_spec.rb @@ -0,0 +1,57 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#each_byte" do + before :each do + ScratchPad.record [] + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.close if @io + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.each_byte {} }.should raise_error(IOError) + end + + it "yields each byte" do + count = 0 + @io.each_byte do |byte| + ScratchPad << byte + break if 4 < count += 1 + end + + ScratchPad.recorded.should == [86, 111, 105, 99, 105] + end + + describe "when no block is given" do + it "returns an Enumerator" do + enum = @io.each_byte + enum.should be_an_instance_of(Enumerator) + enum.first(5).should == [86, 111, 105, 99, 105] + end + + describe "returned Enumerator" do + describe "size" do + it "should return nil" do + @io.each_byte.size.should == nil + end + end + end + end +end + +describe "IO#each_byte" do + before :each do + @io = IOSpecs.io_fixture "empty.txt" + end + + after :each do + @io.close unless @io.closed? + end + + it "returns self on an empty stream" do + @io.each_byte { |b| }.should equal(@io) + end +end diff --git a/spec/ruby/core/io/each_char_spec.rb b/spec/ruby/core/io/each_char_spec.rb new file mode 100644 index 0000000000..5d460a1e7c --- /dev/null +++ b/spec/ruby/core/io/each_char_spec.rb @@ -0,0 +1,12 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/chars' + +describe "IO#each_char" do + it_behaves_like :io_chars, :each_char +end + +describe "IO#each_char" do + it_behaves_like :io_chars_empty, :each_char +end diff --git a/spec/ruby/core/io/each_codepoint_spec.rb b/spec/ruby/core/io/each_codepoint_spec.rb new file mode 100644 index 0000000000..07a4037c8a --- /dev/null +++ b/spec/ruby/core/io/each_codepoint_spec.rb @@ -0,0 +1,43 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/codepoints' + +# See redmine #1667 +describe "IO#each_codepoint" do + it_behaves_like :io_codepoints, :each_codepoint +end + +describe "IO#each_codepoint" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.close if @io + end + + it "calls the given block" do + r = [] + @io.each_codepoint { |c| r << c } + r[24].should == 232 + r.last.should == 10 + end + + it "returns self" do + @io.each_codepoint { |l| l }.should equal(@io) + end +end + +describe "IO#each_codepoint" do + before :each do + @io = IOSpecs.io_fixture("incomplete.txt") + end + + after :each do + @io.close if @io + end + + it "raises an exception at incomplete character before EOF when conversion takes place" do + -> { @io.each_codepoint {} }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/io/each_line_spec.rb b/spec/ruby/core/io/each_line_spec.rb new file mode 100644 index 0000000000..58d26b325d --- /dev/null +++ b/spec/ruby/core/io/each_line_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/each' + +describe "IO#each_line" do + it_behaves_like :io_each, :each_line +end + +describe "IO#each_line" do + it_behaves_like :io_each_default_separator, :each_line +end diff --git a/spec/ruby/core/io/each_spec.rb b/spec/ruby/core/io/each_spec.rb new file mode 100644 index 0000000000..91ecbd19c8 --- /dev/null +++ b/spec/ruby/core/io/each_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/each' + +describe "IO#each" do + it_behaves_like :io_each, :each +end + +describe "IO#each" do + it_behaves_like :io_each_default_separator, :each +end diff --git a/spec/ruby/core/io/eof_spec.rb b/spec/ruby/core/io/eof_spec.rb new file mode 100644 index 0000000000..b4850df437 --- /dev/null +++ b/spec/ruby/core/io/eof_spec.rb @@ -0,0 +1,107 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#eof?" do + before :each do + @name = tmp("empty.txt") + touch @name + end + + after :each do + rm_r @name + end + + it "returns true on an empty stream that has just been opened" do + File.open(@name) { |empty| empty.should.eof? } + end + + it "raises IOError on stream not opened for reading" do + -> do + File.open(@name, "w") { |f| f.eof? } + end.should raise_error(IOError) + end +end + +describe "IO#eof?" do + before :each do + @name = fixture __FILE__, "lines.txt" + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.close if @io && !@io.closed? + end + + it "returns false when not at end of file" do + @io.read 1 + @io.should_not.eof? + end + + it "returns true after reading with read with no parameters" do + @io.read() + @io.should.eof? + end + + it "returns true after reading with read" do + @io.read(File.size(@name)) + @io.should.eof? + end + + it "returns true after reading with sysread" do + @io.sysread(File.size(@name)) + @io.should.eof? + end + + it "returns true after reading with readlines" do + @io.readlines + @io.should.eof? + end + + it "returns false on just opened non-empty stream" do + @io.should_not.eof? + end + + it "does not consume the data from the stream" do + @io.should_not.eof? + @io.getc.should == 'V' + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.eof? }.should raise_error(IOError) + end + + it "raises IOError on stream closed for reading by close_read" do + @io.close_read + -> { @io.eof? }.should raise_error(IOError) + end + + it "returns true on one-byte stream after single-byte read" do + File.open(__dir__ + '/fixtures/one_byte.txt') { |one_byte| + one_byte.read(1) + one_byte.should.eof? + } + end +end + +describe "IO#eof?" do + after :each do + @r.close if @r && !@r.closed? + @w.close if @w && !@w.closed? + end + + it "returns true on receiving side of Pipe when writing side is closed" do + @r, @w = IO.pipe + @w.close + @r.should.eof? + end + + it "returns false on receiving side of Pipe when writing side wrote some data" do + @r, @w = IO.pipe + @w.puts "hello" + @r.should_not.eof? + @w.close + @r.should_not.eof? + @r.read + @r.should.eof? + end +end diff --git a/spec/ruby/core/io/external_encoding_spec.rb b/spec/ruby/core/io/external_encoding_spec.rb new file mode 100644 index 0000000000..7765c6c0f5 --- /dev/null +++ b/spec/ruby/core/io/external_encoding_spec.rb @@ -0,0 +1,223 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe :io_external_encoding_write, shared: true do + describe "when Encoding.default_internal is nil" do + before :each do + Encoding.default_internal = nil + end + + it "returns nil" do + @io = new_io @name, @object + Encoding.default_external = Encoding::IBM437 + @io.external_encoding.should be_nil + end + + it "returns the external encoding specified when the instance was created" do + @io = new_io @name, "#{@object}:ibm866" + Encoding.default_external = Encoding::IBM437 + @io.external_encoding.should equal(Encoding::IBM866) + end + + it "returns the encoding set by #set_encoding" do + @io = new_io @name, "#{@object}:ibm866" + @io.set_encoding Encoding::EUC_JP, nil + @io.external_encoding.should equal(Encoding::EUC_JP) + end + end + + describe "when Encoding.default_external != Encoding.default_internal" do + before :each do + Encoding.default_external = Encoding::IBM437 + Encoding.default_internal = Encoding::IBM866 + end + + it "returns the value of Encoding.default_external when the instance was created" do + @io = new_io @name, @object + Encoding.default_external = Encoding::UTF_8 + @io.external_encoding.should equal(Encoding::IBM437) + end + + it "returns the external encoding specified when the instance was created" do + @io = new_io @name, "#{@object}:ibm866" + Encoding.default_external = Encoding::IBM437 + @io.external_encoding.should equal(Encoding::IBM866) + end + + it "returns the encoding set by #set_encoding" do + @io = new_io @name, "#{@object}:ibm866" + @io.set_encoding Encoding::EUC_JP, nil + @io.external_encoding.should equal(Encoding::EUC_JP) + end + end + + describe "when Encoding.default_external == Encoding.default_internal" do + before :each do + Encoding.default_external = Encoding::IBM866 + Encoding.default_internal = Encoding::IBM866 + end + + it "returns the value of Encoding.default_external when the instance was created" do + @io = new_io @name, @object + Encoding.default_external = Encoding::UTF_8 + @io.external_encoding.should equal(Encoding::IBM866) + end + + it "returns the external encoding specified when the instance was created" do + @io = new_io @name, "#{@object}:ibm866" + Encoding.default_external = Encoding::IBM437 + @io.external_encoding.should equal(Encoding::IBM866) + end + + it "returns the encoding set by #set_encoding" do + @io = new_io @name, "#{@object}:ibm866" + @io.set_encoding Encoding::EUC_JP, nil + @io.external_encoding.should equal(Encoding::EUC_JP) + end + end +end + +describe "IO#external_encoding" do + before :each do + @external = Encoding.default_external + @internal = Encoding.default_internal + + @name = tmp("io_external_encoding") + touch(@name) + end + + after :each do + Encoding.default_external = @external + Encoding.default_internal = @internal + + @io.close if @io + rm_r @name + end + + it "can be retrieved from a closed stream" do + io = IOSpecs.io_fixture("lines.txt", "r") + io.close + io.external_encoding.should equal(Encoding.default_external) + end + + describe "with 'r' mode" do + describe "when Encoding.default_internal is nil" do + before :each do + Encoding.default_internal = nil + Encoding.default_external = Encoding::IBM866 + end + + it "returns Encoding.default_external if the external encoding is not set" do + @io = new_io @name, "r" + @io.external_encoding.should equal(Encoding::IBM866) + end + + it "returns Encoding.default_external when that encoding is changed after the instance is created" do + @io = new_io @name, "r" + Encoding.default_external = Encoding::IBM437 + @io.external_encoding.should equal(Encoding::IBM437) + end + + it "returns the external encoding specified when the instance was created" do + @io = new_io @name, "r:utf-8" + Encoding.default_external = Encoding::IBM437 + @io.external_encoding.should equal(Encoding::UTF_8) + end + + it "returns the encoding set by #set_encoding" do + @io = new_io @name, "r:utf-8" + @io.set_encoding Encoding::EUC_JP, nil + @io.external_encoding.should equal(Encoding::EUC_JP) + end + end + + describe "when Encoding.default_external == Encoding.default_internal" do + before :each do + Encoding.default_external = Encoding::IBM866 + Encoding.default_internal = Encoding::IBM866 + end + + it "returns the value of Encoding.default_external when the instance was created" do + @io = new_io @name, "r" + Encoding.default_external = Encoding::IBM437 + @io.external_encoding.should equal(Encoding::IBM866) + end + + it "returns the external encoding specified when the instance was created" do + @io = new_io @name, "r:utf-8" + Encoding.default_external = Encoding::IBM437 + @io.external_encoding.should equal(Encoding::UTF_8) + end + + it "returns the encoding set by #set_encoding" do + @io = new_io @name, "r:utf-8" + @io.set_encoding Encoding::EUC_JP, nil + @io.external_encoding.should equal(Encoding::EUC_JP) + end + end + + describe "when Encoding.default_external != Encoding.default_internal" do + before :each do + Encoding.default_external = Encoding::IBM437 + Encoding.default_internal = Encoding::IBM866 + end + + + it "returns the external encoding specified when the instance was created" do + @io = new_io @name, "r:utf-8" + Encoding.default_external = Encoding::IBM437 + @io.external_encoding.should equal(Encoding::UTF_8) + end + + it "returns the encoding set by #set_encoding" do + @io = new_io @name, "r:utf-8" + @io.set_encoding Encoding::EUC_JP, nil + @io.external_encoding.should equal(Encoding::EUC_JP) + end + end + end + + describe "with 'rb' mode" do + it "returns Encoding::BINARY" do + @io = new_io @name, "rb" + @io.external_encoding.should equal(Encoding::BINARY) + end + + it "returns the external encoding specified by the mode argument" do + @io = new_io @name, "rb:ibm437" + @io.external_encoding.should equal(Encoding::IBM437) + end + end + + describe "with 'r+' mode" do + it_behaves_like :io_external_encoding_write, nil, "r+" + end + + describe "with 'w' mode" do + it_behaves_like :io_external_encoding_write, nil, "w" + end + + describe "with 'wb' mode" do + it "returns Encoding::BINARY" do + @io = new_io @name, "wb" + @io.external_encoding.should equal(Encoding::BINARY) + end + + it "returns the external encoding specified by the mode argument" do + @io = new_io @name, "wb:ibm437" + @io.external_encoding.should equal(Encoding::IBM437) + end + end + + describe "with 'w+' mode" do + it_behaves_like :io_external_encoding_write, nil, "w+" + end + + describe "with 'a' mode" do + it_behaves_like :io_external_encoding_write, nil, "a" + end + + describe "with 'a+' mode" do + it_behaves_like :io_external_encoding_write, nil, "a+" + end +end diff --git a/spec/ruby/core/io/fcntl_spec.rb b/spec/ruby/core/io/fcntl_spec.rb new file mode 100644 index 0000000000..30b4876fe3 --- /dev/null +++ b/spec/ruby/core/io/fcntl_spec.rb @@ -0,0 +1,8 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#fcntl" do + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.fcntl(5, 5) }.should raise_error(IOError) + end +end diff --git a/spec/ruby/core/io/fdatasync_spec.rb b/spec/ruby/core/io/fdatasync_spec.rb new file mode 100644 index 0000000000..6242258ea0 --- /dev/null +++ b/spec/ruby/core/io/fdatasync_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../spec_helper' + +describe "IO#fdatasync" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/io/fileno_spec.rb b/spec/ruby/core/io/fileno_spec.rb new file mode 100644 index 0000000000..647609bf42 --- /dev/null +++ b/spec/ruby/core/io/fileno_spec.rb @@ -0,0 +1,12 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#fileno" do + it "returns the numeric file descriptor of the given IO object" do + $stdout.fileno.should == 1 + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.fileno }.should raise_error(IOError) + end +end diff --git a/spec/ruby/core/io/fixtures/bom_UTF-16BE.txt b/spec/ruby/core/io/fixtures/bom_UTF-16BE.txt Binary files differnew file mode 100644 index 0000000000..c7c42e9de4 --- /dev/null +++ b/spec/ruby/core/io/fixtures/bom_UTF-16BE.txt diff --git a/spec/ruby/core/io/fixtures/bom_UTF-16LE.txt b/spec/ruby/core/io/fixtures/bom_UTF-16LE.txt Binary files differnew file mode 100644 index 0000000000..53642b6984 --- /dev/null +++ b/spec/ruby/core/io/fixtures/bom_UTF-16LE.txt diff --git a/spec/ruby/core/io/fixtures/bom_UTF-32BE.txt b/spec/ruby/core/io/fixtures/bom_UTF-32BE.txt Binary files differnew file mode 100644 index 0000000000..c5efe6c278 --- /dev/null +++ b/spec/ruby/core/io/fixtures/bom_UTF-32BE.txt diff --git a/spec/ruby/core/io/fixtures/bom_UTF-32LE.txt b/spec/ruby/core/io/fixtures/bom_UTF-32LE.txt Binary files differnew file mode 100644 index 0000000000..1168384393 --- /dev/null +++ b/spec/ruby/core/io/fixtures/bom_UTF-32LE.txt diff --git a/spec/ruby/core/io/fixtures/bom_UTF-8.txt b/spec/ruby/core/io/fixtures/bom_UTF-8.txt new file mode 100644 index 0000000000..ca971bef61 --- /dev/null +++ b/spec/ruby/core/io/fixtures/bom_UTF-8.txt @@ -0,0 +1 @@ +UTF-8 diff --git a/spec/ruby/core/io/fixtures/classes.rb b/spec/ruby/core/io/fixtures/classes.rb new file mode 100644 index 0000000000..204a2a101b --- /dev/null +++ b/spec/ruby/core/io/fixtures/classes.rb @@ -0,0 +1,218 @@ +# -*- encoding: utf-8 -*- + +module IOSpecs + THREAD_CLOSE_RETRIES = 10 + THREAD_CLOSE_ERROR_MESSAGE = 'stream closed in another thread' + + class SubIO < IO + end + + class SubIOWithRedefinedNew < IO + def self.new(...) + ScratchPad << :redefined_new_called + super + end + + def initialize(...) + ScratchPad << :call_original_initialize + super + end + end + + def self.collector + Proc.new { |x| ScratchPad << x } + end + + def self.lines + [ "Voici la ligne une.\n", + "Qui \303\250 la linea due.\n", + "\n", + "\n", + "Aqu\303\255 est\303\241 la l\303\255nea tres.\n", + "Hier ist Zeile vier.\n", + "\n", + "Est\303\241 aqui a linha cinco.\n", + "Here is line six.\n" ] + end + + def self.lines_without_newline_characters + [ "Voici la ligne une.", + "Qui \303\250 la linea due.", + "", + "", + "Aqu\303\255 est\303\241 la l\303\255nea tres.", + "Hier ist Zeile vier.", + "", + "Est\303\241 aqui a linha cinco.", + "Here is line six." ] + end + + def self.lines_limit + [ "Voici la l", + "igne une.\n", + "Qui è la ", + "linea due.", + "\n", + "\n", + "\n", + "Aquà está", + " la lÃnea", + " tres.\n", + "Hier ist Z", + "eile vier.", + "\n", + "\n", + "Está aqui", + " a linha c", + "inco.\n", + "Here is li", + "ne six.\n" ] + end + + def self.lines_space_separator_limit + [ "Voici ", + "la ", + "ligne ", + "une.\nQui ", + "è ", + "la ", + "linea ", + "due.\n\n\nAqu", + "à ", + "está ", + "la ", + "lÃnea ", + "tres.\nHier", + " ", + "ist ", + "Zeile ", + "vier.\n\nEst", + "á ", + "aqui ", + "a ", + "linha ", + "cinco.\nHer", + "e ", + "is ", + "line ", + "six.\n" ] + end + + def self.lines_r_separator + [ "Voici la ligne une.\nQui \303\250 la linea due.\n\n\nAqu\303\255 est\303\241 la l\303\255nea tr", + "es.\nHier", + " ist Zeile vier", + ".\n\nEst\303\241 aqui a linha cinco.\nHer", + "e is line six.\n" ] + end + + def self.lines_empty_separator + [ "Voici la ligne une.\nQui \303\250 la linea due.\n\n", + "Aqu\303\255 est\303\241 la l\303\255nea tres.\nHier ist Zeile vier.\n\n", + "Est\303\241 aqui a linha cinco.\nHere is line six.\n" ] + end + + def self.lines_space_separator + [ "Voici ", "la ", "ligne ", "une.\nQui ", + "\303\250 ", "la ", "linea ", "due.\n\n\nAqu\303\255 ", + "est\303\241 ", "la ", "l\303\255nea ", "tres.\nHier ", + "ist ", "Zeile ", "vier.\n\nEst\303\241 ", "aqui ", "a ", + "linha ", "cinco.\nHere ", "is ", "line ", "six.\n" ] + end + + def self.lines_space_separator_without_trailing_spaces + [ "Voici", "la", "ligne", "une.\nQui", + "\303\250", "la", "linea", "due.\n\n\nAqu\303\255", + "est\303\241", "la", "l\303\255nea", "tres.\nHier", + "ist", "Zeile", "vier.\n\nEst\303\241", "aqui", "a", + "linha", "cinco.\nHere", "is", "line", "six.\n" ] + end + + def self.lines_arbitrary_separator + [ "Voici la ligne une.\nQui \303\250", + " la linea due.\n\n\nAqu\303\255 est\303\241 la l\303\255nea tres.\nHier ist Zeile vier.\n\nEst\303\241 aqui a linha cinco.\nHere is line six.\n" ] + end + + def self.paragraphs + [ "Voici la ligne une.\nQui \303\250 la linea due.\n\n", + "Aqu\303\255 est\303\241 la l\303\255nea tres.\nHier ist Zeile vier.\n\n", + "Est\303\241 aqui a linha cinco.\nHere is line six.\n" ] + end + + def self.paragraphs_without_trailing_new_line_characters + [ "Voici la ligne une.\nQui \303\250 la linea due.", + "Aqu\303\255 est\303\241 la l\303\255nea tres.\nHier ist Zeile vier.", + "Est\303\241 aqui a linha cinco.\nHere is line six.\n" ] + end + + # Creates an IO instance for an existing fixture file. The + # file should obviously not be deleted. + def self.io_fixture(name, mode = "r:utf-8") + path = fixture __FILE__, name + name = path if File.exist? path + new_io(name, mode) + end + + # Returns a closed instance of IO that was opened to reference + # a fixture file (i.e. the IO instance was perfectly valid at + # one point but is now closed). + def self.closed_io + io = io_fixture "lines.txt" + io.close + io + end + + # Creates a pipe-based IO fixture containing the specified + # contents + def self.pipe_fixture(content) + source, sink = IO.pipe + sink.write content + sink.close + source + end + + # Defines +method+ on +obj+ using the provided +block+. This + # special helper is needed for e.g. IO.open specs to avoid + # mock methods preventing IO#close from running. + def self.io_mock(obj, method, &block) + obj.singleton_class.send(:define_method, method, &block) + end + + module CopyStream + def self.from=(from) + @from = from + end + + def self.from + @from + end + end + + class CopyStreamRead + def initialize(io) + @io = io + end + + def read(size, buf) + @io.read size, buf + end + + def send(*args) + raise "send called" + end + end + + class CopyStreamReadPartial + def initialize(io) + @io = io + end + + def readpartial(size, buf) + @io.readpartial size, buf + end + + def send(*args) + raise "send called" + end + end +end diff --git a/spec/ruby/core/io/fixtures/copy_in_out.rb b/spec/ruby/core/io/fixtures/copy_in_out.rb new file mode 100644 index 0000000000..b9d4085a47 --- /dev/null +++ b/spec/ruby/core/io/fixtures/copy_in_out.rb @@ -0,0 +1,2 @@ +STDOUT.sync = false +IO.copy_stream(STDIN, STDOUT) diff --git a/spec/ruby/core/io/fixtures/copy_stream.txt b/spec/ruby/core/io/fixtures/copy_stream.txt new file mode 100644 index 0000000000..a2d827b351 --- /dev/null +++ b/spec/ruby/core/io/fixtures/copy_stream.txt @@ -0,0 +1,6 @@ +Line one + +Line three +Line four + +Line last diff --git a/spec/ruby/core/io/fixtures/empty.txt b/spec/ruby/core/io/fixtures/empty.txt new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/spec/ruby/core/io/fixtures/empty.txt diff --git a/spec/ruby/core/io/fixtures/incomplete.txt b/spec/ruby/core/io/fixtures/incomplete.txt new file mode 100644 index 0000000000..23d432f642 --- /dev/null +++ b/spec/ruby/core/io/fixtures/incomplete.txt @@ -0,0 +1 @@ +ðŸ
\ No newline at end of file diff --git a/spec/ruby/core/io/fixtures/lines.txt b/spec/ruby/core/io/fixtures/lines.txt new file mode 100644 index 0000000000..0959997e7b --- /dev/null +++ b/spec/ruby/core/io/fixtures/lines.txt @@ -0,0 +1,9 @@ +Voici la ligne une. +Qui è la linea due. + + +Aquà está la lÃnea tres. +Hier ist Zeile vier. + +Está aqui a linha cinco. +Here is line six. diff --git a/spec/ruby/core/io/fixtures/no_bom_UTF-8.txt b/spec/ruby/core/io/fixtures/no_bom_UTF-8.txt new file mode 100644 index 0000000000..7edc66b06a --- /dev/null +++ b/spec/ruby/core/io/fixtures/no_bom_UTF-8.txt @@ -0,0 +1 @@ +UTF-8 diff --git a/spec/ruby/core/io/fixtures/numbered_lines.txt b/spec/ruby/core/io/fixtures/numbered_lines.txt new file mode 100644 index 0000000000..70e49a3d98 --- /dev/null +++ b/spec/ruby/core/io/fixtures/numbered_lines.txt @@ -0,0 +1,5 @@ +Line 1: One +Line 2: Two +Line 3: Three +Line 4: Four +Line 5: Five diff --git a/spec/ruby/core/io/fixtures/one_byte.txt b/spec/ruby/core/io/fixtures/one_byte.txt new file mode 100644 index 0000000000..56a6051ca2 --- /dev/null +++ b/spec/ruby/core/io/fixtures/one_byte.txt @@ -0,0 +1 @@ +1
\ No newline at end of file diff --git a/spec/ruby/core/io/fixtures/read_binary.txt b/spec/ruby/core/io/fixtures/read_binary.txt new file mode 100644 index 0000000000..fa036dca4b --- /dev/null +++ b/spec/ruby/core/io/fixtures/read_binary.txt @@ -0,0 +1 @@ +abcâdef diff --git a/spec/ruby/core/io/fixtures/read_euc_jp.txt b/spec/ruby/core/io/fixtures/read_euc_jp.txt new file mode 100644 index 0000000000..0e17cd717a --- /dev/null +++ b/spec/ruby/core/io/fixtures/read_euc_jp.txt @@ -0,0 +1 @@ +¤¢¤ê¤¬¤È¤¦ diff --git a/spec/ruby/core/io/fixtures/read_text.txt b/spec/ruby/core/io/fixtures/read_text.txt new file mode 100644 index 0000000000..5a7a80f6e4 --- /dev/null +++ b/spec/ruby/core/io/fixtures/read_text.txt @@ -0,0 +1 @@ +abcâdef diff --git a/spec/ruby/core/io/fixtures/reopen_stdout.rb b/spec/ruby/core/io/fixtures/reopen_stdout.rb new file mode 100644 index 0000000000..506ba072bd --- /dev/null +++ b/spec/ruby/core/io/fixtures/reopen_stdout.rb @@ -0,0 +1,3 @@ +STDOUT.reopen ARGV[0] +system "echo from system" +exec "echo from exec" diff --git a/spec/ruby/core/io/flush_spec.rb b/spec/ruby/core/io/flush_spec.rb new file mode 100644 index 0000000000..f7d5ba77fc --- /dev/null +++ b/spec/ruby/core/io/flush_spec.rb @@ -0,0 +1,37 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#flush" do + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.flush }.should raise_error(IOError) + end + + describe "on a pipe" do + before :each do + @r, @w = IO.pipe + end + + after :each do + @r.close + begin + @w.close + rescue Errno::EPIPE + end + end + + # [ruby-core:90895] RJIT worker may leave fd open in a forked child. + # For instance, RJIT creates a worker before @r.close with fork(), @r.close happens, + # and the RJIT worker keeps the pipe open until the worker execve(). + # TODO: consider acquiring GVL from RJIT worker. + guard_not -> { defined?(RubyVM::RJIT) && RubyVM::RJIT.enabled? } do + it "raises Errno::EPIPE if sync=false and the read end is closed" do + @w.sync = false + @w.write "foo" + @r.close + + -> { @w.flush }.should raise_error(Errno::EPIPE, /Broken pipe/) + -> { @w.close }.should raise_error(Errno::EPIPE, /Broken pipe/) + end + end + end +end diff --git a/spec/ruby/core/io/for_fd_spec.rb b/spec/ruby/core/io/for_fd_spec.rb new file mode 100644 index 0000000000..2d86361b73 --- /dev/null +++ b/spec/ruby/core/io/for_fd_spec.rb @@ -0,0 +1,10 @@ +require_relative '../../spec_helper' +require_relative 'shared/new' + +describe "IO.for_fd" do + it_behaves_like :io_new, :for_fd +end + +describe "IO.for_fd" do + it_behaves_like :io_new_errors, :for_fd +end diff --git a/spec/ruby/core/io/foreach_spec.rb b/spec/ruby/core/io/foreach_spec.rb new file mode 100644 index 0000000000..6abe8901ba --- /dev/null +++ b/spec/ruby/core/io/foreach_spec.rb @@ -0,0 +1,98 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/readlines' + +describe "IO.foreach" do + before :each do + @name = fixture __FILE__, "lines.txt" + @count = 0 + ScratchPad.record [] + end + + it "updates $. with each yield" do + IO.foreach(@name) { $..should == @count += 1 } + end + + ruby_version_is ""..."4.0" do + describe "when the filename starts with |" do + it "gets data from the standard out of the subprocess" do + cmd = "|sh -c 'echo hello;echo line2'" + platform_is :windows do + cmd = "|cmd.exe /C echo hello&echo line2" + end + + suppress_warning do # https://bugs.ruby-lang.org/issues/19630 + IO.foreach(cmd) { |l| ScratchPad << l } + end + ScratchPad.recorded.should == ["hello\n", "line2\n"] + end + + platform_is_not :windows do + it "gets data from a fork when passed -" do + parent_pid = $$ + + suppress_warning do # https://bugs.ruby-lang.org/issues/19630 + IO.foreach("|-") { |l| ScratchPad << l } + end + + if $$ == parent_pid + ScratchPad.recorded.should == ["hello\n", "from a fork\n"] + else # child + puts "hello" + puts "from a fork" + exit! + end + end + end + end + + ruby_version_is "3.3" do + # https://bugs.ruby-lang.org/issues/19630 + it "warns about deprecation given a path with a pipe" do + cmd = "|echo ok" + -> { + IO.foreach(cmd).to_a + }.should complain(/IO process creation with a leading '\|'/) + end + end + end +end + +describe "IO.foreach" do + before :each do + @external = Encoding.default_external + Encoding.default_external = Encoding::UTF_8 + + @name = fixture __FILE__, "lines.txt" + ScratchPad.record [] + end + + after :each do + Encoding.default_external = @external + end + + it "sets $_ to nil" do + $_ = "test" + IO.foreach(@name) { } + $_.should be_nil + end + + describe "when no block is given" do + it "returns an Enumerator" do + IO.foreach(@name).should be_an_instance_of(Enumerator) + IO.foreach(@name).to_a.should == IOSpecs.lines + end + + describe "returned Enumerator" do + describe "size" do + it "should return nil" do + IO.foreach(@name).size.should == nil + end + end + end + end + + it_behaves_like :io_readlines, :foreach, IOSpecs.collector + it_behaves_like :io_readlines_options_19, :foreach, IOSpecs.collector +end diff --git a/spec/ruby/core/io/fsync_spec.rb b/spec/ruby/core/io/fsync_spec.rb new file mode 100644 index 0000000000..6e6123de94 --- /dev/null +++ b/spec/ruby/core/io/fsync_spec.rb @@ -0,0 +1,24 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#fsync" do + before :each do + @name = tmp("io_fsync.txt") + ScratchPad.clear + end + + after :each do + rm_r @name + end + + it "raises an IOError on closed stream" do + -> { IOSpecs.closed_io.fsync }.should raise_error(IOError) + end + + it "writes the buffered data to permanent storage" do + File.open(@name, "w") do |f| + f.write "one hit wonder" + f.fsync.should == 0 + end + end +end diff --git a/spec/ruby/core/io/getbyte_spec.rb b/spec/ruby/core/io/getbyte_spec.rb new file mode 100644 index 0000000000..b4351160e6 --- /dev/null +++ b/spec/ruby/core/io/getbyte_spec.rb @@ -0,0 +1,58 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#getbyte" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.close if @io + end + + it "returns the next byte from the stream" do + @io.readline.should == "Voici la ligne une.\n" + letters = @io.getbyte, @io.getbyte, @io.getbyte, @io.getbyte, @io.getbyte + letters.should == [81, 117, 105, 32, 195] + end + + it "returns nil when invoked at the end of the stream" do + @io.read + @io.getbyte.should == nil + end + + it "raises an IOError on closed stream" do + -> { IOSpecs.closed_io.getbyte }.should raise_error(IOError) + end +end + +describe "IO#getbyte" do + before :each do + @io = IOSpecs.io_fixture "empty.txt" + end + + after :each do + @io.close if @io + end + + it "returns nil on empty stream" do + @io.getbyte.should == nil + end +end + +describe "IO#getbyte" do + before :each do + @name = tmp("io_getbyte.txt") + @io = new_io(@name, 'w') + end + + after :each do + @io.close if @io + rm_r @name if @name + end + + it "raises an IOError if the stream is not readable" do + -> { @io.getbyte }.should raise_error(IOError) + end +end diff --git a/spec/ruby/core/io/getc_spec.rb b/spec/ruby/core/io/getc_spec.rb new file mode 100644 index 0000000000..3949b5cb28 --- /dev/null +++ b/spec/ruby/core/io/getc_spec.rb @@ -0,0 +1,42 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#getc" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.close if @io + end + + it "returns the next character from the stream" do + @io.readline.should == "Voici la ligne une.\n" + letters = @io.getc, @io.getc, @io.getc, @io.getc, @io.getc + letters.should == ["Q", "u", "i", " ", "è"] + end + + it "returns nil when invoked at the end of the stream" do + @io.read + @io.getc.should be_nil + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.getc }.should raise_error(IOError) + end +end + +describe "IO#getc" do + before :each do + @io = IOSpecs.io_fixture "empty.txt" + end + + after :each do + @io.close if @io + end + + it "returns nil on empty stream" do + @io.getc.should be_nil + end +end diff --git a/spec/ruby/core/io/gets_spec.rb b/spec/ruby/core/io/gets_spec.rb new file mode 100644 index 0000000000..ca64bf860e --- /dev/null +++ b/spec/ruby/core/io/gets_spec.rb @@ -0,0 +1,360 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/gets_ascii' + +describe "IO#gets" do + it_behaves_like :io_gets_ascii, :gets +end + +describe "IO#gets" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + @count = 0 + end + + after :each do + @io.close if @io + end + + it "assigns the returned line to $_" do + IOSpecs.lines.each do |line| + @io.gets + $_.should == line + end + end + + it "sets $_ to nil after the last line has been read" do + while @io.gets + end + $_.should be_nil + end + + it "returns nil if called at the end of the stream" do + IOSpecs.lines.length.times { @io.gets } + @io.gets.should == nil + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.gets }.should raise_error(IOError) + end + + describe "with no separator" do + it "returns the next line of string that is separated by $/" do + IOSpecs.lines.each { |line| line.should == @io.gets } + end + + it "updates lineno with each invocation" do + while @io.gets + @io.lineno.should == @count += 1 + end + end + + it "updates $. with each invocation" do + while @io.gets + $..should == @count += 1 + end + end + end + + describe "with nil separator" do + it "returns the entire contents" do + @io.gets(nil).should == IOSpecs.lines.join("") + end + + it "updates lineno with each invocation" do + while @io.gets(nil) + @io.lineno.should == @count += 1 + end + end + + it "updates $. with each invocation" do + while @io.gets(nil) + $..should == @count += 1 + end + end + end + + describe "with an empty String separator" do + # Two successive newlines in the input separate paragraphs. + # When there are more than two successive newlines, only two are kept. + it "returns the next paragraph" do + @io.gets("").should == IOSpecs.lines[0,3].join("") + @io.gets("").should == IOSpecs.lines[4,3].join("") + @io.gets("").should == IOSpecs.lines[7,2].join("") + end + + it "reads until the beginning of the next paragraph" do + # There are three newlines between the first and second paragraph + @io.gets("") + @io.gets.should == IOSpecs.lines[4] + end + + it "updates lineno with each invocation" do + while @io.gets("") + @io.lineno.should == @count += 1 + end + end + + it "updates $. with each invocation" do + while @io.gets("") + $..should == @count += 1 + end + end + end + + describe "with an arbitrary String separator" do + it "reads up to and including the separator" do + @io.gets("la linea").should == "Voici la ligne une.\nQui \303\250 la linea" + end + + it "updates lineno with each invocation" do + while (@io.gets("la")) + @io.lineno.should == @count += 1 + end + end + + it "updates $. with each invocation" do + while @io.gets("la") + $..should == @count += 1 + end + end + + describe "that consists of multiple bytes" do + platform_is_not :windows do + it "should match the separator even if the buffer is filled over successive reads" do + IO.pipe do |read, write| + + # Write part of the string with the separator split between two write calls. We want + # the read to intertwine such that when the read starts the full data isn't yet + # available in the buffer. + write.write("Aquà está la lÃnea tres\r\n") + + t = Thread.new do + # Continue reading until the separator is encountered or the pipe is closed. + read.gets("\r\n\r\n") + end + + # Write the other half of the separator, which should cause the `gets` call to now + # match. Explicitly close the pipe for good measure so a bug in `gets` doesn't block forever. + Thread.pass until t.stop? + + write.write("\r\nelse\r\n\r\n") + write.close + + t.value.bytes.should == "Aquà está la lÃnea tres\r\n\r\n".bytes + read.read(8).bytes.should == "else\r\n\r\n".bytes + end + end + end + end + end + + describe "when passed chomp" do + it "returns the first line without a trailing newline character" do + @io.gets(chomp: true).should == IOSpecs.lines_without_newline_characters[0] + end + + it "raises exception when options passed as Hash" do + -> { @io.gets({ chomp: true }) }.should raise_error(TypeError) + + -> { + @io.gets("\n", 1, { chomp: true }) + }.should raise_error(ArgumentError, "wrong number of arguments (given 3, expected 0..2)") + end + end +end + +describe "IO#gets" do + before :each do + @name = tmp("io_gets") + end + + after :each do + rm_r @name + end + + it "raises an IOError if the stream is opened for append only" do + -> { File.open(@name, "a:utf-8") { |f| f.gets } }.should raise_error(IOError) + end + + it "raises an IOError if the stream is opened for writing only" do + -> { File.open(@name, "w:utf-8") { |f| f.gets } }.should raise_error(IOError) + end +end + +describe "IO#gets" do + before :each do + @name = tmp("io_gets") + touch(@name) { |f| f.write "one\n\ntwo\n\nthree\nfour\n" } + @io = new_io @name, "r:utf-8" + end + + after :each do + @io.close if @io + rm_r @name + end + + it "calls #to_int to convert a single object argument to an Integer limit" do + obj = mock("io gets limit") + obj.should_receive(:to_int).and_return(6) + + @io.gets(obj).should == "one\n" + end + + it "calls #to_int to convert the second object argument to an Integer limit" do + obj = mock("io gets limit") + obj.should_receive(:to_int).and_return(2) + + @io.gets(nil, obj).should == "on" + end + + it "calls #to_str to convert the first argument to a String when passed a limit" do + obj = mock("io gets separator") + obj.should_receive(:to_str).and_return($/) + + @io.gets(obj, 5).should == "one\n" + end + + it "reads to the default separator when passed a single argument greater than the number of bytes to the separator" do + @io.gets(6).should == "one\n" + end + + it "reads limit bytes when passed a single argument less than the number of bytes to the default separator" do + @io.gets(3).should == "one" + end + + it "reads limit bytes when passed nil and a limit" do + @io.gets(nil, 6).should == "one\n\nt" + end + + it "reads all bytes when the limit is higher than the available bytes" do + @io.gets(nil, 100).should == "one\n\ntwo\n\nthree\nfour\n" + end + + it "reads until the next paragraph when passed '' and a limit greater than the next paragraph" do + @io.gets("", 6).should == "one\n\n" + end + + it "reads limit bytes when passed '' and a limit less than the next paragraph" do + @io.gets("", 3).should == "one" + end + + it "reads all bytes when pass a separator and reading more than all bytes" do + @io.gets("\t", 100).should == "one\n\ntwo\n\nthree\nfour\n" + end + + it "returns empty string when 0 passed as a limit" do + @io.gets(0).should == "" + @io.gets(nil, 0).should == "" + @io.gets("", 0).should == "" + end + + it "does not accept limit that doesn't fit in a C off_t" do + -> { @io.gets(2**128) }.should raise_error(RangeError) + end +end + +describe "IO#gets" do + before :each do + @name = tmp("io_gets") + # create data "æœæ—¥" + "\xE3\x81" * 100 to avoid utf-8 conflicts + data = "æœæ—¥" + ([227,129].pack('C*') * 100).force_encoding('utf-8') + touch(@name) { |f| f.write data } + @io = new_io @name, "r:utf-8" + end + + after :each do + @io.close if @io + rm_r @name + end + + it "reads limit bytes and extra bytes when limit is reached not at character boundary" do + [@io.gets(1), @io.gets(1)].should == ["æœ", "æ—¥"] + end + + it "read limit bytes and extra bytes with maximum of 16" do + # create str "æœæ—¥\xE3" + "\x81\xE3" * 8 to avoid utf-8 conflicts + str = "æœæ—¥" + ([227] + [129,227] * 8).pack('C*').force_encoding('utf-8') + @io.gets(7).should == str + end +end + +describe "IO#gets" do + before :each do + @external = Encoding.default_external + @internal = Encoding.default_internal + + Encoding.default_external = Encoding::UTF_8 + Encoding.default_internal = nil + + @name = tmp("io_gets") + touch(@name) { |f| f.write "line" } + end + + after :each do + @io.close if @io + rm_r @name + Encoding.default_external = @external + Encoding.default_internal = @internal + end + + it "uses the default external encoding" do + @io = new_io @name, 'r' + @io.gets.encoding.should == Encoding::UTF_8 + end + + it "uses the IO object's external encoding, when set" do + @io = new_io @name, 'r' + @io.set_encoding Encoding::US_ASCII + @io.gets.encoding.should == Encoding::US_ASCII + end + + it "transcodes into the default internal encoding" do + Encoding.default_internal = Encoding::US_ASCII + @io = new_io @name, 'r' + @io.gets.encoding.should == Encoding::US_ASCII + end + + it "transcodes into the IO object's internal encoding, when set" do + Encoding.default_internal = Encoding::US_ASCII + @io = new_io @name, 'r' + @io.set_encoding Encoding::UTF_8, Encoding::UTF_16 + @io.gets.encoding.should == Encoding::UTF_16 + end + + it "overwrites the default external encoding with the IO object's own external encoding" do + Encoding.default_external = Encoding::BINARY + Encoding.default_internal = Encoding::UTF_8 + @io = new_io @name, 'r' + @io.set_encoding Encoding::IBM866 + @io.gets.encoding.should == Encoding::UTF_8 + end + + it "ignores the internal encoding if the default external encoding is BINARY" do + Encoding.default_external = Encoding::BINARY + Encoding.default_internal = Encoding::UTF_8 + @io = new_io @name, 'r' + @io.gets.encoding.should == Encoding::BINARY + end + + ruby_version_is ''...'3.3' do + it "transcodes to internal encoding if the IO object's external encoding is BINARY" do + Encoding.default_external = Encoding::BINARY + Encoding.default_internal = Encoding::UTF_8 + @io = new_io @name, 'r' + @io.set_encoding Encoding::BINARY, Encoding::UTF_8 + @io.gets.encoding.should == Encoding::UTF_8 + end + end + + ruby_version_is '3.3' do + it "ignores the internal encoding if the IO object's external encoding is BINARY" do + Encoding.default_external = Encoding::BINARY + Encoding.default_internal = Encoding::UTF_8 + @io = new_io @name, 'r' + @io.set_encoding Encoding::BINARY, Encoding::UTF_8 + @io.gets.encoding.should == Encoding::BINARY + end + end +end diff --git a/spec/ruby/core/io/initialize_spec.rb b/spec/ruby/core/io/initialize_spec.rb new file mode 100644 index 0000000000..026252a13d --- /dev/null +++ b/spec/ruby/core/io/initialize_spec.rb @@ -0,0 +1,60 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#initialize" do + before :each do + @name = tmp("io_initialize.txt") + @io = IO.new(new_fd(@name)) + @fd = @io.fileno + end + + after :each do + @io.close if @io + rm_r @name + end + + it "reassociates the IO instance with the new descriptor when passed an Integer" do + fd = new_fd @name, "r:utf-8" + @io.send :initialize, fd, 'r' + @io.fileno.should == fd + end + + it "calls #to_int to coerce the object passed as an fd" do + obj = mock('fileno') + fd = new_fd @name, "r:utf-8" + obj.should_receive(:to_int).and_return(fd) + @io.send :initialize, obj, 'r' + @io.fileno.should == fd + end + + it "accepts options as keyword arguments" do + fd = new_fd @name, "w:utf-8" + + @io.send(:initialize, fd, "w", flags: File::CREAT) + @io.fileno.should == fd + + -> { + @io.send(:initialize, fd, "w", {flags: File::CREAT}) + }.should raise_error(ArgumentError, "wrong number of arguments (given 3, expected 1..2)") + end + + it "raises a TypeError when passed an IO" do + -> { @io.send :initialize, STDOUT, 'w' }.should raise_error(TypeError) + end + + it "raises a TypeError when passed nil" do + -> { @io.send :initialize, nil, 'w' }.should raise_error(TypeError) + end + + it "raises a TypeError when passed a String" do + -> { @io.send :initialize, "4", 'w' }.should raise_error(TypeError) + end + + it "raises IOError on closed stream" do + -> { @io.send :initialize, IOSpecs.closed_io.fileno }.should raise_error(IOError) + end + + it "raises an Errno::EBADF when given an invalid file descriptor" do + -> { @io.send :initialize, -1, 'w' }.should raise_error(Errno::EBADF) + end +end diff --git a/spec/ruby/core/io/inspect_spec.rb b/spec/ruby/core/io/inspect_spec.rb new file mode 100644 index 0000000000..c653c307c4 --- /dev/null +++ b/spec/ruby/core/io/inspect_spec.rb @@ -0,0 +1,23 @@ +require_relative '../../spec_helper' + +describe "IO#inspect" do + after :each do + @r.close if @r && !@r.closed? + @w.close if @w && !@w.closed? + end + + it "contains the file descriptor number" do + @r, @w = IO.pipe + @r.inspect.should include("fd #{@r.fileno}") + end + + it "contains \"(closed)\" if the stream is closed" do + @r, @w = IO.pipe + @r.close + @r.inspect.should include("(closed)") + end + + it "reports IO as its Method object's owner" do + IO.instance_method(:inspect).owner.should == IO + end +end diff --git a/spec/ruby/core/io/internal_encoding_spec.rb b/spec/ruby/core/io/internal_encoding_spec.rb new file mode 100644 index 0000000000..7a583d4bcb --- /dev/null +++ b/spec/ruby/core/io/internal_encoding_spec.rb @@ -0,0 +1,145 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe :io_internal_encoding, shared: true do + describe "when Encoding.default_internal is not set" do + before :each do + Encoding.default_internal = nil + end + + it "returns nil if the internal encoding is not set" do + @io = new_io @name, @object + @io.internal_encoding.should be_nil + end + + it "returns nil if Encoding.default_internal is changed after the instance is created" do + @io = new_io @name, @object + Encoding.default_internal = Encoding::IBM437 + @io.internal_encoding.should be_nil + end + + it "returns the value set when the instance was created" do + @io = new_io @name, "#{@object}:utf-8:euc-jp" + Encoding.default_internal = Encoding::IBM437 + @io.internal_encoding.should equal(Encoding::EUC_JP) + end + + it "returns the value set by #set_encoding" do + @io = new_io @name, @object + @io.set_encoding(Encoding::US_ASCII, Encoding::IBM437) + @io.internal_encoding.should equal(Encoding::IBM437) + end + end + + describe "when Encoding.default_internal == Encoding.default_external" do + before :each do + Encoding.default_external = Encoding::IBM866 + Encoding.default_internal = Encoding::IBM866 + end + + it "returns nil" do + @io = new_io @name, @object + @io.internal_encoding.should be_nil + end + + it "returns nil regardless of Encoding.default_internal changes" do + @io = new_io @name, @object + Encoding.default_internal = Encoding::IBM437 + @io.internal_encoding.should be_nil + end + end + + describe "when Encoding.default_internal != Encoding.default_external" do + before :each do + Encoding.default_external = Encoding::IBM437 + Encoding.default_internal = Encoding::IBM866 + end + + it "returns the value of Encoding.default_internal when the instance was created if the internal encoding is not set" do + @io = new_io @name, @object + @io.internal_encoding.should equal(Encoding::IBM866) + end + + it "does not change when Encoding.default_internal is changed" do + @io = new_io @name, @object + Encoding.default_internal = Encoding::IBM437 + @io.internal_encoding.should equal(Encoding::IBM866) + end + + it "returns the internal encoding set when the instance was created" do + @io = new_io @name, "#{@object}:utf-8:euc-jp" + @io.internal_encoding.should equal(Encoding::EUC_JP) + end + + it "does not change when set and Encoding.default_internal is changed" do + @io = new_io @name, "#{@object}:utf-8:euc-jp" + Encoding.default_internal = Encoding::IBM437 + @io.internal_encoding.should equal(Encoding::EUC_JP) + end + + it "returns the value set by #set_encoding" do + @io = new_io @name, @object + @io.set_encoding(Encoding::US_ASCII, Encoding::IBM437) + @io.internal_encoding.should equal(Encoding::IBM437) + end + + it "returns nil when Encoding.default_external is BINARY and the internal encoding is not set" do + Encoding.default_external = Encoding::BINARY + @io = new_io @name, @object + @io.internal_encoding.should be_nil + end + + it "returns nil when the external encoding is BINARY and the internal encoding is not set" do + @io = new_io @name, "#{@object}:binary" + @io.internal_encoding.should be_nil + end + end +end + +describe "IO#internal_encoding" do + before :each do + @external = Encoding.default_external + @internal = Encoding.default_internal + + @name = tmp("io_internal_encoding") + touch(@name) + end + + after :each do + @io.close if @io + rm_r @name + + Encoding.default_external = @external + Encoding.default_internal = @internal + end + + it "can be retrieved from a closed stream" do + io = IOSpecs.io_fixture("lines.txt", "r") + io.close + io.internal_encoding.should equal(Encoding.default_internal) + end + + describe "with 'r' mode" do + it_behaves_like :io_internal_encoding, nil, "r" + end + + describe "with 'r+' mode" do + it_behaves_like :io_internal_encoding, nil, "r+" + end + + describe "with 'w' mode" do + it_behaves_like :io_internal_encoding, nil, "w" + end + + describe "with 'w+' mode" do + it_behaves_like :io_internal_encoding, nil, "w+" + end + + describe "with 'a' mode" do + it_behaves_like :io_internal_encoding, nil, "a" + end + + describe "with 'a+' mode" do + it_behaves_like :io_internal_encoding, nil, "a+" + end +end diff --git a/spec/ruby/core/io/io_spec.rb b/spec/ruby/core/io/io_spec.rb new file mode 100644 index 0000000000..0feb1a8774 --- /dev/null +++ b/spec/ruby/core/io/io_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' + +describe "IO" do + it "includes File::Constants" do + IO.include?(File::Constants).should == true + end + + it "includes Enumerable" do + IO.include?(Enumerable).should == true + end +end diff --git a/spec/ruby/core/io/ioctl_spec.rb b/spec/ruby/core/io/ioctl_spec.rb new file mode 100644 index 0000000000..3f7b5ad5d7 --- /dev/null +++ b/spec/ruby/core/io/ioctl_spec.rb @@ -0,0 +1,32 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#ioctl" do + platform_is_not :windows do + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.ioctl(5, 5) }.should raise_error(IOError) + end + end + + platform_is :linux do + guard -> { RUBY_PLATFORM.include?("86") } do # x86 / x86_64 + it "resizes an empty String to match the output size" do + File.open(__FILE__, 'r') do |f| + buffer = +'' + # FIONREAD in /usr/include/asm-generic/ioctls.h + f.ioctl 0x541B, buffer + buffer.unpack('I').first.should be_kind_of(Integer) + end + end + end + + it "raises a system call error when ioctl fails" do + File.open(__FILE__, 'r') do |f| + -> { + # TIOCGWINSZ in /usr/include/asm-generic/ioctls.h + f.ioctl 0x5413, nil + }.should raise_error(SystemCallError) + end + end + end +end diff --git a/spec/ruby/core/io/isatty_spec.rb b/spec/ruby/core/io/isatty_spec.rb new file mode 100644 index 0000000000..3b6c69b190 --- /dev/null +++ b/spec/ruby/core/io/isatty_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/tty' + +describe "IO#isatty" do + it_behaves_like :io_tty, :isatty +end diff --git a/spec/ruby/core/io/lineno_spec.rb b/spec/ruby/core/io/lineno_spec.rb new file mode 100644 index 0000000000..e82cdd9f17 --- /dev/null +++ b/spec/ruby/core/io/lineno_spec.rb @@ -0,0 +1,138 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#lineno" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.close if @io + end + + it "raises an IOError on a closed stream" do + -> { IOSpecs.closed_io.lineno }.should raise_error(IOError) + end + + it "raises an IOError on a write-only stream" do + name = tmp("io_lineno.txt") + begin + File.open(name, 'w') do |f| + -> { f.lineno }.should raise_error(IOError) + end + ensure + rm_r name + end + end + + it "raises an IOError on a duplexed stream with the read side closed" do + cmd = platform_is(:windows) ? 'rem' : 'cat' + IO.popen(cmd, 'r+') do |p| + p.close_read + -> { p.lineno }.should raise_error(IOError) + end + end + + it "returns the current line number" do + @io.lineno.should == 0 + + count = 0 + while @io.gets + @io.lineno.should == count += 1 + end + + @io.rewind + @io.lineno.should == 0 + end +end + +describe "IO#lineno=" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.close if @io + end + + it "raises an IOError on a closed stream" do + -> { IOSpecs.closed_io.lineno = 5 }.should raise_error(IOError) + end + + it "raises an IOError on a write-only stream" do + name = tmp("io_lineno.txt") + begin + File.open(name, 'w') do |f| + -> { f.lineno = 0 }.should raise_error(IOError) + end + ensure + rm_r name + end + end + + it "raises an IOError on a duplexed stream with the read side closed" do + cmd = platform_is(:windows) ? 'rem' : 'cat' + IO.popen(cmd, 'r+') do |p| + p.close_read + -> { p.lineno = 0 }.should raise_error(IOError) + end + end + + it "calls #to_int on a non-numeric argument" do + obj = mock('123') + obj.should_receive(:to_int).and_return(123) + + @io.lineno = obj + @io.lineno.should == 123 + end + + it "truncates a Float argument" do + @io.lineno = 1.5 + @io.lineno.should == 1 + + @io.lineno = 92233.72036854775808 + @io.lineno.should == 92233 + end + + it "raises TypeError if cannot convert argument to Integer implicitly" do + -> { @io.lineno = "1" }.should raise_error(TypeError, 'no implicit conversion of String into Integer') + -> { @io.lineno = nil }.should raise_error(TypeError, 'no implicit conversion from nil to integer') + end + + it "does not accept Integers that don't fit in a C int" do + -> { @io.lineno = 2**32 }.should raise_error(RangeError) + end + + it "sets the current line number to the given value" do + @io.lineno = count = 500 + + while @io.gets + @io.lineno.should == count += 1 + end + + @io.rewind + @io.lineno.should == 0 + end + + it "does not change $." do + original_line = $. + numbers = [-2**30, -2**16, -2**8, -100, -10, -1, 0, 1, 10, 2**8, 2**16, 2**30] + numbers.each do |num| + @io.lineno = num + @io.lineno.should == num + $..should == original_line + end + end + + it "does not change $. until next read" do + $. = 0 + $..should == 0 + + @io.lineno = count = 500 + $..should == 0 + + while @io.gets + $..should == count += 1 + end + end +end diff --git a/spec/ruby/core/io/new_spec.rb b/spec/ruby/core/io/new_spec.rb new file mode 100644 index 0000000000..979ac0efcb --- /dev/null +++ b/spec/ruby/core/io/new_spec.rb @@ -0,0 +1,18 @@ +require_relative '../../spec_helper' +require_relative 'shared/new' + +# NOTE: should be synchronized with library/stringio/initialize_spec.rb + +describe "IO.new" do + it_behaves_like :io_new, :new + + it "does not use the given block and warns to use IO::open" do + -> { + @io = IO.send(@method, @fd) { raise } + }.should complain(/warning: IO::new\(\) does not take block; use IO::open\(\) instead/) + end +end + +describe "IO.new" do + it_behaves_like :io_new_errors, :new +end diff --git a/spec/ruby/core/io/nonblock_spec.rb b/spec/ruby/core/io/nonblock_spec.rb new file mode 100644 index 0000000000..99dc0cafd0 --- /dev/null +++ b/spec/ruby/core/io/nonblock_spec.rb @@ -0,0 +1,48 @@ +require_relative '../../spec_helper' + +platform_is_not :windows do + describe "IO#nonblock?" do + before :all do + require 'io/nonblock' + end + + it "returns false for a file by default" do + File.open(__FILE__) do |f| + f.nonblock?.should == false + end + end + + it "returns true for pipe by default" do + r, w = IO.pipe + begin + r.nonblock?.should == true + w.nonblock?.should == true + ensure + r.close + w.close + end + end + + it "returns true for socket by default" do + require 'socket' + TCPServer.open(0) do |socket| + socket.nonblock?.should == true + end + end + end + + describe "IO#nonblock=" do + before :all do + require 'io/nonblock' + end + + it "changes the IO to non-blocking mode" do + File.open(__FILE__) do |f| + f.nonblock = true + f.nonblock?.should == true + f.nonblock = false + f.nonblock?.should == false + end + end + end +end diff --git a/spec/ruby/core/io/open_spec.rb b/spec/ruby/core/io/open_spec.rb new file mode 100644 index 0000000000..d151da9ce5 --- /dev/null +++ b/spec/ruby/core/io/open_spec.rb @@ -0,0 +1,99 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/new' + +describe "IO.open" do + it_behaves_like :io_new, :open +end + +describe "IO.open" do + it_behaves_like :io_new_errors, :open +end + +# These specs use a special mock helper to avoid mock +# methods from preventing IO#close from running and +# which would prevent the file referenced by @fd from +# being deleted on Windows. + +describe "IO.open" do + before :each do + @name = tmp("io_open.txt") + @fd = new_fd @name + ScratchPad.clear + end + + after :each do + rm_r @name + end + + it "calls #close after yielding to the block" do + IO.open(@fd, "w") do |io| + IOSpecs.io_mock(io, :close) do + super() + ScratchPad.record :called + end + io.closed?.should be_false + end + ScratchPad.recorded.should == :called + end + + it "propagate an exception in the block after calling #close" do + -> do + IO.open(@fd, "w") do |io| + IOSpecs.io_mock(io, :close) do + super() + ScratchPad.record :called + end + raise Exception + end + end.should raise_error(Exception) + ScratchPad.recorded.should == :called + end + + it "propagates an exception raised by #close that is not a StandardError" do + -> do + IO.open(@fd, "w") do |io| + IOSpecs.io_mock(io, :close) do + super() + ScratchPad.record :called + raise Exception + end + end + end.should raise_error(Exception) + ScratchPad.recorded.should == :called + end + + it "propagates an exception raised by #close that is a StandardError" do + -> do + IO.open(@fd, "w") do |io| + IOSpecs.io_mock(io, :close) do + super() + ScratchPad.record :called + raise StandardError + end + end + end.should raise_error(StandardError) + ScratchPad.recorded.should == :called + end + + it "does not propagate an IOError with 'closed stream' message raised by #close" do + IO.open(@fd, "w") do |io| + IOSpecs.io_mock(io, :close) do + super() + ScratchPad.record :called + raise IOError, 'closed stream' + end + end + ScratchPad.recorded.should == :called + end + + it "does not set last error when an IOError with 'closed stream' raised by #close" do + IO.open(@fd, "w") do |io| + IOSpecs.io_mock(io, :close) do + super() + raise IOError, 'closed stream' + end + end + $!.should == nil + end +end diff --git a/spec/ruby/core/io/output_spec.rb b/spec/ruby/core/io/output_spec.rb new file mode 100644 index 0000000000..2aafb305f4 --- /dev/null +++ b/spec/ruby/core/io/output_spec.rb @@ -0,0 +1,27 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#<<" do + it "writes an object to the IO stream" do + -> { + $stderr << "Oh noes, an error!" + }.should output_to_fd("Oh noes, an error!", $stderr) + end + + it "calls #to_s on the object to print it" do + -> { + $stderr << 1337 + }.should output_to_fd("1337", $stderr) + end + + it "raises an error if the stream is closed" do + io = IOSpecs.closed_io + -> { io << "test" }.should raise_error(IOError) + end + + it "returns self" do + -> { + ($stderr << "to_stderr").should == $stderr + }.should output(nil, "to_stderr") + end +end diff --git a/spec/ruby/core/io/path_spec.rb b/spec/ruby/core/io/path_spec.rb new file mode 100644 index 0000000000..798adb2163 --- /dev/null +++ b/spec/ruby/core/io/path_spec.rb @@ -0,0 +1,12 @@ +require_relative '../../spec_helper' + +describe "IO#path" do + it "returns the path of the file associated with the IO object" do + path = tmp("io_path.txt") + File.open(path, "w") do |file| + IO.new(file.fileno, path: file.path, autoclose: false).path.should == file.path + end + ensure + File.unlink(path) + end +end diff --git a/spec/ruby/core/io/pid_spec.rb b/spec/ruby/core/io/pid_spec.rb new file mode 100644 index 0000000000..bc09fe7c3b --- /dev/null +++ b/spec/ruby/core/io/pid_spec.rb @@ -0,0 +1,35 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#pid" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.close if @io + end + + it "returns nil for IO not associated with a process" do + @io.pid.should == nil + end +end + +describe "IO#pid" do + before :each do + @io = IO.popen ruby_cmd('STDIN.read'), "r+" + end + + after :each do + @io.close if @io && !@io.closed? + end + + it "returns the ID of a process associated with stream" do + @io.pid.should_not be_nil + end + + it "raises an IOError on closed stream" do + @io.close + -> { @io.pid }.should raise_error(IOError) + end +end diff --git a/spec/ruby/core/io/pipe_spec.rb b/spec/ruby/core/io/pipe_spec.rb new file mode 100644 index 0000000000..aee0d9003f --- /dev/null +++ b/spec/ruby/core/io/pipe_spec.rb @@ -0,0 +1,225 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO.pipe" do + after :each do + @r.close if @r && !@r.closed? + @w.close if @w && !@w.closed? + end + + it "creates a two-ended pipe" do + @r, @w = IO.pipe + @w.puts "test_create_pipe\\n" + @w.close + @r.read(16).should == "test_create_pipe" + end + + it "returns two IO objects" do + @r, @w = IO.pipe + @r.should be_kind_of(IO) + @w.should be_kind_of(IO) + end + + it "returns instances of a subclass when called on a subclass" do + @r, @w = IOSpecs::SubIO.pipe + @r.should be_an_instance_of(IOSpecs::SubIO) + @w.should be_an_instance_of(IOSpecs::SubIO) + end + + it "does not use IO.new method to create pipes and allows its overriding" do + ScratchPad.record [] + + # so redefined .new is not called, but original #initialize is + @r, @w = IOSpecs::SubIOWithRedefinedNew.pipe + ScratchPad.recorded.should == [:call_original_initialize, :call_original_initialize] # called 2 times - for each pipe (r and w) + + @r.should be_an_instance_of(IOSpecs::SubIOWithRedefinedNew) + @w.should be_an_instance_of(IOSpecs::SubIOWithRedefinedNew) + end +end + +describe "IO.pipe" do + describe "passed a block" do + it "yields two IO objects" do + IO.pipe do |r, w| + r.should be_kind_of(IO) + w.should be_kind_of(IO) + end + end + + it "returns the result of the block" do + IO.pipe { |r, w| :result }.should == :result + end + + it "closes both IO objects" do + r, w = IO.pipe do |_r, _w| + [_r, _w] + end + r.should.closed? + w.should.closed? + end + + it "closes both IO objects when the block raises" do + r = w = nil + -> do + IO.pipe do |_r, _w| + r = _r + w = _w + raise RuntimeError + end + end.should raise_error(RuntimeError) + r.should.closed? + w.should.closed? + end + + it "allows IO objects to be closed within the block" do + r, w = IO.pipe do |_r, _w| + _r.close + _w.close + [_r, _w] + end + r.should.closed? + w.should.closed? + end + end +end + +describe "IO.pipe" do + before :each do + @default_external = Encoding.default_external + @default_internal = Encoding.default_internal + end + + after :each do + Encoding.default_external = @default_external + Encoding.default_internal = @default_internal + end + + it "sets the external encoding of the read end to the default when passed no arguments" do + Encoding.default_external = Encoding::ISO_8859_1 + + IO.pipe do |r, w| + r.external_encoding.should == Encoding::ISO_8859_1 + r.internal_encoding.should be_nil + end + end + + it "sets the internal encoding of the read end to the default when passed no arguments" do + Encoding.default_external = Encoding::ISO_8859_1 + Encoding.default_internal = Encoding::UTF_8 + + IO.pipe do |r, w| + r.external_encoding.should == Encoding::ISO_8859_1 + r.internal_encoding.should == Encoding::UTF_8 + end + end + + it "sets the internal encoding to nil if the same as the external" do + Encoding.default_external = Encoding::UTF_8 + Encoding.default_internal = Encoding::UTF_8 + + IO.pipe do |r, w| + r.external_encoding.should == Encoding::UTF_8 + r.internal_encoding.should be_nil + end + end + + it "sets the external encoding of the read end when passed an Encoding argument" do + IO.pipe(Encoding::UTF_8) do |r, w| + r.external_encoding.should == Encoding::UTF_8 + r.internal_encoding.should be_nil + end + end + + it "sets the external and internal encodings of the read end when passed two Encoding arguments" do + IO.pipe(Encoding::UTF_8, Encoding::UTF_16BE) do |r, w| + r.external_encoding.should == Encoding::UTF_8 + r.internal_encoding.should == Encoding::UTF_16BE + end + end + + it "sets the external encoding of the read end when passed the name of an Encoding" do + IO.pipe("UTF-8") do |r, w| + r.external_encoding.should == Encoding::UTF_8 + r.internal_encoding.should be_nil + end + end + + it "accepts 'bom|' prefix for external encoding" do + IO.pipe("BOM|UTF-8") do |r, w| + r.external_encoding.should == Encoding::UTF_8 + r.internal_encoding.should be_nil + end + end + + it "sets the external and internal encodings specified as a String and separated with a colon" do + IO.pipe("UTF-8:ISO-8859-1") do |r, w| + r.external_encoding.should == Encoding::UTF_8 + r.internal_encoding.should == Encoding::ISO_8859_1 + end + end + + it "accepts 'bom|' prefix for external encoding when specifying 'external:internal'" do + IO.pipe("BOM|UTF-8:ISO-8859-1") do |r, w| + r.external_encoding.should == Encoding::UTF_8 + r.internal_encoding.should == Encoding::ISO_8859_1 + end + end + + it "sets the external and internal encoding when passed two String arguments" do + IO.pipe("UTF-8", "UTF-16BE") do |r, w| + r.external_encoding.should == Encoding::UTF_8 + r.internal_encoding.should == Encoding::UTF_16BE + end + end + + it "accepts an options Hash with one String encoding argument" do + IO.pipe("BOM|UTF-8:ISO-8859-1", invalid: :replace) do |r, w| + r.external_encoding.should == Encoding::UTF_8 + r.internal_encoding.should == Encoding::ISO_8859_1 + end + end + + it "accepts an options Hash with two String encoding arguments" do + IO.pipe("UTF-8", "ISO-8859-1", invalid: :replace) do |r, w| + r.external_encoding.should == Encoding::UTF_8 + r.internal_encoding.should == Encoding::ISO_8859_1 + end + end + + it "calls #to_hash to convert an options argument" do + options = mock("io pipe encoding options") + options.should_receive(:to_hash).and_return({ invalid: :replace }) + IO.pipe("UTF-8", "ISO-8859-1", **options) { |r, w| } + end + + it "calls #to_str to convert the first argument to a String" do + obj = mock("io_pipe_encoding") + obj.should_receive(:to_str).and_return("UTF-8:UTF-16BE") + IO.pipe(obj) do |r, w| + r.external_encoding.should == Encoding::UTF_8 + r.internal_encoding.should == Encoding::UTF_16BE + end + end + + it "calls #to_str to convert the second argument to a String" do + obj = mock("io_pipe_encoding") + obj.should_receive(:to_str).at_least(1).times.and_return("UTF-16BE") + IO.pipe(Encoding::UTF_8, obj) do |r, w| + r.external_encoding.should == Encoding::UTF_8 + r.internal_encoding.should == Encoding::UTF_16BE + end + end + + it "sets no external encoding for the write end" do + IO.pipe(Encoding::UTF_8) do |r, w| + w.external_encoding.should be_nil + end + end + + it "sets no internal encoding for the write end" do + IO.pipe(Encoding::UTF_8) do |r, w| + w.external_encoding.should be_nil + end + end +end diff --git a/spec/ruby/core/io/popen_spec.rb b/spec/ruby/core/io/popen_spec.rb new file mode 100644 index 0000000000..6043862614 --- /dev/null +++ b/spec/ruby/core/io/popen_spec.rb @@ -0,0 +1,287 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative '../process/fixtures/common' + +describe "IO.popen" do + ProcessSpecs.use_system_ruby(self) + + before :each do + @fname = tmp("IO_popen_spec") + @io = nil + @var = "$FOO" + platform_is :windows do + @var = "%FOO%" + end + end + + after :each do + @io.close if @io and !@io.closed? + rm_r @fname + end + + it "returns an open IO" do + @io = IO.popen(ruby_cmd('exit'), "r") + @io.closed?.should be_false + end + + it "reads a read-only pipe" do + @io = IO.popen('echo foo', "r") + @io.read.should == "foo\n" + end + + it "raises IOError when writing a read-only pipe" do + @io = IO.popen('echo foo', "r") + -> { @io.write('bar') }.should raise_error(IOError) + @io.read.should == "foo\n" + end + + it "sees an infinitely looping subprocess exit when read pipe is closed" do + io = IO.popen ruby_cmd('r = loop{puts "y"; 0} rescue 1; exit r'), 'r' + io.close + + $?.exitstatus.should_not == 0 + end + + it "writes to a write-only pipe" do + @io = IO.popen(ruby_cmd('IO.copy_stream(STDIN,STDOUT)', args: "> #{@fname}"), "w") + @io.write("bar") + @io.close + + File.read(@fname).should == "bar" + end + + it "raises IOError when reading a write-only pipe" do + @io = IO.popen(ruby_cmd('IO.copy_stream(STDIN,STDOUT)'), "w") + -> { @io.read }.should raise_error(IOError) + end + + it "reads and writes a read/write pipe" do + @io = IO.popen(ruby_cmd('IO.copy_stream(STDIN,STDOUT)'), "r+") + @io.write("bar") + @io.read(3).should == "bar" + end + + it "waits for the child to finish" do + @io = IO.popen(ruby_cmd('IO.copy_stream(STDIN,STDOUT)', args: "> #{@fname}"), "w") + @io.write("bar") + @io.close + + $?.exitstatus.should == 0 + + File.read(@fname).should == "bar" + end + + it "does not throw an exception if child exited and has been waited for" do + @io = IO.popen([*ruby_exe, '-e', 'sleep']) + pid = @io.pid + Process.kill "KILL", pid + @io.close + platform_is_not :windows do + $?.should.signaled? + end + platform_is :windows do + $?.should.exited? + end + end + + it "returns an instance of a subclass when called on a subclass" do + @io = IOSpecs::SubIO.popen(ruby_cmd('exit'), "r") + @io.should be_an_instance_of(IOSpecs::SubIO) + end + + it "coerces mode argument with #to_str" do + mode = mock("mode") + mode.should_receive(:to_str).and_return("r") + @io = IO.popen(ruby_cmd('exit 0'), mode) + end + + it "accepts a path using the chdir: keyword argument" do + path = File.dirname(@fname) + + @io = IO.popen(ruby_cmd("puts Dir.pwd"), "r", chdir: path) + @io.read.chomp.should == path + end + + it "accepts a path using the chdir: keyword argument and a coercible path" do + path = File.dirname(@fname) + object = mock("path") + object.should_receive(:to_path).and_return(path) + + @io = IO.popen(ruby_cmd("puts Dir.pwd"), "r", chdir: object) + @io.read.chomp.should == path + end + + describe "with a block" do + it "yields an open IO to the block" do + IO.popen(ruby_cmd('exit'), "r") do |io| + io.closed?.should be_false + end + end + + it "yields an instance of a subclass when called on a subclass" do + IOSpecs::SubIO.popen(ruby_cmd('exit'), "r") do |io| + io.should be_an_instance_of(IOSpecs::SubIO) + end + end + + it "closes the IO after yielding" do + io = IO.popen(ruby_cmd('exit'), "r") { |_io| _io } + io.closed?.should be_true + end + + it "allows the IO to be closed inside the block" do + io = IO.popen(ruby_cmd('exit'), 'r') { |_io| _io.close; _io } + io.closed?.should be_true + end + + it "returns the value of the block" do + IO.popen(ruby_cmd('exit'), "r") { :hello }.should == :hello + end + end + + platform_is_not :windows do + it "starts returns a forked process if the command is -" do + io = IO.popen("-") + + if io # parent + begin + io.gets.should == "hello from child\n" + ensure + io.close + end + else # child + puts "hello from child" + exit! + end + end + end + + it "has the given external encoding" do + @io = IO.popen(ruby_cmd('exit'), external_encoding: Encoding::EUC_JP) + @io.external_encoding.should == Encoding::EUC_JP + end + + it "has the given internal encoding" do + @io = IO.popen(ruby_cmd('exit'), internal_encoding: Encoding::EUC_JP) + @io.internal_encoding.should == Encoding::EUC_JP + end + + it "sets the internal encoding to nil if it's the same as the external encoding" do + @io = IO.popen(ruby_cmd('exit'), external_encoding: Encoding::EUC_JP, + internal_encoding: Encoding::EUC_JP) + @io.internal_encoding.should be_nil + end + + context "with a leading ENV Hash" do + it "accepts a single String command" do + IO.popen({"FOO" => "bar"}, "echo #{@var}") do |io| + io.read.should == "bar\n" + end + end + + it "accepts a single String command, and an IO mode" do + IO.popen({"FOO" => "bar"}, "echo #{@var}", "r") do |io| + io.read.should == "bar\n" + end + end + + it "accepts a single String command with a trailing Hash of Process.exec options" do + IO.popen({"FOO" => "bar"}, ruby_cmd('STDERR.puts ENV["FOO"]'), + err: [:child, :out]) do |io| + io.read.should == "bar\n" + end + end + + it "accepts a single String command with a trailing Hash of Process.exec options, and an IO mode" do + IO.popen({"FOO" => "bar"}, ruby_cmd('STDERR.puts ENV["FOO"]'), "r", + err: [:child, :out]) do |io| + io.read.should == "bar\n" + end + end + + it "accepts an Array of command and arguments" do + exe, *args = ruby_exe + IO.popen({"FOO" => "bar"}, [[exe, "specfu"], *args, "-e", "puts ENV['FOO']"]) do |io| + io.read.should == "bar\n" + end + end + + it "accepts an Array of command and arguments, and an IO mode" do + exe, *args = ruby_exe + IO.popen({"FOO" => "bar"}, [[exe, "specfu"], *args, "-e", "puts ENV['FOO']"], "r") do |io| + io.read.should == "bar\n" + end + end + + it "accepts an Array command with a separate trailing Hash of Process.exec options" do + IO.popen({"FOO" => "bar"}, [*ruby_exe, "-e", "STDERR.puts ENV['FOO']"], + err: [:child, :out]) do |io| + io.read.should == "bar\n" + end + end + + it "accepts an Array command with a separate trailing Hash of Process.exec options, and an IO mode" do + IO.popen({"FOO" => "bar"}, [*ruby_exe, "-e", "STDERR.puts ENV['FOO']"], + "r", err: [:child, :out]) do |io| + io.read.should == "bar\n" + end + end + end + + context "with a leading Array argument" do + it "uses the Array as command plus args for the child process" do + IO.popen([*ruby_exe, "-e", "puts 'hello'"]) do |io| + io.read.should == "hello\n" + end + end + + it "accepts a leading ENV Hash" do + IO.popen([{"FOO" => "bar"}, *ruby_exe, "-e", "puts ENV['FOO']"]) do |io| + io.read.should == "bar\n" + end + end + + it "accepts a trailing Hash of Process.exec options" do + IO.popen([*ruby_exe, "does_not_exist", {err: [:child, :out]}]) do |io| + io.read.should =~ /LoadError/ + end + end + + it "accepts an IO mode argument following the Array" do + IO.popen([*ruby_exe, "does_not_exist", {err: [:child, :out]}], "r") do |io| + io.read.should =~ /LoadError/ + end + end + + it "accepts [env, command, arg1, arg2, ..., exec options]" do + IO.popen([{"FOO" => "bar"}, *ruby_exe, "-e", "STDERR.puts ENV[:FOO.to_s]", + err: [:child, :out]]) do |io| + io.read.should == "bar\n" + end + end + + it "accepts '[env, command, arg1, arg2, ..., exec options], mode'" do + IO.popen([{"FOO" => "bar"}, *ruby_exe, "-e", "STDERR.puts ENV[:FOO.to_s]", + err: [:child, :out]], "r") do |io| + io.read.should == "bar\n" + end + end + + it "accepts '[env, command, arg1, arg2, ..., exec options], mode, IO options'" do + IO.popen([{"FOO" => "bar"}, *ruby_exe, "-e", "STDERR.puts ENV[:FOO.to_s]", + err: [:child, :out]], "r", + internal_encoding: Encoding::EUC_JP) do |io| + io.read.should == "bar\n" + io.internal_encoding.should == Encoding::EUC_JP + end + end + + it "accepts '[env, command, arg1, arg2, ...], mode, IO + exec options'" do + IO.popen([{"FOO" => "bar"}, *ruby_exe, "-e", "STDERR.puts ENV[:FOO.to_s]"], "r", + err: [:child, :out], internal_encoding: Encoding::EUC_JP) do |io| + io.read.should == "bar\n" + io.internal_encoding.should == Encoding::EUC_JP + end + end + end +end diff --git a/spec/ruby/core/io/pos_spec.rb b/spec/ruby/core/io/pos_spec.rb new file mode 100644 index 0000000000..e6cda2643d --- /dev/null +++ b/spec/ruby/core/io/pos_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/pos' + +describe "IO#pos" do + it_behaves_like :io_pos, :pos +end + +describe "IO#pos=" do + it_behaves_like :io_set_pos, :pos= +end diff --git a/spec/ruby/core/io/pread_spec.rb b/spec/ruby/core/io/pread_spec.rb new file mode 100644 index 0000000000..dc7bcedf3e --- /dev/null +++ b/spec/ruby/core/io/pread_spec.rb @@ -0,0 +1,140 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' + +guard -> { platform_is_not :windows or ruby_version_is "3.3" } do + describe "IO#pread" do + before :each do + @fname = tmp("io_pread.txt") + @contents = "1234567890" + touch(@fname) { |f| f.write @contents } + @file = File.open(@fname, "r+") + end + + after :each do + @file.close + rm_r @fname + end + + it "accepts a length, and an offset" do + @file.pread(4, 0).should == "1234" + @file.pread(3, 4).should == "567" + end + + it "accepts a length, an offset, and an output buffer" do + buffer = +"foo" + @file.pread(3, 4, buffer).should.equal?(buffer) + buffer.should == "567" + end + + it "shrinks the buffer in case of less bytes read" do + buffer = +"foo" + @file.pread(1, 0, buffer) + buffer.should == "1" + end + + it "grows the buffer in case of more bytes read" do + buffer = +"foo" + @file.pread(5, 0, buffer) + buffer.should == "12345" + end + + it "preserves the encoding of the given buffer" do + buffer = ''.encode(Encoding::ISO_8859_1) + @file.pread(10, 0, buffer) + + buffer.encoding.should == Encoding::ISO_8859_1 + end + + it "does not advance the file pointer" do + @file.pread(4, 0).should == "1234" + @file.read.should == "1234567890" + end + + it "ignores the current offset" do + @file.pos = 3 + @file.pread(4, 0).should == "1234" + end + + it "returns an empty string for maxlen = 0" do + @file.pread(0, 4).should == "" + end + + it "returns a buffer for maxlen = 0 when buffer specified" do + buffer = +"foo" + @file.pread(0, 4, buffer).should.equal?(buffer) + buffer.should == "foo" + end + + it "ignores the offset for maxlen = 0, even if it is out of file bounds" do + @file.pread(0, 400).should == "" + end + + it "does not reset the buffer when reading with maxlen = 0" do + buffer = +"foo" + @file.pread(0, 4, buffer) + buffer.should == "foo" + + @file.pread(0, 400, buffer) + buffer.should == "foo" + end + + it "converts maxlen to Integer using #to_int" do + maxlen = mock('maxlen') + maxlen.should_receive(:to_int).and_return(4) + @file.pread(maxlen, 0).should == "1234" + end + + it "converts offset to Integer using #to_int" do + offset = mock('offset') + offset.should_receive(:to_int).and_return(0) + @file.pread(4, offset).should == "1234" + end + + it "converts a buffer to String using to_str" do + buffer = mock('buffer') + buffer.should_receive(:to_str).at_least(1).and_return(+"foo") + @file.pread(4, 0, buffer) + buffer.should_not.is_a?(String) + buffer.to_str.should == "1234" + end + + it "raises TypeError if maxlen is not an Integer and cannot be coerced into Integer" do + maxlen = Object.new + -> { @file.pread(maxlen, 0) }.should raise_error(TypeError, 'no implicit conversion of Object into Integer') + end + + it "raises TypeError if offset is not an Integer and cannot be coerced into Integer" do + offset = Object.new + -> { @file.pread(4, offset) }.should raise_error(TypeError, 'no implicit conversion of Object into Integer') + end + + it "raises ArgumentError for negative values of maxlen" do + -> { @file.pread(-4, 0) }.should raise_error(ArgumentError, 'negative string size (or size too big)') + end + + it "raised Errno::EINVAL for negative values of offset" do + -> { @file.pread(4, -1) }.should raise_error(Errno::EINVAL, /Invalid argument/) + end + + it "raises TypeError if the buffer is not a String and cannot be coerced into String" do + buffer = Object.new + -> { @file.pread(4, 0, buffer) }.should raise_error(TypeError, 'no implicit conversion of Object into String') + end + + it "raises EOFError if end-of-file is reached" do + -> { @file.pread(1, 10) }.should raise_error(EOFError) + end + + it "raises IOError when file is not open in read mode" do + File.open(@fname, "w") do |file| + -> { file.pread(1, 1) }.should raise_error(IOError) + end + end + + it "raises IOError when file is closed" do + file = File.open(@fname, "r+") + file.close + -> { file.pread(1, 1) }.should raise_error(IOError) + end + end +end diff --git a/spec/ruby/core/io/print_spec.rb b/spec/ruby/core/io/print_spec.rb new file mode 100644 index 0000000000..085852024c --- /dev/null +++ b/spec/ruby/core/io/print_spec.rb @@ -0,0 +1,66 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#print" do + before :each do + @old_record_separator = $\ + @old_field_separator = $, + suppress_warning { + $\ = '->' + $, = '^^' + } + @name = tmp("io_print") + end + + after :each do + suppress_warning { + $\ = @old_record_separator + $, = @old_field_separator + } + rm_r @name + end + + it "returns nil" do + touch(@name) { |f| f.print.should be_nil } + end + + it "writes $_.to_s followed by $\\ (if any) to the stream if no arguments given" do + o = mock('o') + o.should_receive(:to_s).and_return("mockmockmock") + $_ = o + + touch(@name) { |f| f.print } + IO.read(@name).should == "mockmockmock#{$\}" + + # Set $_ to something known + string = File.open(__FILE__) {|f| f.gets } + + touch(@name) { |f| f.print } + IO.read(@name).should == "#{string}#{$\}" + end + + it "calls obj.to_s and not obj.to_str then writes the record separator" do + o = mock('o') + o.should_not_receive(:to_str) + o.should_receive(:to_s).and_return("hello") + + touch(@name) { |f| f.print(o) } + + IO.read(@name).should == "hello#{$\}" + end + + it "writes each obj.to_s to the stream separated by $, (if any) and appends $\\ (if any) given multiple objects" do + o, o2 = Object.new, Object.new + def o.to_s(); 'o'; end + def o2.to_s(); 'o2'; end + + suppress_warning { + touch(@name) { |f| f.print(o, o2) } + } + IO.read(@name).should == "#{o.to_s}#{$,}#{o2.to_s}#{$\}" + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.print("stuff") }.should raise_error(IOError) + end +end diff --git a/spec/ruby/core/io/printf_spec.rb b/spec/ruby/core/io/printf_spec.rb new file mode 100644 index 0000000000..baa00f14ce --- /dev/null +++ b/spec/ruby/core/io/printf_spec.rb @@ -0,0 +1,32 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#printf" do + before :each do + @name = tmp("io_printf.txt") + @io = new_io @name + @io.sync = true + end + + after :each do + @io.close if @io + rm_r @name + end + + it "calls #to_str to convert the format object to a String" do + obj = mock("printf format") + obj.should_receive(:to_str).and_return("%s") + + @io.printf obj, "printf" + File.read(@name).should == "printf" + end + + it "writes the #sprintf formatted string" do + @io.printf "%d %s", 5, "cookies" + File.read(@name).should == "5 cookies" + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.printf("stuff") }.should raise_error(IOError) + end +end diff --git a/spec/ruby/core/io/putc_spec.rb b/spec/ruby/core/io/putc_spec.rb new file mode 100644 index 0000000000..73473ad821 --- /dev/null +++ b/spec/ruby/core/io/putc_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' +require_relative '../../shared/io/putc' + +describe "IO#putc" do + before :each do + @name = tmp("io_putc.txt") + @io_object = @io = new_io(@name) + end + + it_behaves_like :io_putc, :putc +end diff --git a/spec/ruby/core/io/puts_spec.rb b/spec/ruby/core/io/puts_spec.rb new file mode 100644 index 0000000000..a186ddaa5d --- /dev/null +++ b/spec/ruby/core/io/puts_spec.rb @@ -0,0 +1,139 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#puts" do + before :each do + @before_separator = $/ + @name = tmp("io_puts.txt") + @io = new_io @name + ScratchPad.record(+"") + def @io.write(str) + ScratchPad << str + end + end + + after :each do + ScratchPad.clear + @io.close if @io + rm_r @name + suppress_warning {$/ = @before_separator} + end + + it "writes just a newline when given no args" do + @io.puts.should == nil + ScratchPad.recorded.should == "\n" + end + + it "writes just a newline when given just a newline" do + -> { $stdout.puts "\n" }.should output_to_fd("\n", $stdout) + end + + it "writes empty string with a newline when given nil as an arg" do + @io.puts(nil).should == nil + ScratchPad.recorded.should == "\n" + end + + it "writes empty string with a newline when given nil as multiple args" do + @io.puts(nil, nil).should == nil + ScratchPad.recorded.should == "\n\n" + end + + it "calls :to_ary before writing non-string objects, regardless of it being implemented in the receiver" do + object = mock('hola') + object.should_receive(:method_missing).with(:to_ary) + object.should_receive(:to_s).and_return("#<Object:0x...>") + + @io.puts(object).should == nil + ScratchPad.recorded.should == "#<Object:0x...>\n" + end + + it "calls :to_ary before writing non-string objects" do + object = mock('hola') + object.should_receive(:to_ary).and_return(["hola"]) + + @io.puts(object).should == nil + ScratchPad.recorded.should == "hola\n" + end + + it "calls :to_s before writing non-string objects that don't respond to :to_ary" do + object = mock('hola') + object.should_receive(:to_s).and_return("hola") + + @io.puts(object).should == nil + ScratchPad.recorded.should == "hola\n" + end + + it "returns general object info if :to_s does not return a string" do + object = mock('hola') + object.should_receive(:to_s).and_return(false) + + @io.puts(object).should == nil + ScratchPad.recorded.should == object.inspect.split(" ")[0] + ">\n" + end + + it "writes each arg if given several" do + @io.puts(1, "two", 3).should == nil + ScratchPad.recorded.should == "1\ntwo\n3\n" + end + + it "flattens a nested array before writing it" do + @io.puts([1, 2, [3]]).should == nil + ScratchPad.recorded.should == "1\n2\n3\n" + end + + it "writes nothing for an empty array" do + x = [] + @io.should_not_receive(:write) + @io.puts(x).should == nil + end + + it "writes [...] for a recursive array arg" do + x = [] + x << 2 << x + @io.puts(x).should == nil + ScratchPad.recorded.should == "2\n[...]\n" + end + + it "writes a newline after objects that do not end in newlines" do + @io.puts(5).should == nil + ScratchPad.recorded.should == "5\n" + end + + it "does not write a newline after objects that end in newlines" do + @io.puts("5\n").should == nil + ScratchPad.recorded.should == "5\n" + end + + it "ignores the $/ separator global" do + suppress_warning {$/ = ":"} + @io.puts(5).should == nil + ScratchPad.recorded.should == "5\n" + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.puts("stuff") }.should raise_error(IOError) + end + + it "writes crlf when IO is opened with newline: :crlf" do + File.open(@name, 'wt', newline: :crlf) do |file| + file.puts + end + File.binread(@name).should == "\r\n" + end + + it "writes cr when IO is opened with newline: :cr" do + File.open(@name, 'wt', newline: :cr) do |file| + file.puts + end + File.binread(@name).should == "\r" + end + + platform_is_not :windows do # https://bugs.ruby-lang.org/issues/12436 + it "writes lf when IO is opened with newline: :lf" do + File.open(@name, 'wt', newline: :lf) do |file| + file.puts + end + File.binread(@name).should == "\n" + end + end +end diff --git a/spec/ruby/core/io/pwrite_spec.rb b/spec/ruby/core/io/pwrite_spec.rb new file mode 100644 index 0000000000..2bc508b37d --- /dev/null +++ b/spec/ruby/core/io/pwrite_spec.rb @@ -0,0 +1,69 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' + +guard -> { platform_is_not :windows or ruby_version_is "3.3" } do + describe "IO#pwrite" do + before :each do + @fname = tmp("io_pwrite.txt") + @file = File.open(@fname, "w+") + end + + after :each do + @file.close + rm_r @fname + end + + it "returns the number of bytes written" do + @file.pwrite("foo", 0).should == 3 + end + + it "accepts a string and an offset" do + @file.pwrite("foo", 2) + @file.pread(3, 2).should == "foo" + end + + it "does not advance the pointer in the file" do + @file.pwrite("bar", 3) + @file.write("foo") + @file.pread(6, 0).should == "foobar" + end + + it "calls #to_s on the object to be written" do + object = mock("to_s") + object.should_receive(:to_s).and_return("foo") + @file.pwrite(object, 0) + @file.pread(3, 0).should == "foo" + end + + it "calls #to_int on the offset" do + offset = mock("to_int") + offset.should_receive(:to_int).and_return(2) + @file.pwrite("foo", offset) + @file.pread(3, 2).should == "foo" + end + + it "raises IOError when file is not open in write mode" do + File.open(@fname, "r") do |file| + -> { file.pwrite("foo", 1) }.should raise_error(IOError, "not opened for writing") + end + end + + it "raises IOError when file is closed" do + file = File.open(@fname, "w+") + file.close + -> { file.pwrite("foo", 1) }.should raise_error(IOError, "closed stream") + end + + it "raises a NoMethodError if object does not respond to #to_s" do + -> { + @file.pwrite(BasicObject.new, 0) + }.should raise_error(NoMethodError, /undefined method [`']to_s'/) + end + + it "raises a TypeError if the offset cannot be converted to an Integer" do + -> { + @file.pwrite("foo", Object.new) + }.should raise_error(TypeError, "no implicit conversion of Object into Integer") + end + end +end diff --git a/spec/ruby/core/io/read_nonblock_spec.rb b/spec/ruby/core/io/read_nonblock_spec.rb new file mode 100644 index 0000000000..51e7cd6bd2 --- /dev/null +++ b/spec/ruby/core/io/read_nonblock_spec.rb @@ -0,0 +1,148 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#read_nonblock" do + before :each do + @read, @write = IO.pipe + end + + after :each do + @read.close if @read && !@read.closed? + @write.close if @write && !@write.closed? + end + + it "raises an exception extending IO::WaitReadable when there is no data" do + -> { @read.read_nonblock(5) }.should raise_error(IO::WaitReadable) { |e| + platform_is_not :windows do + e.should be_kind_of(Errno::EAGAIN) + end + platform_is :windows do + e.should be_kind_of(Errno::EWOULDBLOCK) + end + } + end + + context "when exception option is set to false" do + context "when there is no data" do + it "returns :wait_readable" do + @read.read_nonblock(5, exception: false).should == :wait_readable + end + end + + context "when the end is reached" do + it "returns nil" do + @write << "hello" + @write.close + + @read.read_nonblock(5) + + @read.read_nonblock(5, exception: false).should be_nil + end + end + end + + platform_is_not :windows do + it 'sets the IO in nonblock mode' do + require 'io/nonblock' + @write.write "abc" + @read.read_nonblock(1).should == "a" + @read.should.nonblock? + end + end + + it "returns at most the number of bytes requested" do + @write << "hello" + @read.read_nonblock(4).should == "hell" + end + + it "reads after ungetc with data in the buffer" do + @write.write("foobar") + @read.set_encoding( + 'utf-8', universal_newline: false + ) + c = @read.getc + @read.ungetc(c) + @read.read_nonblock(3).should == "foo" + @read.read_nonblock(3).should == "bar" + end + + it "raises an exception after ungetc with data in the buffer and character conversion enabled" do + @write.write("foobar") + @read.set_encoding( + 'utf-8', universal_newline: true + ) + c = @read.getc + @read.ungetc(c) + -> { @read.read_nonblock(3).should == "foo" }.should raise_error(IOError) + end + + it "returns less data if that is all that is available" do + @write << "hello" + @read.read_nonblock(10).should == "hello" + end + + it "allows for reading 0 bytes before any write" do + @read.read_nonblock(0).should == "" + end + + it "allows for reading 0 bytes after a write" do + @write.write "1" + @read.read_nonblock(0).should == "" + @read.read_nonblock(1).should == "1" + end + + it "raises ArgumentError when length is less than 0" do + -> { @read.read_nonblock(-1) }.should raise_error(ArgumentError) + end + + it "reads into the passed buffer" do + buffer = +"" + @write.write("1") + @read.read_nonblock(1, buffer) + buffer.should == "1" + end + + it "returns the passed buffer" do + buffer = +"" + @write.write("1") + output = @read.read_nonblock(1, buffer) + output.should equal(buffer) + end + + it "discards the existing buffer content upon successful read" do + buffer = +"existing content" + @write.write("hello world") + @write.close + @read.read_nonblock(11, buffer) + buffer.should == "hello world" + end + + it "discards the existing buffer content upon error" do + buffer = +"existing content" + @write.close + -> { @read.read_nonblock(1, buffer) }.should raise_error(EOFError) + buffer.should be_empty + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.read_nonblock(5) }.should raise_error(IOError) + end + + it "raises EOFError when the end is reached" do + @write << "hello" + @write.close + + @read.read_nonblock(5) + + -> { @read.read_nonblock(5) }.should raise_error(EOFError) + end + + it "preserves the encoding of the given buffer" do + buffer = ''.encode(Encoding::ISO_8859_1) + @write.write("abc") + @write.close + @read.read_nonblock(10, buffer) + + buffer.encoding.should == Encoding::ISO_8859_1 + end +end diff --git a/spec/ruby/core/io/read_spec.rb b/spec/ruby/core/io/read_spec.rb new file mode 100644 index 0000000000..988ec2ce30 --- /dev/null +++ b/spec/ruby/core/io/read_spec.rb @@ -0,0 +1,757 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO.read" do + before :each do + @fname = tmp("io_read.txt") + @contents = "1234567890" + touch(@fname) { |f| f.write @contents } + end + + after :each do + rm_r @fname + end + + it "reads the contents of a file" do + IO.read(@fname).should == @contents + end + + it "calls #to_path on non-String arguments" do + p = mock('path') + p.should_receive(:to_path).and_return(@fname) + IO.read(p) + end + + # https://bugs.ruby-lang.org/issues/19354 + it "accepts options as keyword arguments" do + IO.read(@fname, 3, 0, mode: "r+").should == @contents[0, 3] + + -> { + IO.read(@fname, 3, 0, {mode: "r+"}) + }.should raise_error(ArgumentError, /wrong number of arguments/) + end + + it "accepts an empty options Hash" do + IO.read(@fname, **{}).should == @contents + end + + it "accepts a length, and empty options Hash" do + IO.read(@fname, 3, **{}).should == @contents[0, 3] + end + + it "accepts a length, offset, and empty options Hash" do + IO.read(@fname, 3, 0, **{}).should == @contents[0, 3] + end + + it "raises an IOError if the options Hash specifies write mode" do + -> { IO.read(@fname, 3, 0, mode: "w") }.should raise_error(IOError) + end + + it "raises an IOError if the options Hash specifies append only mode" do + -> { IO.read(@fname, mode: "a") }.should raise_error(IOError) + end + + it "reads the file if the options Hash includes read mode" do + IO.read(@fname, mode: "r").should == @contents + end + + it "reads the file if the options Hash includes read/write mode" do + IO.read(@fname, mode: "r+").should == @contents + end + + it "reads the file if the options Hash includes read/write append mode" do + IO.read(@fname, mode: "a+").should == @contents + end + + platform_is_not :windows do + ruby_version_is ""..."3.3" do + it "uses an :open_args option" do + string = IO.read(@fname, nil, 0, open_args: ["r", nil, {encoding: Encoding::US_ASCII}]) + string.encoding.should == Encoding::US_ASCII + + string = IO.read(@fname, nil, 0, open_args: ["r", nil, {}]) + string.encoding.should == Encoding::UTF_8 + end + end + end + + it "disregards other options if :open_args is given" do + string = IO.read(@fname,mode: "w", encoding: Encoding::UTF_32LE, open_args: ["r", encoding: Encoding::UTF_8]) + string.encoding.should == Encoding::UTF_8 + end + + it "doesn't require mode to be specified in :open_args" do + string = IO.read(@fname, nil, 0, open_args: [{encoding: Encoding::US_ASCII}]) + string.encoding.should == Encoding::US_ASCII + end + + it "doesn't require mode to be specified in :open_args even if flags option passed" do + string = IO.read(@fname, nil, 0, open_args: [{encoding: Encoding::US_ASCII, flags: File::CREAT}]) + string.encoding.should == Encoding::US_ASCII + end + + it "treats second nil argument as no length limit" do + IO.read(@fname, nil).should == @contents + IO.read(@fname, nil, 5).should == IO.read(@fname, @contents.length, 5) + end + + it "treats third nil argument as 0" do + IO.read(@fname, nil, nil).should == @contents + IO.read(@fname, 5, nil).should == IO.read(@fname, 5, 0) + end + + it "reads the contents of a file up to a certain size when specified" do + IO.read(@fname, 5).should == @contents.slice(0..4) + end + + it "reads the contents of a file from an offset of a specific size when specified" do + IO.read(@fname, 5, 3).should == @contents.slice(3, 5) + end + + it "returns nil at end-of-file when length is passed" do + IO.read(@fname, 1, 10).should == nil + end + + it "returns an empty string when reading zero bytes" do + IO.read(@fname, 0).should == '' + end + + it "returns a String in BINARY when passed a size" do + IO.read(@fname, 1).encoding.should == Encoding::BINARY + IO.read(@fname, 0).encoding.should == Encoding::BINARY + end + + it "raises an Errno::ENOENT when the requested file does not exist" do + rm_r @fname + -> { IO.read @fname }.should raise_error(Errno::ENOENT) + end + + it "raises a TypeError when not passed a String type" do + -> { IO.read nil }.should raise_error(TypeError) + end + + it "raises an ArgumentError when not passed a valid length" do + -> { IO.read @fname, -1 }.should raise_error(ArgumentError) + end + + ruby_version_is ''...'3.3' do + it "raises an Errno::EINVAL when not passed a valid offset" do + -> { IO.read @fname, 0, -1 }.should raise_error(Errno::EINVAL) + -> { IO.read @fname, -1, -1 }.should raise_error(Errno::EINVAL) + end + end + + ruby_version_is '3.3' do + it "raises an ArgumentError when not passed a valid offset" do + -> { IO.read @fname, 0, -1 }.should raise_error(ArgumentError) + -> { IO.read @fname, -1, -1 }.should raise_error(ArgumentError) + end + end + + it "uses the external encoding specified via the :external_encoding option" do + str = IO.read(@fname, external_encoding: Encoding::ISO_8859_1) + str.encoding.should == Encoding::ISO_8859_1 + end + + it "uses the external encoding specified via the :encoding option" do + str = IO.read(@fname, encoding: Encoding::ISO_8859_1) + str.encoding.should == Encoding::ISO_8859_1 + end + + platform_is :windows do + it "reads the file in text mode" do + # 0x1A is CTRL+Z and is EOF in Windows text mode. + File.binwrite(@fname, "\x1Abbb") + IO.read(@fname).should.empty? + end + end +end + +ruby_version_is ""..."4.0" do + describe "IO.read from a pipe" do + it "runs the rest as a subprocess and returns the standard output" do + cmd = "|sh -c 'echo hello'" + platform_is :windows do + cmd = "|cmd.exe /C echo hello" + end + + suppress_warning do # https://bugs.ruby-lang.org/issues/19630 + IO.read(cmd).should == "hello\n" + end + end + + platform_is_not :windows do + it "opens a pipe to a fork if the rest is -" do + str = nil + suppress_warning do # https://bugs.ruby-lang.org/issues/19630 + str = IO.read("|-") + end + + if str # parent + str.should == "hello from child\n" + else #child + puts "hello from child" + exit! + end + end + end + + it "reads only the specified number of bytes requested" do + cmd = "|sh -c 'echo hello'" + platform_is :windows do + cmd = "|cmd.exe /C echo hello" + end + + suppress_warning do # https://bugs.ruby-lang.org/issues/19630 + IO.read(cmd, 1).should == "h" + end + end + + platform_is_not :windows do + it "raises Errno::ESPIPE if passed an offset" do + -> { + suppress_warning do # https://bugs.ruby-lang.org/issues/19630 + IO.read("|sh -c 'echo hello'", 1, 1) + end + }.should raise_error(Errno::ESPIPE) + end + end + + quarantine! do # The process tried to write to a nonexistent pipe. + platform_is :windows do + # TODO: It should raise Errno::ESPIPE on Windows as well + # once https://bugs.ruby-lang.org/issues/12230 is fixed. + it "raises Errno::EINVAL if passed an offset" do + -> { + suppress_warning do # https://bugs.ruby-lang.org/issues/19630 + IO.read("|cmd.exe /C echo hello", 1, 1) + end + }.should raise_error(Errno::EINVAL) + end + end + end + + ruby_version_is "3.3" do + # https://bugs.ruby-lang.org/issues/19630 + it "warns about deprecation" do + cmd = "|echo ok" + -> { + IO.read(cmd) + }.should complain(/IO process creation with a leading '\|'/) + end + end + end +end + +describe "IO.read on an empty file" do + before :each do + @fname = tmp("io_read_empty.txt") + touch(@fname) + end + + after :each do + rm_r @fname + end + + it "returns nil when length is passed" do + IO.read(@fname, 1).should == nil + end + + it "returns an empty string when no length is passed" do + IO.read(@fname).should == "" + end +end + +describe "IO#read" do + + before :each do + @fname = tmp("io_read.txt") + @contents = "1234567890" + touch(@fname) { |f| f.write @contents } + + @io = open @fname, "r+" + end + + after :each do + @io.close + rm_r @fname + end + + it "can be read from consecutively" do + @io.read(1).should == '1' + @io.read(2).should == '23' + @io.read(3).should == '456' + @io.read(4).should == '7890' + end + + it "treats first nil argument as no length limit" do + @io.read(nil).should == @contents + end + + it "raises an ArgumentError when not passed a valid length" do + -> { @io.read(-1) }.should raise_error(ArgumentError) + end + + it "clears the output buffer if there is nothing to read" do + @io.pos = 10 + + buf = +'non-empty string' + + @io.read(10, buf).should == nil + + buf.should == '' + + buf = +'non-empty string' + + @io.read(nil, buf).should == "" + + buf.should == '' + + buf = +'non-empty string' + + @io.read(0, buf).should == "" + + buf.should == '' + end + + it "raise FrozenError if the output buffer is frozen" do + @io.read + -> { @io.read(0, 'frozen-string'.freeze) }.should raise_error(FrozenError) + -> { @io.read(1, 'frozen-string'.freeze) }.should raise_error(FrozenError) + -> { @io.read(nil, 'frozen-string'.freeze) }.should raise_error(FrozenError) + end + + ruby_bug "", ""..."3.3" do + it "raise FrozenError if the output buffer is frozen (2)" do + @io.read + -> { @io.read(1, ''.freeze) }.should raise_error(FrozenError) + end + end + + it "consumes zero bytes when reading zero bytes" do + @io.read(0).should == '' + @io.pos.should == 0 + + @io.getc.should == '1' + end + + it "is at end-of-file when everything has been read" do + @io.read + @io.should.eof? + end + + it "reads the contents of a file" do + @io.read.should == @contents + end + + it "places the specified number of bytes in the buffer" do + buf = +"" + @io.read 5, buf + + buf.should == "12345" + end + + it "expands the buffer when too small" do + buf = +"ABCDE" + @io.read nil, buf + + buf.should == @contents + end + + it "overwrites the buffer" do + buf = +"ABCDEFGHIJ" + @io.read nil, buf + + buf.should == @contents + end + + it "truncates the buffer when too big" do + buf = +"ABCDEFGHIJKLMNO" + @io.read nil, buf + buf.should == @contents + + @io.rewind + + buf = +"ABCDEFGHIJKLMNO" + @io.read 5, buf + buf.should == @contents[0..4] + end + + it "preserves the encoding of the given buffer" do + buffer = ''.encode(Encoding::ISO_8859_1) + @io.read(10, buffer) + + buffer.encoding.should == Encoding::ISO_8859_1 + end + + # https://bugs.ruby-lang.org/issues/20416 + it "does not preserve the encoding of the given buffer when max length is not provided" do + buffer = ''.encode(Encoding::ISO_8859_1) + @io.read(nil, buffer) + + buffer.encoding.should_not == Encoding::ISO_8859_1 + end + + it "returns the given buffer" do + buf = +"" + + @io.read(nil, buf).should equal buf + end + + it "returns the given buffer when there is nothing to read" do + buf = +"" + + @io.read + @io.read(nil, buf).should equal buf + end + + it "coerces the second argument to string and uses it as a buffer" do + buf = +"ABCDE" + obj = mock("buff") + obj.should_receive(:to_str).any_number_of_times.and_return(buf) + + @io.read(15, obj).should_not equal obj + buf.should == @contents + end + + it "returns an empty string at end-of-file" do + @io.read + @io.read.should == '' + end + + it "reads the contents of a file when more bytes are specified" do + @io.read(@contents.length + 1).should == @contents + end + + it "returns an empty string at end-of-file" do + @io.read + @io.read.should == '' + end + + it "returns an empty string when the current pos is bigger than the content size" do + @io.pos = 1000 + @io.read.should == '' + end + + it "returns nil at end-of-file with a length" do + @io.read + @io.read(1).should == nil + end + + it "with length argument returns nil when the current pos is bigger than the content size" do + @io.pos = 1000 + @io.read(1).should == nil + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.read }.should raise_error(IOError) + end + + it "raises ArgumentError when length is less than 0" do + -> { @io.read(-1) }.should raise_error(ArgumentError) + end + + platform_is_not :windows do + it "raises IOError when stream is closed by another thread" do + r, w = IO.pipe + t = Thread.new do + begin + r.read(1) + rescue => e + e + end + end + + Thread.pass until t.stop? + r.close + t.join + t.value.should be_kind_of(IOError) + w.close + end + end +end + +platform_is :windows do + describe "IO#read on Windows" do + before :each do + @fname = tmp("io_read.txt") + touch(@fname, "wb") { |f| f.write "a\r\nb\r\nc" } + end + + after :each do + @io.close if @io + rm_r @fname + end + + it "normalizes line endings in text mode" do + @io = new_io(@fname, "r") + @io.read.should == "a\nb\nc" + end + + it "does not normalize line endings in binary mode" do + @io = new_io(@fname, "rb") + @io.read.should == "a\r\nb\r\nc" + end + end +end + +describe "IO#read" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.close if @io + end + + it "ignores unicode encoding" do + @io.readline.should == "Voici la ligne une.\n" + # read "Qui è" + @io.read(5).should == "Qui " + [195].pack('C*') + end +end + +describe "IO#read in binary mode" do + before :each do + @internal = Encoding.default_internal + @name = fixture __FILE__, "read_binary.txt" + end + + after :each do + Encoding.default_internal = @internal + end + + it "does not transcode file contents when Encoding.default_internal is set" do + Encoding.default_internal = "utf-8" + + result = File.open(@name, "rb") { |f| f.read }.chomp + + result.encoding.should == Encoding::BINARY + xE2 = [226].pack('C*') + result.should == ("abc" + xE2 + "def").force_encoding(Encoding::BINARY) + end +end + +describe "IO#read in text mode" do + before :each do + @external = Encoding.default_external + @internal = Encoding.default_internal + @name = fixture __FILE__, "read_text.txt" + end + + after :each do + Encoding.default_external = @external + Encoding.default_internal = @internal + end + + it "reads data according to the internal encoding" do + Encoding.default_internal = "utf-8" + Encoding.default_external = "utf-8" + + result = File.open(@name, "rt") { |f| f.read }.chomp + + result.encoding.should == Encoding::UTF_8 + result.should == "abcâdef" + end +end + +describe "IO.read with BOM" do + it "reads a file without a bom" do + name = fixture __FILE__, "no_bom_UTF-8.txt" + result = File.read(name, mode: "rb:BOM|utf-8") + result.force_encoding("binary").should == "UTF-8\n" + end + + it "reads a file with a utf-8 bom" do + name = fixture __FILE__, "bom_UTF-8.txt" + result = File.read(name, mode: "rb:BOM|utf-16le") + result.force_encoding("binary").should == "UTF-8\n" + end + + it "reads a file with a utf-16le bom" do + name = fixture __FILE__, "bom_UTF-16LE.txt" + result = File.read(name, mode: "rb:BOM|utf-8") + result.force_encoding("binary").should == "U\x00T\x00F\x00-\x001\x006\x00L\x00E\x00\n\x00" + end + + it "reads a file with a utf-16be bom" do + name = fixture __FILE__, "bom_UTF-16BE.txt" + result = File.read(name, mode: "rb:BOM|utf-8") + result.force_encoding("binary").should == "\x00U\x00T\x00F\x00-\x001\x006\x00B\x00E\x00\n" + end + + it "reads a file with a utf-32le bom" do + name = fixture __FILE__, "bom_UTF-32LE.txt" + result = File.read(name, mode: "rb:BOM|utf-8") + result.force_encoding("binary").should == "U\x00\x00\x00T\x00\x00\x00F\x00\x00\x00-\x00\x00\x003\x00\x00\x002\x00\x00\x00L\x00\x00\x00E\x00\x00\x00\n\x00\x00\x00" + end + + it "reads a file with a utf-32be bom" do + name = fixture __FILE__, "bom_UTF-32BE.txt" + result = File.read(name, mode: "rb:BOM|utf-8") + result.force_encoding("binary").should == "\x00\x00\x00U\x00\x00\x00T\x00\x00\x00F\x00\x00\x00-\x00\x00\x003\x00\x00\x002\x00\x00\x00B\x00\x00\x00E\x00\x00\x00\n" + end +end + +describe :io_read_internal_encoding, shared: true do + it "returns a transcoded String" do + @io.read.should == "ã‚りãŒã¨ã†\n" + end + + it "sets the String encoding to the internal encoding" do + @io.read.encoding.should equal(Encoding::UTF_8) + end + + describe "when passed nil for limit" do + it "sets the buffer to a transcoded String" do + result = @io.read(nil, buf = +"") + buf.should equal(result) + buf.should == "ã‚りãŒã¨ã†\n" + end + + it "sets the buffer's encoding to the internal encoding" do + buf = "".dup.force_encoding Encoding::ISO_8859_1 + @io.read(nil, buf) + buf.encoding.should equal(Encoding::UTF_8) + end + end +end + +describe :io_read_size_internal_encoding, shared: true do + it "reads bytes when passed a size" do + @io.read(2).should == [164, 162].pack('C*').force_encoding(Encoding::BINARY) + end + + it "returns a String in BINARY when passed a size" do + @io.read(4).encoding.should equal(Encoding::BINARY) + @io.read(0).encoding.should equal(Encoding::BINARY) + end + + it "does not change the buffer's encoding when passed a limit" do + buf = "".dup.force_encoding Encoding::ISO_8859_1 + @io.read(4, buf) + buf.should == [164, 162, 164, 234].pack('C*').force_encoding(Encoding::ISO_8859_1) + buf.encoding.should equal(Encoding::ISO_8859_1) + end + + it "truncates the buffer but does not change the buffer's encoding when no data remains" do + buf = "abc".dup.force_encoding Encoding::ISO_8859_1 + @io.read + + @io.read(1, buf).should be_nil + buf.size.should == 0 + buf.encoding.should equal(Encoding::ISO_8859_1) + end +end + +describe "IO#read" do + describe "when IO#external_encoding and IO#internal_encoding are nil" do + before :each do + @name = tmp("io_read.txt") + touch(@name) { |f| f.write "\x00\x01\x02" } + @io = new_io @name, "r+" + end + + after :each do + @io.close if @io + rm_r @name + end + + it "sets the String encoding to Encoding.default_external" do + @io.read.encoding.should equal(Encoding.default_external) + end + end + + describe "with internal encoding" do + after :each do + @io.close if @io + end + + describe "not specified" do + before :each do + @io = IOSpecs.io_fixture "read_euc_jp.txt", "r:euc-jp" + end + + it "does not transcode the String" do + @io.read.should == ("ã‚りãŒã¨ã†\n").encode(Encoding::EUC_JP) + end + + it "sets the String encoding to the external encoding" do + @io.read.encoding.should equal(Encoding::EUC_JP) + end + + it_behaves_like :io_read_size_internal_encoding, nil + end + + describe "specified by open mode" do + before :each do + @io = IOSpecs.io_fixture "read_euc_jp.txt", "r:euc-jp:utf-8" + end + + it_behaves_like :io_read_internal_encoding, nil + it_behaves_like :io_read_size_internal_encoding, nil + end + + describe "specified by mode: option" do + before :each do + @io = IOSpecs.io_fixture "read_euc_jp.txt", mode: "r:euc-jp:utf-8" + end + + it_behaves_like :io_read_internal_encoding, nil + it_behaves_like :io_read_size_internal_encoding, nil + end + + describe "specified by internal_encoding: option" do + before :each do + options = { mode: "r", + internal_encoding: "utf-8", + external_encoding: "euc-jp" } + @io = IOSpecs.io_fixture "read_euc_jp.txt", options + end + + it_behaves_like :io_read_internal_encoding, nil + it_behaves_like :io_read_size_internal_encoding, nil + end + + describe "specified by encoding: option" do + before :each do + options = { mode: "r", encoding: "euc-jp:utf-8" } + @io = IOSpecs.io_fixture "read_euc_jp.txt", options + end + + it_behaves_like :io_read_internal_encoding, nil + it_behaves_like :io_read_size_internal_encoding, nil + end + end +end + +describe "IO#read with large data" do + before :each do + # TODO: what is the significance of this mystery math? + @data_size = 8096 * 2 + 1024 + @data = "*" * @data_size + + @fname = tmp("io_read.txt") + touch(@fname) { |f| f.write @data } + end + + after :each do + rm_r @fname + end + + it "reads all the data at once" do + File.open(@fname, 'r') { |io| ScratchPad.record io.read } + + ScratchPad.recorded.size.should == @data_size + ScratchPad.recorded.should == @data + end + + it "reads only the requested number of bytes" do + read_size = @data_size / 2 + File.open(@fname, 'r') { |io| ScratchPad.record io.read(read_size) } + + ScratchPad.recorded.size.should == read_size + ScratchPad.recorded.should == @data[0, read_size] + end +end diff --git a/spec/ruby/core/io/readbyte_spec.rb b/spec/ruby/core/io/readbyte_spec.rb new file mode 100644 index 0000000000..14426c28ac --- /dev/null +++ b/spec/ruby/core/io/readbyte_spec.rb @@ -0,0 +1,24 @@ +require_relative '../../spec_helper' + +describe "IO#readbyte" do + before :each do + @io = File.open(__FILE__, 'r') + end + + after :each do + @io.close + end + + it "reads one byte from the stream" do + byte = @io.readbyte + byte.should == ?r.getbyte(0) + @io.pos.should == 1 + end + + it "raises EOFError on EOF" do + @io.seek(999999) + -> do + @io.readbyte + end.should raise_error EOFError + end +end diff --git a/spec/ruby/core/io/readchar_spec.rb b/spec/ruby/core/io/readchar_spec.rb new file mode 100644 index 0000000000..a66773851a --- /dev/null +++ b/spec/ruby/core/io/readchar_spec.rb @@ -0,0 +1,110 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe :io_readchar_internal_encoding, shared: true do + it "returns a transcoded String" do + @io.readchar.should == "ã‚" + end + + it "sets the String encoding to the internal encoding" do + @io.readchar.encoding.should equal(Encoding::UTF_8) + end +end + +describe "IO#readchar" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.close unless @io.closed? + end + + it "returns the next string from the stream" do + @io.readchar.should == 'V' + @io.readchar.should == 'o' + @io.readchar.should == 'i' + # read the rest of line + @io.readline.should == "ci la ligne une.\n" + @io.readchar.should == 'Q' + end + + it "raises an EOFError when invoked at the end of the stream" do + @io.read + -> { @io.readchar }.should raise_error(EOFError) + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.readchar }.should raise_error(IOError) + end +end + +describe "IO#readchar with internal encoding" do + after :each do + @io.close if @io + end + + describe "not specified" do + before :each do + @io = IOSpecs.io_fixture "read_euc_jp.txt", "r:euc-jp" + end + + it "does not transcode the String" do + @io.readchar.should == ("ã‚").encode(Encoding::EUC_JP) + end + + it "sets the String encoding to the external encoding" do + @io.readchar.encoding.should equal(Encoding::EUC_JP) + end + end + + describe "specified by open mode" do + before :each do + @io = IOSpecs.io_fixture "read_euc_jp.txt", "r:euc-jp:utf-8" + end + + it_behaves_like :io_readchar_internal_encoding, nil + end + + describe "specified by mode: option" do + before :each do + @io = IOSpecs.io_fixture "read_euc_jp.txt", mode: "r:euc-jp:utf-8" + end + + it_behaves_like :io_readchar_internal_encoding, nil + end + + describe "specified by internal_encoding: option" do + before :each do + options = { mode: "r", + internal_encoding: "utf-8", + external_encoding: "euc-jp" } + @io = IOSpecs.io_fixture "read_euc_jp.txt", options + end + + it_behaves_like :io_readchar_internal_encoding, nil + end + + describe "specified by encoding: option" do + before :each do + options = { mode: "r", encoding: "euc-jp:utf-8" } + @io = IOSpecs.io_fixture "read_euc_jp.txt", options + end + + it_behaves_like :io_readchar_internal_encoding, nil + end +end + +describe "IO#readchar" do + before :each do + @io = IOSpecs.io_fixture "empty.txt" + end + + after :each do + @io.close unless @io.closed? + end + + it "raises EOFError on empty stream" do + -> { @io.readchar }.should raise_error(EOFError) + end +end diff --git a/spec/ruby/core/io/readline_spec.rb b/spec/ruby/core/io/readline_spec.rb new file mode 100644 index 0000000000..a814c1be90 --- /dev/null +++ b/spec/ruby/core/io/readline_spec.rb @@ -0,0 +1,84 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#readline" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.close unless @io.closed? + end + + it "returns the next line on the stream" do + @io.readline.should == "Voici la ligne une.\n" + @io.readline.should == "Qui è la linea due.\n" + end + + it "goes back to first position after a rewind" do + @io.readline.should == "Voici la ligne une.\n" + @io.rewind + @io.readline.should == "Voici la ligne une.\n" + end + + it "returns characters after the position set by #seek" do + @io.seek(1) + @io.readline.should == "oici la ligne une.\n" + end + + it "raises EOFError on end of stream" do + IOSpecs.lines.length.times { @io.readline } + -> { @io.readline }.should raise_error(EOFError) + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.readline }.should raise_error(IOError) + end + + it "assigns the returned line to $_" do + IOSpecs.lines.each do |line| + @io.readline + $_.should == line + end + end + + describe "when passed limit" do + it "reads limit bytes" do + @io.readline(3).should == "Voi" + end + + it "returns an empty string when passed 0 as a limit" do + @io.readline(0).should == "" + end + + it "does not accept Integers that don't fit in a C off_t" do + -> { @io.readline(2**128) }.should raise_error(RangeError) + end + end + + describe "when passed separator and limit" do + it "reads limit bytes till the separator" do + # Voici la ligne une.\ + @io.readline(" ", 4).should == "Voic" + @io.readline(" ", 4).should == "i " + @io.readline(" ", 4).should == "la " + @io.readline(" ", 4).should == "lign" + @io.readline(" ", 4).should == "e " + end + end + + describe "when passed chomp" do + it "returns the first line without a trailing newline character" do + @io.readline(chomp: true).should == IOSpecs.lines_without_newline_characters[0] + end + + it "raises exception when options passed as Hash" do + -> { @io.readline({ chomp: true }) }.should raise_error(TypeError) + + -> { + @io.readline("\n", 1, { chomp: true }) + }.should raise_error(ArgumentError, "wrong number of arguments (given 3, expected 0..2)") + end + end +end diff --git a/spec/ruby/core/io/readlines_spec.rb b/spec/ruby/core/io/readlines_spec.rb new file mode 100644 index 0000000000..b4770775d1 --- /dev/null +++ b/spec/ruby/core/io/readlines_spec.rb @@ -0,0 +1,259 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/readlines' + +describe "IO#readlines" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + @orig_extenc = Encoding.default_external + Encoding.default_external = Encoding::UTF_8 + end + + after :each do + @io.close unless @io.closed? + Encoding.default_external = @orig_extenc + end + + it "raises an IOError if the stream is closed" do + @io.close + -> { @io.readlines }.should raise_error(IOError) + end + + describe "when passed no arguments" do + before :each do + suppress_warning {@sep, $/ = $/, " "} + end + + after :each do + suppress_warning {$/ = @sep} + end + + it "returns an Array containing lines based on $/" do + @io.readlines.should == IOSpecs.lines_space_separator + end + end + + describe "when passed no arguments" do + it "updates self's position" do + @io.readlines + @io.pos.should eql(137) + end + + it "updates self's lineno based on the number of lines read" do + @io.readlines + @io.lineno.should eql(9) + end + + it "does not change $_" do + $_ = "test" + @io.readlines + $_.should == "test" + end + + it "returns an empty Array when self is at the end" do + @io.readlines.should == IOSpecs.lines + @io.readlines.should == [] + end + end + + describe "when passed nil" do + it "returns the remaining content as one line starting at the current position" do + @io.readlines(nil).should == [IOSpecs.lines.join] + end + end + + describe "when passed an empty String" do + it "returns an Array containing all paragraphs" do + @io.readlines("").should == IOSpecs.paragraphs + end + end + + describe "when passed a separator" do + it "returns an Array containing lines based on the separator" do + @io.readlines("r").should == IOSpecs.lines_r_separator + end + + it "returns an empty Array when self is at the end" do + @io.readlines + @io.readlines("r").should == [] + end + + it "updates self's lineno based on the number of lines read" do + @io.readlines("r") + @io.lineno.should eql(5) + end + + it "updates self's position based on the number of characters read" do + @io.readlines("r") + @io.pos.should eql(137) + end + + it "does not change $_" do + $_ = "test" + @io.readlines("r") + $_.should == "test" + end + + it "tries to convert the passed separator to a String using #to_str" do + obj = mock('to_str') + obj.stub!(:to_str).and_return("r") + @io.readlines(obj).should == IOSpecs.lines_r_separator + end + end + + describe "when passed limit" do + it "raises ArgumentError when passed 0 as a limit" do + -> { @io.readlines(0) }.should raise_error(ArgumentError) + end + + it "does not accept Integers that don't fit in a C off_t" do + -> { @io.readlines(2**128) }.should raise_error(RangeError) + end + end + + describe "when passed chomp" do + it "returns the first line without a trailing newline character" do + @io.readlines(chomp: true).should == IOSpecs.lines_without_newline_characters + end + + it "raises exception when options passed as Hash" do + -> { @io.readlines({ chomp: true }) }.should raise_error(TypeError) + + -> { + @io.readlines("\n", 1, { chomp: true }) + }.should raise_error(ArgumentError, "wrong number of arguments (given 3, expected 0..2)") + end + end + + describe "when passed arbitrary keyword argument" do + it "tolerates it" do + @io.readlines(chomp: true, foo: :bar).should == IOSpecs.lines_without_newline_characters + end + end +end + +describe "IO#readlines" do + before :each do + @name = tmp("io_readlines") + end + + after :each do + rm_r @name + end + + it "raises an IOError if the stream is opened for append only" do + -> do + File.open(@name, "a:utf-8") { |f| f.readlines } + end.should raise_error(IOError) + end + + it "raises an IOError if the stream is opened for write only" do + -> do + File.open(@name, "w:utf-8") { |f| f.readlines } + end.should raise_error(IOError) + end +end + +describe "IO.readlines" do + before :each do + @external = Encoding.default_external + Encoding.default_external = Encoding::UTF_8 + + @name = fixture __FILE__, "lines.txt" + ScratchPad.record [] + end + + after :each do + Encoding.default_external = @external + end + + it "does not change $_" do + $_ = "test" + IO.readlines(@name) + $_.should == "test" + end + + ruby_version_is ""..."4.0" do + describe "when passed a string that starts with a |" do + it "gets data from the standard out of the subprocess" do + cmd = "|sh -c 'echo hello;echo line2'" + platform_is :windows do + cmd = "|cmd.exe /C echo hello&echo line2" + end + + lines = nil + suppress_warning do # https://bugs.ruby-lang.org/issues/19630 + lines = IO.readlines(cmd) + end + lines.should == ["hello\n", "line2\n"] + end + + platform_is_not :windows do + it "gets data from a fork when passed -" do + lines = nil + suppress_warning do # https://bugs.ruby-lang.org/issues/19630 + lines = IO.readlines("|-") + end + + if lines # parent + lines.should == ["hello\n", "from a fork\n"] + else + puts "hello" + puts "from a fork" + exit! + end + end + end + end + + ruby_version_is "3.3" do + # https://bugs.ruby-lang.org/issues/19630 + it "warns about deprecation given a path with a pipe" do + cmd = "|echo ok" + -> { + IO.readlines(cmd) + }.should complain(/IO process creation with a leading '\|'/) + end + end + end + + it_behaves_like :io_readlines, :readlines + it_behaves_like :io_readlines_options_19, :readlines +end + +describe "IO.readlines" do + before :each do + @external = Encoding.default_external + @internal = Encoding.default_internal + @name = fixture __FILE__, "lines.txt" + @dollar_slash = $/ + end + + after :each do + Encoding.default_external = @external + Encoding.default_internal = @internal + suppress_warning {$/ = @dollar_slash} + end + + it "encodes lines using the default external encoding" do + Encoding.default_external = Encoding::UTF_8 + lines = IO.readlines(@name) + lines.all? { |s| s.encoding == Encoding::UTF_8 }.should be_true + end + + it "encodes lines using the default internal encoding, when set" do + Encoding.default_external = Encoding::UTF_8 + Encoding.default_internal = Encoding::UTF_16 + suppress_warning {$/ = $/.encode Encoding::UTF_16} + lines = IO.readlines(@name) + lines.all? { |s| s.encoding == Encoding::UTF_16 }.should be_true + end + + it "ignores the default internal encoding if the external encoding is BINARY" do + Encoding.default_external = Encoding::BINARY + Encoding.default_internal = Encoding::UTF_8 + lines = IO.readlines(@name) + lines.all? { |s| s.encoding == Encoding::BINARY }.should be_true + end +end diff --git a/spec/ruby/core/io/readpartial_spec.rb b/spec/ruby/core/io/readpartial_spec.rb new file mode 100644 index 0000000000..176c33cf9e --- /dev/null +++ b/spec/ruby/core/io/readpartial_spec.rb @@ -0,0 +1,115 @@ +# encoding: binary +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#readpartial" do + before :each do + @rd, @wr = IO.pipe + @rd.binmode + @wr.binmode + end + + after :each do + @rd.close unless @rd.closed? + @wr.close unless @wr.closed? + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.readpartial(10) }.should raise_error(IOError) + + @rd.close + -> { @rd.readpartial(10) }.should raise_error(IOError) + end + + it "reads at most the specified number of bytes" do + @wr.write("foobar") + + # buffered read + @rd.read(1).should == 'f' + # return only specified number, not the whole buffer + @rd.readpartial(1).should == "o" + end + + it "reads after ungetc with data in the buffer" do + @wr.write("foobar") + c = @rd.getc + @rd.ungetc(c) + @rd.readpartial(3).should == "foo" + @rd.readpartial(3).should == "bar" + end + + it "reads after ungetc with multibyte characters in the buffer" do + @wr.write("∂φ/∂x = gaîté") + c = @rd.getc + @rd.ungetc(c) + @rd.readpartial(3).should == "\xE2\x88\x82" + @rd.readpartial(3).should == "\xCF\x86/" + end + + it "reads after ungetc without data in the buffer" do + @wr.write("f") + c = @rd.getc + @rd.ungetc(c) + @rd.readpartial(2).should == "f" + + # now, also check that the ungot char is cleared and + # not returned again + @wr.write("b") + @rd.readpartial(2).should == "b" + end + + it "discards the existing buffer content upon successful read" do + buffer = +"existing content" + @wr.write("hello world") + @wr.close + @rd.readpartial(11, buffer).should.equal?(buffer) + buffer.should == "hello world" + end + + it "raises EOFError on EOF" do + @wr.write("abc") + @wr.close + @rd.readpartial(10).should == 'abc' + -> { @rd.readpartial(10) }.should raise_error(EOFError) + end + + it "discards the existing buffer content upon error" do + buffer = +'hello' + @wr.close + -> { @rd.readpartial(1, buffer) }.should raise_error(EOFError) + buffer.should be_empty + end + + it "raises IOError if the stream is closed" do + @wr.close + -> { @rd.readpartial(1) }.should raise_error(IOError) + end + + it "raises ArgumentError if the negative argument is provided" do + -> { @rd.readpartial(-1) }.should raise_error(ArgumentError) + end + + it "immediately returns an empty string if the length argument is 0" do + @rd.readpartial(0).should == "" + end + + it "raises IOError if the stream is closed and the length argument is 0" do + @rd.close + -> { @rd.readpartial(0) }.should raise_error(IOError, "closed stream") + end + + it "clears and returns the given buffer if the length argument is 0" do + buffer = +"existing content" + @rd.readpartial(0, buffer).should == buffer + buffer.should == "" + end + + it "preserves the encoding of the given buffer" do + buffer = ''.encode(Encoding::ISO_8859_1) + @wr.write("abc") + @wr.close + @rd.readpartial(10, buffer) + + buffer.encoding.should == Encoding::ISO_8859_1 + end +end diff --git a/spec/ruby/core/io/reopen_spec.rb b/spec/ruby/core/io/reopen_spec.rb new file mode 100644 index 0000000000..8ff0f217f4 --- /dev/null +++ b/spec/ruby/core/io/reopen_spec.rb @@ -0,0 +1,313 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +require 'fcntl' + +describe "IO#reopen" do + before :each do + @name = tmp("io_reopen.txt") + @other_name = tmp("io_reopen_other.txt") + + @io = new_io @name + @other_io = File.open @other_name, "w" + end + + after :each do + @io.close unless @io.closed? + @other_io.close unless @other_io.closed? + rm_r @name, @other_name + end + + it "calls #to_io to convert an object" do + obj = mock("io") + obj.should_receive(:to_io).and_return(@other_io) + @io.reopen obj + end + + it "changes the class of the instance to the class of the object returned by #to_io" do + obj = mock("io") + obj.should_receive(:to_io).and_return(@other_io) + @io.reopen(obj).should be_an_instance_of(File) + end + + it "raises an IOError if the object returned by #to_io is closed" do + obj = mock("io") + obj.should_receive(:to_io).and_return(IOSpecs.closed_io) + -> { @io.reopen obj }.should raise_error(IOError) + end + + it "raises a TypeError if #to_io does not return an IO instance" do + obj = mock("io") + obj.should_receive(:to_io).and_return("something else") + -> { @io.reopen obj }.should raise_error(TypeError) + end + + it "raises an IOError when called on a closed stream with an object" do + @io.close + obj = mock("io") + obj.should_not_receive(:to_io) + -> { @io.reopen(STDOUT) }.should raise_error(IOError) + end + + it "raises an IOError if the IO argument is closed" do + -> { @io.reopen(IOSpecs.closed_io) }.should raise_error(IOError) + end + + it "raises an IOError when called on a closed stream with an IO" do + @io.close + -> { @io.reopen(STDOUT) }.should raise_error(IOError) + end +end + +describe "IO#reopen with a String" do + before :each do + @name = fixture __FILE__, "numbered_lines.txt" + @other_name = tmp("io_reopen.txt") + touch @other_name + @io = IOSpecs.io_fixture "lines.txt" + + @tmp_file = tmp("reopen") + end + + after :each do + @io.close unless @io.closed? + rm_r @other_name, @tmp_file + end + + it "does not raise an exception when called on a closed stream with a path" do + @io.close + @io.reopen @name, "r" + @io.closed?.should be_false + @io.gets.should == "Line 1: One\n" + end + + it "returns self" do + @io.reopen(@name).should equal(@io) + end + + it "positions a newly created instance at the beginning of the new stream" do + @io.reopen(@name) + @io.gets.should == "Line 1: One\n" + end + + it "positions an instance that has been read from at the beginning of the new stream" do + @io.gets + @io.reopen(@name) + @io.gets.should == "Line 1: One\n" + end + + platform_is_not :windows do + it "passes all mode flags through" do + @io.reopen(@tmp_file, "ab") + (@io.fcntl(Fcntl::F_GETFL) & File::APPEND).should == File::APPEND + end + end + + platform_is_not :windows do + # TODO Should this work on Windows? + it "affects exec/system/fork performed after it" do + ruby_exe fixture(__FILE__, "reopen_stdout.rb"), args: @tmp_file + File.read(@tmp_file).should == "from system\nfrom exec\n" + end + end + + it "calls #to_path on non-String arguments" do + obj = mock('path') + obj.should_receive(:to_path).and_return(@other_name) + @io.reopen(obj) + end +end + +describe "IO#reopen with a String" do + before :each do + @name = tmp("io_reopen.txt") + @other_name = tmp("io_reopen_other.txt") + @other_io = nil + + rm_r @other_name + end + + after :each do + @io.close unless @io.closed? + @other_io.close if @other_io and not @other_io.closed? + rm_r @name, @other_name + end + + it "opens a path after writing to the original file descriptor" do + @io = new_io @name, "w" + + @io.print "original data" + @io.reopen @other_name + @io.print "new data" + @io.flush + + File.read(@name).should == "original data" + File.read(@other_name).should == "new data" + end + + it "always resets the close-on-exec flag to true on non-STDIO objects" do + @io = new_io @name, "w" + + @io.close_on_exec = true + @io.reopen @other_name + @io.should.close_on_exec? + + @io.close_on_exec = false + @io.reopen @other_name + @io.should.close_on_exec? + end + + it "creates the file if it doesn't exist if the IO is opened in write mode" do + @io = new_io @name, "w" + + @io.reopen(@other_name) + File.should.exist?(@other_name) + end + + it "creates the file if it doesn't exist if the IO is opened in write mode" do + @io = new_io @name, "a" + + @io.reopen(@other_name) + File.should.exist?(@other_name) + end +end + +describe "IO#reopen with a String" do + before :each do + @name = tmp("io_reopen.txt") + @other_name = tmp("io_reopen_other.txt") + + touch @name + rm_r @other_name + end + + after :each do + @io.close + rm_r @name, @other_name + end + + it "raises an Errno::ENOENT if the file does not exist and the IO is not opened in write mode" do + @io = new_io @name, "r" + -> { @io.reopen(@other_name) }.should raise_error(Errno::ENOENT) + end +end + +describe "IO#reopen with an IO at EOF" do + before :each do + @name = tmp("io_reopen.txt") + touch(@name) { |f| f.puts "a line" } + @other_name = tmp("io_reopen_other.txt") + touch(@other_name) do |f| + f.puts "Line 1" + f.puts "Line 2" + end + + @io = new_io @name, "r" + @other_io = new_io @other_name, "r" + @io.read + end + + after :each do + @io.close unless @io.closed? + @other_io.close unless @other_io.closed? + rm_r @name, @other_name + end + + it "resets the EOF status to false" do + @io.eof?.should be_true + @io.reopen @other_io + @io.eof?.should be_false + end +end + +describe "IO#reopen with an IO" do + before :each do + @name = tmp("io_reopen.txt") + @other_name = tmp("io_reopen_other.txt") + touch(@other_name) do |f| + f.puts "Line 1" + f.puts "Line 2" + end + + @io = new_io @name + @other_io = IO.new(new_fd(@other_name, "r"), "r") + end + + after :each do + @io.close unless @io.closed? + @other_io.close unless @other_io.closed? + rm_r @name, @other_name + end + + it "does not call #to_io" do + # Why do we not use #should_not_receive(:to_io) here? Because + # MRI actually changes the class of @io in the call to #reopen + # but does not preserve the existing singleton class of @io. + def @io.to_io; flunk; end + @io.reopen(@other_io).should be_an_instance_of(IO) + end + + it "does not change the object_id" do + obj_id = @io.object_id + @io.reopen @other_io + @io.object_id.should == obj_id + end + + it "reads from the beginning if the other IO has not been read from" do + @io.reopen @other_io + @io.gets.should == "Line 1\n" + end + + it "reads from the current position of the other IO's stream" do + @other_io.gets.should == "Line 1\n" + @io.reopen @other_io + @io.gets.should == "Line 2\n" + end +end + +describe "IO#reopen with an IO" do + before :each do + @name = tmp("io_reopen.txt") + @other_name = tmp("io_reopen_other.txt") + + @io = new_io @name + @other_io = File.open @other_name, "w" + end + + after :each do + @io.close unless @io.closed? + @other_io.close unless @other_io.closed? + rm_r @name, @other_name + end + + it "associates the IO instance with the other IO's stream" do + File.read(@other_name).should == "" + @io.reopen @other_io + @io.print "io data" + @io.flush + File.read(@name).should == "" + File.read(@other_name).should == "io data" + end + + it "always resets the close-on-exec flag to true on non-STDIO objects" do + @other_io.close_on_exec = true + @io.close_on_exec = true + @io.reopen @other_io + @io.should.close_on_exec? + + @other_io.close_on_exec = false + @io.close_on_exec = false + @io.reopen @other_io + @io.should.close_on_exec? + end + + it "may change the class of the instance" do + @io.reopen @other_io + @io.should be_an_instance_of(File) + end + + it "sets path equals to the other IO's path if other IO is File" do + @io.reopen @other_io + @io.path.should == @other_io.path + end +end diff --git a/spec/ruby/core/io/rewind_spec.rb b/spec/ruby/core/io/rewind_spec.rb new file mode 100644 index 0000000000..5579cbd988 --- /dev/null +++ b/spec/ruby/core/io/rewind_spec.rb @@ -0,0 +1,53 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#rewind" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.close unless @io.closed? + end + + it "positions the instance to the beginning of input" do + @io.readline.should == "Voici la ligne une.\n" + @io.readline.should == "Qui è la linea due.\n" + @io.rewind + @io.readline.should == "Voici la ligne une.\n" + end + + it "positions the instance to the beginning of output for write-only IO" do + name = tmp("io_rewind_spec") + io = File.open(name, "w") + io.write("Voici la ligne une.\n") + io.rewind + io.pos.should == 0 + ensure + io.close + rm_r name + end + + it "positions the instance to the beginning of input and clears EOF" do + value = @io.read + @io.rewind + @io.should_not.eof? + value.should == @io.read + end + + it "sets lineno to 0" do + @io.readline.should == "Voici la ligne une.\n" + @io.lineno.should == 1 + @io.rewind + @io.lineno.should == 0 + end + + it "returns 0" do + @io.rewind.should == 0 + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.rewind }.should raise_error(IOError) + end +end diff --git a/spec/ruby/core/io/seek_spec.rb b/spec/ruby/core/io/seek_spec.rb new file mode 100644 index 0000000000..2fa4a73ac9 --- /dev/null +++ b/spec/ruby/core/io/seek_spec.rb @@ -0,0 +1,79 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/pos' + +describe "IO#seek" do + it_behaves_like :io_set_pos, :seek +end + +describe "IO#seek" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.close unless @io.closed? + end + + it "moves the read position relative to the current position with SEEK_CUR" do + -> { @io.seek(-1) }.should raise_error(Errno::EINVAL) + @io.seek(10, IO::SEEK_CUR) + @io.readline.should == "igne une.\n" + @io.seek(-5, IO::SEEK_CUR) + @io.readline.should == "une.\n" + end + + it "moves the read position relative to the start with SEEK_SET" do + @io.seek(1) + @io.pos.should == 1 + @io.rewind + @io.seek(43, IO::SEEK_SET) + @io.readline.should == "Aquà está la lÃnea tres.\n" + @io.seek(5, IO::SEEK_SET) + @io.readline.should == " la ligne une.\n" + end + + it "moves the read position relative to the end with SEEK_END" do + @io.seek(0, IO::SEEK_END) + @io.tell.should == 137 + @io.seek(-25, IO::SEEK_END) + @io.readline.should == "cinco.\n" + end + + it "moves the read position and clears EOF with SEEK_SET" do + value = @io.read + @io.seek(0, IO::SEEK_SET) + @io.should_not.eof? + value.should == @io.read + end + + it "moves the read position and clears EOF with SEEK_CUR" do + value = @io.read + @io.seek(-1, IO::SEEK_CUR) + @io.should_not.eof? + value[-1].should == @io.read[0] + end + + it "moves the read position and clears EOF with SEEK_END" do + value = @io.read + @io.seek(-1, IO::SEEK_END) + @io.should_not.eof? + value[-1].should == @io.read[0] + end + + platform_is :darwin do + it "supports seek offsets greater than 2^32" do + begin + zero = File.open('/dev/zero') + offset = 2**33 + zero.seek(offset, File::SEEK_SET) + pos = zero.pos + + pos.should == offset + ensure + zero.close rescue nil + end + end + end +end diff --git a/spec/ruby/core/io/select_spec.rb b/spec/ruby/core/io/select_spec.rb new file mode 100644 index 0000000000..3893e7620f --- /dev/null +++ b/spec/ruby/core/io/select_spec.rb @@ -0,0 +1,164 @@ +require_relative '../../spec_helper' + +describe "IO.select" do + before :each do + @rd, @wr = IO.pipe + end + + after :each do + @rd.close unless @rd.closed? + @wr.close unless @wr.closed? + end + + it "blocks for duration of timeout and returns nil if there are no objects ready for I/O" do + IO.select([@rd], nil, nil, 0.001).should == nil + end + + it "returns immediately all objects that are ready for I/O when timeout is 0" do + @wr.syswrite("be ready") + IO.pipe do |_, wr| + result = IO.select [@rd], [wr], nil, 0 + result.should == [[@rd], [wr], []] + end + end + + it "returns nil after timeout if there are no objects ready for I/O" do + result = IO.select [@rd], nil, nil, 0 + result.should == nil + end + + it "returns supplied objects when they are ready for I/O" do + main = Thread.current + t = Thread.new { + Thread.pass until main.status == "sleep" + @wr.write "be ready" + } + result = IO.select [@rd], nil, nil, nil + result.should == [[@rd], [], []] + t.join + end + + it "leaves out IO objects for which there is no I/O ready" do + @wr.write "be ready" + platform_is :aix do + # In AIX, when a pipe is readable, select(2) returns the write side + # of the pipe as "readable", even though you cannot actually read + # anything from the write side. + result = IO.select [@wr, @rd], nil, nil, nil + result.should == [[@wr, @rd], [], []] + end + platform_is_not :aix do + # Order matters here. We want to see that @wr doesn't expand the size + # of the returned array, so it must be 1st. + result = IO.select [@wr, @rd], nil, nil, nil + result.should == [[@rd], [], []] + end + end + + it "returns supplied objects correctly when monitoring the same object in different arrays" do + filename = tmp("IO_select_pipe_file") + io = File.open(filename, 'w+') + result = IO.select [io], [io], nil, 0 + result.should == [[io], [io], []] + io.close + rm_r filename + end + + it "returns the pipe read end in read set if the pipe write end is closed concurrently" do + main = Thread.current + t = Thread.new { + Thread.pass until main.stop? + @wr.close + } + IO.select([@rd]).should == [[@rd], [], []] + ensure + t.join + end + + it "invokes to_io on supplied objects that are not IO and returns the supplied objects" do + # make some data available + @wr.write("foobar") + + obj = mock("read_io") + obj.should_receive(:to_io).at_least(1).and_return(@rd) + IO.select([obj]).should == [[obj], [], []] + + IO.pipe do |_, wr| + obj = mock("write_io") + obj.should_receive(:to_io).at_least(1).and_return(wr) + IO.select(nil, [obj]).should == [[], [obj], []] + end + end + + it "raises TypeError if supplied objects are not IO" do + -> { IO.select([Object.new]) }.should raise_error(TypeError) + -> { IO.select(nil, [Object.new]) }.should raise_error(TypeError) + + obj = mock("io") + obj.should_receive(:to_io).any_number_of_times.and_return(nil) + + -> { IO.select([obj]) }.should raise_error(TypeError) + -> { IO.select(nil, [obj]) }.should raise_error(TypeError) + end + + it "raises a TypeError if the specified timeout value is not Numeric" do + -> { IO.select([@rd], nil, nil, Object.new) }.should raise_error(TypeError) + end + + it "raises TypeError if the first three arguments are not Arrays" do + -> { IO.select(Object.new)}.should raise_error(TypeError) + -> { IO.select(nil, Object.new)}.should raise_error(TypeError) + -> { IO.select(nil, nil, Object.new)}.should raise_error(TypeError) + end + + it "raises an ArgumentError when passed a negative timeout" do + -> { IO.select(nil, nil, nil, -5)}.should raise_error(ArgumentError) + end + + describe "returns the available descriptors when the file descriptor" do + it "is in both read and error arrays" do + @wr.write("foobar") + result = IO.select([@rd], nil, [@rd]) + result.should == [[@rd], [], []] + end + + it "is in both write and error arrays" do + result = IO.select(nil, [@wr], [@wr]) + result.should == [[], [@wr], []] + end + + it "is in both read and write arrays" do + filename = tmp("IO_select_read_write_file") + w = File.open(filename, 'w+') + begin + IO.select([w], [w], []).should == [[w], [w], []] + ensure + w.close + rm_r filename + end + + IO.select([@wr], [@wr], []).should == [[], [@wr], []] + + @wr.write("foobar") + # CRuby on macOS returns [[@rd], [@rd], []], weird but we accept it here, probably only for pipe read-end + [ + [[@rd], [], []], + [[@rd], [@rd], []] + ].should.include? IO.select([@rd], [@rd], []) + end + end +end + +describe "IO.select when passed nil for timeout" do + it "sleeps forever and sets the thread status to 'sleep'" do + t = Thread.new do + IO.select(nil, nil, nil, nil) + end + + Thread.pass while t.status && t.status != "sleep" + t.join unless t.status + t.status.should == "sleep" + t.kill + t.join + end +end diff --git a/spec/ruby/core/io/set_encoding_by_bom_spec.rb b/spec/ruby/core/io/set_encoding_by_bom_spec.rb new file mode 100644 index 0000000000..30d5ce5a5a --- /dev/null +++ b/spec/ruby/core/io/set_encoding_by_bom_spec.rb @@ -0,0 +1,262 @@ +require_relative '../../spec_helper' + +describe "IO#set_encoding_by_bom" do + before :each do + @name = tmp('io_set_encoding_by_bom.txt') + touch(@name) + @io = new_io(@name, 'rb') + end + + after :each do + @io.close unless @io.closed? + rm_r @name + end + + it "returns nil if not readable" do + not_readable_io = new_io(@name, 'wb') + + not_readable_io.set_encoding_by_bom.should be_nil + not_readable_io.external_encoding.should == Encoding::ASCII_8BIT + ensure + not_readable_io.close + end + + it "returns the result encoding if found BOM UTF-8 sequence" do + File.binwrite(@name, "\u{FEFF}") + + @io.set_encoding_by_bom.should == Encoding::UTF_8 + @io.external_encoding.should == Encoding::UTF_8 + @io.read.b.should == "".b + @io.rewind + @io.set_encoding(Encoding::ASCII_8BIT) + + File.binwrite(@name, "\u{FEFF}abc") + + @io.set_encoding_by_bom.should == Encoding::UTF_8 + @io.external_encoding.should == Encoding::UTF_8 + @io.read.b.should == "abc".b + end + + it "returns the result encoding if found BOM UTF_16LE sequence" do + File.binwrite(@name, "\xFF\xFE") + + @io.set_encoding_by_bom.should == Encoding::UTF_16LE + @io.external_encoding.should == Encoding::UTF_16LE + @io.read.b.should == "".b + @io.rewind + @io.set_encoding(Encoding::ASCII_8BIT) + + File.binwrite(@name, "\xFF\xFEabc") + + @io.set_encoding_by_bom.should == Encoding::UTF_16LE + @io.external_encoding.should == Encoding::UTF_16LE + @io.read.b.should == "abc".b + end + + it "returns the result encoding if found BOM UTF_16BE sequence" do + File.binwrite(@name, "\xFE\xFF") + + @io.set_encoding_by_bom.should == Encoding::UTF_16BE + @io.external_encoding.should == Encoding::UTF_16BE + @io.read.b.should == "".b + @io.rewind + @io.set_encoding(Encoding::ASCII_8BIT) + + File.binwrite(@name, "\xFE\xFFabcd") + + @io.set_encoding_by_bom.should == Encoding::UTF_16BE + @io.external_encoding.should == Encoding::UTF_16BE + @io.read.b.should == "abcd".b + end + + it "returns the result encoding if found BOM UTF_32LE sequence" do + File.binwrite(@name, "\xFF\xFE\x00\x00") + + @io.set_encoding_by_bom.should == Encoding::UTF_32LE + @io.external_encoding.should == Encoding::UTF_32LE + @io.read.b.should == "".b + @io.rewind + @io.set_encoding(Encoding::ASCII_8BIT) + + File.binwrite(@name, "\xFF\xFE\x00\x00abc") + + @io.set_encoding_by_bom.should == Encoding::UTF_32LE + @io.external_encoding.should == Encoding::UTF_32LE + @io.read.b.should == "abc".b + end + + it "returns the result encoding if found BOM UTF_32BE sequence" do + File.binwrite(@name, "\x00\x00\xFE\xFF") + + @io.set_encoding_by_bom.should == Encoding::UTF_32BE + @io.external_encoding.should == Encoding::UTF_32BE + @io.read.b.should == "".b + @io.rewind + @io.set_encoding(Encoding::ASCII_8BIT) + + File.binwrite(@name, "\x00\x00\xFE\xFFabcd") + + @io.set_encoding_by_bom.should == Encoding::UTF_32BE + @io.external_encoding.should == Encoding::UTF_32BE + @io.read.b.should == "abcd".b + end + + it "returns nil if io is empty" do + @io.set_encoding_by_bom.should be_nil + @io.external_encoding.should == Encoding::ASCII_8BIT + end + + it "returns nil if UTF-8 BOM sequence is incomplete" do + File.write(@name, "\xEF") + + @io.set_encoding_by_bom.should == nil + @io.external_encoding.should == Encoding::ASCII_8BIT + @io.read.b.should == "\xEF".b + @io.rewind + + File.write(@name, "\xEFa") + + @io.set_encoding_by_bom.should == nil + @io.external_encoding.should == Encoding::ASCII_8BIT + @io.read.b.should == "\xEFa".b + @io.rewind + + File.write(@name, "\xEF\xBB") + + @io.set_encoding_by_bom.should == nil + @io.external_encoding.should == Encoding::ASCII_8BIT + @io.read.b.should == "\xEF\xBB".b + @io.rewind + + File.write(@name, "\xEF\xBBa") + + @io.set_encoding_by_bom.should == nil + @io.external_encoding.should == Encoding::ASCII_8BIT + @io.read.b.should == "\xEF\xBBa".b + end + + it "returns nil if UTF-16BE BOM sequence is incomplete" do + File.write(@name, "\xFE") + + @io.set_encoding_by_bom.should == nil + @io.external_encoding.should == Encoding::ASCII_8BIT + @io.read.b.should == "\xFE".b + @io.rewind + + File.write(@name, "\xFEa") + + @io.set_encoding_by_bom.should == nil + @io.external_encoding.should == Encoding::ASCII_8BIT + @io.read.b.should == "\xFEa".b + end + + it "returns nil if UTF-16LE/UTF-32LE BOM sequence is incomplete" do + File.write(@name, "\xFF") + + @io.set_encoding_by_bom.should == nil + @io.external_encoding.should == Encoding::ASCII_8BIT + @io.read.b.should == "\xFF".b + @io.rewind + + File.write(@name, "\xFFa") + + @io.set_encoding_by_bom.should == nil + @io.external_encoding.should == Encoding::ASCII_8BIT + @io.read.b.should == "\xFFa".b + end + + it "returns UTF-16LE if UTF-32LE BOM sequence is incomplete" do + File.write(@name, "\xFF\xFE") + + @io.set_encoding_by_bom.should == Encoding::UTF_16LE + @io.external_encoding.should == Encoding::UTF_16LE + @io.read.b.should == "".b + @io.rewind + @io.set_encoding(Encoding::ASCII_8BIT) + + File.write(@name, "\xFF\xFE\x00") + + @io.set_encoding_by_bom.should == Encoding::UTF_16LE + @io.external_encoding.should == Encoding::UTF_16LE + @io.read.b.should == "\x00".b + @io.rewind + @io.set_encoding(Encoding::ASCII_8BIT) + + File.write(@name, "\xFF\xFE\x00a") + + @io.set_encoding_by_bom.should == Encoding::UTF_16LE + @io.external_encoding.should == Encoding::UTF_16LE + @io.read.b.should == "\x00a".b + end + + it "returns nil if UTF-32BE BOM sequence is incomplete" do + File.write(@name, "\x00") + + @io.set_encoding_by_bom.should == nil + @io.external_encoding.should == Encoding::ASCII_8BIT + @io.read.b.should == "\x00".b + @io.rewind + + File.write(@name, "\x00a") + + @io.set_encoding_by_bom.should == nil + @io.external_encoding.should == Encoding::ASCII_8BIT + @io.read.b.should == "\x00a".b + @io.rewind + + File.write(@name, "\x00\x00") + + @io.set_encoding_by_bom.should == nil + @io.external_encoding.should == Encoding::ASCII_8BIT + @io.read.b.should == "\x00\x00".b + @io.rewind + + File.write(@name, "\x00\x00a") + + @io.set_encoding_by_bom.should == nil + @io.external_encoding.should == Encoding::ASCII_8BIT + @io.read.b.should == "\x00\x00a".b + @io.rewind + + File.write(@name, "\x00\x00\xFE") + + @io.set_encoding_by_bom.should == nil + @io.external_encoding.should == Encoding::ASCII_8BIT + @io.read.b.should == "\x00\x00\xFE".b + @io.rewind + + File.write(@name, "\x00\x00\xFEa") + + @io.set_encoding_by_bom.should == nil + @io.external_encoding.should == Encoding::ASCII_8BIT + @io.read.b.should == "\x00\x00\xFEa".b + end + + it "returns nil if found BOM sequence not provided" do + File.write(@name, "abc") + + @io.set_encoding_by_bom.should == nil + @io.external_encoding.should == Encoding::ASCII_8BIT + @io.read(3).should == "abc".b + end + + it 'returns exception if io not in binary mode' do + not_binary_io = new_io(@name, 'r') + + -> { not_binary_io.set_encoding_by_bom }.should raise_error(ArgumentError, 'ASCII incompatible encoding needs binmode') + ensure + not_binary_io.close + end + + it 'returns exception if encoding already set' do + @io.set_encoding("utf-8") + + -> { @io.set_encoding_by_bom }.should raise_error(ArgumentError, 'encoding is set to UTF-8 already') + end + + it 'returns exception if encoding conversion is already set' do + @io.set_encoding(Encoding::UTF_8, Encoding::UTF_16BE) + + -> { @io.set_encoding_by_bom }.should raise_error(ArgumentError, 'encoding conversion is set') + end +end diff --git a/spec/ruby/core/io/set_encoding_spec.rb b/spec/ruby/core/io/set_encoding_spec.rb new file mode 100644 index 0000000000..22d9017635 --- /dev/null +++ b/spec/ruby/core/io/set_encoding_spec.rb @@ -0,0 +1,238 @@ +require_relative '../../spec_helper' + +describe :io_set_encoding_write, shared: true do + it "sets the encodings to nil when they were set previously" do + @io = new_io @name, "#{@object}:ibm437:ibm866" + @io.set_encoding nil, nil + + @io.external_encoding.should be_nil + @io.internal_encoding.should be_nil + end + + it "sets the encodings to nil when the IO is built with no explicit encoding" do + @io = new_io @name, @object + + # Checking our assumptions first + @io.external_encoding.should be_nil + @io.internal_encoding.should be_nil + + @io.set_encoding nil, nil + + @io.external_encoding.should be_nil + @io.internal_encoding.should be_nil + end + + it "prevents the encodings from changing when Encoding defaults are changed" do + @io = new_io @name, "#{@object}:utf-8:us-ascii" + @io.set_encoding nil, nil + + Encoding.default_external = Encoding::IBM437 + Encoding.default_internal = Encoding::IBM866 + + @io.external_encoding.should be_nil + @io.internal_encoding.should be_nil + end + + it "sets the encodings to the current Encoding defaults" do + @io = new_io @name, @object + + Encoding.default_external = Encoding::IBM437 + Encoding.default_internal = Encoding::IBM866 + + @io.set_encoding nil, nil + + @io.external_encoding.should == Encoding::IBM437 + @io.internal_encoding.should == Encoding::IBM866 + end +end + +describe "IO#set_encoding when passed nil, nil" do + before :each do + @external = Encoding.default_external + @internal = Encoding.default_internal + + # The defaults + Encoding.default_external = Encoding::UTF_8 + Encoding.default_internal = nil + + @name = tmp('io_set_encoding.txt') + touch(@name) + end + + after :each do + Encoding.default_external = @external + Encoding.default_internal = @internal + + @io.close if @io and not @io.closed? + rm_r @name + end + + describe "with 'r' mode" do + it "sets the encodings to the current Encoding defaults" do + @io = new_io @name, "r" + + Encoding.default_external = Encoding::IBM437 + Encoding.default_internal = Encoding::IBM866 + + @io.set_encoding nil, nil + @io.external_encoding.should equal(Encoding::IBM437) + @io.internal_encoding.should equal(Encoding::IBM866) + end + + it "prevents the #internal_encoding from changing when Encoding.default_internal is changed" do + @io = new_io @name, "r" + @io.set_encoding nil, nil + + Encoding.default_internal = Encoding::IBM437 + + @io.internal_encoding.should be_nil + end + + it "allows the #external_encoding to change when Encoding.default_external is changed" do + @io = new_io @name, "r" + @io.set_encoding nil, nil + + Encoding.default_external = Encoding::IBM437 + + @io.external_encoding.should equal(Encoding::IBM437) + end + end + + describe "with 'rb' mode" do + it "returns Encoding.default_external" do + @io = new_io @name, "rb" + @io.external_encoding.should equal(Encoding::BINARY) + + @io.set_encoding nil, nil + @io.external_encoding.should equal(Encoding.default_external) + end + end + + describe "with 'r+' mode" do + it_behaves_like :io_set_encoding_write, nil, "r+" + end + + describe "with 'w' mode" do + it_behaves_like :io_set_encoding_write, nil, "w" + end + + describe "with 'w+' mode" do + it_behaves_like :io_set_encoding_write, nil, "w+" + end + + describe "with 'a' mode" do + it_behaves_like :io_set_encoding_write, nil, "a" + end + + describe "with 'a+' mode" do + it_behaves_like :io_set_encoding_write, nil, "a+" + end + + describe "with standard IOs" do + it "correctly resets them" do + STDOUT.external_encoding.should == nil + STDOUT.internal_encoding.should == nil + + begin + STDOUT.set_encoding(Encoding::US_ASCII, Encoding::ISO_8859_1) + ensure + STDOUT.set_encoding(nil, nil) + end + + STDOUT.external_encoding.should == nil + STDOUT.internal_encoding.should == nil + end + end +end + +describe "IO#set_encoding" do + before :each do + @name = tmp('io_set_encoding.txt') + touch(@name) + @io = new_io @name + end + + after :each do + @io.close unless @io.closed? + rm_r @name + end + + it "returns self" do + @io.set_encoding(Encoding::UTF_8).should equal(@io) + end + + it "sets the external encoding when passed an Encoding argument" do + @io.set_encoding(Encoding::UTF_8) + @io.external_encoding.should == Encoding::UTF_8 + @io.internal_encoding.should be_nil + end + + it "sets the external and internal encoding when passed two Encoding arguments" do + @io.set_encoding(Encoding::UTF_8, Encoding::UTF_16BE) + @io.external_encoding.should == Encoding::UTF_8 + @io.internal_encoding.should == Encoding::UTF_16BE + end + + it "sets the external encoding when passed the name of an Encoding" do + @io.set_encoding("utf-8") + @io.external_encoding.should == Encoding::UTF_8 + @io.internal_encoding.should be_nil + end + + it "ignores the internal encoding if the same as external when passed Encoding objects" do + @io.set_encoding(Encoding::UTF_8, Encoding::UTF_8) + @io.external_encoding.should == Encoding::UTF_8 + @io.internal_encoding.should be_nil + end + + it "ignores the internal encoding if the same as external when passed encoding names separated by ':'" do + @io.set_encoding("utf-8:utf-8") + @io.external_encoding.should == Encoding::UTF_8 + @io.internal_encoding.should be_nil + end + + it "sets the external and internal encoding when passed the names of Encodings separated by ':'" do + @io.set_encoding("utf-8:utf-16be") + @io.external_encoding.should == Encoding::UTF_8 + @io.internal_encoding.should == Encoding::UTF_16BE + end + + it "sets the external and internal encoding when passed two String arguments" do + @io.set_encoding("utf-8", "utf-16be") + @io.external_encoding.should == Encoding::UTF_8 + @io.internal_encoding.should == Encoding::UTF_16BE + end + + it "calls #to_str to convert an abject to a String" do + obj = mock("io_set_encoding") + obj.should_receive(:to_str).and_return("utf-8:utf-16be") + @io.set_encoding(obj) + @io.external_encoding.should == Encoding::UTF_8 + @io.internal_encoding.should == Encoding::UTF_16BE + end + + it "calls #to_str to convert the second argument to a String" do + obj = mock("io_set_encoding") + obj.should_receive(:to_str).at_least(1).times.and_return("utf-16be") + @io.set_encoding(Encoding::UTF_8, obj) + @io.external_encoding.should == Encoding::UTF_8 + @io.internal_encoding.should == Encoding::UTF_16BE + end + + it "saves encoding options passed as a hash in the last argument" do + File.write(@name, "\xff") + io = File.open(@name) + io.set_encoding(Encoding::EUC_JP, Encoding::SHIFT_JIS, invalid: :replace, replace: ".") + io.read.should == "." + ensure + io.close + end + + it "raises ArgumentError when no arguments are given" do + -> { @io.set_encoding() }.should raise_error(ArgumentError) + end + + it "raises ArgumentError when too many arguments are given" do + -> { @io.set_encoding(1, 2, 3) }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/io/shared/binwrite.rb b/spec/ruby/core/io/shared/binwrite.rb new file mode 100644 index 0000000000..e51093329b --- /dev/null +++ b/spec/ruby/core/io/shared/binwrite.rb @@ -0,0 +1,91 @@ +require_relative '../fixtures/classes' + +describe :io_binwrite, shared: true do + before :each do + @filename = tmp("IO_binwrite_file") + $$.to_s + File.open(@filename, "w") do |file| + file << "012345678901234567890123456789" + end + end + + after :each do + rm_r @filename + end + + it "coerces the argument to a string using to_s" do + (obj = mock('test')).should_receive(:to_s).and_return('a string') + IO.send(@method, @filename, obj) + end + + it "returns the number of bytes written" do + IO.send(@method, @filename, "abcde").should == 5 + end + + it "accepts options as a keyword argument" do + IO.send(@method, @filename, "hi", 0, flags: File::CREAT).should == 2 + + -> { + IO.send(@method, @filename, "hi", 0, {flags: File::CREAT}) + }.should raise_error(ArgumentError, "wrong number of arguments (given 4, expected 2..3)") + end + + it "creates a file if missing" do + fn = @filename + "xxx" + begin + File.should_not.exist?(fn) + IO.send(@method, fn, "test") + File.should.exist?(fn) + ensure + rm_r fn + end + end + + it "creates file if missing even if offset given" do + fn = @filename + "xxx" + begin + File.should_not.exist?(fn) + IO.send(@method, fn, "test", 0) + File.should.exist?(fn) + ensure + rm_r fn + end + end + + it "truncates the file and writes the given string" do + IO.send(@method, @filename, "hello, world!") + File.read(@filename).should == "hello, world!" + end + + it "doesn't truncate the file and writes the given string if an offset is given" do + IO.send(@method, @filename, "hello, world!", 0) + File.read(@filename).should == "hello, world!34567890123456789" + IO.send(@method, @filename, "hello, world!", 20) + File.read(@filename).should == "hello, world!3456789hello, world!" + end + + it "doesn't truncate and writes at the given offset after passing empty opts" do + IO.send(@method, @filename, "hello world!", 1, **{}) + File.read(@filename).should == "0hello world!34567890123456789" + end + + it "accepts a :mode option" do + IO.send(@method, @filename, "hello, world!", mode: 'a') + File.read(@filename).should == "012345678901234567890123456789hello, world!" + IO.send(@method, @filename, "foo", 2, mode: 'w') + File.read(@filename).should == "\0\0foo" + end + + it "accepts a :flags option without :mode one" do + IO.send(@method, @filename, "hello, world!", flags: File::CREAT) + File.read(@filename).should == "hello, world!" + end + + it "raises an error if readonly mode is specified" do + -> { IO.send(@method, @filename, "abcde", mode: "r") }.should raise_error(IOError) + end + + it "truncates if empty :opts provided and offset skipped" do + IO.send(@method, @filename, "hello, world!", **{}) + File.read(@filename).should == "hello, world!" + end +end diff --git a/spec/ruby/core/io/shared/chars.rb b/spec/ruby/core/io/shared/chars.rb new file mode 100644 index 0000000000..266566f221 --- /dev/null +++ b/spec/ruby/core/io/shared/chars.rb @@ -0,0 +1,73 @@ +# -*- encoding: utf-8 -*- +describe :io_chars, shared: true do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + ScratchPad.record [] + end + + after :each do + @io.close unless @io.closed? + end + + it "yields each character" do + @io.readline.should == "Voici la ligne une.\n" + + count = 0 + @io.send(@method) do |c| + ScratchPad << c + break if 4 < count += 1 + end + + ScratchPad.recorded.should == ["Q", "u", "i", " ", "è"] + end + + describe "when no block is given" do + it "returns an Enumerator" do + enum = @io.send(@method) + enum.should be_an_instance_of(Enumerator) + enum.first(5).should == ["V", "o", "i", "c", "i"] + end + + describe "returned Enumerator" do + describe "size" do + it "should return nil" do + @io.send(@method).size.should == nil + end + end + end + end + + it "returns itself" do + @io.send(@method) { |c| }.should equal(@io) + end + + it "returns an enumerator for a closed stream" do + IOSpecs.closed_io.send(@method).should be_an_instance_of(Enumerator) + end + + it "raises an IOError when an enumerator created on a closed stream is accessed" do + -> { IOSpecs.closed_io.send(@method).first }.should raise_error(IOError) + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.send(@method) {} }.should raise_error(IOError) + end +end + +describe :io_chars_empty, shared: true do + before :each do + @name = tmp("io_each_char") + @io = new_io @name, "w+:utf-8" + ScratchPad.record [] + end + + after :each do + @io.close unless @io.closed? + rm_r @name + end + + it "does not yield any characters on an empty stream" do + @io.send(@method) { |c| ScratchPad << c } + ScratchPad.recorded.should == [] + end +end diff --git a/spec/ruby/core/io/shared/codepoints.rb b/spec/ruby/core/io/shared/codepoints.rb new file mode 100644 index 0000000000..6872846c1a --- /dev/null +++ b/spec/ruby/core/io/shared/codepoints.rb @@ -0,0 +1,54 @@ +# -*- encoding: utf-8 -*- +require_relative '../fixtures/classes' + +describe :io_codepoints, shared: true do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + @enum = @io.send(@method) + end + + after :each do + @io.close + end + + describe "when no block is given" do + it "returns an Enumerator" do + @enum.should be_an_instance_of(Enumerator) + end + + describe "returned Enumerator" do + describe "size" do + it "should return nil" do + @enum.size.should == nil + end + end + end + end + + it "yields each codepoint" do + @enum.first(25).should == [ + 86, 111, 105, 99, 105, 32, 108, 97, 32, 108, 105, 103, 110, + 101, 32, 117, 110, 101, 46, 10, 81, 117, 105, 32, 232 + ] + end + + it "yields each codepoint starting from the current position" do + @io.pos = 130 + @enum.to_a.should == [101, 32, 115, 105, 120, 46, 10] + end + + it "raises an error if reading invalid sequence" do + @io.pos = 60 # inside of a multibyte sequence + -> { @enum.first }.should raise_error(ArgumentError) + end + + it "does not change $_" do + $_ = "test" + @enum.to_a + $_.should == "test" + end + + it "raises an IOError when self is not readable" do + -> { IOSpecs.closed_io.send(@method).to_a }.should raise_error(IOError) + end +end diff --git a/spec/ruby/core/io/shared/each.rb b/spec/ruby/core/io/shared/each.rb new file mode 100644 index 0000000000..0747f31b8a --- /dev/null +++ b/spec/ruby/core/io/shared/each.rb @@ -0,0 +1,251 @@ +# -*- encoding: utf-8 -*- +require_relative '../fixtures/classes' + +describe :io_each, shared: true do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + ScratchPad.record [] + end + + after :each do + @io.close if @io + end + + describe "with no separator" do + it "yields each line to the passed block" do + @io.send(@method) { |s| ScratchPad << s } + ScratchPad.recorded.should == IOSpecs.lines + end + + it "yields each line starting from the current position" do + @io.pos = 41 + @io.send(@method) { |s| ScratchPad << s } + ScratchPad.recorded.should == IOSpecs.lines[2..-1] + end + + it "returns self" do + @io.send(@method) { |l| l }.should equal(@io) + end + + it "does not change $_" do + $_ = "test" + @io.send(@method) { |s| s } + $_.should == "test" + end + + it "raises an IOError when self is not readable" do + -> { IOSpecs.closed_io.send(@method) {} }.should raise_error(IOError) + end + + it "makes line count accessible via lineno" do + @io.send(@method) { ScratchPad << @io.lineno } + ScratchPad.recorded.should == [ 1,2,3,4,5,6,7,8,9 ] + end + + it "makes line count accessible via $." do + @io.send(@method) { ScratchPad << $. } + ScratchPad.recorded.should == [ 1,2,3,4,5,6,7,8,9 ] + end + + describe "when no block is given" do + it "returns an Enumerator" do + enum = @io.send(@method) + enum.should be_an_instance_of(Enumerator) + + enum.each { |l| ScratchPad << l } + ScratchPad.recorded.should == IOSpecs.lines + end + + describe "returned Enumerator" do + describe "size" do + it "should return nil" do + @io.send(@method).size.should == nil + end + end + end + end + end + + describe "with limit" do + describe "when limit is 0" do + it "raises an ArgumentError" do + # must pass block so Enumerator is evaluated and raises + -> { @io.send(@method, 0){} }.should raise_error(ArgumentError) + end + end + + it "does not accept Integers that don't fit in a C off_t" do + -> { @io.send(@method, 2**128){} }.should raise_error(RangeError) + end + end + + describe "when passed a String containing one space as a separator" do + it "uses the passed argument as the line separator" do + @io.send(@method, " ") { |s| ScratchPad << s } + ScratchPad.recorded.should == IOSpecs.lines_space_separator + end + + it "does not change $_" do + $_ = "test" + @io.send(@method, " ") { |s| } + $_.should == "test" + end + + it "tries to convert the passed separator to a String using #to_str" do + obj = mock("to_str") + obj.stub!(:to_str).and_return(" ") + + @io.send(@method, obj) { |l| ScratchPad << l } + ScratchPad.recorded.should == IOSpecs.lines_space_separator + end + end + + describe "when passed nil as a separator" do + it "yields self's content starting from the current position when the passed separator is nil" do + @io.pos = 100 + @io.send(@method, nil) { |s| ScratchPad << s } + ScratchPad.recorded.should == ["qui a linha cinco.\nHere is line six.\n"] + end + end + + describe "when passed an empty String as a separator" do + it "yields each paragraph" do + @io.send(@method, "") { |s| ScratchPad << s } + ScratchPad.recorded.should == IOSpecs.paragraphs + end + + it "discards leading newlines" do + @io.readline + @io.readline + @io.send(@method, "") { |s| ScratchPad << s } + ScratchPad.recorded.should == IOSpecs.paragraphs[1..-1] + end + end + + describe "with both separator and limit" do + describe "when no block is given" do + it "returns an Enumerator" do + enum = @io.send(@method, nil, 1024) + enum.should be_an_instance_of(Enumerator) + + enum.each { |l| ScratchPad << l } + ScratchPad.recorded.should == [IOSpecs.lines.join] + end + + describe "returned Enumerator" do + describe "size" do + it "should return nil" do + @io.send(@method, nil, 1024).size.should == nil + end + end + end + end + + describe "when a block is given" do + it "accepts an empty block" do + @io.send(@method, nil, 1024) {}.should equal(@io) + end + + describe "when passed nil as a separator" do + it "yields self's content starting from the current position when the passed separator is nil" do + @io.pos = 100 + @io.send(@method, nil, 1024) { |s| ScratchPad << s } + ScratchPad.recorded.should == ["qui a linha cinco.\nHere is line six.\n"] + end + end + + describe "when passed an empty String as a separator" do + it "yields each paragraph" do + @io.send(@method, "", 1024) { |s| ScratchPad << s } + ScratchPad.recorded.should == IOSpecs.paragraphs + end + + it "discards leading newlines" do + @io.readline + @io.readline + @io.send(@method, "", 1024) { |s| ScratchPad << s } + ScratchPad.recorded.should == IOSpecs.paragraphs[1..-1] + end + end + end + end + + describe "when passed chomp" do + it "yields each line without trailing newline characters to the passed block" do + @io.send(@method, chomp: true) { |s| ScratchPad << s } + ScratchPad.recorded.should == IOSpecs.lines_without_newline_characters + end + + it "raises exception when options passed as Hash" do + -> { + @io.send(@method, { chomp: true }) { |s| } + }.should raise_error(TypeError) + + -> { + @io.send(@method, "\n", 1, { chomp: true }) { |s| } + }.should raise_error(ArgumentError, "wrong number of arguments (given 3, expected 0..2)") + end + end + + describe "when passed chomp and a separator" do + it "yields each line without separator to the passed block" do + @io.send(@method, " ", chomp: true) { |s| ScratchPad << s } + ScratchPad.recorded.should == IOSpecs.lines_space_separator_without_trailing_spaces + end + end + + describe "when passed chomp and empty line as a separator" do + it "yields each paragraph without trailing new line characters" do + @io.send(@method, "", 1024, chomp: true) { |s| ScratchPad << s } + ScratchPad.recorded.should == IOSpecs.paragraphs_without_trailing_new_line_characters + end + end + + describe "when passed chomp and nil as a separator" do + it "yields self's content" do + @io.pos = 100 + @io.send(@method, nil, chomp: true) { |s| ScratchPad << s } + ScratchPad.recorded.should == ["qui a linha cinco.\nHere is line six.\n"] + end + end + + describe "when passed chomp, nil as a separator, and a limit" do + it "yields each line of limit size without truncating trailing new line character" do + # 43 - is a size of the 1st paragraph in the file + @io.send(@method, nil, 43, chomp: true) { |s| ScratchPad << s } + + ScratchPad.recorded.should == [ + "Voici la ligne une.\nQui è la linea due.\n\n\n", + "Aquà está la lÃnea tres.\n" + "Hier ist Zeile ", + "vier.\n\nEstá aqui a linha cinco.\nHere is li", + "ne six.\n" + ] + end + end + + describe "when passed too many arguments" do + it "raises ArgumentError" do + -> { + @io.send(@method, "", 1, "excess argument", chomp: true) {} + }.should raise_error(ArgumentError) + end + end +end + +describe :io_each_default_separator, shared: true do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + ScratchPad.record [] + suppress_warning {@sep, $/ = $/, " "} + end + + after :each do + @io.close if @io + suppress_warning {$/ = @sep} + end + + it "uses $/ as the default line separator" do + @io.send(@method) { |s| ScratchPad << s } + ScratchPad.recorded.should == IOSpecs.lines_space_separator + end +end diff --git a/spec/ruby/core/io/shared/gets_ascii.rb b/spec/ruby/core/io/shared/gets_ascii.rb new file mode 100644 index 0000000000..2bd5470d99 --- /dev/null +++ b/spec/ruby/core/io/shared/gets_ascii.rb @@ -0,0 +1,19 @@ +# encoding: binary +describe :io_gets_ascii, shared: true do + describe "with ASCII separator" do + before :each do + @name = tmp("gets_specs.txt") + touch(@name, "wb") { |f| f.print "this is a test\xFFtesty\ntestier" } + + File.open(@name, "rb") { |f| @data = f.send(@method, "\xFF") } + end + + after :each do + rm_r @name + end + + it "returns the separator's character representation" do + @data.should == "this is a test\xFF" + end + end +end diff --git a/spec/ruby/core/io/shared/new.rb b/spec/ruby/core/io/shared/new.rb new file mode 100644 index 0000000000..e84133493c --- /dev/null +++ b/spec/ruby/core/io/shared/new.rb @@ -0,0 +1,413 @@ +require_relative '../fixtures/classes' + +# NOTE: should be synchronized with library/stringio/initialize_spec.rb + +# This group of specs may ONLY contain specs that do successfully create +# an IO instance from the file descriptor returned by #new_fd helper. +describe :io_new, shared: true do + before :each do + @name = tmp("io_new.txt") + @fd = new_fd @name + @io = nil + end + + after :each do + if @io + @io.close + elsif @fd + IO.new(@fd, "w").close + end + rm_r @name + end + + it "creates an IO instance from an Integer argument" do + @io = IO.send(@method, @fd, "w") + @io.should be_an_instance_of(IO) + end + + it "creates an IO instance when STDOUT is closed" do + suppress_warning do + stdout = STDOUT + stdout_file = tmp("stdout.txt") + + begin + @io = IO.send(@method, @fd, "w") + @io.should be_an_instance_of(IO) + ensure + STDOUT = stdout + rm_r stdout_file + end + end + end + + it "creates an IO instance when STDERR is closed" do + suppress_warning do + stderr = STDERR + stderr_file = tmp("stderr.txt") + STDERR = new_io stderr_file + STDERR.close + + begin + @io = IO.send(@method, @fd, "w") + @io.should be_an_instance_of(IO) + ensure + STDERR = stderr + rm_r stderr_file + end + end + end + + it "calls #to_int on an object to convert to an Integer" do + obj = mock("file descriptor") + obj.should_receive(:to_int).and_return(@fd) + @io = IO.send(@method, obj, "w") + @io.should be_an_instance_of(IO) + end + + it "accepts options as keyword arguments" do + @io = IO.send(@method, @fd, "w", flags: File::CREAT) + @io.write("foo").should == 3 + + -> { + IO.send(@method, @fd, "w", {flags: File::CREAT}) + }.should raise_error(ArgumentError, "wrong number of arguments (given 3, expected 1..2)") + end + + it "accepts a :mode option" do + @io = IO.send(@method, @fd, mode: "w") + @io.write("foo").should == 3 + end + + it "accepts a mode argument set to nil with a valid :mode option" do + @io = IO.send(@method, @fd, nil, mode: "w") + @io.write("foo").should == 3 + end + + it "accepts a mode argument with a :mode option set to nil" do + @io = IO.send(@method, @fd, "w", mode: nil) + @io.write("foo").should == 3 + end + + it "uses the external encoding specified in the mode argument" do + @io = IO.send(@method, @fd, 'w:utf-8') + @io.external_encoding.to_s.should == 'UTF-8' + end + + it "uses the external and the internal encoding specified in the mode argument" do + @io = IO.send(@method, @fd, 'w:utf-8:ISO-8859-1') + @io.external_encoding.to_s.should == 'UTF-8' + @io.internal_encoding.to_s.should == 'ISO-8859-1' + end + + it "uses the external encoding specified via the :external_encoding option" do + @io = IO.send(@method, @fd, 'w', external_encoding: 'utf-8') + @io.external_encoding.to_s.should == 'UTF-8' + end + + it "uses the internal encoding specified via the :internal_encoding option" do + @io = IO.send(@method, @fd, 'w', internal_encoding: 'ibm866') + @io.internal_encoding.to_s.should == 'IBM866' + end + + it "uses the colon-separated encodings specified via the :encoding option" do + @io = IO.send(@method, @fd, 'w', encoding: 'utf-8:ISO-8859-1') + @io.external_encoding.to_s.should == 'UTF-8' + @io.internal_encoding.to_s.should == 'ISO-8859-1' + end + + it "uses the :encoding option as the external encoding when only one is given" do + @io = IO.send(@method, @fd, 'w', encoding: 'ISO-8859-1') + @io.external_encoding.to_s.should == 'ISO-8859-1' + end + + it "uses the :encoding options as the external encoding when it's an Encoding object" do + @io = IO.send(@method, @fd, 'w', encoding: Encoding::ISO_8859_1) + @io.external_encoding.should == Encoding::ISO_8859_1 + end + + it "ignores the :encoding option when the :external_encoding option is present" do + -> { + @io = IO.send(@method, @fd, 'w', external_encoding: 'utf-8', encoding: 'iso-8859-1:iso-8859-1') + }.should complain(/Ignoring encoding parameter/) + @io.external_encoding.to_s.should == 'UTF-8' + end + + it "ignores the :encoding option when the :internal_encoding option is present" do + -> { + @io = IO.send(@method, @fd, 'w', internal_encoding: 'ibm866', encoding: 'iso-8859-1:iso-8859-1') + }.should complain(/Ignoring encoding parameter/) + @io.internal_encoding.to_s.should == 'IBM866' + end + + it "uses the encoding specified via the :mode option hash" do + @io = IO.send(@method, @fd, mode: 'w:utf-8:ISO-8859-1') + @io.external_encoding.to_s.should == 'UTF-8' + @io.internal_encoding.to_s.should == 'ISO-8859-1' + end + + it "ignores the :internal_encoding option when the same as the external encoding" do + @io = IO.send(@method, @fd, 'w', external_encoding: 'utf-8', internal_encoding: 'utf-8') + @io.external_encoding.to_s.should == 'UTF-8' + @io.internal_encoding.to_s.should == '' + end + + it "sets internal encoding to nil when passed '-'" do + @io = IO.send(@method, @fd, 'w', external_encoding: 'utf-8', internal_encoding: '-') + @io.external_encoding.to_s.should == 'UTF-8' + @io.internal_encoding.to_s.should == '' + end + + it "sets binmode from mode string" do + @io = IO.send(@method, @fd, 'wb') + @io.should.binmode? + end + + it "does not set binmode without being asked" do + @io = IO.send(@method, @fd, 'w') + @io.should_not.binmode? + end + + it "sets binmode from :binmode option" do + @io = IO.send(@method, @fd, 'w', binmode: true) + @io.should.binmode? + end + + it "does not set binmode from false :binmode" do + @io = IO.send(@method, @fd, 'w', binmode: false) + @io.should_not.binmode? + end + + it "sets external encoding to binary with binmode in mode string" do + @io = IO.send(@method, @fd, 'wb') + @io.external_encoding.should == Encoding::BINARY + end + + # #5917 + it "sets external encoding to binary with :binmode option" do + @io = IO.send(@method, @fd, 'w', binmode: true) + @io.external_encoding.should == Encoding::BINARY + end + + it "does not use binary encoding when mode encoding is specified" do + @io = IO.send(@method, @fd, 'wb:iso-8859-1') + @io.external_encoding.to_s.should == 'ISO-8859-1' + end + + it "does not use binary encoding when :encoding option is specified" do + @io = IO.send(@method, @fd, 'wb', encoding: "iso-8859-1") + @io.external_encoding.to_s.should == 'ISO-8859-1' + end + + it "does not use binary encoding when :external_encoding option is specified" do + @io = IO.send(@method, @fd, 'wb', external_encoding: "iso-8859-1") + @io.external_encoding.to_s.should == 'ISO-8859-1' + end + + it "does not use binary encoding when :internal_encoding option is specified" do + @io = IO.send(@method, @fd, 'wb', internal_encoding: "ibm866") + @io.internal_encoding.to_s.should == 'IBM866' + end + + it "does not use binary encoding when mode encoding is specified along with binmode: true option" do + @io = IO.send(@method, @fd, 'w:iso-8859-1', binmode: true) + @io.external_encoding.to_s.should == 'ISO-8859-1' + end + + it "does not use textmode argument when mode encoding is specified" do + @io = IO.send(@method, @fd, 'w:ascii-8bit', textmode: true) + @io.external_encoding.to_s.should == 'ASCII-8BIT' + end + + it "does not use binmode argument when external encoding is specified via the :external_encoding option" do + @io = IO.send(@method, @fd, 'w', binmode: true, external_encoding: 'iso-8859-1') + @io.external_encoding.to_s.should == 'ISO-8859-1' + end + + it "does not use textmode argument when external encoding is specified via the :external_encoding option" do + @io = IO.send(@method, @fd, 'w', textmode: true, external_encoding: 'ascii-8bit') + @io.external_encoding.to_s.should == 'ASCII-8BIT' + end + + it "raises ArgumentError for nil options" do + -> { + IO.send(@method, @fd, 'w', nil) + }.should raise_error(ArgumentError) + end + + it "coerces mode with #to_str" do + mode = mock("mode") + mode.should_receive(:to_str).and_return('w') + @io = IO.send(@method, @fd, mode) + end + + it "coerces mode with #to_int" do + mode = mock("mode") + mode.should_receive(:to_int).and_return(File::WRONLY) + @io = IO.send(@method, @fd, mode) + end + + it "coerces mode with #to_str when passed in options" do + mode = mock("mode") + mode.should_receive(:to_str).and_return('w') + @io = IO.send(@method, @fd, mode: mode) + end + + it "coerces mode with #to_int when passed in options" do + mode = mock("mode") + mode.should_receive(:to_int).and_return(File::WRONLY) + @io = IO.send(@method, @fd, mode: mode) + end + + it "coerces :encoding option with #to_str" do + encoding = mock("encoding") + encoding.should_receive(:to_str).and_return('utf-8') + @io = IO.send(@method, @fd, 'w', encoding: encoding) + end + + it "coerces :external_encoding option with #to_str" do + encoding = mock("encoding") + encoding.should_receive(:to_str).and_return('utf-8') + @io = IO.send(@method, @fd, 'w', external_encoding: encoding) + end + + it "coerces :internal_encoding option with #to_str" do + encoding = mock("encoding") + encoding.should_receive(:to_str).at_least(:once).and_return('utf-8') + @io = IO.send(@method, @fd, 'w', internal_encoding: encoding) + end + + it "coerces options as third argument with #to_hash" do + options = mock("options") + options.should_receive(:to_hash).and_return({}) + @io = IO.send(@method, @fd, 'w', **options) + end + + it "coerces options as second argument with #to_hash" do + options = mock("options") + options.should_receive(:to_hash).and_return({}) + @io = IO.send(@method, @fd, **options) + end + + it "accepts an :autoclose option" do + @io = IO.send(@method, @fd, 'w', autoclose: false) + @io.should_not.autoclose? + @io.autoclose = true + end + + it "accepts any truthy option :autoclose" do + @io = IO.send(@method, @fd, 'w', autoclose: 42) + @io.should.autoclose? + end +end + +# This group of specs may ONLY contain specs that do not actually create +# an IO instance from the file descriptor returned by #new_fd helper. +describe :io_new_errors, shared: true do + before :each do + @name = tmp("io_new.txt") + @fd = new_fd @name + end + + after :each do + IO.new(@fd, "w").close if @fd + rm_r @name + end + + it "raises an Errno::EBADF if the file descriptor is not valid" do + -> { IO.send(@method, -1, "w") }.should raise_error(Errno::EBADF) + end + + it "raises an IOError if passed a closed stream" do + -> { IO.send(@method, IOSpecs.closed_io.fileno, 'w') }.should raise_error(IOError) + end + + platform_is_not :windows do + it "raises an Errno::EINVAL if the new mode is not compatible with the descriptor's current mode" do + -> { IO.send(@method, @fd, "r") }.should raise_error(Errno::EINVAL) + end + end + + it "raises ArgumentError if passed an empty mode string" do + -> { IO.send(@method, @fd, "") }.should raise_error(ArgumentError) + end + + it "raises an error if passed modes two ways" do + -> { + IO.send(@method, @fd, "w", mode: "w") + }.should raise_error(ArgumentError) + end + + it "raises an error if passed encodings two ways" do + -> { + @io = IO.send(@method, @fd, 'w:ISO-8859-1', encoding: 'ISO-8859-1') + }.should raise_error(ArgumentError) + -> { + @io = IO.send(@method, @fd, 'w:ISO-8859-1', external_encoding: 'ISO-8859-1') + }.should raise_error(ArgumentError) + -> { + @io = IO.send(@method, @fd, 'w:ISO-8859-1', internal_encoding: 'ISO-8859-1') + }.should raise_error(ArgumentError) + -> { + @io = IO.send(@method, @fd, 'w:ISO-8859-1:UTF-8', internal_encoding: 'ISO-8859-1') + }.should raise_error(ArgumentError) + end + + it "raises an error if passed matching binary/text mode two ways" do + -> { + @io = IO.send(@method, @fd, "wb", binmode: true) + }.should raise_error(ArgumentError) + -> { + @io = IO.send(@method, @fd, "wt", textmode: true) + }.should raise_error(ArgumentError) + + -> { + @io = IO.send(@method, @fd, "wb", textmode: false) + }.should raise_error(ArgumentError) + -> { + @io = IO.send(@method, @fd, "wt", binmode: false) + }.should raise_error(ArgumentError) + end + + it "raises an error if passed conflicting binary/text mode two ways" do + -> { + @io = IO.send(@method, @fd, "wb", binmode: false) + }.should raise_error(ArgumentError) + -> { + @io = IO.send(@method, @fd, "wt", textmode: false) + }.should raise_error(ArgumentError) + + -> { + @io = IO.send(@method, @fd, "wb", textmode: true) + }.should raise_error(ArgumentError) + -> { + @io = IO.send(@method, @fd, "wt", binmode: true) + }.should raise_error(ArgumentError) + end + + it "raises an error when trying to set both binmode and textmode" do + -> { + @io = IO.send(@method, @fd, "w", textmode: true, binmode: true) + }.should raise_error(ArgumentError) + -> { + @io = IO.send(@method, @fd, File::Constants::WRONLY, textmode: true, binmode: true) + }.should raise_error(ArgumentError) + end + + it "raises ArgumentError if not passed a hash or nil for options" do + -> { + @io = IO.send(@method, @fd, 'w', false) + }.should raise_error(ArgumentError) + -> { + @io = IO.send(@method, @fd, false, false) + }.should raise_error(ArgumentError) + -> { + @io = IO.send(@method, @fd, nil, false) + }.should raise_error(ArgumentError) + end + + it "raises ArgumentError if passed a hash for mode and nil for options" do + -> { + @io = IO.send(@method, @fd, {mode: 'w'}, nil) + }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/io/shared/pos.rb b/spec/ruby/core/io/shared/pos.rb new file mode 100644 index 0000000000..3fdd3eb2b3 --- /dev/null +++ b/spec/ruby/core/io/shared/pos.rb @@ -0,0 +1,78 @@ +describe :io_pos, shared: true do + before :each do + @fname = tmp('test.txt') + File.open(@fname, 'w') { |f| f.write "123" } + end + + after :each do + rm_r @fname + end + + it "gets the offset" do + File.open @fname do |f| + f.send(@method).should == 0 + f.read 1 + f.send(@method).should == 1 + f.read 2 + f.send(@method).should == 3 + end + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.send(@method) }.should raise_error(IOError) + end + + it "resets #eof?" do + open @fname do |io| + io.read 1 + io.read 1 + io.send(@method) + io.should_not.eof? + end + end +end + +describe :io_set_pos, shared: true do + before :each do + @fname = tmp('test.txt') + File.open(@fname, 'w') { |f| f.write "123" } + end + + after :each do + rm_r @fname + end + + it "sets the offset" do + File.open @fname do |f| + val1 = f.read 1 + f.send @method, 0 + f.read(1).should == val1 + end + end + + it "converts arguments to Integers" do + File.open @fname do |io| + o = mock("o") + o.should_receive(:to_int).and_return(1) + + io.send @method, o + io.pos.should == 1 + end + end + + it "raises TypeError when cannot convert implicitly argument to Integer" do + File.open @fname do |io| + -> { io.send @method, Object.new }.should raise_error(TypeError, "no implicit conversion of Object into Integer") + end + end + + it "does not accept Integers that don't fit in a C off_t" do + File.open @fname do |io| + -> { io.send @method, 2**128 }.should raise_error(RangeError) + end + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.send @method, 0 }.should raise_error(IOError) + end +end diff --git a/spec/ruby/core/io/shared/readlines.rb b/spec/ruby/core/io/shared/readlines.rb new file mode 100644 index 0000000000..6c1fa11a59 --- /dev/null +++ b/spec/ruby/core/io/shared/readlines.rb @@ -0,0 +1,259 @@ +describe :io_readlines, shared: true do + it "raises TypeError if the first parameter is nil" do + -> { IO.send(@method, nil, &@object) }.should raise_error(TypeError) + end + + it "raises an Errno::ENOENT if the file does not exist" do + name = tmp("nonexistent.txt") + -> { IO.send(@method, name, &@object) }.should raise_error(Errno::ENOENT) + end + + it "yields a single string with entire content when the separator is nil" do + result = IO.send(@method, @name, nil, &@object) + (result ? result : ScratchPad.recorded).should == [IO.read(@name)] + end + + it "yields a sequence of paragraphs when the separator is an empty string" do + result = IO.send(@method, @name, "", &@object) + (result ? result : ScratchPad.recorded).should == IOSpecs.lines_empty_separator + end + + it "yields a sequence of lines without trailing newline characters when chomp is passed" do + result = IO.send(@method, @name, chomp: true, &@object) + (result ? result : ScratchPad.recorded).should == IOSpecs.lines_without_newline_characters + end +end + +describe :io_readlines_options_19, shared: true do + before :each do + @filename = tmp("io readlines options") + end + + after :each do + rm_r @filename + end + + describe "when passed name" do + it "calls #to_path to convert the name" do + name = mock("io name to_path") + name.should_receive(:to_path).and_return(@name) + IO.send(@method, name, &@object) + end + + it "defaults to $/ as the separator" do + result = IO.send(@method, @name, &@object) + (result ? result : ScratchPad.recorded).should == IOSpecs.lines + end + end + + describe "when passed name, object" do + it "calls #to_str to convert the object to a separator" do + sep = mock("io readlines separator") + sep.should_receive(:to_str).at_least(1).and_return(" ") + result = IO.send(@method, @name, sep, &@object) + (result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator + end + + describe "when the object is an Integer" do + before :each do + @sep = $/ + end + + after :each do + suppress_warning {$/ = @sep} + end + + it "defaults to $/ as the separator" do + suppress_warning {$/ = " "} + result = IO.send(@method, @name, 10, &@object) + (result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator_limit + end + + it "uses the object as a limit if it is an Integer" do + result = IO.send(@method, @name, 10, &@object) + (result ? result : ScratchPad.recorded).should == IOSpecs.lines_limit + end + + it "ignores the object as a limit if it is negative" do + result = IO.send(@method, @name, -2, &@object) + (result ? result : ScratchPad.recorded).should == IOSpecs.lines + end + + it "does not accept Integers that don't fit in a C off_t" do + -> { IO.send(@method, @name, 2**128, &@object) }.should raise_error(RangeError) + end + + ruby_bug "#18767", ""..."3.3" do + describe "when passed limit" do + it "raises ArgumentError when passed 0 as a limit" do + -> { IO.send(@method, @name, 0, &@object) }.should raise_error(ArgumentError) + end + end + end + end + + describe "when the object is a String" do + it "uses the value as the separator" do + result = IO.send(@method, @name, " ", &@object) + (result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator + end + + it "accepts non-ASCII data as separator" do + result = IO.send(@method, @name, "\303\250".dup.force_encoding("utf-8"), &@object) + (result ? result : ScratchPad.recorded).should == IOSpecs.lines_arbitrary_separator + end + end + + describe "when the object is an options Hash" do + it "raises TypeError exception" do + -> { + IO.send(@method, @name, { chomp: true }, &@object) + }.should raise_error(TypeError) + end + end + + describe "when the object is neither Integer nor String" do + it "raises TypeError exception" do + obj = mock("not io readlines limit") + + -> { + IO.send(@method, @name, obj, &@object) + }.should raise_error(TypeError) + end + end + end + + describe "when passed name, keyword arguments" do + it "uses the keyword arguments as options" do + result = IO.send(@method, @name, mode: "r", &@object) + (result ? result : ScratchPad.recorded).should == IOSpecs.lines + end + end + + describe "when passed name, object, object" do + describe "when the first object is a String" do + it "uses the second object as a limit if it is an Integer" do + result = IO.send(@method, @name, " ", 10, &@object) + (result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator_limit + end + + it "calls #to_int to convert the second object" do + limit = mock("io readlines limit") + limit.should_receive(:to_int).at_least(1).and_return(10) + result = IO.send(@method, @name, " ", limit, &@object) + (result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator_limit + end + end + + describe "when the first object is not a String or Integer" do + it "calls #to_str to convert the object to a String" do + sep = mock("io readlines separator") + sep.should_receive(:to_str).at_least(1).and_return(" ") + result = IO.send(@method, @name, sep, 10, &@object) + (result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator_limit + end + + it "uses the second object as a limit if it is an Integer" do + result = IO.send(@method, @name, " ", 10, &@object) + (result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator_limit + end + + it "calls #to_int to convert the second object" do + limit = mock("io readlines limit") + limit.should_receive(:to_int).at_least(1).and_return(10) + result = IO.send(@method, @name, " ", limit, &@object) + (result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator_limit + end + end + + describe "when the second object is neither Integer nor String" do + it "raises TypeError exception" do + obj = mock("not io readlines limit") + + -> { + IO.send(@method, @name, " ", obj, &@object) + }.should raise_error(TypeError) + end + end + + describe "when the second object is an options Hash" do + it "raises TypeError exception" do + -> { + IO.send(@method, @name, "", { chomp: true }, &@object) + }.should raise_error(TypeError) + end + end + end + + describe "when passed name, object, keyword arguments" do + describe "when the first object is an Integer" do + it "uses the keyword arguments as options" do + -> do + IO.send(@method, @filename, 10, mode: "w", &@object) + end.should raise_error(IOError) + end + end + + describe "when the first object is a String" do + it "uses the keyword arguments as options" do + -> do + IO.send(@method, @filename, " ", mode: "w", &@object) + end.should raise_error(IOError) + end + end + + describe "when the first object is not a String or Integer" do + it "uses the keyword arguments as options" do + sep = mock("io readlines separator") + sep.should_receive(:to_str).at_least(1).and_return(" ") + + -> do + IO.send(@method, @filename, sep, mode: "w", &@object) + end.should raise_error(IOError) + end + end + end + + describe "when passed name, separator, limit, keyword arguments" do + it "calls #to_path to convert the name object" do + name = mock("io name to_path") + name.should_receive(:to_path).and_return(@name) + result = IO.send(@method, name, " ", 10, mode: "r", &@object) + (result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator_limit + end + + it "calls #to_str to convert the separator object" do + sep = mock("io readlines separator") + sep.should_receive(:to_str).at_least(1).and_return(" ") + result = IO.send(@method, @name, sep, 10, mode: "r", &@object) + (result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator_limit + end + + it "calls #to_int to convert the limit argument" do + limit = mock("io readlines limit") + limit.should_receive(:to_int).at_least(1).and_return(10) + result = IO.send(@method, @name, " ", limit, mode: "r", &@object) + (result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator_limit + end + + it "uses the keyword arguments as options" do + -> do + IO.send(@method, @filename, " ", 10, mode: "w", &@object) + end.should raise_error(IOError) + end + + describe "when passed chomp, nil as a separator, and a limit" do + it "yields each line of limit size without truncating trailing new line character" do + # 43 - is a size of the 1st paragraph in the file + result = IO.send(@method, @name, nil, 43, chomp: true, &@object) + + (result ? result : ScratchPad.recorded).should == [ + "Voici la ligne une.\nQui è la linea due.\n\n\n", + "Aquà está la lÃnea tres.\n" + "Hier ist Zeile ", + "vier.\n\nEstá aqui a linha cinco.\nHere is li", + "ne six.\n" + ] + end + end + end +end diff --git a/spec/ruby/core/io/shared/tty.rb b/spec/ruby/core/io/shared/tty.rb new file mode 100644 index 0000000000..89ac08ec86 --- /dev/null +++ b/spec/ruby/core/io/shared/tty.rb @@ -0,0 +1,24 @@ +require_relative '../fixtures/classes' + +describe :io_tty, shared: true do + platform_is_not :windows do + it "returns true if this stream is a terminal device (TTY)" do + begin + # check to enabled tty + File.open('/dev/tty') {} + rescue Errno::ENXIO + skip "workaround for not configured environment like OS X" + else + File.open('/dev/tty') { |f| f.send(@method) }.should == true + end + end + end + + it "returns false if this stream is not a terminal device (TTY)" do + File.open(__FILE__) { |f| f.send(@method) }.should == false + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.send @method }.should raise_error(IOError) + end +end diff --git a/spec/ruby/core/io/shared/write.rb b/spec/ruby/core/io/shared/write.rb new file mode 100644 index 0000000000..964064746a --- /dev/null +++ b/spec/ruby/core/io/shared/write.rb @@ -0,0 +1,154 @@ +# encoding: utf-8 +require_relative '../fixtures/classes' + +describe :io_write, shared: true do + before :each do + @filename = tmp("IO_syswrite_file") + $$.to_s + File.open(@filename, "w") do |file| + file.send(@method, "012345678901234567890123456789") + end + @file = File.open(@filename, "r+") + @readonly_file = File.open(@filename) + end + + after :each do + @readonly_file.close if @readonly_file + @file.close if @file + rm_r @filename + end + + it "coerces the argument to a string using to_s" do + (obj = mock('test')).should_receive(:to_s).and_return('a string') + @file.send(@method, obj) + end + + it "checks if the file is writable if writing more than zero bytes" do + -> { @readonly_file.send(@method, "abcde") }.should raise_error(IOError) + end + + it "returns the number of bytes written" do + written = @file.send(@method, "abcde") + written.should == 5 + end + + it "invokes to_s on non-String argument" do + data = "abcdefgh9876" + (obj = mock(data)).should_receive(:to_s).and_return(data) + @file.send(@method, obj) + @file.seek(0) + @file.read(data.size).should == data + end + + it "writes all of the string's bytes without buffering if mode is sync" do + @file.sync = true + written = @file.send(@method, "abcde") + written.should == 5 + File.open(@filename) do |file| + file.read(10).should == "abcde56789" + end + end + + it "does not warn if called after IO#read" do + @file.read(5) + -> { @file.send(@method, "fghij") }.should_not complain + end + + it "writes to the current position after IO#read" do + @file.read(5) + @file.send(@method, "abcd") + @file.rewind + @file.read.should == "01234abcd901234567890123456789" + end + + it "advances the file position by the count of given bytes" do + @file.send(@method, "abcde") + @file.read(10).should == "5678901234" + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.send(@method, "hello") }.should raise_error(IOError) + end + + describe "on a pipe" do + before :each do + @r, @w = IO.pipe + end + + after :each do + @r.close + @w.close + end + + it "writes the given String to the pipe" do + @w.send(@method, "foo") + @w.close + @r.read.should == "foo" + end + + # [ruby-core:90895] RJIT worker may leave fd open in a forked child. + # For instance, RJIT creates a worker before @r.close with fork(), @r.close happens, + # and the RJIT worker keeps the pipe open until the worker execve(). + # TODO: consider acquiring GVL from RJIT worker. + guard_not -> { defined?(RubyVM::RJIT) && RubyVM::RJIT.enabled? } do + it "raises Errno::EPIPE if the read end is closed and does not die from SIGPIPE" do + @r.close + -> { @w.send(@method, "foo") }.should raise_error(Errno::EPIPE, /Broken pipe/) + end + end + end +end + +describe :io_write_transcode, shared: true do + before :each do + @transcode_filename = tmp("io_write_transcode") + end + + after :each do + rm_r @transcode_filename + end + + it "transcodes the given string when the external encoding is set and neither is BINARY" do + utf8_str = "hello" + + File.open(@transcode_filename, "w", external_encoding: Encoding::UTF_16BE) do |file| + file.external_encoding.should == Encoding::UTF_16BE + file.send(@method, utf8_str) + end + + result = File.binread(@transcode_filename) + expected = [0, 104, 0, 101, 0, 108, 0, 108, 0, 111] # UTF-16BE bytes for "hello" + + result.bytes.should == expected + end + + it "transcodes the given string when the external encoding is set and the string encoding is BINARY" do + str = "été".b + + File.open(@transcode_filename, "w", external_encoding: Encoding::UTF_16BE) do |file| + file.external_encoding.should == Encoding::UTF_16BE + -> { file.send(@method, str) }.should raise_error(Encoding::UndefinedConversionError) + end + end +end + +describe :io_write_no_transcode, shared: true do + before :each do + @transcode_filename = tmp("io_write_no_transcode") + end + + after :each do + rm_r @transcode_filename + end + + it "does not transcode the given string even when the external encoding is set" do + utf8_str = "hello" + + File.open(@transcode_filename, "w", external_encoding: Encoding::UTF_16BE) do |file| + file.external_encoding.should == Encoding::UTF_16BE + file.send(@method, utf8_str) + end + + result = File.binread(@transcode_filename) + result.bytes.should == utf8_str.bytes + end +end diff --git a/spec/ruby/core/io/stat_spec.rb b/spec/ruby/core/io/stat_spec.rb new file mode 100644 index 0000000000..717c45d0a3 --- /dev/null +++ b/spec/ruby/core/io/stat_spec.rb @@ -0,0 +1,25 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#stat" do + before :each do + cmd = platform_is(:windows) ? 'rem' : 'cat' + @io = IO.popen cmd, "r+" + end + + after :each do + @io.close unless @io.closed? + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.stat }.should raise_error(IOError) + end + + it "returns a File::Stat object for the stream" do + STDOUT.stat.should be_an_instance_of(File::Stat) + end + + it "can stat pipes" do + @io.stat.should be_an_instance_of(File::Stat) + end +end diff --git a/spec/ruby/core/io/sync_spec.rb b/spec/ruby/core/io/sync_spec.rb new file mode 100644 index 0000000000..993b7ee244 --- /dev/null +++ b/spec/ruby/core/io/sync_spec.rb @@ -0,0 +1,64 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#sync=" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.close unless @io.closed? + end + + it "sets the sync mode to true or false" do + @io.sync = true + @io.sync.should == true + @io.sync = false + @io.sync.should == false + end + + it "accepts non-boolean arguments" do + @io.sync = 10 + @io.sync.should == true + @io.sync = nil + @io.sync.should == false + @io.sync = Object.new + @io.sync.should == true + end + + it "raises an IOError on closed stream" do + -> { IOSpecs.closed_io.sync = true }.should raise_error(IOError) + end +end + +describe "IO#sync" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.close unless @io.closed? + end + + it "returns the current sync mode" do + @io.sync.should == false + end + + it "raises an IOError on closed stream" do + -> { IOSpecs.closed_io.sync }.should raise_error(IOError) + end +end + +describe "IO#sync" do + it "is false by default for STDIN" do + STDIN.sync.should == false + end + + it "is false by default for STDOUT" do + STDOUT.sync.should == false + end + + it "is true by default for STDERR" do + STDERR.sync.should == true + end +end diff --git a/spec/ruby/core/io/sysopen_spec.rb b/spec/ruby/core/io/sysopen_spec.rb new file mode 100644 index 0000000000..7ad379df3a --- /dev/null +++ b/spec/ruby/core/io/sysopen_spec.rb @@ -0,0 +1,50 @@ +require_relative '../../spec_helper' + +describe "IO.sysopen" do + before :each do + @filename = tmp("rubinius-spec-io-sysopen-#{$$}.txt") + @fd = nil + end + + after :each do + IO.for_fd(@fd).close if @fd + rm_r @filename + end + + it "returns the file descriptor for a given path" do + @fd = IO.sysopen(@filename, "w") + @fd.should be_kind_of(Integer) + @fd.should_not equal(0) + end + + # opening a directory is not supported on Windows + platform_is_not :windows do + it "works on directories" do + @fd = IO.sysopen(tmp("")) # /tmp + @fd.should be_kind_of(Integer) + @fd.should_not equal(0) + end + end + + it "calls #to_path to convert an object to a path" do + path = mock('sysopen to_path') + path.should_receive(:to_path).and_return(@filename) + @fd = IO.sysopen(path, 'w') + end + + it "accepts a mode as second argument" do + -> { @fd = IO.sysopen(@filename, "w") }.should_not raise_error + @fd.should_not equal(0) + end + + it "accepts permissions as third argument" do + @fd = IO.sysopen(@filename, "w", 777) + @fd.should_not equal(0) + end + + it "accepts mode & permission that are nil" do + touch @filename # create the file + @fd = IO.sysopen(@filename, nil, nil) + @fd.should_not equal(0) + end +end diff --git a/spec/ruby/core/io/sysread_spec.rb b/spec/ruby/core/io/sysread_spec.rb new file mode 100644 index 0000000000..d56a27b3af --- /dev/null +++ b/spec/ruby/core/io/sysread_spec.rb @@ -0,0 +1,137 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#sysread on a file" do + before :each do + @file_name = tmp("IO_sysread_file") + $$.to_s + File.open(@file_name, "w") do |f| + # write some stuff + f.write("012345678901234567890123456789\nabcdef") + end + @file = File.open(@file_name, "r+") + end + + after :each do + @file.close + rm_r @file_name + end + + it "reads the specified number of bytes from the file" do + @file.sysread(15).should == "012345678901234" + end + + it "reads the specified number of bytes from the file to the buffer" do + buf = +"" # empty buffer + @file.sysread(15, buf).should == buf + buf.should == "012345678901234" + + @file.rewind + + buf = +"ABCDE" # small buffer + @file.sysread(15, buf).should == buf + buf.should == "012345678901234" + + @file.rewind + + buf = +"ABCDE" * 5 # large buffer + @file.sysread(15, buf).should == buf + buf.should == "012345678901234" + end + + it "coerces the second argument to string and uses it as a buffer" do + buf = +"ABCDE" + (obj = mock("buff")).should_receive(:to_str).any_number_of_times.and_return(buf) + @file.sysread(15, obj).should == buf + buf.should == "012345678901234" + end + + it "advances the position of the file by the specified number of bytes" do + @file.sysread(15) + @file.sysread(5).should == "56789" + end + + it "raises an error when called after buffered reads" do + @file.readline + -> { @file.sysread(5) }.should raise_error(IOError) + end + + it "reads normally even when called immediately after a buffered IO#read" do + @file.read(15) + @file.sysread(5).should == "56789" + end + + it "does not raise error if called after IO#read followed by IO#write" do + @file.read(5) + @file.write("abcde") + -> { @file.sysread(5) }.should_not raise_error(IOError) + end + + it "does not raise error if called after IO#read followed by IO#syswrite" do + @file.read(5) + @file.syswrite("abcde") + -> { @file.sysread(5) }.should_not raise_error(IOError) + end + + it "reads updated content after the flushed buffered IO#write" do + @file.write("abcde") + @file.flush + @file.sysread(5).should == "56789" + File.open(@file_name) do |f| + f.sysread(10).should == "abcde56789" + end + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.sysread(5) }.should raise_error(IOError) + end + + it "immediately returns an empty string if the length argument is 0" do + @file.sysread(0).should == "" + end + + it "immediately returns the given buffer if the length argument is 0" do + buffer = +"existing content" + @file.sysread(0, buffer).should == buffer + buffer.should == "existing content" + end + + it "discards the existing buffer content upon successful read" do + buffer = +"existing content" + @file.sysread(11, buffer).should.equal?(buffer) + buffer.should == "01234567890" + end + + it "discards the existing buffer content upon error" do + buffer = +"existing content" + @file.seek(0, :END) + -> { @file.sysread(1, buffer) }.should raise_error(EOFError) + buffer.should be_empty + end + + it "preserves the encoding of the given buffer" do + buffer = ''.encode(Encoding::ISO_8859_1) + string = @file.sysread(10, buffer) + + buffer.encoding.should == Encoding::ISO_8859_1 + end +end + +describe "IO#sysread" do + before do + @read, @write = IO.pipe + end + + after do + @read.close + @write.close + end + + it "returns a smaller string if less than size bytes are available" do + @write.syswrite "ab" + @read.sysread(3).should == "ab" + end + + it "raises ArgumentError when length is less than 0" do + -> { @read.sysread(-1) }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/io/sysseek_spec.rb b/spec/ruby/core/io/sysseek_spec.rb new file mode 100644 index 0000000000..002f2a14eb --- /dev/null +++ b/spec/ruby/core/io/sysseek_spec.rb @@ -0,0 +1,49 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/pos' + +describe "IO#sysseek" do + it_behaves_like :io_set_pos, :sysseek +end + +describe "IO#sysseek" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.close unless @io.closed? + end + + it "moves the read position relative to the current position with SEEK_CUR" do + @io.sysseek(10, IO::SEEK_CUR) + @io.readline.should == "igne une.\n" + end + + it "raises an error when called after buffered reads" do + @io.readline + -> { @io.sysseek(-5, IO::SEEK_CUR) }.should raise_error(IOError) + end + + it "seeks normally even when called immediately after a buffered IO#read" do + @io.read(15) + @io.sysseek(-5, IO::SEEK_CUR).should == 10 + end + + it "moves the read position relative to the start with SEEK_SET" do + @io.sysseek(43, IO::SEEK_SET) + @io.readline.should == "Aquà está la lÃnea tres.\n" + end + + it "moves the read position relative to the end with SEEK_END" do + @io.sysseek(1, IO::SEEK_END) + + # this is the safest way of checking the EOF when + # sys-* methods are invoked + -> { @io.sysread(1) }.should raise_error(EOFError) + + @io.sysseek(-25, IO::SEEK_END) + @io.sysread(7).should == "cinco.\n" + end +end diff --git a/spec/ruby/core/io/syswrite_spec.rb b/spec/ruby/core/io/syswrite_spec.rb new file mode 100644 index 0000000000..8bf61a27c3 --- /dev/null +++ b/spec/ruby/core/io/syswrite_spec.rb @@ -0,0 +1,82 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/write' + +describe "IO#syswrite on a file" do + before :each do + @filename = tmp("IO_syswrite_file") + $$.to_s + File.open(@filename, "w") do |file| + file.syswrite("012345678901234567890123456789") + end + @file = File.open(@filename, "r+") + @readonly_file = File.open(@filename) + end + + after :each do + @file.close + @readonly_file.close + rm_r @filename + end + + it "writes all of the string's bytes but does not buffer them" do + written = @file.syswrite("abcde") + written.should == 5 + File.open(@filename) do |file| + file.sysread(10).should == "abcde56789" + file.seek(0) + @file.fsync + file.sysread(10).should == "abcde56789" + end + end + + it "does not modify the passed argument" do + File.open(@filename, "w") do |f| + f.set_encoding(Encoding::IBM437) + # A character whose codepoint differs between UTF-8 and IBM437 + f.syswrite("Æ’".freeze) + end + + File.binread(@filename).bytes.should == [198, 146] + end + + it "warns if called immediately after a buffered IO#write" do + @file.write("abcde") + -> { @file.syswrite("fghij") }.should complain(/syswrite/) + end + + it "does not warn if called after IO#write with intervening IO#sysread" do + @file.syswrite("abcde") + @file.sysread(5) + -> { @file.syswrite("fghij") }.should_not complain + end + + it "writes to the actual file position when called after buffered IO#read" do + @file.read(5) + @file.syswrite("abcde") + File.open(@filename) do |file| + file.sysread(10).should == "01234abcde" + end + end +end + +describe "IO#syswrite on a pipe" do + it "returns the written bytes if the fd is in nonblock mode and write would block" do + require 'io/nonblock' + r, w = IO.pipe + begin + w.nonblock = true + larger_than_pipe_capacity = 2 * 1024 * 1024 + written = w.syswrite("a"*larger_than_pipe_capacity) + written.should > 0 + written.should < larger_than_pipe_capacity + ensure + w.close + r.close + end + end +end + +describe "IO#syswrite" do + it_behaves_like :io_write, :syswrite + it_behaves_like :io_write_no_transcode, :syswrite +end diff --git a/spec/ruby/core/io/tell_spec.rb b/spec/ruby/core/io/tell_spec.rb new file mode 100644 index 0000000000..0d6c6b02d3 --- /dev/null +++ b/spec/ruby/core/io/tell_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/pos' + +describe "IO#tell" do + it_behaves_like :io_pos, :tell +end diff --git a/spec/ruby/core/io/to_i_spec.rb b/spec/ruby/core/io/to_i_spec.rb new file mode 100644 index 0000000000..acf138c663 --- /dev/null +++ b/spec/ruby/core/io/to_i_spec.rb @@ -0,0 +1,12 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#to_i" do + it "returns the numeric file descriptor of the given IO object" do + $stdout.to_i.should == 1 + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.to_i }.should raise_error(IOError) + end +end diff --git a/spec/ruby/core/io/to_io_spec.rb b/spec/ruby/core/io/to_io_spec.rb new file mode 100644 index 0000000000..55a0977740 --- /dev/null +++ b/spec/ruby/core/io/to_io_spec.rb @@ -0,0 +1,21 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#to_io" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + end + + after :each do + @io.close unless @io.closed? + end + + it "returns self for open stream" do + @io.to_io.should equal(@io) + end + + it "returns self for closed stream" do + io = IOSpecs.closed_io + io.to_io.should equal(io) + end +end diff --git a/spec/ruby/core/io/try_convert_spec.rb b/spec/ruby/core/io/try_convert_spec.rb new file mode 100644 index 0000000000..a9e99de7aa --- /dev/null +++ b/spec/ruby/core/io/try_convert_spec.rb @@ -0,0 +1,49 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO.try_convert" do + before :each do + @name = tmp("io_try_convert.txt") + @io = new_io @name + end + + after :each do + @io.close unless @io.closed? + rm_r @name + end + + it "returns the passed IO object" do + IO.try_convert(@io).should equal(@io) + end + + it "does not call #to_io on an IO instance" do + @io.should_not_receive(:to_io) + IO.try_convert(@io) + end + + it "calls #to_io to coerce an object" do + obj = mock("io") + obj.should_receive(:to_io).and_return(@io) + IO.try_convert(obj).should equal(@io) + end + + it "returns nil when the passed object does not respond to #to_io" do + IO.try_convert(mock("io")).should be_nil + end + + it "return nil when BasicObject is passed" do + IO.try_convert(BasicObject.new).should be_nil + end + + it "raises a TypeError if the object does not return an IO from #to_io" do + obj = mock("io") + obj.should_receive(:to_io).and_return("io") + -> { IO.try_convert(obj) }.should raise_error(TypeError, "can't convert MockObject to IO (MockObject#to_io gives String)") + end + + it "propagates an exception raised by #to_io" do + obj = mock("io") + obj.should_receive(:to_io).and_raise(TypeError.new) + ->{ IO.try_convert(obj) }.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/io/tty_spec.rb b/spec/ruby/core/io/tty_spec.rb new file mode 100644 index 0000000000..3b76c6d2b8 --- /dev/null +++ b/spec/ruby/core/io/tty_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/tty' + +describe "IO#tty?" do + it_behaves_like :io_tty, :tty? +end diff --git a/spec/ruby/core/io/ungetbyte_spec.rb b/spec/ruby/core/io/ungetbyte_spec.rb new file mode 100644 index 0000000000..716743a6af --- /dev/null +++ b/spec/ruby/core/io/ungetbyte_spec.rb @@ -0,0 +1,54 @@ +require_relative '../../spec_helper' + +describe "IO#ungetbyte" do + before :each do + @name = tmp("io_ungetbyte") + touch(@name) { |f| f.write "a" } + @io = new_io @name, "r" + end + + after :each do + @io.close unless @io.closed? + rm_r @name + end + + it "does nothing when passed nil" do + @io.ungetbyte(nil).should be_nil + @io.getbyte.should == 97 + end + + it "puts back each byte in a String argument" do + @io.ungetbyte("cat").should be_nil + @io.getbyte.should == 99 + @io.getbyte.should == 97 + @io.getbyte.should == 116 + @io.getbyte.should == 97 + end + + it "calls #to_str to convert the argument" do + str = mock("io ungetbyte") + str.should_receive(:to_str).and_return("dog") + + @io.ungetbyte(str).should be_nil + @io.getbyte.should == 100 + @io.getbyte.should == 111 + @io.getbyte.should == 103 + @io.getbyte.should == 97 + end + + it "never raises RangeError" do + for i in [4095, 0x4f7574206f6620636861722072616e67ff] do + @io.ungetbyte(i).should be_nil + @io.getbyte.should == 255 + end + end + + it "raises IOError on stream not opened for reading" do + -> { STDOUT.ungetbyte(42) }.should raise_error(IOError, "not opened for reading") + end + + it "raises an IOError if the IO is closed" do + @io.close + -> { @io.ungetbyte(42) }.should raise_error(IOError) + end +end diff --git a/spec/ruby/core/io/ungetc_spec.rb b/spec/ruby/core/io/ungetc_spec.rb new file mode 100644 index 0000000000..47a4e99ebf --- /dev/null +++ b/spec/ruby/core/io/ungetc_spec.rb @@ -0,0 +1,138 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "IO#ungetc" do + before :each do + @io = IOSpecs.io_fixture "lines.txt" + + @empty = tmp('empty.txt') + end + + after :each do + @io.close unless @io.closed? + rm_r @empty + end + + it "pushes back one character onto stream" do + @io.getc.should == ?V + @io.ungetc(86) + @io.getc.should == ?V + + @io.ungetc(10) + @io.getc.should == ?\n + + @io.getc.should == ?o + @io.getc.should == ?i + # read the rest of line + @io.readline.should == "ci la ligne une.\n" + @io.getc.should == ?Q + @io.ungetc(99) + @io.getc.should == ?c + end + + it "interprets the codepoint in the external encoding" do + @io.set_encoding(Encoding::UTF_8) + @io.ungetc(233) + c = @io.getc + c.encoding.should == Encoding::UTF_8 + c.should == "é" + c.bytes.should == [195, 169] + + @io.set_encoding(Encoding::IBM437) + @io.ungetc(130) + c = @io.getc + c.encoding.should == Encoding::IBM437 + c.bytes.should == [130] + c.encode(Encoding::UTF_8).should == "é" + end + + it "pushes back one character when invoked at the end of the stream" do + # read entire content + @io.read + @io.ungetc(100) + @io.getc.should == ?d + end + + it "pushes back one character when invoked at the start of the stream" do + @io.read(0) + @io.ungetc(100) + @io.getc.should == ?d + end + + it "pushes back one character when invoked on empty stream" do + touch(@empty) + + File.open(@empty) { |empty| + empty.getc().should == nil + empty.ungetc(10) + empty.getc.should == ?\n + } + end + + it "affects EOF state" do + touch(@empty) + + File.open(@empty) { |empty| + empty.should.eof? + empty.getc.should == nil + empty.ungetc(100) + empty.should_not.eof? + } + end + + it "adjusts the stream position" do + @io.pos.should == 0 + + # read one char + c = @io.getc + @io.pos.should == 1 + @io.ungetc(c) + @io.pos.should == 0 + + # read all + @io.read + pos = @io.pos + @io.ungetc(98) + @io.pos.should == pos - 1 + end + + it "makes subsequent unbuffered operations to raise IOError" do + @io.getc + @io.ungetc(100) + -> { @io.sysread(1) }.should raise_error(IOError) + end + + it "raises TypeError if passed nil" do + @io.getc.should == ?V + proc{@io.ungetc(nil)}.should raise_error(TypeError) + end + + it "puts one or more characters back in the stream" do + @io.gets + @io.ungetc("Aquà ").should be_nil + @io.gets.chomp.should == "Aquà Qui è la linea due." + end + + it "calls #to_str to convert the argument if it is not an Integer" do + chars = mock("io ungetc") + chars.should_receive(:to_str).and_return("Aquà ") + + @io.ungetc(chars).should be_nil + @io.gets.chomp.should == "Aquà Voici la ligne une." + end + + it "returns nil when invoked on stream that was not yet read" do + @io.ungetc(100).should be_nil + end + + it "raises IOError on stream not opened for reading" do + -> { STDOUT.ungetc(100) }.should raise_error(IOError, "not opened for reading") + end + + it "raises IOError on closed stream" do + @io.getc + @io.close + -> { @io.ungetc(100) }.should raise_error(IOError) + end +end diff --git a/spec/ruby/core/io/write_nonblock_spec.rb b/spec/ruby/core/io/write_nonblock_spec.rb new file mode 100644 index 0000000000..5bfc690f9b --- /dev/null +++ b/spec/ruby/core/io/write_nonblock_spec.rb @@ -0,0 +1,96 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/write' + +# See https://bugs.ruby-lang.org/issues/5954#note-5 +platform_is_not :windows do + describe "IO#write_nonblock on a file" do + before :each do + @filename = tmp("IO_syswrite_file") + $$.to_s + File.open(@filename, "w") do |file| + file.write_nonblock("012345678901234567890123456789") + end + @file = File.open(@filename, "r+") + @readonly_file = File.open(@filename) + end + + after :each do + @file.close if @file + @readonly_file.close if @readonly_file + rm_r @filename + end + + it "writes all of the string's bytes but does not buffer them" do + written = @file.write_nonblock("abcde") + written.should == 5 + File.open(@filename) do |file| + file.sysread(10).should == "abcde56789" + file.seek(0) + @file.fsync + file.sysread(10).should == "abcde56789" + end + end + + it "does not modify the passed argument" do + File.open(@filename, "w") do |f| + f.set_encoding(Encoding::IBM437) + # A character whose codepoint differs between UTF-8 and IBM437 + f.write_nonblock("Æ’".freeze) + end + + File.binread(@filename).bytes.should == [198, 146] + end + + it "checks if the file is writable if writing zero bytes" do + -> { + @readonly_file.write_nonblock("") + }.should raise_error(IOError) + end + end + + describe "IO#write_nonblock" do + it_behaves_like :io_write, :write_nonblock + it_behaves_like :io_write_no_transcode, :write_nonblock + end +end + +describe 'IO#write_nonblock' do + before do + @read, @write = IO.pipe + end + + after do + @read.close + @write.close + end + + it "raises an exception extending IO::WaitWritable when the write would block" do + -> { + loop { @write.write_nonblock('a' * 10_000) } + }.should raise_error(IO::WaitWritable) { |e| + platform_is_not :windows do + e.should be_kind_of(Errno::EAGAIN) + end + platform_is :windows do + e.should be_kind_of(Errno::EWOULDBLOCK) + end + } + end + + context "when exception option is set to false" do + it "returns :wait_writable when the operation would block" do + loop { + break if @write.write_nonblock("a" * 10_000, exception: false) == :wait_writable + } + @write.write_nonblock("a" * 10_000, exception: false).should == :wait_writable + end + end + + platform_is_not :windows do + it 'sets the IO in nonblock mode' do + require 'io/nonblock' + @write.write_nonblock('a') + @write.should.nonblock? + end + end +end diff --git a/spec/ruby/core/io/write_spec.rb b/spec/ruby/core/io/write_spec.rb new file mode 100644 index 0000000000..e58100f846 --- /dev/null +++ b/spec/ruby/core/io/write_spec.rb @@ -0,0 +1,297 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/write' +require_relative 'shared/binwrite' + +describe "IO#write on a file" do + before :each do + @filename = tmp("IO_syswrite_file") + $$.to_s + File.open(@filename, "w") do |file| + file.write("012345678901234567890123456789") + end + @file = File.open(@filename, "r+") + @readonly_file = File.open(@filename) + end + + after :each do + @file.close + @readonly_file.close + rm_r @filename + end + + it "does not check if the file is writable if writing zero bytes" do + -> { @readonly_file.write("") }.should_not raise_error + end + + before :each do + @external = Encoding.default_external + @internal = Encoding.default_internal + + Encoding.default_external = Encoding::UTF_8 + end + + after :each do + Encoding.default_external = @external + Encoding.default_internal = @internal + end + + it "returns a length of 0 when writing a blank string" do + @file.write('').should == 0 + end + + it "returns a length of 0 when writing blank strings" do + @file.write('', '', '').should == 0 + end + + it "returns a length of 0 when passed no arguments" do + @file.write().should == 0 + end + + it "returns the number of bytes written" do + @file.write("hellø").should == 6 + end + + it "does not modify the passed argument" do + File.open(@filename, "w") do |f| + f.set_encoding(Encoding::IBM437) + # A character whose codepoint differs between UTF-8 and IBM437 + f.write("Æ’".freeze) + end + + File.binread(@filename).bytes.should == [159] + end + + it "does not modify arguments when passed multiple arguments and external encoding not set" do + a, b = "a".freeze, "b".freeze + + File.open(@filename, "w") do |f| + f.write(a, b) + end + + File.binread(@filename).bytes.should == [97, 98] + a.encoding.should == Encoding::UTF_8 + b.encoding.should == Encoding::UTF_8 + end + + it "uses the encoding from the given option for non-ascii encoding" do + File.open(@filename, "w", encoding: Encoding::UTF_32LE) do |file| + file.write("hi").should == 8 + end + File.binread(@filename).should == "h\u0000\u0000\u0000i\u0000\u0000\u0000" + end + + it "uses the encoding from the given option for non-ascii encoding even if in binary mode" do + File.open(@filename, "w", encoding: Encoding::UTF_32LE, binmode: true) do |file| + file.should.binmode? + file.write("hi").should == 8 + end + File.binread(@filename).should == "h\u0000\u0000\u0000i\u0000\u0000\u0000" + + File.open(@filename, "wb", encoding: Encoding::UTF_32LE) do |file| + file.should.binmode? + file.write("hi").should == 8 + end + File.binread(@filename).should == "h\u0000\u0000\u0000i\u0000\u0000\u0000" + end + + it "uses the encoding from the given option for non-ascii encoding when multiple arguments passes" do + File.open(@filename, "w", encoding: Encoding::UTF_32LE) do |file| + file.write("h", "i").should == 8 + end + File.binread(@filename).should == "h\u0000\u0000\u0000i\u0000\u0000\u0000" + end + + it "raises a invalid byte sequence error if invalid bytes are being written" do + # pack "\xFEhi" to avoid utf-8 conflict + xFEhi = ([254].pack('C*') + 'hi').force_encoding('utf-8') + File.open(@filename, "w", encoding: Encoding::US_ASCII) do |file| + -> { file.write(xFEhi) }.should raise_error(Encoding::InvalidByteSequenceError) + end + end + + it "writes binary data if no encoding is given" do + File.open(@filename, "w") do |file| + file.write('Hëllö'.encode('ISO-8859-1')) + end + ë = ([235].pack('U')).encode('ISO-8859-1') + ö = ([246].pack('U')).encode('ISO-8859-1') + res = "H#{ë}ll#{ö}" + File.binread(@filename).should == res.force_encoding(Encoding::BINARY) + end + + platform_is_not :windows do + it "writes binary data if no encoding is given and multiple arguments passed" do + File.open(@filename, "w") do |file| + file.write("\x87".b, "Ä…") # 0x87 isn't a valid UTF-8 binary representation of a character + end + File.binread(@filename).bytes.should == [0x87, 0xC4, 0x85] + + File.open(@filename, "w") do |file| + file.write("\x61".encode("utf-32le"), "Ä…") + end + File.binread(@filename).bytes.should == [0x61, 0x00, 0x00, 0x00, 0xC4, 0x85] + end + end +end + +describe "IO.write" do + it_behaves_like :io_binwrite, :write + + it "uses an :open_args option" do + IO.write(@filename, 'hi', open_args: ["w", nil, {encoding: Encoding::UTF_32LE}]).should == 8 + end + + it "disregards other options if :open_args is given" do + IO.write(@filename, 'hi', 2, mode: "r", encoding: Encoding::UTF_32LE, open_args: ["w"]).should == 2 + File.read(@filename).should == "\0\0hi" + end + + it "requires mode to be specified in :open_args" do + -> { + IO.write(@filename, 'hi', open_args: [{encoding: Encoding::UTF_32LE, binmode: true}]) + }.should raise_error(IOError, "not opened for writing") + + IO.write(@filename, 'hi', open_args: ["w", {encoding: Encoding::UTF_32LE, binmode: true}]).should == 8 + IO.write(@filename, 'hi', open_args: [{encoding: Encoding::UTF_32LE, binmode: true, mode: "w"}]).should == 8 + end + + it "requires mode to be specified in :open_args even if flags option passed" do + -> { + IO.write(@filename, 'hi', open_args: [{encoding: Encoding::UTF_32LE, binmode: true, flags: File::CREAT}]) + }.should raise_error(IOError, "not opened for writing") + + IO.write(@filename, 'hi', open_args: ["w", {encoding: Encoding::UTF_32LE, binmode: true, flags: File::CREAT}]).should == 8 + IO.write(@filename, 'hi', open_args: [{encoding: Encoding::UTF_32LE, binmode: true, flags: File::CREAT, mode: "w"}]).should == 8 + end + + it "uses the given encoding and returns the number of bytes written" do + IO.write(@filename, 'hi', mode: "w", encoding: Encoding::UTF_32LE).should == 8 + end + + it "raises ArgumentError if encoding is specified in mode parameter and is given as :encoding option" do + -> { + IO.write(@filename, 'hi', mode: "w:UTF-16LE:UTF-16BE", encoding: Encoding::UTF_32LE) + }.should raise_error(ArgumentError, "encoding specified twice") + + -> { + IO.write(@filename, 'hi', mode: "w:UTF-16BE", encoding: Encoding::UTF_32LE) + }.should raise_error(ArgumentError, "encoding specified twice") + end + + it "writes the file with the permissions in the :perm parameter" do + rm_r @filename + IO.write(@filename, 'write :perm spec', mode: "w", perm: 0o755).should == 16 + (File.stat(@filename).mode & 0o777) == 0o755 + end + + it "writes binary data if no encoding is given" do + IO.write(@filename, 'Hëllö'.encode('ISO-8859-1')) + xEB = [235].pack('C*') + xF6 = [246].pack('C*') + File.binread(@filename).should == ("H" + xEB + "ll" + xF6).force_encoding(Encoding::BINARY) + end + + platform_is_not :windows do + describe "on a FIFO" do + before :each do + @fifo = tmp("File_open_fifo") + File.mkfifo(@fifo) + end + + after :each do + rm_r @fifo + end + + # rb_cloexec_open() is currently missing a retry on EINTR. + # @ioquatix is looking into fixing it. Quarantined until it's done. + quarantine! do + it "writes correctly" do + thr = Thread.new do + IO.read(@fifo) + end + begin + string = "hi" + IO.write(@fifo, string).should == string.length + ensure + thr.join + end + end + end + end + + ruby_version_is "3.3"..."4.0" do + # https://bugs.ruby-lang.org/issues/19630 + it "warns about deprecation given a path with a pipe" do + -> { + -> { + IO.write("|cat", "xxx") + }.should output_to_fd("xxx") + }.should complain(/IO process creation with a leading '\|'/) + end + end + end +end + +describe "IO#write" do + it_behaves_like :io_write, :write + it_behaves_like :io_write_transcode, :write + + it "accepts multiple arguments" do + IO.pipe do |r, w| + w.write("foo", "bar") + w.close + + r.read.should == "foobar" + end + end +end + +platform_is :windows do + describe "IO#write on Windows" do + before :each do + @fname = tmp("io_write.txt") + end + + after :each do + rm_r @fname + @io.close if @io and !@io.closed? + end + + it "normalizes line endings in text mode" do + @io = new_io(@fname, "wt") + @io.write "a\nb\nc" + @io.close + File.binread(@fname).should == "a\r\nb\r\nc" + end + + it "does not normalize line endings in binary mode" do + @io = new_io(@fname, "wb") + @io.write "a\r\nb\r\nc" + @io.close + File.binread(@fname).should == "a\r\nb\r\nc" + end + end +end + +describe "IO#write on STDOUT" do + # https://bugs.ruby-lang.org/issues/14413 + platform_is_not :windows do + it "raises SignalException SIGPIPE if the stream is closed instead of Errno::EPIPE like other IOs" do + stderr_file = tmp("stderr") + begin + IO.popen([*ruby_exe, "-e", "loop { puts :ok }"], "r", err: stderr_file) do |io| + io.gets.should == "ok\n" + io.close + end + status = $? + status.should_not.success? + status.should.signaled? + Signal.signame(status.termsig).should == 'PIPE' + File.read(stderr_file).should.empty? + ensure + rm_r stderr_file + end + end + end +end diff --git a/spec/ruby/core/kernel/Array_spec.rb b/spec/ruby/core/kernel/Array_spec.rb new file mode 100644 index 0000000000..b4a8bb7599 --- /dev/null +++ b/spec/ruby/core/kernel/Array_spec.rb @@ -0,0 +1,97 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel" do + it "has private instance method Array()" do + Kernel.should have_private_instance_method(:Array) + end +end + +describe :kernel_Array, shared: true do + before :each do + @array = [1, 2, 3] + end + + it "does not call #to_ary on an Array" do + @array.should_not_receive(:to_ary) + @object.send(@method, @array).should == @array + end + + it "calls #to_ary to convert the argument to an Array" do + obj = mock("Array([1,2,3])") + obj.should_receive(:to_ary).and_return(@array) + obj.should_not_receive(:to_a) + + @object.send(@method, obj).should == @array + end + + it "does not call #to_a on an Array" do + @array.should_not_receive(:to_a) + @object.send(@method, @array).should == @array + end + + it "calls #to_a if the argument does not respond to #to_ary" do + obj = mock("Array([1,2,3])") + obj.should_receive(:to_a).and_return(@array) + + @object.send(@method, obj).should == @array + end + + it "calls #to_a if #to_ary returns nil" do + obj = mock("Array([1,2,3])") + obj.should_receive(:to_ary).and_return(nil) + obj.should_receive(:to_a).and_return(@array) + + @object.send(@method, obj).should == @array + end + + it "returns an Array containing the argument if #to_a returns nil" do + obj = mock("Array([1,2,3])") + obj.should_receive(:to_a).and_return(nil) + + @object.send(@method, obj).should == [obj] + end + + it "calls #to_ary first, even if it's private" do + obj = KernelSpecs::PrivateToAry.new + + @object.send(@method, obj).should == [1, 2] + end + + it "calls #to_a if #to_ary is not defined, even if it's private" do + obj = KernelSpecs::PrivateToA.new + + @object.send(@method, obj).should == [3, 4] + end + + it "returns an Array containing the argument if it responds to neither #to_ary nor #to_a" do + obj = mock("Array(x)") + @object.send(@method, obj).should == [obj] + end + + it "returns an empty Array when passed nil" do + @object.send(@method, nil).should == [] + end + + it "raises a TypeError if #to_ary does not return an Array" do + obj = mock("Array() string") + obj.should_receive(:to_ary).and_return("string") + + -> { @object.send(@method, obj) }.should raise_error(TypeError) + end + + it "raises a TypeError if #to_a does not return an Array" do + obj = mock("Array() string") + obj.should_receive(:to_a).and_return("string") + + -> { @object.send(@method, obj) }.should raise_error(TypeError) + end +end + +describe "Kernel.Array" do + it_behaves_like :kernel_Array, :Array_method, KernelSpecs +end + +describe "Kernel#Array" do + it_behaves_like :kernel_Array, :Array_function, KernelSpecs +end diff --git a/spec/ruby/core/kernel/Complex_spec.rb b/spec/ruby/core/kernel/Complex_spec.rb new file mode 100644 index 0000000000..346d50ab5e --- /dev/null +++ b/spec/ruby/core/kernel/Complex_spec.rb @@ -0,0 +1,276 @@ +require_relative '../../spec_helper' +require_relative '../../shared/kernel/complex' +require_relative 'fixtures/Complex' + +describe "Kernel.Complex()" do + describe "when passed [Complex, Complex]" do + it "returns a new Complex number based on the two given numbers" do + Complex(Complex(3, 4), Complex(5, 6)).should == Complex(3 - 6, 4 + 5) + Complex(Complex(1.5, 2), Complex(-5, 6.3)).should == Complex(1.5 - 6.3, 2 - 5) + end + end + + describe "when passed [Complex]" do + it "returns the passed Complex number" do + Complex(Complex(1, 2)).should == Complex(1, 2) + Complex(Complex(-3.4, bignum_value)).should == Complex(-3.4, bignum_value) + end + end + + describe "when passed [Integer, Integer]" do + it "returns a new Complex number" do + Complex(1, 2).should be_an_instance_of(Complex) + Complex(1, 2).real.should == 1 + Complex(1, 2).imag.should == 2 + + Complex(-3, -5).should be_an_instance_of(Complex) + Complex(-3, -5).real.should == -3 + Complex(-3, -5).imag.should == -5 + + Complex(3.5, -4.5).should be_an_instance_of(Complex) + Complex(3.5, -4.5).real.should == 3.5 + Complex(3.5, -4.5).imag.should == -4.5 + + Complex(bignum_value, 30).should be_an_instance_of(Complex) + Complex(bignum_value, 30).real.should == bignum_value + Complex(bignum_value, 30).imag.should == 30 + end + end + + describe "when passed [Integer/Float]" do + it "returns a new Complex number with 0 as the imaginary component" do + # Guard against the Mathn library + guard -> { !defined?(Math.rsqrt) } do + Complex(1).should be_an_instance_of(Complex) + Complex(1).imag.should == 0 + Complex(1).real.should == 1 + + Complex(-3).should be_an_instance_of(Complex) + Complex(-3).imag.should == 0 + Complex(-3).real.should == -3 + + Complex(-4.5).should be_an_instance_of(Complex) + Complex(-4.5).imag.should == 0 + Complex(-4.5).real.should == -4.5 + + Complex(bignum_value).should be_an_instance_of(Complex) + Complex(bignum_value).imag.should == 0 + Complex(bignum_value).real.should == bignum_value + end + end + end + + describe "when passed [String]" do + it_behaves_like :kernel_complex, :Complex_method, KernelSpecs + + context "invalid argument" do + it "raises Encoding::CompatibilityError if String is in not ASCII-compatible encoding" do + -> { + Complex("79+4i".encode("UTF-16")) + }.should raise_error(Encoding::CompatibilityError, "ASCII incompatible encoding: UTF-16") + end + + it "raises ArgumentError for unrecognised Strings" do + -> { + Complex("ruby") + }.should raise_error(ArgumentError, 'invalid value for convert(): "ruby"') + end + + it "raises ArgumentError for trailing garbage" do + -> { + Complex("79+4iruby") + }.should raise_error(ArgumentError, 'invalid value for convert(): "79+4iruby"') + end + + it "does not understand Float::INFINITY" do + -> { + Complex("Infinity") + }.should raise_error(ArgumentError, 'invalid value for convert(): "Infinity"') + + -> { + Complex("-Infinity") + }.should raise_error(ArgumentError, 'invalid value for convert(): "-Infinity"') + end + + it "does not understand Float::NAN" do + -> { + Complex("NaN") + }.should raise_error(ArgumentError, 'invalid value for convert(): "NaN"') + end + + it "does not understand a sequence of _" do + -> { + Complex("7__9+4__0i") + }.should raise_error(ArgumentError, 'invalid value for convert(): "7__9+4__0i"') + end + + it "does not allow null-byte" do + -> { + Complex("1-2i\0") + }.should raise_error(ArgumentError, "string contains null byte") + end + end + + context "invalid argument and exception: false passed" do + it "raises Encoding::CompatibilityError if String is in not ASCII-compatible encoding" do + -> { + Complex("79+4i".encode("UTF-16"), exception: false) + }.should raise_error(Encoding::CompatibilityError, "ASCII incompatible encoding: UTF-16") + end + + it "returns nil for unrecognised Strings" do + Complex("ruby", exception: false).should == nil + end + + it "returns nil when trailing garbage" do + Complex("79+4iruby", exception: false).should == nil + end + + it "returns nil for Float::INFINITY" do + Complex("Infinity", exception: false).should == nil + Complex("-Infinity", exception: false).should == nil + end + + it "returns nil for Float::NAN" do + Complex("NaN", exception: false).should == nil + end + + it "returns nil when there is a sequence of _" do + Complex("7__9+4__0i", exception: false).should == nil + end + + it "returns nil when String contains null-byte" do + Complex("1-2i\0", exception: false).should == nil + end + end + end + + describe "when passes [String, String]" do + it "needs to be reviewed for spec completeness" + end + + describe "when passed an Object which responds to #to_c" do + it "returns the passed argument" do + obj = Object.new; def obj.to_c; 1i end + Complex(obj).should == Complex(0, 1) + end + end + + describe "when passed a Numeric which responds to #real? with false" do + it "returns the passed argument" do + n = mock_numeric("unreal") + n.should_receive(:real?).any_number_of_times.and_return(false) + Complex(n).should equal(n) + end + end + + describe "when passed a Numeric which responds to #real? with true" do + it "returns a Complex with the passed argument as the real component and 0 as the imaginary component" do + n = mock_numeric("real") + n.should_receive(:real?).any_number_of_times.and_return(true) + result = Complex(n) + result.real.should equal(n) + result.imag.should equal(0) + end + end + + describe "when passed Numerics n1 and n2 and at least one responds to #real? with false" do + [[false, false], [false, true], [true, false]].each do |r1, r2| + it "returns n1 + n2 * Complex(0, 1)" do + n1 = mock_numeric("n1") + n2 = mock_numeric("n2") + n3 = mock_numeric("n3") + n4 = mock_numeric("n4") + n1.should_receive(:real?).any_number_of_times.and_return(r1) + n2.should_receive(:real?).any_number_of_times.and_return(r2) + n2.should_receive(:*).with(Complex(0, 1)).and_return(n3) + n1.should_receive(:+).with(n3).and_return(n4) + Complex(n1, n2).should equal(n4) + end + end + end + + describe "when passed two Numerics and both respond to #real? with true" do + it "returns a Complex with the passed arguments as real and imaginary components respectively" do + n1 = mock_numeric("n1") + n2 = mock_numeric("n2") + n1.should_receive(:real?).any_number_of_times.and_return(true) + n2.should_receive(:real?).any_number_of_times.and_return(true) + result = Complex(n1, n2) + result.real.should equal(n1) + result.imag.should equal(n2) + end + end + + describe "when passed a single non-Numeric" do + it "coerces the passed argument using #to_c" do + n = mock("n") + c = Complex(0, 0) + n.should_receive(:to_c).and_return(c) + Complex(n).should equal(c) + end + end + + describe "when passed a non-Numeric second argument" do + it "raises TypeError" do + -> { Complex(:sym, :sym) }.should raise_error(TypeError) + -> { Complex(0, :sym) }.should raise_error(TypeError) + end + end + + describe "when passed nil" do + it "raises TypeError" do + -> { Complex(nil) }.should raise_error(TypeError, "can't convert nil into Complex") + -> { Complex(0, nil) }.should raise_error(TypeError, "can't convert nil into Complex") + -> { Complex(nil, 0) }.should raise_error(TypeError, "can't convert nil into Complex") + end + end + + describe "when passed exception: false" do + describe "and [Numeric]" do + it "returns a complex number" do + Complex("123", exception: false).should == Complex(123) + end + end + + describe "and [non-Numeric]" do + it "swallows an error" do + Complex(:sym, exception: false).should == nil + end + end + + describe "and [non-Numeric, Numeric] argument" do + it "throws a TypeError" do + -> { Complex(:sym, 0, exception: false) }.should raise_error(TypeError, "not a real") + end + end + + describe "and [anything, non-Numeric] argument" do + it "swallows an error" do + Complex("a", :sym, exception: false).should == nil + Complex(:sym, :sym, exception: false).should == nil + Complex(0, :sym, exception: false).should == nil + end + end + + describe "and non-numeric String arguments" do + it "swallows an error" do + Complex("a", "b", exception: false).should == nil + Complex("a", 0, exception: false).should == nil + Complex(0, "b", exception: false).should == nil + end + end + + describe "and nil arguments" do + it "swallows an error" do + Complex(nil, exception: false).should == nil + Complex(0, nil, exception: false).should == nil + Complex(nil, 0, exception: false).should == nil + end + end + end + + it "freezes its result" do + Complex(1).frozen?.should == true + end +end diff --git a/spec/ruby/core/kernel/Float_spec.rb b/spec/ruby/core/kernel/Float_spec.rb new file mode 100644 index 0000000000..9c436b05f7 --- /dev/null +++ b/spec/ruby/core/kernel/Float_spec.rb @@ -0,0 +1,413 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe :kernel_float, shared: true do + it "returns the identical Float for numeric Floats" do + float = 1.12 + float2 = @object.send(:Float, float) + float2.should == float + float2.should equal float + end + + it "returns a Float for Fixnums" do + @object.send(:Float, 1).should == 1.0 + end + + it "returns a Float for Complex with only a real part" do + @object.send(:Float, Complex(1)).should == 1.0 + end + + it "returns a Float for Bignums" do + @object.send(:Float, 1000000000000).should == 1000000000000.0 + end + + it "raises an ArgumentError for nil" do + -> { @object.send(:Float, nil) }.should raise_error(TypeError) + end + + it "returns the identical NaN for NaN" do + nan = nan_value + nan.nan?.should be_true + nan2 = @object.send(:Float, nan) + nan2.nan?.should be_true + nan2.should equal(nan) + end + + it "returns the same Infinity for Infinity" do + infinity = infinity_value + infinity2 = @object.send(:Float, infinity) + infinity2.should == infinity_value + infinity.should equal(infinity2) + end + + it "converts Strings to floats without calling #to_f" do + string = +"10" + string.should_not_receive(:to_f) + @object.send(:Float, string).should == 10.0 + end + + it "converts Strings with decimal points into Floats" do + @object.send(:Float, "10.0").should == 10.0 + end + + it "raises an ArgumentError for a String of word characters" do + -> { @object.send(:Float, "float") }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError for a String with string in error message" do + -> { @object.send(:Float, "foo") }.should raise_error(ArgumentError) { |e| + e.message.should == 'invalid value for Float(): "foo"' + } + end + + it "raises an ArgumentError if there are two decimal points in the String" do + -> { @object.send(:Float, "10.0.0") }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError for a String of numbers followed by word characters" do + -> { @object.send(:Float, "10D") }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError for a String of word characters followed by numbers" do + -> { @object.send(:Float, "D10") }.should raise_error(ArgumentError) + end + + it "is strict about the string form even across newlines" do + -> { @object.send(:Float, "not a number\n10") }.should raise_error(ArgumentError) + -> { @object.send(:Float, "10\nnot a number") }.should raise_error(ArgumentError) + end + + it "converts String subclasses to floats without calling #to_f" do + my_string = Class.new(String) do + def to_f() 1.2 end + end + + @object.send(:Float, my_string.new("10")).should == 10.0 + end + + it "returns a positive Float if the string is prefixed with +" do + @object.send(:Float, "+10").should == 10.0 + @object.send(:Float, " +10").should == 10.0 + end + + it "returns a negative Float if the string is prefixed with +" do + @object.send(:Float, "-10").should == -10.0 + @object.send(:Float, " -10").should == -10.0 + end + + it "raises an ArgumentError if a + or - is embedded in a String" do + -> { @object.send(:Float, "1+1") }.should raise_error(ArgumentError) + -> { @object.send(:Float, "1-1") }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if a String has a trailing + or -" do + -> { @object.send(:Float, "11+") }.should raise_error(ArgumentError) + -> { @object.send(:Float, "11-") }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError for a String with a leading _" do + -> { @object.send(:Float, "_1") }.should raise_error(ArgumentError) + end + + it "returns a value for a String with an embedded _" do + @object.send(:Float, "1_000").should == 1000.0 + end + + it "raises an ArgumentError for a String with a trailing _" do + -> { @object.send(:Float, "10_") }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError for a String of \\0" do + -> { @object.send(:Float, "\0") }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError for a String with a leading \\0" do + -> { @object.send(:Float, "\01") }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError for a String with an embedded \\0" do + -> { @object.send(:Float, "1\01") }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError for a String with a trailing \\0" do + -> { @object.send(:Float, "1\0") }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError for a String that is just an empty space" do + -> { @object.send(:Float, " ") }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError for a String that with an embedded space" do + -> { @object.send(:Float, "1 2") }.should raise_error(ArgumentError) + end + + it "returns a value for a String with a leading space" do + @object.send(:Float, " 1").should == 1.0 + end + + it "returns a value for a String with a trailing space" do + @object.send(:Float, "1 ").should == 1.0 + end + + it "returns a value for a String with any leading whitespace" do + @object.send(:Float, "\t\n1").should == 1.0 + end + + it "returns a value for a String with any trailing whitespace" do + @object.send(:Float, "1\t\n").should == 1.0 + end + + ruby_version_is ""..."3.4" do + it "raises ArgumentError if a fractional part is missing" do + -> { @object.send(:Float, "1.") }.should raise_error(ArgumentError) + -> { @object.send(:Float, "+1.") }.should raise_error(ArgumentError) + -> { @object.send(:Float, "-1.") }.should raise_error(ArgumentError) + -> { @object.send(:Float, "1.e+0") }.should raise_error(ArgumentError) + -> { @object.send(:Float, "1.e-2") }.should raise_error(ArgumentError) + end + end + + ruby_version_is "3.4" do + it "allows String representation without a fractional part" do + @object.send(:Float, "1.").should == 1.0 + @object.send(:Float, "+1.").should == 1.0 + @object.send(:Float, "-1.").should == -1.0 + @object.send(:Float, "1.e+0").should == 1.0 + @object.send(:Float, "1.e-2").should be_close(0.01, TOLERANCE) + end + end + + %w(e E).each do |e| + it "raises an ArgumentError if #{e} is the trailing character" do + -> { @object.send(:Float, "2#{e}") }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if #{e} is the leading character" do + -> { @object.send(:Float, "#{e}2") }.should raise_error(ArgumentError) + end + + it "returns Infinity for '2#{e}1000'" do + @object.send(:Float, "2#{e}1000").should == Float::INFINITY + end + + it "returns 0 for '2#{e}-1000'" do + @object.send(:Float, "2#{e}-1000").should == 0 + end + + it "allows embedded _ in a number on either side of the #{e}" do + @object.send(:Float, "2_0#{e}100").should == 20e100 + @object.send(:Float, "20#{e}1_00").should == 20e100 + @object.send(:Float, "2_0#{e}1_00").should == 20e100 + end + + it "raises an exception if a space is embedded on either side of the '#{e}'" do + -> { @object.send(:Float, "2 0#{e}100") }.should raise_error(ArgumentError) + -> { @object.send(:Float, "20#{e}1 00") }.should raise_error(ArgumentError) + end + + it "raises an exception if there's a leading _ on either side of the '#{e}'" do + -> { @object.send(:Float, "_20#{e}100") }.should raise_error(ArgumentError) + -> { @object.send(:Float, "20#{e}_100") }.should raise_error(ArgumentError) + end + + it "raises an exception if there's a trailing _ on either side of the '#{e}'" do + -> { @object.send(:Float, "20_#{e}100") }.should raise_error(ArgumentError) + -> { @object.send(:Float, "20#{e}100_") }.should raise_error(ArgumentError) + end + + it "allows decimal points on the left side of the '#{e}'" do + @object.send(:Float, "2.0#{e}2").should == 2e2 + end + + it "raises an ArgumentError if there's a decimal point on the right side of the '#{e}'" do + -> { @object.send(:Float, "20#{e}2.0") }.should raise_error(ArgumentError) + end + end + + context "for hexadecimal literals" do + it "interprets the 0x prefix as hexadecimal" do + @object.send(:Float, "0x10").should == 16.0 + @object.send(:Float, "0x0F").should == 15.0 + @object.send(:Float, "0x0f").should == 15.0 + end + + it "interprets negative hex value" do + @object.send(:Float, "-0x10").should == -16.0 + end + + it "accepts embedded _ if the number does not contain a-f" do + @object.send(:Float, "0x1_0").should == 16.0 + end + + ruby_version_is ""..."3.4.3" do + it "does not accept embedded _ if the number contains a-f" do + -> { @object.send(:Float, "0x1_0a") }.should raise_error(ArgumentError) + @object.send(:Float, "0x1_0a", exception: false).should be_nil + end + end + + ruby_version_is "3.4.3" do + it "accepts embedded _ if the number contains a-f" do + @object.send(:Float, "0x1_0a").should == 0x10a.to_f + end + end + + it "does not accept _ before, after or inside the 0x prefix" do + -> { @object.send(:Float, "_0x10") }.should raise_error(ArgumentError) + -> { @object.send(:Float, "0_x10") }.should raise_error(ArgumentError) + -> { @object.send(:Float, "0x_10") }.should raise_error(ArgumentError) + @object.send(:Float, "_0x10", exception: false).should be_nil + @object.send(:Float, "0_x10", exception: false).should be_nil + @object.send(:Float, "0x_10", exception: false).should be_nil + end + + it "parses negative hexadecimal string as negative float" do + @object.send(:Float, "-0x7b").should == -123.0 + end + + ruby_version_is "3.4" do + it "accepts a fractional part" do + @object.send(:Float, "0x0.8").should == 0.5 + end + end + + describe "with binary exponent" do + %w(p P).each do |p| + it "interprets the fractional part (on the left side of '#{p}') in hexadecimal" do + @object.send(:Float, "0x10#{p}0").should == 16.0 + end + + it "interprets the exponent (on the right of '#{p}') in decimal" do + @object.send(:Float, "0x1#{p}10").should == 1024.0 + end + + it "raises an ArgumentError if #{p} is the trailing character" do + -> { @object.send(:Float, "0x1#{p}") }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if #{p} is the leading character" do + -> { @object.send(:Float, "0x#{p}1") }.should raise_error(ArgumentError) + end + + it "returns Infinity for '0x1#{p}10000'" do + @object.send(:Float, "0x1#{p}10000").should == Float::INFINITY + end + + it "returns 0 for '0x1#{p}-10000'" do + @object.send(:Float, "0x1#{p}-10000").should == 0 + end + + it "allows embedded _ in a number on either side of the #{p}" do + @object.send(:Float, "0x1_0#{p}10").should == 16384.0 + @object.send(:Float, "0x10#{p}1_0").should == 16384.0 + @object.send(:Float, "0x1_0#{p}1_0").should == 16384.0 + end + + it "raises an exception if a space is embedded on either side of the '#{p}'" do + -> { @object.send(:Float, "0x1 0#{p}10") }.should raise_error(ArgumentError) + -> { @object.send(:Float, "0x10#{p}1 0") }.should raise_error(ArgumentError) + end + + it "raises an exception if there's a leading _ on either side of the '#{p}'" do + -> { @object.send(:Float, "0x_10#{p}10") }.should raise_error(ArgumentError) + -> { @object.send(:Float, "0x10#{p}_10") }.should raise_error(ArgumentError) + end + + it "raises an exception if there's a trailing _ on either side of the '#{p}'" do + -> { @object.send(:Float, "0x10_#{p}10") }.should raise_error(ArgumentError) + -> { @object.send(:Float, "0x10#{p}10_") }.should raise_error(ArgumentError) + end + + it "allows hexadecimal points on the left side of the '#{p}'" do + @object.send(:Float, "0x1.8#{p}0").should == 1.5 + end + + it "raises an ArgumentError if there's a decimal point on the right side of the '#{p}'" do + -> { @object.send(:Float, "0x1#{p}1.0") }.should raise_error(ArgumentError) + end + end + end + end + + it "returns a Float that can be a parameter to #Float again" do + float = @object.send(:Float, "10") + @object.send(:Float, float).should == 10.0 + end + + it "otherwise, converts the given argument to a Float by calling #to_f" do + (obj = mock('1.2')).should_receive(:to_f).once.and_return(1.2) + obj.should_not_receive(:to_i) + @object.send(:Float, obj).should == 1.2 + end + + it "returns the identical NaN if to_f is called and it returns NaN" do + nan = nan_value + (nan_to_f = mock('NaN')).should_receive(:to_f).once.and_return(nan) + nan2 = @object.send(:Float, nan_to_f) + nan2.nan?.should be_true + nan2.should equal(nan) + end + + it "returns the identical Infinity if #to_f is called and it returns Infinity" do + infinity = infinity_value + (infinity_to_f = mock('Infinity')).should_receive(:to_f).once.and_return(infinity) + infinity2 = @object.send(:Float, infinity_to_f) + infinity2.should equal(infinity) + end + + it "raises a TypeError if #to_f is not provided" do + -> { @object.send(:Float, mock('x')) }.should raise_error(TypeError) + end + + it "raises a TypeError if #to_f returns a String" do + (obj = mock('ha!')).should_receive(:to_f).once.and_return('ha!') + -> { @object.send(:Float, obj) }.should raise_error(TypeError) + end + + it "raises a TypeError if #to_f returns an Integer" do + (obj = mock('123')).should_receive(:to_f).once.and_return(123) + -> { @object.send(:Float, obj) }.should raise_error(TypeError) + end + + it "raises a RangeError when passed a Complex argument" do + c = Complex(2, 3) + -> { @object.send(:Float, c) }.should raise_error(RangeError) + end + + describe "when passed exception: false" do + describe "and valid input" do + it "returns a Float number" do + @object.send(:Float, 1, exception: false).should == 1.0 + @object.send(:Float, "1", exception: false).should == 1.0 + @object.send(:Float, "1.23", exception: false).should == 1.23 + end + end + + describe "and invalid input" do + it "swallows an error" do + @object.send(:Float, "abc", exception: false).should == nil + @object.send(:Float, :sym, exception: false).should == nil + end + end + + describe "and nil" do + it "swallows it" do + @object.send(:Float, nil, exception: false).should == nil + end + end + end +end + +describe "Kernel.Float" do + it_behaves_like :kernel_float, :Float, Kernel +end + +describe "Kernel#Float" do + it_behaves_like :kernel_float, :Float, Object.new +end + +describe "Kernel#Float" do + it "is a private method" do + Kernel.should have_private_instance_method(:Float) + end +end diff --git a/spec/ruby/core/kernel/Hash_spec.rb b/spec/ruby/core/kernel/Hash_spec.rb new file mode 100644 index 0000000000..cbe098a8ac --- /dev/null +++ b/spec/ruby/core/kernel/Hash_spec.rb @@ -0,0 +1,63 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel#hash" do + it "is provided" do + 1.respond_to?(:hash).should == true + end + + it "is stable" do + 1.hash.should == 1.hash + end +end + +describe "Kernel" do + it "has private instance method Hash()" do + Kernel.should have_private_instance_method(:Hash) + end +end + +describe :kernel_Hash, shared: true do + before :each do + @hash = { a: 1} + end + + it "converts nil to a Hash" do + @object.send(@method, nil).should == {} + end + + it "converts an empty array to a Hash" do + @object.send(@method, []).should == {} + end + + it "does not call #to_hash on an Hash" do + @hash.should_not_receive(:to_hash) + @object.send(@method, @hash).should == @hash + end + + it "calls #to_hash to convert the argument to an Hash" do + obj = mock("Hash(a: 1)") + obj.should_receive(:to_hash).and_return(@hash) + + @object.send(@method, obj).should == @hash + end + + it "raises a TypeError if it doesn't respond to #to_hash" do + -> { @object.send(@method, mock("")) }.should raise_error(TypeError) + end + + it "raises a TypeError if #to_hash does not return an Hash" do + obj = mock("Hash() string") + obj.should_receive(:to_hash).and_return("string") + + -> { @object.send(@method, obj) }.should raise_error(TypeError) + end +end + +describe "Kernel.Hash" do + it_behaves_like :kernel_Hash, :Hash_method, KernelSpecs +end + +describe "Kernel#Hash" do + it_behaves_like :kernel_Hash, :Hash_function, KernelSpecs +end diff --git a/spec/ruby/core/kernel/Integer_spec.rb b/spec/ruby/core/kernel/Integer_spec.rb new file mode 100644 index 0000000000..74dd3e0dd2 --- /dev/null +++ b/spec/ruby/core/kernel/Integer_spec.rb @@ -0,0 +1,823 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe :kernel_integer, shared: true do + it "returns a Bignum for a Bignum" do + Integer(2e100).should == 2e100 + end + + it "returns a Fixnum for a Fixnum" do + Integer(100).should == 100 + end + + it "raises a TypeError when to_int returns not-an-Integer object and to_i returns nil" do + obj = mock("object") + obj.should_receive(:to_int).and_return("1") + obj.should_receive(:to_i).and_return(nil) + -> { Integer(obj) }.should raise_error(TypeError) + end + + it "return a result of to_i when to_int does not return an Integer" do + obj = mock("object") + obj.should_receive(:to_int).and_return("1") + obj.should_receive(:to_i).and_return(42) + Integer(obj).should == 42 + end + + it "raises a TypeError when passed nil" do + -> { Integer(nil) }.should raise_error(TypeError) + end + + it "returns an Integer object" do + Integer(2).should be_an_instance_of(Integer) + Integer(9**99).should be_an_instance_of(Integer) + end + + it "truncates Floats" do + Integer(3.14).should == 3 + Integer(90.8).should == 90 + end + + it "calls to_i on Rationals" do + Integer(Rational(8,3)).should == 2 + Integer(3.quo(2)).should == 1 + end + + it "returns the value of to_int if the result is a Fixnum" do + obj = mock("object") + obj.should_receive(:to_int).and_return(1) + obj.should_not_receive(:to_i) + Integer(obj).should == 1 + end + + it "returns the value of to_int if the result is a Bignum" do + obj = mock("object") + obj.should_receive(:to_int).and_return(2 * 10**100) + obj.should_not_receive(:to_i) + Integer(obj).should == 2 * 10**100 + end + + it "calls to_i on an object whose to_int returns nil" do + obj = mock("object") + obj.should_receive(:to_int).and_return(nil) + obj.should_receive(:to_i).and_return(1) + Integer(obj).should == 1 + end + + it "raises a TypeError if to_i returns a value that is not an Integer" do + obj = mock("object") + obj.should_receive(:to_i).and_return("1") + -> { Integer(obj) }.should raise_error(TypeError) + end + + it "raises a TypeError if no to_int or to_i methods exist" do + obj = mock("object") + -> { Integer(obj) }.should raise_error(TypeError) + end + + it "raises a TypeError if to_int returns nil and no to_i exists" do + obj = mock("object") + obj.should_receive(:to_i).and_return(nil) + -> { Integer(obj) }.should raise_error(TypeError) + end + + it "raises a FloatDomainError when passed NaN" do + -> { Integer(nan_value) }.should raise_error(FloatDomainError) + end + + it "raises a FloatDomainError when passed Infinity" do + -> { Integer(infinity_value) }.should raise_error(FloatDomainError) + end + + describe "when passed exception: false" do + describe "and to_i returns a value that is not an Integer" do + it "swallows an error" do + obj = mock("object") + obj.should_receive(:to_i).and_return("1") + Integer(obj, exception: false).should == nil + end + end + + describe "and no to_int or to_i methods exist" do + it "swallows an error" do + obj = mock("object") + Integer(obj, exception: false).should == nil + end + end + + describe "and to_int returns nil and no to_i exists" do + it "swallows an error" do + obj = mock("object") + obj.should_receive(:to_i).and_return(nil) + Integer(obj, exception: false).should == nil + end + end + + describe "and passed NaN" do + it "swallows an error" do + Integer(nan_value, exception: false).should == nil + end + end + + describe "and passed Infinity" do + it "swallows an error" do + Integer(infinity_value, exception: false).should == nil + end + end + + describe "and passed nil" do + it "swallows an error" do + Integer(nil, exception: false).should == nil + end + end + + describe "and passed a String that contains numbers" do + it "normally parses it and returns an Integer" do + Integer("42", exception: false).should == 42 + end + end + + describe "and passed a String that can't be converted to an Integer" do + it "swallows an error" do + Integer("abc", exception: false).should == nil + end + end + end +end + +describe :kernel_integer_string, shared: true do + it "raises an ArgumentError if the String is a null byte" do + -> { Integer("\0") }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if the String starts with a null byte" do + -> { Integer("\01") }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if the String ends with a null byte" do + -> { Integer("1\0") }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if the String contains a null byte" do + -> { Integer("1\01") }.should raise_error(ArgumentError) + end + + it "ignores leading whitespace" do + Integer(" 1").should == 1 + Integer(" 1").should == 1 + Integer("\t\n1").should == 1 + end + + it "ignores trailing whitespace" do + Integer("1 ").should == 1 + Integer("1 ").should == 1 + Integer("1\t\n").should == 1 + end + + it "raises an ArgumentError if there are leading _s" do + -> { Integer("_1") }.should raise_error(ArgumentError) + -> { Integer("___1") }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if there are trailing _s" do + -> { Integer("1_") }.should raise_error(ArgumentError) + -> { Integer("1___") }.should raise_error(ArgumentError) + end + + it "ignores an embedded _" do + Integer("1_1").should == 11 + end + + it "raises an ArgumentError if there are multiple embedded _s" do + -> { Integer("1__1") }.should raise_error(ArgumentError) + -> { Integer("1___1") }.should raise_error(ArgumentError) + end + + it "ignores a single leading +" do + Integer("+1").should == 1 + end + + it "raises an ArgumentError if there is a space between the + and number" do + -> { Integer("+ 1") }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if there are multiple leading +s" do + -> { Integer("++1") }.should raise_error(ArgumentError) + -> { Integer("+++1") }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if there are trailing +s" do + -> { Integer("1+") }.should raise_error(ArgumentError) + -> { Integer("1+++") }.should raise_error(ArgumentError) + end + + it "makes the number negative if there's a leading -" do + Integer("-1").should == -1 + end + + it "raises an ArgumentError if there are multiple leading -s" do + -> { Integer("--1") }.should raise_error(ArgumentError) + -> { Integer("---1") }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if there are trailing -s" do + -> { Integer("1-") }.should raise_error(ArgumentError) + -> { Integer("1---") }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if there is a period" do + -> { Integer("0.0") }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError for an empty String" do + -> { Integer("") }.should raise_error(ArgumentError) + end + + describe "when passed exception: false" do + describe "and multiple leading -s" do + it "swallows an error" do + Integer("---1", exception: false).should == nil + end + end + + describe "and multiple trailing -s" do + it "swallows an error" do + Integer("1---", exception: false).should == nil + end + end + + describe "and an argument that contains a period" do + it "swallows an error" do + Integer("0.0", exception: false).should == nil + end + end + + describe "and an empty string" do + it "swallows an error" do + Integer("", exception: false).should == nil + end + end + end + + it "parses the value as 0 if the string consists of a single zero character" do + Integer("0").should == 0 + end + + %w(x X).each do |x| + it "parses the value as a hex number if there's a leading 0#{x}" do + Integer("0#{x}1").should == 0x1 + Integer("0#{x}dd").should == 0xdd + end + + it "is a positive hex number if there's a leading +0#{x}" do + Integer("+0#{x}1").should == 0x1 + Integer("+0#{x}dd").should == 0xdd + end + + it "is a negative hex number if there's a leading -0#{x}" do + Integer("-0#{x}1").should == -0x1 + Integer("-0#{x}dd").should == -0xdd + end + + it "raises an ArgumentError if the number cannot be parsed as hex" do + -> { Integer("0#{x}g") }.should raise_error(ArgumentError) + end + end + + %w(b B).each do |b| + it "parses the value as a binary number if there's a leading 0#{b}" do + Integer("0#{b}1").should == 0b1 + Integer("0#{b}10").should == 0b10 + end + + it "is a positive binary number if there's a leading +0#{b}" do + Integer("+0#{b}1").should == 0b1 + Integer("+0#{b}10").should == 0b10 + end + + it "is a negative binary number if there's a leading -0#{b}" do + Integer("-0#{b}1").should == -0b1 + Integer("-0#{b}10").should == -0b10 + end + + it "raises an ArgumentError if the number cannot be parsed as binary" do + -> { Integer("0#{b}2") }.should raise_error(ArgumentError) + end + end + + ["o", "O", ""].each do |o| + it "parses the value as an octal number if there's a leading 0#{o}" do + Integer("0#{o}1").should == 0O1 + Integer("0#{o}10").should == 0O10 + end + + it "is a positive octal number if there's a leading +0#{o}" do + Integer("+0#{o}1").should == 0O1 + Integer("+0#{o}10").should == 0O10 + end + + it "is a negative octal number if there's a leading -0#{o}" do + Integer("-0#{o}1").should == -0O1 + Integer("-0#{o}10").should == -0O10 + end + + it "raises an ArgumentError if the number cannot be parsed as octal" do + -> { Integer("0#{o}9") }.should raise_error(ArgumentError) + end + end + + %w(D d).each do |d| + it "parses the value as a decimal number if there's a leading 0#{d}" do + Integer("0#{d}1").should == 1 + Integer("0#{d}10").should == 10 + end + + it "is a positive decimal number if there's a leading +0#{d}" do + Integer("+0#{d}1").should == 1 + Integer("+0#{d}10").should == 10 + end + + it "is a negative decimal number if there's a leading -0#{d}" do + Integer("-0#{d}1").should == -1 + Integer("-0#{d}10").should == -10 + end + + it "raises an ArgumentError if the number cannot be parsed as decimal" do + -> { Integer("0#{d}a") }.should raise_error(ArgumentError) + end + end +end + +describe :kernel_integer_string_base, shared: true do + it "raises an ArgumentError if the String is a null byte" do + -> { Integer("\0", 2) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if the String starts with a null byte" do + -> { Integer("\01", 3) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if the String ends with a null byte" do + -> { Integer("1\0", 4) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if the String contains a null byte" do + -> { Integer("1\01", 5) }.should raise_error(ArgumentError) + end + + it "ignores leading whitespace" do + Integer(" 16", 16).should == 22 + Integer(" 16", 16).should == 22 + Integer("\t\n16", 16).should == 22 + end + + it "ignores trailing whitespace" do + Integer("16 ", 16).should == 22 + Integer("16 ", 16).should == 22 + Integer("16\t\n", 16).should == 22 + end + + it "raises an ArgumentError if there are leading _s" do + -> { Integer("_1", 7) }.should raise_error(ArgumentError) + -> { Integer("___1", 7) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if there are trailing _s" do + -> { Integer("1_", 12) }.should raise_error(ArgumentError) + -> { Integer("1___", 12) }.should raise_error(ArgumentError) + end + + it "ignores an embedded _" do + Integer("1_1", 4).should == 5 + end + + it "raises an ArgumentError if there are multiple embedded _s" do + -> { Integer("1__1", 4) }.should raise_error(ArgumentError) + -> { Integer("1___1", 4) }.should raise_error(ArgumentError) + end + + it "ignores a single leading +" do + Integer("+10", 3).should == 3 + end + + it "raises an ArgumentError if there is a space between the + and number" do + -> { Integer("+ 1", 3) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if there are multiple leading +s" do + -> { Integer("++1", 3) }.should raise_error(ArgumentError) + -> { Integer("+++1", 3) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if there are trailing +s" do + -> { Integer("1+", 3) }.should raise_error(ArgumentError) + -> { Integer("1+++", 12) }.should raise_error(ArgumentError) + end + + it "makes the number negative if there's a leading -" do + Integer("-19", 20).should == -29 + end + + it "raises an ArgumentError if there are multiple leading -s" do + -> { Integer("--1", 9) }.should raise_error(ArgumentError) + -> { Integer("---1", 9) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if there are trailing -s" do + -> { Integer("1-", 12) }.should raise_error(ArgumentError) + -> { Integer("1---", 12) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if there is a period" do + -> { Integer("0.0", 3) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError for an empty String" do + -> { Integer("", 12) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError for a base of 1" do + -> { Integer("1", 1) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError for a base of 37" do + -> { Integer("1", 37) }.should raise_error(ArgumentError) + end + + it "accepts wholly lowercase alphabetic strings for bases > 10" do + Integer('ab',12).should == 131 + Integer('af',20).should == 215 + Integer('ghj',30).should == 14929 + end + + it "accepts wholly uppercase alphabetic strings for bases > 10" do + Integer('AB',12).should == 131 + Integer('AF',20).should == 215 + Integer('GHJ',30).should == 14929 + end + + it "accepts mixed-case alphabetic strings for bases > 10" do + Integer('Ab',12).should == 131 + Integer('aF',20).should == 215 + Integer('GhJ',30).should == 14929 + end + + it "accepts alphanumeric strings for bases > 10" do + Integer('a3e',19).should == 3681 + Integer('12q',31).should == 1049 + Integer('c00o',29).should == 292692 + end + + it "raises an ArgumentError for letters invalid in the given base" do + -> { Integer('z',19) }.should raise_error(ArgumentError) + -> { Integer('c00o',2) }.should raise_error(ArgumentError) + end + + %w(x X).each do |x| + it "parses the value as a hex number if there's a leading 0#{x} and a base of 16" do + Integer("0#{x}10", 16).should == 16 + Integer("0#{x}dd", 16).should == 221 + end + + it "is a positive hex number if there's a leading +0#{x} and base of 16" do + Integer("+0#{x}1", 16).should == 0x1 + Integer("+0#{x}dd", 16).should == 0xdd + end + + it "is a negative hex number if there's a leading -0#{x} and a base of 16" do + Integer("-0#{x}1", 16).should == -0x1 + Integer("-0#{x}dd", 16).should == -0xdd + end + + 2.upto(15) do |base| + it "raises an ArgumentError if the number begins with 0#{x} and the base is #{base}" do + -> { Integer("0#{x}1", base) }.should raise_error(ArgumentError) + end + end + + it "raises an ArgumentError if the number cannot be parsed as hex and the base is 16" do + -> { Integer("0#{x}g", 16) }.should raise_error(ArgumentError) + end + end + + %w(b B).each do |b| + it "parses the value as a binary number if there's a leading 0#{b} and the base is 2" do + Integer("0#{b}1", 2).should == 0b1 + Integer("0#{b}10", 2).should == 0b10 + end + + it "is a positive binary number if there's a leading +0#{b} and a base of 2" do + Integer("+0#{b}1", 2).should == 0b1 + Integer("+0#{b}10", 2).should == 0b10 + end + + it "is a negative binary number if there's a leading -0#{b} and a base of 2" do + Integer("-0#{b}1", 2).should == -0b1 + Integer("-0#{b}10", 2).should == -0b10 + end + + it "raises an ArgumentError if the number cannot be parsed as binary and the base is 2" do + -> { Integer("0#{b}2", 2) }.should raise_error(ArgumentError) + end + end + + ["o", "O"].each do |o| + it "parses the value as an octal number if there's a leading 0#{o} and a base of 8" do + Integer("0#{o}1", 8).should == 0O1 + Integer("0#{o}10", 8).should == 0O10 + end + + it "is a positive octal number if there's a leading +0#{o} and a base of 8" do + Integer("+0#{o}1", 8).should == 0O1 + Integer("+0#{o}10", 8).should == 0O10 + end + + it "is a negative octal number if there's a leading -0#{o} and a base of 8" do + Integer("-0#{o}1", 8).should == -0O1 + Integer("-0#{o}10", 8).should == -0O10 + end + + it "raises an ArgumentError if the number cannot be parsed as octal and the base is 8" do + -> { Integer("0#{o}9", 8) }.should raise_error(ArgumentError) + end + + 2.upto(7) do |base| + it "raises an ArgumentError if the number begins with 0#{o} and the base is #{base}" do + -> { Integer("0#{o}1", base) }.should raise_error(ArgumentError) + end + end + end + + %w(D d).each do |d| + it "parses the value as a decimal number if there's a leading 0#{d} and a base of 10" do + Integer("0#{d}1", 10).should == 1 + Integer("0#{d}10",10).should == 10 + end + + it "is a positive decimal number if there's a leading +0#{d} and a base of 10" do + Integer("+0#{d}1", 10).should == 1 + Integer("+0#{d}10", 10).should == 10 + end + + it "is a negative decimal number if there's a leading -0#{d} and a base of 10" do + Integer("-0#{d}1", 10).should == -1 + Integer("-0#{d}10", 10).should == -10 + end + + it "raises an ArgumentError if the number cannot be parsed as decimal and the base is 10" do + -> { Integer("0#{d}a", 10) }.should raise_error(ArgumentError) + end + + 2.upto(9) do |base| + it "raises an ArgumentError if the number begins with 0#{d} and the base is #{base}" do + -> { Integer("0#{d}1", base) }.should raise_error(ArgumentError) + end + end + end + + it "raises an ArgumentError if a base is given for a non-String value" do + -> { Integer(98, 15) }.should raise_error(ArgumentError) + end + + it "tries to convert the base to an integer using to_int" do + obj = mock('8') + obj.should_receive(:to_int).and_return(8) + + Integer("777", obj).should == 0777 + end + + # https://bugs.ruby-lang.org/issues/19349 + ruby_version_is ''...'3.3' do + it "ignores the base if it is not an integer and does not respond to #to_i" do + Integer("777", "8").should == 777 + end + end + + ruby_version_is '3.3' do + it "raises a TypeError if it is not an integer and does not respond to #to_i" do + -> { + Integer("777", "8") + }.should raise_error(TypeError, "no implicit conversion of String into Integer") + end + end + + describe "when passed exception: false" do + describe "and valid argument" do + it "returns an Integer number" do + Integer("100", 10, exception: false).should == 100 + Integer("100", 2, exception: false).should == 4 + end + end + + describe "and invalid argument" do + it "swallows an error" do + Integer("999", 2, exception: false).should == nil + Integer("abc", 10, exception: false).should == nil + end + end + end +end + +describe :kernel_Integer, shared: true do + it "raises an ArgumentError when the String contains digits out of range of radix 2" do + str = "23456789abcdefghijklmnopqrstuvwxyz" + -> { @object.send(@method, str, 2) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when the String contains digits out of range of radix 3" do + str = "3456789abcdefghijklmnopqrstuvwxyz" + -> { @object.send(@method, str, 3) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when the String contains digits out of range of radix 4" do + str = "456789abcdefghijklmnopqrstuvwxyz" + -> { @object.send(@method, str, 4) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when the String contains digits out of range of radix 5" do + str = "56789abcdefghijklmnopqrstuvwxyz" + -> { @object.send(@method, str, 5) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when the String contains digits out of range of radix 6" do + str = "6789abcdefghijklmnopqrstuvwxyz" + -> { @object.send(@method, str, 6) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when the String contains digits out of range of radix 7" do + str = "789abcdefghijklmnopqrstuvwxyz" + -> { @object.send(@method, str, 7) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when the String contains digits out of range of radix 8" do + str = "89abcdefghijklmnopqrstuvwxyz" + -> { @object.send(@method, str, 8) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when the String contains digits out of range of radix 9" do + str = "9abcdefghijklmnopqrstuvwxyz" + -> { @object.send(@method, str, 9) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when the String contains digits out of range of radix 10" do + str = "abcdefghijklmnopqrstuvwxyz" + -> { @object.send(@method, str, 10) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when the String contains digits out of range of radix 11" do + str = "bcdefghijklmnopqrstuvwxyz" + -> { @object.send(@method, str, 11) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when the String contains digits out of range of radix 12" do + str = "cdefghijklmnopqrstuvwxyz" + -> { @object.send(@method, str, 12) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when the String contains digits out of range of radix 13" do + str = "defghijklmnopqrstuvwxyz" + -> { @object.send(@method, str, 13) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when the String contains digits out of range of radix 14" do + str = "efghijklmnopqrstuvwxyz" + -> { @object.send(@method, str, 14) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when the String contains digits out of range of radix 15" do + str = "fghijklmnopqrstuvwxyz" + -> { @object.send(@method, str, 15) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when the String contains digits out of range of radix 16" do + str = "ghijklmnopqrstuvwxyz" + -> { @object.send(@method, str, 16) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when the String contains digits out of range of radix 17" do + str = "hijklmnopqrstuvwxyz" + -> { @object.send(@method, str, 17) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when the String contains digits out of range of radix 18" do + str = "ijklmnopqrstuvwxyz" + -> { @object.send(@method, str, 18) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when the String contains digits out of range of radix 19" do + str = "jklmnopqrstuvwxyz" + -> { @object.send(@method, str, 19) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when the String contains digits out of range of radix 20" do + str = "klmnopqrstuvwxyz" + -> { @object.send(@method, str, 20) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when the String contains digits out of range of radix 21" do + str = "lmnopqrstuvwxyz" + -> { @object.send(@method, str, 21) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when the String contains digits out of range of radix 22" do + str = "mnopqrstuvwxyz" + -> { @object.send(@method, str, 22) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when the String contains digits out of range of radix 23" do + str = "nopqrstuvwxyz" + -> { @object.send(@method, str, 23) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when the String contains digits out of range of radix 24" do + str = "opqrstuvwxyz" + -> { @object.send(@method, str, 24) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when the String contains digits out of range of radix 25" do + str = "pqrstuvwxyz" + -> { @object.send(@method, str, 25) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when the String contains digits out of range of radix 26" do + str = "qrstuvwxyz" + -> { @object.send(@method, str, 26) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when the String contains digits out of range of radix 27" do + str = "rstuvwxyz" + -> { @object.send(@method, str, 27) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when the String contains digits out of range of radix 28" do + str = "stuvwxyz" + -> { @object.send(@method, str, 28) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when the String contains digits out of range of radix 29" do + str = "tuvwxyz" + -> { @object.send(@method, str, 29) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when the String contains digits out of range of radix 30" do + str = "uvwxyz" + -> { @object.send(@method, str, 30) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when the String contains digits out of range of radix 31" do + str = "vwxyz" + -> { @object.send(@method, str, 31) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when the String contains digits out of range of radix 32" do + str = "wxyz" + -> { @object.send(@method, str, 32) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when the String contains digits out of range of radix 33" do + str = "xyz" + -> { @object.send(@method, str, 33) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when the String contains digits out of range of radix 34" do + str = "yz" + -> { @object.send(@method, str, 34) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when the String contains digits out of range of radix 35" do + str = "z" + -> { @object.send(@method, str, 35) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when the String contains digits out of range of radix 36" do + -> { @object.send(@method, "{", 36) }.should raise_error(ArgumentError) + end +end + +describe "Kernel.Integer" do + it_behaves_like :kernel_Integer, :Integer_method, KernelSpecs + + # TODO: fix these specs + it_behaves_like :kernel_integer, :Integer, Kernel + it_behaves_like :kernel_integer_string, :Integer + + it_behaves_like :kernel_integer_string_base, :Integer + + it "is a public method" do + Kernel.Integer(10).should == 10 + end +end + +describe "Kernel#Integer" do + it_behaves_like :kernel_Integer, :Integer_function, KernelSpecs + + # TODO: fix these specs + it_behaves_like :kernel_integer, :Integer, Object.new + it_behaves_like :kernel_integer_string, :Integer + + it_behaves_like :kernel_integer_string_base, :Integer + + it "is a private method" do + Kernel.should have_private_instance_method(:Integer) + end +end diff --git a/spec/ruby/core/kernel/Rational_spec.rb b/spec/ruby/core/kernel/Rational_spec.rb new file mode 100644 index 0000000000..cc11a35451 --- /dev/null +++ b/spec/ruby/core/kernel/Rational_spec.rb @@ -0,0 +1,236 @@ +require_relative '../../spec_helper' +require_relative '../rational/fixtures/rational' + +describe "Kernel.Rational" do + describe "passed Integer" do + # Guard against the Mathn library + guard -> { !defined?(Math.rsqrt) } do + it "returns a new Rational number with 1 as the denominator" do + Rational(1).should eql(Rational(1, 1)) + Rational(-3).should eql(Rational(-3, 1)) + Rational(bignum_value).should eql(Rational(bignum_value, 1)) + end + end + end + + describe "passed two integers" do + it "returns a new Rational number" do + rat = Rational(1, 2) + rat.numerator.should == 1 + rat.denominator.should == 2 + rat.should be_an_instance_of(Rational) + + rat = Rational(-3, -5) + rat.numerator.should == 3 + rat.denominator.should == 5 + rat.should be_an_instance_of(Rational) + + rat = Rational(bignum_value, 3) + rat.numerator.should == bignum_value + rat.denominator.should == 3 + rat.should be_an_instance_of(Rational) + end + + it "reduces the Rational" do + rat = Rational(2, 4) + rat.numerator.should == 1 + rat.denominator.should == 2 + + rat = Rational(3, 9) + rat.numerator.should == 1 + rat.denominator.should == 3 + end + end + + describe "when passed a String" do + it "converts the String to a Rational using the same method as String#to_r" do + r = Rational(13, 25) + s_r = ".52".to_r + r_s = Rational(".52") + + r_s.should == r + r_s.should == s_r + end + + it "scales the Rational value of the first argument by the Rational value of the second" do + Rational(".52", ".6").should == Rational(13, 15) + Rational(".52", "1.6").should == Rational(13, 40) + end + + it "does not use the same method as Float#to_r" do + r = Rational(3, 5) + f_r = 0.6.to_r + r_s = Rational("0.6") + + r_s.should == r + r_s.should_not == f_r + end + end + + describe "when passed a Numeric" do + it "calls #to_r to convert the first argument to a Rational" do + num = RationalSpecs::SubNumeric.new(2) + + Rational(num).should == Rational(2) + end + end + + describe "when passed a Complex" do + context "[Complex]" do + it "returns a Rational from the real part if the imaginary part is 0" do + Rational(Complex(1, 0)).should == Rational(1) + end + + it "raises a RangeError if the imaginary part is not 0" do + -> { Rational(Complex(1, 2)) }.should raise_error(RangeError, "can't convert 1+2i into Rational") + end + end + + context "[Numeric, Complex]" do + it "uses the real part if the imaginary part is 0" do + Rational(1, Complex(2, 0)).should == Rational(1, 2) + end + + it "divides a numerator by the Complex denominator if the imaginary part is not 0" do + Rational(1, Complex(2, 1)).should == Complex(2/5r, -1/5r) + end + end + end + + context "when passed neither a Numeric nor a String" do + it "converts to Rational with #to_r method" do + obj = Object.new + def obj.to_r; 1/2r; end + + Rational(obj).should == 1/2r + end + + it "tries to convert to Integer with #to_int method if it does not respond to #to_r" do + obj = Object.new + def obj.to_int; 1; end + + Rational(obj).should == 1r + end + + it "raises TypeError if it neither responds to #to_r nor #to_int method" do + -> { Rational([]) }.should raise_error(TypeError, "can't convert Array into Rational") + -> { Rational({}) }.should raise_error(TypeError, "can't convert Hash into Rational") + -> { Rational(nil) }.should raise_error(TypeError, "can't convert nil into Rational") + end + + it "swallows exception raised in #to_int method" do + object = Object.new + def object.to_int() raise NoMethodError; end + + -> { Rational(object) }.should raise_error(TypeError) + -> { Rational(object, 1) }.should raise_error(TypeError) + -> { Rational(1, object) }.should raise_error(TypeError) + end + + it "raises TypeError if #to_r does not return Rational" do + obj = Object.new + def obj.to_r; []; end + + -> { Rational(obj) }.should raise_error(TypeError, "can't convert Object to Rational (Object#to_r gives Array)") + end + end + + it "raises a ZeroDivisionError if the second argument is 0" do + -> { Rational(1, 0) }.should raise_error(ZeroDivisionError, "divided by 0") + -> { Rational(1, 0.0) }.should raise_error(ZeroDivisionError, "divided by 0") + end + + it "raises a TypeError if the first argument is nil" do + -> { Rational(nil) }.should raise_error(TypeError, "can't convert nil into Rational") + end + + it "raises a TypeError if the second argument is nil" do + -> { Rational(1, nil) }.should raise_error(TypeError, "can't convert nil into Rational") + end + + it "raises a TypeError if the first argument is a Symbol" do + -> { Rational(:sym) }.should raise_error(TypeError) + end + + it "raises a TypeError if the second argument is a Symbol" do + -> { Rational(1, :sym) }.should raise_error(TypeError) + end + + describe "when passed exception: false" do + describe "and [non-Numeric]" do + it "swallows an error" do + Rational(:sym, exception: false).should == nil + Rational("abc", exception: false).should == nil + end + + it "swallows an exception raised in #to_r" do + obj = Object.new + def obj.to_r; raise; end + Rational(obj, exception: false).should == nil + end + + it "swallows an exception raised in #to_int" do + obj = Object.new + def obj.to_int; raise; end + Rational(obj, exception: false).should == nil + end + end + + describe "and [non-Numeric, Numeric]" do + it "swallows an error" do + Rational(:sym, 1, exception: false).should == nil + Rational("abc", 1, exception: false).should == nil + end + + it "swallows an exception raised in #to_r" do + obj = Object.new + def obj.to_r; raise; end + Rational(obj, 1, exception: false).should == nil + end + + it "swallows an exception raised in #to_int" do + obj = Object.new + def obj.to_int; raise; end + Rational(obj, 1, exception: false).should == nil + end + end + + describe "and [anything, non-Numeric]" do + it "swallows an error" do + Rational(:sym, :sym, exception: false).should == nil + Rational("abc", :sym, exception: false).should == nil + end + + it "swallows an exception raised in #to_r" do + obj = Object.new + def obj.to_r; raise; end + Rational(obj, obj, exception: false).should == nil + end + + it "swallows an exception raised in #to_int" do + obj = Object.new + def obj.to_int; raise; end + Rational(obj, obj, exception: false).should == nil + end + end + + describe "and non-Numeric String arguments" do + it "swallows an error" do + Rational("a", "b", exception: false).should == nil + Rational("a", 0, exception: false).should == nil + Rational(0, "b", exception: false).should == nil + end + end + + describe "and nil arguments" do + it "swallows an error" do + Rational(nil, exception: false).should == nil + Rational(nil, nil, exception: false).should == nil + end + end + end + + it "freezes its result" do + Rational(1).frozen?.should == true + end +end diff --git a/spec/ruby/core/kernel/String_spec.rb b/spec/ruby/core/kernel/String_spec.rb new file mode 100644 index 0000000000..7caec6eda5 --- /dev/null +++ b/spec/ruby/core/kernel/String_spec.rb @@ -0,0 +1,106 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe :kernel_String, shared: true do + it "converts nil to a String" do + @object.send(@method, nil).should == "" + end + + it "converts a Float to a String" do + @object.send(@method, 1.12).should == "1.12" + end + + it "converts a boolean to a String" do + @object.send(@method, false).should == "false" + @object.send(@method, true).should == "true" + end + + it "converts a constant to a String" do + @object.send(@method, Object).should == "Object" + end + + it "calls #to_s to convert an arbitrary object to a String" do + obj = mock('test') + obj.should_receive(:to_s).and_return("test") + + @object.send(@method, obj).should == "test" + end + + it "raises a TypeError if #to_s does not exist" do + obj = mock('to_s') + class << obj + undef_method :to_s + end + + -> { @object.send(@method, obj) }.should raise_error(TypeError) + end + + # #5158 + it "raises a TypeError if respond_to? returns false for #to_s" do + obj = mock("to_s") + class << obj + def respond_to?(meth, include_private=false) + meth == :to_s ? false : super + end + end + + -> { @object.send(@method, obj) }.should raise_error(TypeError) + end + + it "raises a TypeError if #to_s is not defined, even though #respond_to?(:to_s) returns true" do + # cannot use a mock because of how RSpec affects #method_missing + obj = Object.new + class << obj + undef_method :to_s + def respond_to?(meth, include_private=false) + meth == :to_s ? true : super + end + end + + -> { @object.send(@method, obj) }.should raise_error(TypeError) + end + + it "calls #to_s if #respond_to?(:to_s) returns true" do + obj = mock('to_s') + class << obj + undef_method :to_s + def method_missing(meth, *args) + meth == :to_s ? "test" : super + end + end + + @object.send(@method, obj).should == "test" + end + + it "raises a TypeError if #to_s does not return a String" do + (obj = mock('123')).should_receive(:to_s).and_return(123) + -> { @object.send(@method, obj) }.should raise_error(TypeError) + end + + it "returns the same object if it is already a String" do + string = +"Hello" + string.should_not_receive(:to_s) + string2 = @object.send(@method, string) + string.should equal(string2) + end + + it "returns the same object if it is an instance of a String subclass" do + subklass = Class.new(String) + string = subklass.new("Hello") + string.should_not_receive(:to_s) + string2 = @object.send(@method, string) + string.should equal(string2) + end +end + +describe "Kernel.String" do + it_behaves_like :kernel_String, :String, Kernel +end + +describe "Kernel#String" do + it_behaves_like :kernel_String, :String, Object.new + + it "is a private method" do + Kernel.should have_private_instance_method(:String) + end +end diff --git a/spec/ruby/core/kernel/__callee___spec.rb b/spec/ruby/core/kernel/__callee___spec.rb new file mode 100644 index 0000000000..3059ea8b57 --- /dev/null +++ b/spec/ruby/core/kernel/__callee___spec.rb @@ -0,0 +1,48 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/__callee__' + +describe "Kernel.__callee__" do + it "returns the current method, even when aliased" do + KernelSpecs::CalleeTest.new.f.should == :f + end + + it "returns the aliased name when aliased method" do + KernelSpecs::CalleeTest.new.g.should == :g + end + + it "returns the caller from blocks too" do + KernelSpecs::CalleeTest.new.in_block.should == [:in_block, :in_block] + end + + it "returns the caller from define_method too" do + KernelSpecs::CalleeTest.new.dm.should == :dm + end + + it "returns the caller from block inside define_method too" do + KernelSpecs::CalleeTest.new.dm_block.should == [:dm_block, :dm_block] + end + + it "returns method name even from send" do + KernelSpecs::CalleeTest.new.from_send.should == :from_send + end + + it "returns method name even from eval" do + KernelSpecs::CalleeTest.new.from_eval.should == :from_eval + end + + it "returns nil from inside a class body" do + KernelSpecs::CalleeTest.new.from_class_body.should == nil + end + + it "returns nil when not called from a method" do + __callee__.should == nil + end + + it "returns the caller from a define_method called from the same class" do + c = Class.new do + define_method(:f) { 1.times{ break __callee__ } } + def g; f end + end + c.new.g.should == :f + end +end diff --git a/spec/ruby/core/kernel/__dir___spec.rb b/spec/ruby/core/kernel/__dir___spec.rb new file mode 100644 index 0000000000..242adbf48b --- /dev/null +++ b/spec/ruby/core/kernel/__dir___spec.rb @@ -0,0 +1,27 @@ +require_relative '../../spec_helper' + +describe "Kernel#__dir__" do + it "returns the real name of the directory containing the currently-executing file" do + __dir__.should == File.realpath(File.dirname(__FILE__)) + end + + it "returns the expanded path of the directory when used in the main script" do + fixtures_dir = File.dirname(fixture(__FILE__, '__dir__.rb')) + Dir.chdir(fixtures_dir) do + ruby_exe("__dir__.rb").should == "__dir__.rb\n#{fixtures_dir}\n" + end + end + + context "when used in eval with a given filename" do + it "returns File.dirname(filename)" do + eval("__dir__", nil, "foo.rb").should == "." + eval("__dir__", nil, "foo/bar.rb").should == "foo" + end + end + + context "when used in eval with top level binding" do + it "returns nil" do + eval("__dir__", binding).should == nil + end + end +end diff --git a/spec/ruby/core/kernel/__method___spec.rb b/spec/ruby/core/kernel/__method___spec.rb new file mode 100644 index 0000000000..578d25640d --- /dev/null +++ b/spec/ruby/core/kernel/__method___spec.rb @@ -0,0 +1,40 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/__method__' + +describe "Kernel.__method__" do + it "returns the current method, even when aliased" do + KernelSpecs::MethodTest.new.f.should == :f + end + + it "returns the original name when aliased method" do + KernelSpecs::MethodTest.new.g.should == :f + end + + it "returns the caller from blocks too" do + KernelSpecs::MethodTest.new.in_block.should == [:in_block, :in_block] + end + + it "returns the caller from define_method too" do + KernelSpecs::MethodTest.new.dm.should == :dm + end + + it "returns the caller from block inside define_method too" do + KernelSpecs::MethodTest.new.dm_block.should == [:dm_block, :dm_block] + end + + it "returns method name even from send" do + KernelSpecs::MethodTest.new.from_send.should == :from_send + end + + it "returns method name even from eval" do + KernelSpecs::MethodTest.new.from_eval.should == :from_eval + end + + it "returns nil from inside a class body" do + KernelSpecs::MethodTest.new.from_class_body.should == nil + end + + it "returns nil when not called from a method" do + __method__.should == nil + end +end diff --git a/spec/ruby/core/kernel/abort_spec.rb b/spec/ruby/core/kernel/abort_spec.rb new file mode 100644 index 0000000000..f8152718c5 --- /dev/null +++ b/spec/ruby/core/kernel/abort_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative '../../shared/process/abort' + +describe "Kernel#abort" do + it "is a private method" do + Kernel.should have_private_instance_method(:abort) + end + + it_behaves_like :process_abort, :abort, KernelSpecs::Method.new +end + +describe "Kernel.abort" do + it_behaves_like :process_abort, :abort, Kernel +end diff --git a/spec/ruby/core/kernel/at_exit_spec.rb b/spec/ruby/core/kernel/at_exit_spec.rb new file mode 100644 index 0000000000..ebd9a71d15 --- /dev/null +++ b/spec/ruby/core/kernel/at_exit_spec.rb @@ -0,0 +1,19 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative '../../shared/kernel/at_exit' + +describe "Kernel.at_exit" do + it_behaves_like :kernel_at_exit, :at_exit + + it "is a private method" do + Kernel.should have_private_instance_method(:at_exit) + end + + it "raises ArgumentError if called without a block" do + -> { at_exit }.should raise_error(ArgumentError, "called without a block") + end +end + +describe "Kernel#at_exit" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/kernel/autoload_spec.rb b/spec/ruby/core/kernel/autoload_spec.rb new file mode 100644 index 0000000000..5edb70541d --- /dev/null +++ b/spec/ruby/core/kernel/autoload_spec.rb @@ -0,0 +1,178 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +# These specs only illustrate the basic autoload cases +# and where toplevel autoload behaves differently from +# Module#autoload. See those specs for more examples. + +autoload :KSAutoloadA, "autoload_a.rb" +autoload :KSAutoloadB, fixture(__FILE__, "autoload_b.rb") +define_autoload_KSAutoloadCallsRequire = -> { + autoload :KSAutoloadCallsRequire, "main_autoload_not_exist.rb" +} + +def check_autoload(const) + autoload? const +end + +describe "Kernel#autoload" do + before :each do + @loaded_features = $".dup + end + + after :each do + $".replace @loaded_features + end + + it "is a private method" do + Kernel.should have_private_instance_method(:autoload) + end + + it "registers a file to load the first time the named constant is accessed" do + Object.autoload?(:KSAutoloadA).should == "autoload_a.rb" + end + + it "registers a file to load the first time the named constant is accessed" do + check_autoload(:KSAutoloadA).should == "autoload_a.rb" + end + + it "sets the autoload constant in Object's constant table" do + Object.should have_constant(:KSAutoloadA) + end + + it "loads the file when the constant is accessed" do + KSAutoloadB.loaded.should == :ksautoload_b + end + + it "calls main.require(path) to load the file" do + define_autoload_KSAutoloadCallsRequire.call + main = TOPLEVEL_BINDING.eval("self") + main.should_receive(:require).with("main_autoload_not_exist.rb") + # The constant won't be defined since require is mocked to do nothing + -> { KSAutoloadCallsRequire }.should raise_error(NameError) + end + + it "can autoload in instance_eval" do + Object.new.instance_eval do + autoload :KSAutoloadD, fixture(__FILE__, "autoload_d.rb") + KSAutoloadD.loaded.should == :ksautoload_d + end + end + + describe "inside a Class.new method body" do + # NOTE: this spec is being discussed in https://github.com/ruby/spec/pull/839 + it "should define on the new anonymous class" do + cls = Class.new do + def go + autoload :Object, 'bogus' + autoload? :Object + end + end + + cls.new.go.should == 'bogus' + cls.autoload?(:Object).should == 'bogus' + end + end + + describe "when Object is frozen" do + it "raises a FrozenError before defining the constant" do + ruby_exe(fixture(__FILE__, "autoload_frozen.rb")).should == "FrozenError - nil" + end + end + + describe "when called from included module's method" do + before :all do + @path = fixture(__FILE__, "autoload_from_included_module.rb") + KernelSpecs::AutoloadMethodIncluder.new.setup_autoload(@path) + end + + it "setups the autoload on the included module" do + KernelSpecs::AutoloadMethod.autoload?(:AutoloadFromIncludedModule).should == @path + end + + it "the autoload is reachable from the class too" do + KernelSpecs::AutoloadMethodIncluder.autoload?(:AutoloadFromIncludedModule).should == @path + end + + it "the autoload relative to the included module works" do + KernelSpecs::AutoloadMethod::AutoloadFromIncludedModule.loaded.should == :autoload_from_included_module + end + end +end + +describe "Kernel#autoload?" do + it "is a private method" do + Kernel.should have_private_instance_method(:autoload?) + end + + it "returns the name of the file that will be autoloaded" do + check_autoload(:KSAutoloadA).should == "autoload_a.rb" + end + + it "returns nil if no file has been registered for a constant" do + check_autoload(:Manualload).should be_nil + end +end + +Kernel.autoload :KSAutoloadBB, "no_autoload.rb" + +describe "Kernel.autoload" do + before :all do + @non_existent = fixture __FILE__, "no_autoload.rb" + end + + before :each do + @loaded_features = $".dup + + ScratchPad.clear + end + + after :each do + $".replace @loaded_features + end + + it "registers a file to load the first time the toplevel constant is accessed" do + Kernel.autoload :KSAutoloadAA, @non_existent + Kernel.autoload?(:KSAutoloadAA).should == @non_existent + end + + it "sets the autoload constant in Object's constant table" do + Object.should have_constant(:KSAutoloadBB) + end + + it "calls #to_path on non-String filenames" do + p = mock('path') + p.should_receive(:to_path).and_return @non_existent + Kernel.autoload :KSAutoloadAA, p + end + + describe "when called from included module's method" do + before :all do + @path = fixture(__FILE__, "autoload_from_included_module2.rb") + KernelSpecs::AutoloadMethodIncluder2.new.setup_autoload(@path) + end + + it "setups the autoload on the included module" do + KernelSpecs::AutoloadMethod2.autoload?(:AutoloadFromIncludedModule2).should == @path + end + + it "the autoload is reachable from the class too" do + KernelSpecs::AutoloadMethodIncluder2.autoload?(:AutoloadFromIncludedModule2).should == @path + end + + it "the autoload relative to the included module works" do + KernelSpecs::AutoloadMethod2::AutoloadFromIncludedModule2.loaded.should == :autoload_from_included_module2 + end + end +end + +describe "Kernel.autoload?" do + it "returns the name of the file that will be autoloaded" do + Kernel.autoload :KSAutoload, "autoload.rb" + Kernel.autoload?(:KSAutoload).should == "autoload.rb" + end + + it "returns nil if no file has been registered for a constant" do + Kernel.autoload?(:Manualload).should be_nil + end +end diff --git a/spec/ruby/core/kernel/backtick_spec.rb b/spec/ruby/core/kernel/backtick_spec.rb new file mode 100644 index 0000000000..834d5636c1 --- /dev/null +++ b/spec/ruby/core/kernel/backtick_spec.rb @@ -0,0 +1,84 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel#`" do + before :each do + @original_external = Encoding.default_external + end + + after :each do + Encoding.default_external = @original_external + end + + it "is a private method" do + Kernel.should have_private_instance_method(:`) + end + + it "returns the standard output of the executed sub-process" do + ip = 'world' + `echo disc #{ip}`.should == "disc world\n" + end + + it "lets the standard error stream pass through to the inherited stderr" do + cmd = ruby_cmd('STDERR.print "error stream"') + -> { + `#{cmd}`.should == "" + }.should output_to_fd("error stream", STDERR) + end + + it "produces a String in the default external encoding" do + Encoding.default_external = Encoding::SHIFT_JIS + `echo disc`.encoding.should equal(Encoding::SHIFT_JIS) + end + + it "raises an Errno::ENOENT if the command is not executable" do + -> { `nonexistent_command` }.should raise_error(Errno::ENOENT) + end + + platform_is_not :windows do + it "handles invalid UTF-8 bytes in command" do + `echo "testing\xC2 a non UTF-8 string"`.b.should == "testing\xC2 a non UTF-8 string\n".b + end + + it "sets $? to the exit status of the executed sub-process" do + ip = 'world' + `echo disc #{ip}` + $?.should be_kind_of(Process::Status) + $?.should_not.stopped? + $?.should.exited? + $?.exitstatus.should == 0 + $?.should.success? + `echo disc #{ip}; exit 99` + $?.should be_kind_of(Process::Status) + $?.should_not.stopped? + $?.should.exited? + $?.exitstatus.should == 99 + $?.should_not.success? + end + end + + platform_is :windows do + it "sets $? to the exit status of the executed sub-process" do + ip = 'world' + `echo disc #{ip}` + $?.should be_kind_of(Process::Status) + $?.should_not.stopped? + $?.should.exited? + $?.exitstatus.should == 0 + $?.should.success? + `echo disc #{ip}& exit 99` + $?.should be_kind_of(Process::Status) + $?.should_not.stopped? + $?.should.exited? + $?.exitstatus.should == 99 + $?.should_not.success? + end + end +end + +describe "Kernel.`" do + it "tries to convert the given argument to String using #to_str" do + (obj = mock('echo test')).should_receive(:to_str).and_return("echo test") + Kernel.`(obj).should == "test\n" #` fix vim syntax highlighting + end +end diff --git a/spec/ruby/core/kernel/binding_spec.rb b/spec/ruby/core/kernel/binding_spec.rb new file mode 100644 index 0000000000..f1c9c6ec9f --- /dev/null +++ b/spec/ruby/core/kernel/binding_spec.rb @@ -0,0 +1,51 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel.binding" do + it "returns a binding for the caller" do + Kernel.binding.eval("self").should == self + end +end + +describe "Kernel#binding" do + it "is a private method" do + Kernel.should have_private_instance_method(:binding) + end + + before :each do + @b1 = KernelSpecs::Binding.new(99).get_binding + ScratchPad.clear + end + + it "returns a Binding object" do + @b1.kind_of?(Binding).should == true + end + + it "encapsulates the execution context properly" do + eval("@secret", @b1).should == 100 + eval("a", @b1).should == true + eval("b", @b1).should == true + eval("@@super_secret", @b1).should == "password" + + eval("square(2)", @b1).should == 4 + eval("self.square(2)", @b1).should == 4 + + eval("a = false", @b1) + eval("a", @b1).should == false + end + + it "raises a NameError on undefined variable" do + -> { eval("a_fake_variable", @b1) }.should raise_error(NameError) + end + + it "uses the closure's self as self in the binding" do + m = mock(:whatever) + eval('self', m.send(:binding)).should == self + end + + it "uses the class as self in a Class.new block" do + m = mock(:whatever) + cls = Class.new { ScratchPad.record eval('self', m.send(:binding)) } + ScratchPad.recorded.should == cls + end +end diff --git a/spec/ruby/core/kernel/block_given_spec.rb b/spec/ruby/core/kernel/block_given_spec.rb new file mode 100644 index 0000000000..aece4c821d --- /dev/null +++ b/spec/ruby/core/kernel/block_given_spec.rb @@ -0,0 +1,43 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe :kernel_block_given, shared: true do + it "returns true if and only if a block is supplied" do + @object.accept_block {}.should == true + @object.accept_block_as_argument {}.should == true + @object.accept_block_inside_block {}.should == true + @object.accept_block_as_argument_inside_block {}.should == true + + @object.accept_block.should == false + @object.accept_block_as_argument.should == false + @object.accept_block_inside_block.should == false + @object.accept_block_as_argument_inside_block.should == false + end + + # Clarify: Based on http://www.ruby-forum.com/topic/137822 it appears + # that Matz wanted this to be true in 1.9. + it "returns false when a method defined by define_method is called with a block" do + @object.defined_block {}.should == false + @object.defined_block_inside_block {}.should == false + end +end + +describe "Kernel#block_given?" do + it_behaves_like :kernel_block_given, :block_given?, KernelSpecs::BlockGiven + + it "returns false outside of a method" do + block_given?.should == false + end + + it "is a private method" do + Kernel.should have_private_instance_method(:block_given?) + end +end + +describe "Kernel.block_given?" do + it_behaves_like :kernel_block_given, :block_given?, KernelSpecs::KernelBlockGiven +end + +describe "self.send(:block_given?)" do + it_behaves_like :kernel_block_given, :block_given?, KernelSpecs::SelfBlockGiven +end diff --git a/spec/ruby/core/kernel/caller_locations_spec.rb b/spec/ruby/core/kernel/caller_locations_spec.rb new file mode 100644 index 0000000000..a917dba504 --- /dev/null +++ b/spec/ruby/core/kernel/caller_locations_spec.rb @@ -0,0 +1,110 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/caller_locations' + +describe 'Kernel#caller_locations' do + it 'is a private method' do + Kernel.should have_private_instance_method(:caller_locations) + end + + it 'returns an Array of caller locations' do + KernelSpecs::CallerLocationsTest.locations.should_not.empty? + end + + it 'returns an Array of caller locations using a custom offset' do + locations = KernelSpecs::CallerLocationsTest.locations(2) + + locations[0].absolute_path.should.end_with?('mspec.rb') + end + + it 'returns an Array of caller locations using a custom limit' do + locations = KernelSpecs::CallerLocationsTest.locations(1, 1) + + locations.length.should == 1 + end + + it "can be called with a range" do + locations1 = caller_locations(0) + locations2 = caller_locations(2..4) + locations1[2..4].map(&:to_s).should == locations2.map(&:to_s) + end + + it "works with endless ranges" do + locations1 = caller_locations(0) + locations2 = caller_locations(eval("(2..)")) + locations2.map(&:to_s).should == locations1[2..-1].map(&:to_s) + end + + it "works with beginless ranges" do + locations1 = caller_locations(0) + locations2 = caller_locations((...5)) + locations2.map(&:to_s)[eval("(2..)")].should == locations1[(...5)].map(&:to_s)[eval("(2..)")] + end + + it "can be called with a range whose end is negative" do + locations1 = caller_locations(0) + locations2 = caller_locations(2..-1) + locations3 = caller_locations(2..-2) + locations1[2..-1].map(&:to_s).should == locations2.map(&:to_s) + locations1[2..-2].map(&:to_s).should == locations3.map(&:to_s) + end + + it "must return nil if omitting more locations than available" do + caller_locations(100).should == nil + caller_locations(100..-1).should == nil + end + + it "must return [] if omitting exactly the number of locations available" do + omit = caller_locations(0).length + caller_locations(omit).should == [] + end + + it 'returns the locations as Thread::Backtrace::Location instances' do + locations = KernelSpecs::CallerLocationsTest.locations + + locations.each do |location| + location.kind_of?(Thread::Backtrace::Location).should == true + end + end + + it "must return the same locations when called with 1..-1 and when called with no arguments" do + caller_locations.map(&:to_s).should == caller_locations(1..-1).map(&:to_s) + end + + guard -> { Kernel.instance_method(:tap).source_location } do + ruby_version_is ""..."3.4" do + it "includes core library methods defined in Ruby" do + file, line = Kernel.instance_method(:tap).source_location + file.should.start_with?('<internal:') + + loc = nil + tap { loc = caller_locations(1, 1)[0] } + loc.label.should == "tap" + loc.path.should.start_with? "<internal:" + end + end + + ruby_version_is "3.4"..."4.0" do + it "includes core library methods defined in Ruby" do + file, line = Kernel.instance_method(:tap).source_location + file.should.start_with?('<internal:') + + loc = nil + tap { loc = caller_locations(1, 1)[0] } + loc.label.should == "Kernel#tap" + loc.path.should.start_with? "<internal:" + end + end + + ruby_version_is "4.0" do + it "does not include core library methods defined in Ruby" do + file, line = Kernel.instance_method(:tap).source_location + file.should.start_with?('<internal:') + + loc = nil + tap { loc = caller_locations(1, 1)[0] } + loc.label.should == "Kernel#tap" + loc.path.should == __FILE__ + end + end + end +end diff --git a/spec/ruby/core/kernel/caller_spec.rb b/spec/ruby/core/kernel/caller_spec.rb new file mode 100644 index 0000000000..7cd703de5a --- /dev/null +++ b/spec/ruby/core/kernel/caller_spec.rb @@ -0,0 +1,109 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/caller' + +describe 'Kernel#caller' do + it 'is a private method' do + Kernel.should have_private_instance_method(:caller) + end + + it 'returns an Array of caller locations' do + KernelSpecs::CallerTest.locations.should_not.empty? + end + + it 'returns an Array of caller locations using a custom offset' do + locations = KernelSpecs::CallerTest.locations(2) + + locations[0].should =~ %r{runner/mspec.rb} + end + + it 'returns an Array of caller locations using a custom limit' do + locations = KernelSpecs::CallerTest.locations(1, 1) + + locations.length.should == 1 + end + + it 'returns an Array of caller locations using a range' do + locations = KernelSpecs::CallerTest.locations(1..1) + + locations.length.should == 1 + end + + it 'returns the locations as String instances' do + locations = KernelSpecs::CallerTest.locations + line = __LINE__ - 1 + + locations[0].should include("#{__FILE__}:#{line}:in") + end + + it "returns an Array with the block given to #at_exit at the base of the stack" do + path = fixture(__FILE__, "caller_at_exit.rb") + lines = ruby_exe(path).lines + lines.size.should == 2 + lines[0].should =~ /\A#{path}:6:in [`'](?:Object#)?foo'\n\z/ + lines[1].should =~ /\A#{path}:2:in [`']block in <main>'\n\z/ + end + + it "can be called with a range" do + locations1 = caller(0) + locations2 = caller(2..4) + locations1[2..4].should == locations2 + end + + it "works with endless ranges" do + locations1 = KernelSpecs::CallerTest.locations(0) + locations2 = KernelSpecs::CallerTest.locations(eval("(2..)")) + locations2.should == locations1[2..-1] + end + + it "works with beginless ranges" do + locations1 = KernelSpecs::CallerTest.locations(0) + locations2 = KernelSpecs::CallerTest.locations((..5)) + locations2[eval("(2..)")].should == locations1[(..5)][eval("(2..)")] + end + + it "can be called with a range whose end is negative" do + locations1 = caller(0) + locations2 = caller(2..-1) + locations3 = caller(2..-2) + locations1[2..-1].should == locations2 + locations1[2..-2].should == locations3 + end + + it "must return nil if omitting more locations than available" do + caller(100).should == nil + caller(100..-1).should == nil + end + + it "must return [] if omitting exactly the number of locations available" do + omit = caller(0).length + caller(omit).should == [] + end + + it "must return the same locations when called with 1..-1 and when called with no arguments" do + caller.should == caller(1..-1) + end + + guard -> { Kernel.instance_method(:tap).source_location } do + ruby_version_is ""..."4.0" do + it "includes core library methods defined in Ruby" do + file, line = Kernel.instance_method(:tap).source_location + file.should.start_with?('<internal:') + + loc = nil + tap { loc = caller(1, 1)[0] } + loc.should =~ /\A<internal:.*in [`'](?:Kernel#)?tap'\z/ + end + end + + ruby_version_is "4.0" do + it "includes core library methods defined in Ruby" do + file, line = Kernel.instance_method(:tap).source_location + file.should.start_with?('<internal:') + + loc = nil + tap { loc = caller(1, 1)[0] } + loc.should =~ /\A#{ __FILE__ }:.*in [`'](?:Kernel#)?tap'\z/ + end + end + end +end diff --git a/spec/ruby/core/kernel/case_compare_spec.rb b/spec/ruby/core/kernel/case_compare_spec.rb new file mode 100644 index 0000000000..b8d30960e8 --- /dev/null +++ b/spec/ruby/core/kernel/case_compare_spec.rb @@ -0,0 +1,135 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + + +module Specs + module Kernel + + class HasNone + end + + class HasOpEqual + def ==(other) + other.kind_of? HasOpEqual + end + end + + class HasEqual + def equal?(other) + false + end + end + + class HasOppoOpEqual + def ==(other) + false + end + + def equal?(other) + false + end + end + end +end + + +describe "Kernel#=== for a class with default #== and #equal?" do + before :each do + @o1 = Specs::Kernel::HasNone.new + @o2 = @o1.dup + end + + it "returns true if other object has same object id" do + @o1.object_id.should == @o1.object_id + (@o1 === @o1).should == true + end + + it "returns false if other object does not have same object id" do + @o1.object_id.should_not == @o2.object_id + (@o1 === @o2).should == false + end +end + +describe "Kernel#=== for a class with #== overridden to consider other object's class" do + before :each do + @o = Object.new + @o1 = Specs::Kernel::HasOpEqual.new + @o2 = @o1.dup + end + + it "returns true if #== returns true even if #equal? is false" do + @o1.should_not equal(@o2) + (@o1 == @o2).should == true + (@o1 === @o2).should == true + end + + it "returns true if #equal? returns true" do + @o1.should equal(@o1) + (@o1 === @o1).should == true + end + + it "returns false if neither #== nor #equal? returns true" do + @o1.should_not equal(@o) + (@o1 == @o).should == false + (@o1 === @o).should == false + end +end + +describe "Kernel#=== for a class with #equal? overridden to always be false" do + before :each do + @o = Object.new + @o1 = Specs::Kernel::HasEqual.new + @o2 = @o1.dup + end + + it "returns true if #== returns true even if #equal? is false" do + @o1.should_not equal(@o1) + (@o1 == @o1).should == true + (@o1 === @o1).should == true + end + + it "returns false if neither #== nor #equal? returns true" do + @o1.should_not equal(@o) + (@o1 == @o).should == false + (@o1 === @o).should == false + end +end + +describe "Kernel#=== for a class with #== and #equal? overridden to always be false" do + before :each do + @o = Object.new + @o1 = Specs::Kernel::HasOppoOpEqual.new + @o2 = @o1.dup + end + + it "returns true if the object id is the same even if both #== and #equal? return false" do + @o1.object_id.should == @o1.object_id + + @o1.should_not equal(@o1) + (@o1 == @o1).should == false + + (@o1 === @o1).should == true + end + + it "returns false if the object id is not the same and both #== and #equal? return false" do + @o1.object_id.should_not == @o2.object_id + + @o1.should_not equal(@o2) + (@o1 == @o2).should == false + + (@o1 === @o2).should == false + end +end + +describe "Kernel#=== does not call #object_id nor #equal?" do + before :each do + @o1 = Object.new + @o1.should_not_receive(:object_id) + @o1.should_not_receive(:equal?) + end + + it "but still returns true for #== or #=== on the same object" do + (@o1 == @o1).should == true + (@o1 === @o1).should == true + end +end diff --git a/spec/ruby/core/kernel/catch_spec.rb b/spec/ruby/core/kernel/catch_spec.rb new file mode 100644 index 0000000000..9f59d3b384 --- /dev/null +++ b/spec/ruby/core/kernel/catch_spec.rb @@ -0,0 +1,127 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel.catch" do + before :each do + ScratchPad.clear + end + + it "executes its block and catches a thrown value matching its argument" do + catch :thrown_key do + ScratchPad.record :catch_block + throw :thrown_key + ScratchPad.record :throw_failed + end + ScratchPad.recorded.should == :catch_block + end + + it "returns the second value passed to throw" do + catch(:thrown_key) { throw :thrown_key, :catch_value }.should == :catch_value + end + + it "returns the last expression evaluated if throw was not called" do + catch(:thrown_key) { 1; :catch_block }.should == :catch_block + end + + it "passes the given symbol to its block" do + catch :thrown_key do |tag| + ScratchPad.record tag + end + ScratchPad.recorded.should == :thrown_key + end + + it "raises an ArgumentError if a Symbol is thrown for a String catch value" do + -> { catch("exit") { throw :exit } }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if a String with different identity is thrown" do + -> { catch("exit".dup) { throw "exit".dup } }.should raise_error(ArgumentError) + end + + it "catches a Symbol when thrown a matching Symbol" do + catch :thrown_key do + ScratchPad.record :catch_block + throw :thrown_key + end + ScratchPad.recorded.should == :catch_block + end + + it "catches a String when thrown a String with the same identity" do + key = "thrown_key" + catch key do + ScratchPad.record :catch_block + throw key + end + ScratchPad.recorded.should == :catch_block + end + + it "accepts an object as an argument" do + catch(Object.new) { :catch_block }.should == :catch_block + end + + it "yields an object when called without arguments" do + catch { |tag| tag }.should be_an_instance_of(Object) + end + + it "can be used even in a method different from where throw is called" do + class CatchSpecs + def self.throwing_method + throw :blah, :thrown_value + end + def self.catching_method + catch :blah do + throwing_method + end + end + end + CatchSpecs.catching_method.should == :thrown_value + end + + describe "when nested" do + before :each do + ScratchPad.record [] + end + + it "catches across invocation boundaries" do + catch :one do + ScratchPad << 1 + catch :two do + ScratchPad << 2 + catch :three do + ScratchPad << 3 + throw :one + ScratchPad << 4 + end + ScratchPad << 5 + end + ScratchPad << 6 + end + + ScratchPad.recorded.should == [1, 2, 3] + end + + it "catches in the nested invocation with the same key object" do + catch :thrown_key do + ScratchPad << 1 + catch :thrown_key do + ScratchPad << 2 + throw :thrown_key + ScratchPad << 3 + end + ScratchPad << 4 + end + + ScratchPad.recorded.should == [1, 2, 4] + end + end + + it "raises LocalJumpError if no block is given" do + -> { catch :blah }.should raise_error(LocalJumpError) + end +end + +describe "Kernel#catch" do + it "is a private method" do + Kernel.should have_private_instance_method(:catch) + end +end diff --git a/spec/ruby/core/kernel/chomp_spec.rb b/spec/ruby/core/kernel/chomp_spec.rb new file mode 100644 index 0000000000..d30e77c35a --- /dev/null +++ b/spec/ruby/core/kernel/chomp_spec.rb @@ -0,0 +1,65 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe :kernel_chomp, shared: true do + it "removes the final newline of $_" do + KernelSpecs.chomp("abc\n", @method).should == "abc" + end + + it "removes the final carriage return of $_" do + KernelSpecs.chomp("abc\r", @method).should == "abc" + end + + it "removes the final carriage return, newline of $_" do + KernelSpecs.chomp("abc\r\n", @method).should == "abc" + end + + it "removes only the final newline of $_" do + KernelSpecs.chomp("abc\n\n", @method).should == "abc\n" + end + + it "removes the value of $/ from the end of $_" do + KernelSpecs.chomp("abcde", @method, "cde").should == "ab" + end +end + +describe :kernel_chomp_private, shared: true do + it "is a private method" do + KernelSpecs.has_private_method(@method).should be_true + end +end + +describe "Kernel.chomp" do + it_behaves_like :kernel_chomp, "Kernel.chomp" +end + +describe "Kernel#chomp" do + it_behaves_like :kernel_chomp, "chomp" + + it_behaves_like :kernel_chomp_private, :chomp +end + +describe :kernel_chomp_encoded, shared: true do + before :each do + @external = Encoding.default_external + Encoding.default_external = Encoding::UTF_8 + end + + after :each do + Encoding.default_external = @external + end + + it "removes the final carriage return, newline from a multi-byte $_" do + script = fixture __FILE__, "#{@method}.rb" + KernelSpecs.run_with_dash_n(script).should == "ã‚れ" + end +end + +describe "Kernel.chomp" do + it_behaves_like :kernel_chomp_encoded, "chomp" +end + +describe "Kernel#chomp" do + it_behaves_like :kernel_chomp_encoded, "chomp_f" +end diff --git a/spec/ruby/core/kernel/chop_spec.rb b/spec/ruby/core/kernel/chop_spec.rb new file mode 100644 index 0000000000..9b91c011bc --- /dev/null +++ b/spec/ruby/core/kernel/chop_spec.rb @@ -0,0 +1,53 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe :kernel_chop, shared: true do + it "removes the final character of $_" do + KernelSpecs.chop("abc", @method).should == "ab" + end + + it "removes the final carriage return, newline of $_" do + KernelSpecs.chop("abc\r\n", @method).should == "abc" + end +end + +describe :kernel_chop_private, shared: true do + it "is a private method" do + KernelSpecs.has_private_method(@method).should be_true + end +end + +describe "Kernel.chop" do + it_behaves_like :kernel_chop, "Kernel.chop" +end + +describe "Kernel#chop" do + it_behaves_like :kernel_chop_private, :chop + + it_behaves_like :kernel_chop, "chop" +end + +describe :kernel_chop_encoded, shared: true do + before :each do + @external = Encoding.default_external + Encoding.default_external = Encoding::UTF_8 + end + + after :each do + Encoding.default_external = @external + end + + it "removes the final multi-byte character from $_" do + script = fixture __FILE__, "#{@method}.rb" + KernelSpecs.run_with_dash_n(script).should == "ã‚" + end +end + +describe "Kernel.chop" do + it_behaves_like :kernel_chop_encoded, "chop" +end + +describe "Kernel#chop" do + it_behaves_like :kernel_chop_encoded, "chop_f" +end diff --git a/spec/ruby/core/kernel/class_spec.rb b/spec/ruby/core/kernel/class_spec.rb new file mode 100644 index 0000000000..b1d9df1671 --- /dev/null +++ b/spec/ruby/core/kernel/class_spec.rb @@ -0,0 +1,26 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel#class" do + it "returns the class of the object" do + Object.new.class.should equal(Object) + + 1.class.should equal(Integer) + 3.14.class.should equal(Float) + :hello.class.should equal(Symbol) + "hello".class.should equal(String) + [1, 2].class.should equal(Array) + { 1 => 2 }.class.should equal(Hash) + end + + it "returns Class for a class" do + BasicObject.class.should equal(Class) + String.class.should equal(Class) + end + + it "returns the first non-singleton class" do + a = +"hello" + def a.my_singleton_method; end + a.class.should equal(String) + end +end diff --git a/spec/ruby/core/kernel/clone_spec.rb b/spec/ruby/core/kernel/clone_spec.rb new file mode 100644 index 0000000000..5adcbbe603 --- /dev/null +++ b/spec/ruby/core/kernel/clone_spec.rb @@ -0,0 +1,177 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/dup_clone' + +describe "Kernel#clone" do + it_behaves_like :kernel_dup_clone, :clone + + before :each do + ScratchPad.clear + @obj = KernelSpecs::Duplicate.new 1, :a + end + + it "calls #initialize_copy on the new instance" do + clone = @obj.clone + ScratchPad.recorded.should_not == @obj.object_id + ScratchPad.recorded.should == clone.object_id + end + + it "uses the internal allocator and does not call #allocate" do + klass = Class.new + instance = klass.new + + def klass.allocate + raise "allocate should not be called" + end + + clone = instance.clone + clone.class.should equal klass + end + + describe "with no arguments" do + it "copies frozen state from the original" do + o2 = @obj.clone + o2.should_not.frozen? + + @obj.freeze + o3 = @obj.clone + o3.should.frozen? + end + + it 'copies frozen?' do + o = ''.freeze.clone + o.frozen?.should be_true + end + end + + describe "with freeze: nil" do + it "copies frozen state from the original, like #clone without arguments" do + o2 = @obj.clone(freeze: nil) + o2.should_not.frozen? + + @obj.freeze + o3 = @obj.clone(freeze: nil) + o3.should.frozen? + end + + it "copies frozen?" do + o = "".freeze.clone(freeze: nil) + o.frozen?.should be_true + end + end + + describe "with freeze: true" do + it 'makes a frozen copy if the original is frozen' do + @obj.freeze + @obj.clone(freeze: true).should.frozen? + end + + it 'freezes the copy even if the original was not frozen' do + @obj.clone(freeze: true).should.frozen? + end + + it "calls #initialize_clone with kwargs freeze: true" do + obj = KernelSpecs::CloneFreeze.new + obj.clone(freeze: true) + ScratchPad.recorded.should == [obj, { freeze: true }] + end + + it "calls #initialize_clone with kwargs freeze: true even if #initialize_clone only takes a single argument" do + obj = KernelSpecs::Clone.new + -> { obj.clone(freeze: true) }.should raise_error(ArgumentError, 'wrong number of arguments (given 2, expected 1)') + end + end + + describe "with freeze: false" do + it 'does not freeze the copy if the original is frozen' do + @obj.freeze + @obj.clone(freeze: false).should_not.frozen? + end + + it 'does not freeze the copy if the original is not frozen' do + @obj.clone(freeze: false).should_not.frozen? + end + + it "calls #initialize_clone with kwargs freeze: false" do + obj = KernelSpecs::CloneFreeze.new + obj.clone(freeze: false) + ScratchPad.recorded.should == [obj, { freeze: false }] + end + + it "calls #initialize_clone with kwargs freeze: false even if #initialize_clone only takes a single argument" do + obj = KernelSpecs::Clone.new + -> { obj.clone(freeze: false) }.should raise_error(ArgumentError, 'wrong number of arguments (given 2, expected 1)') + end + end + + describe "with freeze: anything else" do + it 'raises ArgumentError when passed not true/false/nil' do + -> { @obj.clone(freeze: 1) }.should raise_error(ArgumentError, /unexpected value for freeze: Integer/) + -> { @obj.clone(freeze: "") }.should raise_error(ArgumentError, /unexpected value for freeze: String/) + -> { @obj.clone(freeze: Object.new) }.should raise_error(ArgumentError, /unexpected value for freeze: Object/) + end + end + + it "copies instance variables" do + clone = @obj.clone + clone.one.should == 1 + clone.two.should == :a + end + + it "copies singleton methods" do + def @obj.special() :the_one end + clone = @obj.clone + clone.special.should == :the_one + end + + it "copies modules included in the singleton class" do + class << @obj + include KernelSpecs::DuplicateM + end + + clone = @obj.clone + clone.repr.should == "KernelSpecs::Duplicate" + end + + it "copies constants defined in the singleton class" do + class << @obj + CLONE = :clone + end + + clone = @obj.clone + class << clone + CLONE.should == :clone + end + end + + it "replaces a singleton object's metaclass with a new copy with the same superclass" do + cls = Class.new do + def bar + ['a'] + end + end + + object = cls.new + object.define_singleton_method(:bar) do + ['b', *super()] + end + object.bar.should == ['b', 'a'] + + cloned = object.clone + + cloned.singleton_methods.should == [:bar] + + # bar should replace previous one + cloned.define_singleton_method(:bar) do + ['c', *super()] + end + cloned.bar.should == ['c', 'a'] + + # bar should be removed and call through to superclass + cloned.singleton_class.class_eval do + remove_method :bar + end + + cloned.bar.should == ['a'] + end +end diff --git a/spec/ruby/core/kernel/comparison_spec.rb b/spec/ruby/core/kernel/comparison_spec.rb new file mode 100644 index 0000000000..affdc5c00d --- /dev/null +++ b/spec/ruby/core/kernel/comparison_spec.rb @@ -0,0 +1,31 @@ +require_relative '../../spec_helper' + +describe "Kernel#<=>" do + it "returns 0 if self" do + obj = Object.new + obj.<=>(obj).should == 0 + end + + it "returns 0 if self is == to the argument" do + obj = mock('has ==') + obj.should_receive(:==).and_return(true) + obj.<=>(Object.new).should == 0 + end + + it "returns nil if self is eql? but not == to the argument" do + obj = mock('has eql?') + obj.should_not_receive(:eql?) + obj.<=>(Object.new).should be_nil + end + + it "returns nil if self.==(arg) returns nil" do + obj = mock('wrong ==') + obj.should_receive(:==).and_return(nil) + obj.<=>(Object.new).should be_nil + end + + it "returns nil if self is not == to the argument" do + obj = Object.new + obj.<=>(3.14).should be_nil + end +end diff --git a/spec/ruby/core/kernel/define_singleton_method_spec.rb b/spec/ruby/core/kernel/define_singleton_method_spec.rb new file mode 100644 index 0000000000..24acec84f5 --- /dev/null +++ b/spec/ruby/core/kernel/define_singleton_method_spec.rb @@ -0,0 +1,120 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel#define_singleton_method" do + describe "when given an UnboundMethod" do + class DefineSingletonMethodSpecClass + MY_CONST = 42 + define_singleton_method(:another_test_method, self.method(:constants)) + end + + it "correctly calls the new method" do + klass = DefineSingletonMethodSpecClass + klass.another_test_method.should == klass.constants + end + + it "adds the new method to the methods list" do + DefineSingletonMethodSpecClass.should have_method(:another_test_method) + end + + it "defines any Child class method from any Parent's class methods" do + um = KernelSpecs::Parent.method(:parent_class_method).unbind + KernelSpecs::Child.send :define_singleton_method, :child_class_method, um + KernelSpecs::Child.child_class_method.should == :foo + ->{KernelSpecs::Parent.child_class_method}.should raise_error(NoMethodError) + end + + it "will raise when attempting to define an object's singleton method from another object's singleton method" do + other = KernelSpecs::Parent.new + p = KernelSpecs::Parent.new + class << p + def singleton_method + :single + end + end + um = p.method(:singleton_method).unbind + ->{ other.send :define_singleton_method, :other_singleton_method, um }.should raise_error(TypeError) + end + + end + + it "defines a new method with the given name and the given block as body in self" do + class DefineSingletonMethodSpecClass + define_singleton_method(:block_test1) { self } + define_singleton_method(:block_test2, &-> { self }) + end + + o = DefineSingletonMethodSpecClass + o.block_test1.should == o + o.block_test2.should == o + end + + it "raises a TypeError when the given method is no Method/Proc" do + -> { + Class.new { define_singleton_method(:test, "self") } + }.should raise_error(TypeError) + + -> { + Class.new { define_singleton_method(:test, 1234) } + }.should raise_error(TypeError) + end + + it "defines a new singleton method for objects" do + obj = Object.new + obj.define_singleton_method(:test) { "world!" } + obj.test.should == "world!" + -> { + Object.new.test + }.should raise_error(NoMethodError) + end + + it "maintains the Proc's scope" do + class DefineMethodByProcClass + in_scope = true + method_proc = proc { in_scope } + + define_singleton_method(:proc_test, &method_proc) + end + + DefineMethodByProcClass.proc_test.should == true + end + + it "raises an ArgumentError when no block is given" do + obj = Object.new + -> { + obj.define_singleton_method(:test) + }.should raise_error(ArgumentError) + end + + it "does not use the caller block when no block is given" do + o = Object.new + def o.define(name) + define_singleton_method(name) + end + + -> { + o.define(:foo) { raise "not used" } + }.should raise_error(ArgumentError) + end + + it "always defines the method with public visibility" do + cls = Class.new + def cls.define(name, &block) + private + define_singleton_method(name, &block) + end + + -> { + suppress_warning do + cls.define(:foo) { :ok } + end + cls.foo.should == :ok + }.should_not raise_error(NoMethodError) + end + + it "cannot define a singleton method with a frozen singleton class" do + o = Object.new + o.freeze + -> { o.define_singleton_method(:foo) { 1 } }.should raise_error(FrozenError) + end +end diff --git a/spec/ruby/core/kernel/display_spec.rb b/spec/ruby/core/kernel/display_spec.rb new file mode 100644 index 0000000000..9d429a9fac --- /dev/null +++ b/spec/ruby/core/kernel/display_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel#display" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/kernel/dup_spec.rb b/spec/ruby/core/kernel/dup_spec.rb new file mode 100644 index 0000000000..70198abdb7 --- /dev/null +++ b/spec/ruby/core/kernel/dup_spec.rb @@ -0,0 +1,67 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/dup_clone' + +describe "Kernel#dup" do + it_behaves_like :kernel_dup_clone, :dup + + before :each do + ScratchPad.clear + @obj = KernelSpecs::Duplicate.new 1, :a + end + + it "calls #initialize_copy on the new instance" do + dup = @obj.dup + ScratchPad.recorded.should_not == @obj.object_id + ScratchPad.recorded.should == dup.object_id + end + + it "uses the internal allocator and does not call #allocate" do + klass = Class.new + instance = klass.new + + def klass.allocate + raise "allocate should not be called" + end + + dup = instance.dup + dup.class.should equal klass + end + + it "does not copy frozen state from the original" do + @obj.freeze + dup = @obj.dup + + dup.should_not.frozen? + end + + it "copies instance variables" do + dup = @obj.dup + dup.one.should == 1 + dup.two.should == :a + end + + it "does not copy singleton methods" do + def @obj.special() :the_one end + dup = @obj.dup + -> { dup.special }.should raise_error(NameError) + end + + it "does not copy modules included in the singleton class" do + class << @obj + include KernelSpecs::DuplicateM + end + + dup = @obj.dup + -> { dup.repr }.should raise_error(NameError) + end + + it "does not copy constants defined in the singleton class" do + class << @obj + CLONE = :clone + end + + dup = @obj.dup + -> { class << dup; CLONE; end }.should raise_error(NameError) + end +end diff --git a/spec/ruby/core/kernel/enum_for_spec.rb b/spec/ruby/core/kernel/enum_for_spec.rb new file mode 100644 index 0000000000..0092e20468 --- /dev/null +++ b/spec/ruby/core/kernel/enum_for_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../spec_helper' + +describe "Kernel#enum_for" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/kernel/eql_spec.rb b/spec/ruby/core/kernel/eql_spec.rb new file mode 100644 index 0000000000..e62a601a79 --- /dev/null +++ b/spec/ruby/core/kernel/eql_spec.rb @@ -0,0 +1,10 @@ +require_relative '../../spec_helper' +require_relative '../../shared/kernel/equal' + +describe "Kernel#eql?" do + it "is a public instance method" do + Kernel.should have_public_instance_method(:eql?) + end + + it_behaves_like :object_equal, :eql? +end diff --git a/spec/ruby/core/kernel/equal_value_spec.rb b/spec/ruby/core/kernel/equal_value_spec.rb new file mode 100644 index 0000000000..2be151263d --- /dev/null +++ b/spec/ruby/core/kernel/equal_value_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel#==" do + it "returns true only if obj and other are the same object" do + o1 = mock('o1') + o2 = mock('o2') + (o1 == o1).should == true + (o2 == o2).should == true + (o1 == o2).should == false + (nil == nil).should == true + (o1 == nil).should == false + (nil == o2).should == false + end +end diff --git a/spec/ruby/core/kernel/eval_spec.rb b/spec/ruby/core/kernel/eval_spec.rb new file mode 100644 index 0000000000..e027294347 --- /dev/null +++ b/spec/ruby/core/kernel/eval_spec.rb @@ -0,0 +1,570 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +EvalSpecs::A.new.c + +describe "Kernel#eval" do + it "is a private method" do + Kernel.should have_private_instance_method(:eval) + end + + it "is a module function" do + Kernel.respond_to?(:eval).should == true + end + + it "evaluates the code within" do + eval("2 + 3").should == 5 + end + + it "coerces an object to string" do + eval(EvalSpecs::CoercedObject.new).should == 5 + end + + it "evaluates within the scope of the eval" do + EvalSpecs::A::B.name.should == "EvalSpecs::A::B" + end + + it "evaluates such that constants are scoped to the class of the eval" do + EvalSpecs::A::C.name.should == "EvalSpecs::A::C" + end + + it "finds a local in an enclosing scope" do + a = 1 + eval("a").should == 1 + end + + it "updates a local in an enclosing scope" do + a = 1 + eval("a = 2") + a.should == 2 + end + + it "updates a local in a surrounding block scope" do + EvalSpecs.new.f do + a = 1 + eval("a = 2") + a.should == 2 + end + end + + it "updates a local in a scope above a surrounding block scope" do + a = 1 + EvalSpecs.new.f do + eval("a = 2") + a.should == 2 + end + a.should == 2 + end + + it "updates a local in a scope above when modified in a nested block scope" do + a = 1 + es = EvalSpecs.new + eval("es.f { es.f { a = 2 } }") + a.should == 2 + end + + it "finds locals in a nested eval" do + eval('test = 10; eval("test")').should == 10 + end + + it "does not share locals across eval scopes" do + code = fixture __FILE__, "eval_locals.rb" + ruby_exe(code).chomp.should == "NameError" + end + + it "doesn't accept a Proc object as a binding" do + x = 1 + bind = proc {} + + -> { eval("x", bind) }.should raise_error(TypeError) + end + + it "does not make Proc locals visible to evaluated code" do + bind = proc { inner = 4 } + -> { eval("inner", bind.binding) }.should raise_error(NameError) + end + + # REWRITE ME: This obscures the real behavior of where locals are stored + # in eval bindings. + it "allows a binding to be captured inside an eval" do + outer_binding = binding + level1 = eval("binding", outer_binding) + level2 = eval("binding", level1) + + eval("x = 2", outer_binding) + eval("y = 3", level1) + + eval("w=1", outer_binding) + eval("w", outer_binding).should == 1 + eval("w=1", level1).should == 1 + eval("w", level1).should == 1 + eval("w=1", level2).should == 1 + eval("w", level2).should == 1 + + eval("x", outer_binding).should == 2 + eval("x=2", level1) + eval("x", level1).should == 2 + eval("x=2", level2) + eval("x", level2).should == 2 + + eval("y=3", outer_binding) + eval("y", outer_binding).should == 3 + eval("y=3", level1) + eval("y", level1).should == 3 + eval("y=3", level2) + eval("y", level2).should == 3 + end + + it "uses the same scope for local variables when given the same binding" do + outer_binding = binding + + eval("if false; a = 1; end", outer_binding) + eval("a", outer_binding).should be_nil + end + + it "allows creating a new class in a binding" do + bind = proc {} + eval("class EvalBindingProcA; end; EvalBindingProcA.name", bind.binding).should =~ /EvalBindingProcA$/ + end + + it "allows creating a new class in a binding created by #eval" do + bind = eval "binding" + eval("class EvalBindingA; end; EvalBindingA.name", bind).should =~ /EvalBindingA$/ + end + + it "includes file and line information in syntax error" do + expected = 'speccing.rb' + -> { + eval('if true', TOPLEVEL_BINDING, expected) + }.should raise_error(SyntaxError) { |e| + e.message.should =~ /#{expected}:1:.+/ + } + end + + it "evaluates string with given filename and negative linenumber" do + expected_file = 'speccing.rb' + -> { + eval('if true', TOPLEVEL_BINDING, expected_file, -100) + }.should raise_error(SyntaxError) { |e| + e.message.should =~ /#{expected_file}:-100:.+/ + } + end + + it "sets constants at the toplevel from inside a block" do + # The class Object bit is needed to workaround some mspec oddness + class Object + [1].each { eval "Const = 1"} + Const.should == 1 + remove_const :Const + end + end + + ruby_version_is ""..."3.3" do + it "uses (eval) filename if none is provided" do + eval("__FILE__").should == "(eval)" + eval("__FILE__", binding).should == "(eval)" + eval("__FILE__", binding, "success").should == "success" + eval("eval '__FILE__', binding").should == "(eval)" + eval("eval '__FILE__', binding", binding).should == "(eval)" + eval("eval '__FILE__', binding", binding, 'success').should == '(eval)' + eval("eval '__FILE__', binding, 'success'", binding).should == 'success' + end + + it 'uses (eval) for __FILE__ and 1 for __LINE__ with a binding argument' do + eval("[__FILE__, __LINE__]", binding).should == ["(eval)", 1] + end + end + + context "parameter forwarding" do + it "allows anonymous rest parameter forwarding" do + object = Object.new + def object.foo(a, b, c) + [a, b, c] + end + def object.bar(*) + eval "foo(*)" + end + + object.bar(1, 2, 3).should == [1, 2, 3] + end + + it "allows anonymous keyword parameters forwarding" do + object = Object.new + def object.foo(a:, b:, c:) + [a, b, c] + end + def object.bar(**) + eval "foo(**)" + end + + object.bar(a: 1, b: 2, c: 3).should == [1, 2, 3] + end + + it "allows anonymous block parameter forwarding" do + object = Object.new + def object.foo(&block) + block.call + end + def object.bar(&) + eval "foo(&)" + end + + object.bar { :foobar }.should == :foobar + end + + it "allows ... forwarding" do + object = Object.new + def object.foo(a, b:, &block) + [a, b, block.call] + end + def object.bar(...) + eval "foo(...)" + end + + object.bar(1, b: 2) { 3 }.should == [1, 2, 3] + end + + it "allows parameter forwarding to super" do + m = Module.new do + def foo(a, b:, &block) + [a, b, block.call] + end + end + + c = Class.new do + include m + + def foo(a, b:, &block) + eval "super" + end + end + + object = c.new + object.foo(1, b: 2) { 3 }.should == [1, 2, 3] + end + end + + ruby_version_is "3.3" do + it "uses (eval at __FILE__:__LINE__) if none is provided" do + eval("__FILE__").should == "(eval at #{__FILE__}:#{__LINE__})" + eval("__FILE__", binding).should == "(eval at #{__FILE__}:#{__LINE__})" + eval("__FILE__", binding, "success").should == "success" + eval("eval '__FILE__', binding").should == "(eval at (eval at #{__FILE__}:#{__LINE__}):1)" + eval("eval '__FILE__', binding", binding).should == "(eval at (eval at #{__FILE__}:#{__LINE__}):1)" + eval("eval '__FILE__', binding", binding, 'success').should == "(eval at success:1)" + eval("eval '__FILE__', binding, 'success'", binding).should == 'success' + end + + it 'uses (eval at __FILE__:__LINE__) for __FILE__ and 1 for __LINE__ with a binding argument' do + eval("[__FILE__, __LINE__]", binding).should == ["(eval at #{__FILE__}:#{__LINE__})", 1] + end + end + # Found via Rubinius bug github:#149 + it "does not alter the value of __FILE__ in the binding" do + first_time = EvalSpecs.call_eval + second_time = EvalSpecs.call_eval + + # This bug is seen by calling the method twice and comparing the values + # of __FILE__ each time. If the bug is present, calling eval will set the + # value of __FILE__ to the eval's "filename" argument. + + second_time.should_not == "(eval)" + first_time.should == second_time + end + + it "can be aliased" do + alias aliased_eval eval + x = 2 + aliased_eval('x += 40') + x.should == 42 + end + + # See http://jira.codehaus.org/browse/JRUBY-5163 + it "uses the receiver as self inside the eval" do + eval("self").should equal(self) + Kernel.eval("self").should equal(Kernel) + end + + it "does not pass the block to the method being eval'ed" do + -> { + eval('KernelSpecs::EvalTest.call_yield') { "content" } + }.should raise_error(LocalJumpError) + end + + it "returns from the scope calling #eval when evaluating 'return'" do + -> { eval("return :eval") }.call.should == :eval + end + + it "returns from the method calling #eval when evaluating 'return'" do + def eval_return(n) + eval("return n*2") + end + -> { eval_return(3) }.call.should == 6 + end + + it "returns from the method calling #eval when evaluating 'return' in BEGIN" do + def eval_return(n) + eval("BEGIN {return n*3}") + end + -> { eval_return(4) }.call.should == 12 + end + + it "unwinds through a Proc-style closure and returns from a lambda-style closure in the closure chain" do + code = fixture __FILE__, "eval_return_with_lambda.rb" + ruby_exe(code).chomp.should == "a,b,c,eval,f" + end + + it "raises a LocalJumpError if there is no lambda-style closure in the chain" do + code = fixture __FILE__, "eval_return_without_lambda.rb" + ruby_exe(code).chomp.should == "a,b,c,e,LocalJumpError,f" + end + + it "can be called with Method#call" do + method(:eval).call("2 * 3").should == 6 + end + + it "has the correct default definee when called through Method#call" do + class EvalSpecs + method(:eval).call("def eval_spec_method_call; end") + EvalSpecs.should have_instance_method(:eval_spec_method_call) + end + end + + it "makes flip-flop operator work correctly" do + ScratchPad.record [] + + eval "10.times { |i| ScratchPad << i if (i == 4)...(i == 4) }" + ScratchPad.recorded.should == [4, 5, 6, 7, 8, 9] + + ScratchPad.clear + end + + it "returns nil if given an empty string" do + eval("").should == nil + end + + context "with shebang" do + it "ignores shebang with ruby interpreter" do + pid = eval(<<~CODE.b) + #!/usr/bin/env ruby + Process.pid + CODE + + pid.should == Process.pid + end + + it "ignores shebang with non-ruby interpreter" do + pid = eval(<<~CODE.b) + #!/usr/bin/env puma + Process.pid + CODE + + pid.should == Process.pid + end + end + + # See language/magic_comment_spec.rb for more magic comments specs + describe "with a magic encoding comment" do + it "uses the magic comment encoding for the encoding of literal strings" do + code = "# encoding: UTF-8\n'é'.encoding".b + code.encoding.should == Encoding::BINARY + eval(code).should == Encoding::UTF_8 + end + + it "uses the magic comment encoding for parsing constants" do + code = <<CODE.b +# encoding: UTF-8 +class EvalSpecs + VÏ€ = 3.14 +end +CODE + code.encoding.should == Encoding::BINARY + eval(code) + EvalSpecs.constants(false).should include(:"VÏ€") + EvalSpecs::VÏ€.should == 3.14 + ensure + EvalSpecs.send(:remove_const, :VÏ€) + end + + it "allows an emacs-style magic comment encoding" do + code = <<CODE.b +# -*- encoding: UTF-8 -*- +class EvalSpecs +VÏ€emacs = 3.14 +end +CODE + code.encoding.should == Encoding::BINARY + eval(code) + EvalSpecs.constants(false).should include(:"VÏ€emacs") + EvalSpecs::VÏ€emacs.should == 3.14 + ensure + EvalSpecs.send(:remove_const, :VÏ€emacs) + end + + it "allows spaces before the magic encoding comment" do + code = <<CODE.b +\t \t # encoding: UTF-8 +class EvalSpecs + VÏ€spaces = 3.14 +end +CODE + code.encoding.should == Encoding::BINARY + eval(code) + EvalSpecs.constants(false).should include(:"VÏ€spaces") + EvalSpecs::VÏ€spaces.should == 3.14 + ensure + EvalSpecs.send(:remove_const, :VÏ€spaces) + end + + it "allows a shebang line before the magic encoding comment" do + code = <<CODE.b +#!/usr/bin/env ruby +# encoding: UTF-8 +class EvalSpecs + VÏ€shebang = 3.14 +end +CODE + code.encoding.should == Encoding::BINARY + eval(code) + EvalSpecs.constants(false).should include(:"VÏ€shebang") + EvalSpecs::VÏ€shebang.should == 3.14 + ensure + EvalSpecs.send(:remove_const, :VÏ€shebang) + end + + it "allows a shebang line and some spaces before the magic encoding comment" do + code = <<CODE.b +#!/usr/bin/env ruby + # encoding: UTF-8 +class EvalSpecs + VÏ€shebang_spaces = 3.14 +end +CODE + code.encoding.should == Encoding::BINARY + eval(code) + EvalSpecs.constants(false).should include(:"VÏ€shebang_spaces") + EvalSpecs::VÏ€shebang_spaces.should == 3.14 + ensure + EvalSpecs.send(:remove_const, :VÏ€shebang_spaces) + end + + it "allows a magic encoding comment and a subsequent frozen_string_literal magic comment" do + frozen_string_default = "test".frozen? + + code = <<CODE.b +# encoding: UTF-8 +# frozen_string_literal: #{!frozen_string_default} +class EvalSpecs + VÏ€string = "frozen" +end +CODE + code.encoding.should == Encoding::BINARY + eval(code) + EvalSpecs.constants(false).should include(:"VÏ€string") + EvalSpecs::VÏ€string.should == "frozen" + EvalSpecs::VÏ€string.encoding.should == Encoding::UTF_8 + EvalSpecs::VÏ€string.frozen?.should == !frozen_string_default + ensure + EvalSpecs.send(:remove_const, :VÏ€string) + end + + it "allows a magic encoding comment and a frozen_string_literal magic comment on the same line in emacs style" do + code = <<CODE.b +# -*- encoding: UTF-8; frozen_string_literal: true -*- +class EvalSpecs +VÏ€same_line = "frozen" +end +CODE + code.encoding.should == Encoding::BINARY + eval(code) + EvalSpecs.constants(false).should include(:"VÏ€same_line") + EvalSpecs::VÏ€same_line.should == "frozen" + EvalSpecs::VÏ€same_line.encoding.should == Encoding::UTF_8 + EvalSpecs::VÏ€same_line.frozen?.should be_true + ensure + EvalSpecs.send(:remove_const, :VÏ€same_line) + end + + it "ignores the magic encoding comment if it is after a frozen_string_literal magic comment" do + frozen_string_default = "test".frozen? + code = <<CODE.b +# frozen_string_literal: #{!frozen_string_default} +# encoding: UTF-8 +class EvalSpecs + VÏ€frozen_first = "frozen" +end +CODE + code.encoding.should == Encoding::BINARY + eval(code) + EvalSpecs.constants(false).should_not include(:"VÏ€frozen_first") + binary_constant = "VÏ€frozen_first".b.to_sym + EvalSpecs.constants(false).should include(binary_constant) + value = EvalSpecs.const_get(binary_constant) + value.should == "frozen" + value.encoding.should == Encoding::BINARY + value.frozen?.should == !frozen_string_default + ensure + EvalSpecs.send(:remove_const, binary_constant) + end + + it "ignores the frozen_string_literal magic comment if it appears after a token and warns if $VERBOSE is true" do + frozen_string_default = "test".frozen? + code = <<CODE +some_token_before_magic_comment = :anything +# frozen_string_literal: #{!frozen_string_default} +class EvalSpecs + VÏ€string_not_frozen = "not frozen" +end +CODE + -> { eval(code) }.should complain(/warning: [`']frozen_string_literal' is ignored after any tokens/, verbose: true) + EvalSpecs::VÏ€string_not_frozen.frozen?.should == frozen_string_default + EvalSpecs.send :remove_const, :VÏ€string_not_frozen + + -> { eval(code) }.should_not complain(verbose: false) + EvalSpecs::VÏ€string_not_frozen.frozen?.should == frozen_string_default + EvalSpecs.send :remove_const, :VÏ€string_not_frozen + end + end + + describe 'with refinements' do + it "activates refinements from the eval scope" do + refinery = Module.new do + refine EvalSpecs::A do + def foo + "bar" + end + end + end + + result = nil + + Module.new do + using refinery + + result = eval "EvalSpecs::A.new.foo" + end + + result.should == "bar" + end + + it "activates refinements from the binding" do + refinery = Module.new do + refine EvalSpecs::A do + def foo + "bar" + end + end + end + + b = nil + m = Module.new do + using refinery + b = binding + end + + result = eval "EvalSpecs::A.new.foo", b + + result.should == "bar" + end + end +end diff --git a/spec/ruby/core/kernel/exec_spec.rb b/spec/ruby/core/kernel/exec_spec.rb new file mode 100644 index 0000000000..3d9520ad67 --- /dev/null +++ b/spec/ruby/core/kernel/exec_spec.rb @@ -0,0 +1,18 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel#exec" do + it "is a private method" do + Kernel.should have_private_instance_method(:exec) + end + + it "runs the specified command, replacing current process" do + ruby_exe('exec "echo hello"; puts "fail"').should == "hello\n" + end +end + +describe "Kernel.exec" do + it "runs the specified command, replacing current process" do + ruby_exe('Kernel.exec "echo hello"; puts "fail"').should == "hello\n" + end +end diff --git a/spec/ruby/core/kernel/exit_spec.rb b/spec/ruby/core/kernel/exit_spec.rb new file mode 100644 index 0000000000..93cec3fee5 --- /dev/null +++ b/spec/ruby/core/kernel/exit_spec.rb @@ -0,0 +1,27 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative '../../shared/process/exit' + +describe "Kernel#exit" do + it "is a private method" do + Kernel.should have_private_instance_method(:exit) + end + + it_behaves_like :process_exit, :exit, KernelSpecs::Method.new +end + +describe "Kernel.exit" do + it_behaves_like :process_exit, :exit, Kernel +end + +describe "Kernel#exit!" do + it "is a private method" do + Kernel.should have_private_instance_method(:exit!) + end + + it_behaves_like :process_exit!, :exit!, "self" +end + +describe "Kernel.exit!" do + it_behaves_like :process_exit!, :exit!, "Kernel" +end diff --git a/spec/ruby/core/kernel/extend_spec.rb b/spec/ruby/core/kernel/extend_spec.rb new file mode 100644 index 0000000000..6342d8cae1 --- /dev/null +++ b/spec/ruby/core/kernel/extend_spec.rb @@ -0,0 +1,91 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +module KernelSpecs::M + def self.extend_object(o) + ScratchPad << "extend_object" + super + end + + def self.extended(o) + ScratchPad << "extended" + super + end + + def self.append_features(o) + ScratchPad << "append_features" + super + end +end + +describe "Kernel#extend" do + before :each do + ScratchPad.record [] + end + + it "requires multiple arguments" do + Object.new.method(:extend).arity.should < 0 + end + + it "calls extend_object on argument" do + o = mock('o') + o.extend KernelSpecs::M + ScratchPad.recorded.include?("extend_object").should == true + end + + it "does not calls append_features on arguments metaclass" do + o = mock('o') + o.extend KernelSpecs::M + ScratchPad.recorded.include?("append_features").should == false + end + + it "calls extended on argument" do + o = mock('o') + o.extend KernelSpecs::M + ScratchPad.recorded.include?("extended").should == true + end + + it "makes the class a kind_of? the argument" do + c = Class.new do + extend KernelSpecs::M + end + (c.kind_of? KernelSpecs::M).should == true + end + + it "raises an ArgumentError when no arguments given" do + -> { Object.new.extend }.should raise_error(ArgumentError) + end + + it "raises a TypeError when the argument is not a Module" do + o = mock('o') + klass = Class.new + -> { o.extend(klass) }.should raise_error(TypeError) + end + + describe "on frozen instance" do + before :each do + @frozen = Object.new.freeze + @module = KernelSpecs::M + end + + it "raises an ArgumentError when no arguments given" do + -> { @frozen.extend }.should raise_error(ArgumentError) + end + + it "raises a FrozenError" do + -> { @frozen.extend @module }.should raise_error(FrozenError) + end + end + + it "updated class methods of a module when it extends self and includes another module" do + a = Module.new do + extend self + end + b = Module.new do + def foo; :foo; end + end + + a.include b + a.foo.should == :foo + end +end diff --git a/spec/ruby/core/kernel/fail_spec.rb b/spec/ruby/core/kernel/fail_spec.rb new file mode 100644 index 0000000000..fab622037e --- /dev/null +++ b/spec/ruby/core/kernel/fail_spec.rb @@ -0,0 +1,42 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel#fail" do + it "is a private method" do + Kernel.should have_private_instance_method(:fail) + end + + it "raises a RuntimeError" do + -> { fail }.should raise_error(RuntimeError) + end + + it "accepts an Object with an exception method returning an Exception" do + obj = Object.new + def obj.exception(msg) + StandardError.new msg + end + -> { fail obj, "..." }.should raise_error(StandardError, "...") + end + + it "instantiates the specified exception class" do + error_class = Class.new(RuntimeError) + -> { fail error_class }.should raise_error(error_class) + end + + it "uses the specified message" do + -> { + begin + fail "the duck is not irish." + rescue => e + e.message.should == "the duck is not irish." + raise + else + raise Exception + end + }.should raise_error(RuntimeError) + end +end + +describe "Kernel.fail" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/kernel/fixtures/Complex.rb b/spec/ruby/core/kernel/fixtures/Complex.rb new file mode 100644 index 0000000000..bf14d55ad5 --- /dev/null +++ b/spec/ruby/core/kernel/fixtures/Complex.rb @@ -0,0 +1,5 @@ +module KernelSpecs + def self.Complex_method(string) + Complex(string) + end +end diff --git a/spec/ruby/core/kernel/fixtures/__callee__.rb b/spec/ruby/core/kernel/fixtures/__callee__.rb new file mode 100644 index 0000000000..7138dbc5aa --- /dev/null +++ b/spec/ruby/core/kernel/fixtures/__callee__.rb @@ -0,0 +1,34 @@ +module KernelSpecs + class CalleeTest + def f + __callee__ + end + + alias_method :g, :f + + def in_block + (1..2).map { __callee__ } + end + + define_method(:dm) do + __callee__ + end + + define_method(:dm_block) do + (1..2).map { __callee__ } + end + + def from_send + send "__callee__" + end + + def from_eval + eval "__callee__" + end + + @@method = __callee__ + def from_class_body + @@method + end + end +end diff --git a/spec/ruby/core/kernel/fixtures/__dir__.rb b/spec/ruby/core/kernel/fixtures/__dir__.rb new file mode 100644 index 0000000000..bf9a15e3c8 --- /dev/null +++ b/spec/ruby/core/kernel/fixtures/__dir__.rb @@ -0,0 +1,2 @@ +puts __FILE__ +puts __dir__ diff --git a/spec/ruby/core/kernel/fixtures/__method__.rb b/spec/ruby/core/kernel/fixtures/__method__.rb new file mode 100644 index 0000000000..9300366b37 --- /dev/null +++ b/spec/ruby/core/kernel/fixtures/__method__.rb @@ -0,0 +1,34 @@ +module KernelSpecs + class MethodTest + def f + __method__ + end + + alias_method :g, :f + + def in_block + (1..2).map { __method__ } + end + + define_method(:dm) do + __method__ + end + + define_method(:dm_block) do + (1..2).map { __method__ } + end + + def from_send + send "__method__" + end + + def from_eval + eval "__method__" + end + + @@method = __method__ + def from_class_body + @@method + end + end +end diff --git a/spec/ruby/core/kernel/fixtures/autoload_b.rb b/spec/ruby/core/kernel/fixtures/autoload_b.rb new file mode 100644 index 0000000000..e8be221ec7 --- /dev/null +++ b/spec/ruby/core/kernel/fixtures/autoload_b.rb @@ -0,0 +1,5 @@ +module KSAutoloadB + def self.loaded + :ksautoload_b + end +end diff --git a/spec/ruby/core/kernel/fixtures/autoload_d.rb b/spec/ruby/core/kernel/fixtures/autoload_d.rb new file mode 100644 index 0000000000..552cb5e82c --- /dev/null +++ b/spec/ruby/core/kernel/fixtures/autoload_d.rb @@ -0,0 +1,5 @@ +module KSAutoloadD + def self.loaded + :ksautoload_d + end +end diff --git a/spec/ruby/core/kernel/fixtures/autoload_from_included_module.rb b/spec/ruby/core/kernel/fixtures/autoload_from_included_module.rb new file mode 100644 index 0000000000..f5bedc6992 --- /dev/null +++ b/spec/ruby/core/kernel/fixtures/autoload_from_included_module.rb @@ -0,0 +1,9 @@ +module KernelSpecs + module AutoloadMethod + module AutoloadFromIncludedModule + def self.loaded + :autoload_from_included_module + end + end + end +end diff --git a/spec/ruby/core/kernel/fixtures/autoload_from_included_module2.rb b/spec/ruby/core/kernel/fixtures/autoload_from_included_module2.rb new file mode 100644 index 0000000000..f4f1cfbf7c --- /dev/null +++ b/spec/ruby/core/kernel/fixtures/autoload_from_included_module2.rb @@ -0,0 +1,9 @@ +module KernelSpecs + module AutoloadMethod2 + module AutoloadFromIncludedModule2 + def self.loaded + :autoload_from_included_module2 + end + end + end +end diff --git a/spec/ruby/core/kernel/fixtures/autoload_frozen.rb b/spec/ruby/core/kernel/fixtures/autoload_frozen.rb new file mode 100644 index 0000000000..e9dc42912b --- /dev/null +++ b/spec/ruby/core/kernel/fixtures/autoload_frozen.rb @@ -0,0 +1,7 @@ +Object.freeze + +begin + autoload :ANY_CONSTANT, "no_autoload.rb" +rescue Exception => e + print e.class, " - ", defined?(ANY_CONSTANT).inspect +end diff --git a/spec/ruby/core/kernel/fixtures/caller.rb b/spec/ruby/core/kernel/fixtures/caller.rb new file mode 100644 index 0000000000..ae3e13e9c9 --- /dev/null +++ b/spec/ruby/core/kernel/fixtures/caller.rb @@ -0,0 +1,7 @@ +module KernelSpecs + class CallerTest + def self.locations(*args) + caller(*args) + end + end +end diff --git a/spec/ruby/core/kernel/fixtures/caller_at_exit.rb b/spec/ruby/core/kernel/fixtures/caller_at_exit.rb new file mode 100644 index 0000000000..ca9d808597 --- /dev/null +++ b/spec/ruby/core/kernel/fixtures/caller_at_exit.rb @@ -0,0 +1,7 @@ +at_exit { + foo +} + +def foo + puts caller(0) +end diff --git a/spec/ruby/core/kernel/fixtures/caller_locations.rb b/spec/ruby/core/kernel/fixtures/caller_locations.rb new file mode 100644 index 0000000000..cc4e04d38f --- /dev/null +++ b/spec/ruby/core/kernel/fixtures/caller_locations.rb @@ -0,0 +1,7 @@ +module KernelSpecs + class CallerLocationsTest + def self.locations(*args) + caller_locations(*args) + end + end +end diff --git a/spec/ruby/core/kernel/fixtures/chomp.rb b/spec/ruby/core/kernel/fixtures/chomp.rb new file mode 100644 index 0000000000..f08dbadce5 --- /dev/null +++ b/spec/ruby/core/kernel/fixtures/chomp.rb @@ -0,0 +1,4 @@ +# -*- encoding: utf-8 -*- + +$_ = "ã‚れ\r\n" +print Kernel.chomp diff --git a/spec/ruby/core/kernel/fixtures/chomp_f.rb b/spec/ruby/core/kernel/fixtures/chomp_f.rb new file mode 100644 index 0000000000..3c22cb21e7 --- /dev/null +++ b/spec/ruby/core/kernel/fixtures/chomp_f.rb @@ -0,0 +1,4 @@ +# -*- encoding: utf-8 -*- + +$_ = "ã‚れ\r\n" +print chomp diff --git a/spec/ruby/core/kernel/fixtures/chop.rb b/spec/ruby/core/kernel/fixtures/chop.rb new file mode 100644 index 0000000000..dfd0626723 --- /dev/null +++ b/spec/ruby/core/kernel/fixtures/chop.rb @@ -0,0 +1,4 @@ +# -*- encoding: utf-8 -*- + +$_ = "ã‚れ" +print Kernel.chop diff --git a/spec/ruby/core/kernel/fixtures/chop_f.rb b/spec/ruby/core/kernel/fixtures/chop_f.rb new file mode 100644 index 0000000000..4ec89eb9ec --- /dev/null +++ b/spec/ruby/core/kernel/fixtures/chop_f.rb @@ -0,0 +1,4 @@ +# -*- encoding: utf-8 -*- + +$_ = "ã‚れ" +print chop diff --git a/spec/ruby/core/kernel/fixtures/classes.rb b/spec/ruby/core/kernel/fixtures/classes.rb new file mode 100644 index 0000000000..0e2b81988f --- /dev/null +++ b/spec/ruby/core/kernel/fixtures/classes.rb @@ -0,0 +1,558 @@ +module KernelSpecs + def self.Array_function(arg) + Array(arg) + end + + def self.Array_method(arg) + Kernel.Array(arg) + end + + def self.Hash_function(arg) + Hash(arg) + end + + def self.Hash_method(arg) + Kernel.Hash(arg) + end + + def self.Integer_function(arg) + Integer(arg) + end + + def self.Integer_method(arg) + Kernel.Integer(arg) + end + + def self.putc_function(arg) + putc arg + end + + def self.putc_method(arg) + Kernel.putc arg + end + + def self.has_private_method(name) + IO.popen([*ruby_exe, "-n", "-e", "print Kernel.private_method_defined?(#{name.inspect})"], "r+") do |io| + io.puts + io.close_write + io.read + end == "true" + end + + def self.chop(str, method) + IO.popen([*ruby_exe, "-n", "-e", "$_ = #{str.inspect}; #{method}; print $_"], "r+") do |io| + io.puts + io.close_write + io.read + end + end + + def self.chomp(str, method, sep="\n") + code = "$_ = #{str.inspect}; $/ = #{sep.inspect}; #{method}; print $_" + IO.popen([*ruby_exe, "-W0", "-n", "-e", code], "r+") do |io| + io.puts + io.close_write + io.read + end + end + + def self.run_with_dash_n(file) + IO.popen([*ruby_exe, "-n", file], "r+") do |io| + io.puts + io.close_write + io.read + end + end + + # kind_of?, is_a?, instance_of? + module SomeOtherModule; end + module AncestorModule; end + module MyModule; end + module MyPrependedModule; end + module MyExtensionModule; end + + class AncestorClass < String + include AncestorModule + end + + class InstanceClass < AncestorClass + include MyModule + end + + class KindaClass < AncestorClass + include MyModule + prepend MyPrependedModule + + def initialize + self.extend MyExtensionModule + end + end + + class Method + public :abort, :exit, :exit!, :fork, :system + end + + class Methods + + module MetaclassMethods + def peekaboo + end + + protected + + def nopeeking + end + + private + + def shoo + end + end + + def self.ichi; end + def ni; end + class << self + def san; end + end + + private + + def self.shi; end + def juu_shi; end + + class << self + def roku; end + + private + + def shichi; end + end + + protected + + def self.hachi; end + def ku; end + + class << self + def juu; end + + protected + + def juu_ichi; end + end + + public + + def self.juu_ni; end + def juu_san; end + end + + class PrivateSup + def public_in_sub + end + + private :public_in_sub + end + + class PublicSub < PrivateSup + def public_in_sub + end + end + + class A + # There is Kernel#public_method, so we don't want this one to clash + def pub_method; :public_method; end + + def undefed_method; :undefed_method; end + undef_method :undefed_method + + protected + def protected_method; :protected_method; end + + private + def private_method; :private_method; end + + public + define_method(:defined_method) { :defined } + end + + class B < A + alias aliased_pub_method pub_method + end + + class VisibilityChange + class << self + private :new + end + end + + class Binding + @@super_secret = "password" + + def initialize(n) + @secret = n + end + + def square(n) + n * n + end + + def get_binding + a = true + @bind = binding + + # Add/Change stuff + b = true + @secret += 1 + + @bind + end + end + + + module BlockGiven + def self.accept_block + block_given? + end + + def self.accept_block_as_argument(&block) + block_given? + end + + def self.accept_block_inside_block() + yield_self { + block_given? + } + end + + def self.accept_block_as_argument_inside_block(&block) + yield_self { + block_given? + } + end + + class << self + define_method(:defined_block) do + block_given? + end + + define_method(:defined_block_inside_block) do + yield_self { + block_given? + } + end + end + end + + module SelfBlockGiven + def self.accept_block + self.send(:block_given?) + end + + def self.accept_block_as_argument(&block) + self.send(:block_given?) + end + + def self.accept_block_inside_block + yield_self { + self.send(:block_given?) + } + end + + def self.accept_block_as_argument_inside_block(&block) + yield_self { + self.send(:block_given?) + } + end + + class << self + define_method(:defined_block) do + self.send(:block_given?) + end + + define_method(:defined_block_inside_block) do + yield_self { + self.send(:block_given?) + } + end + end + end + + module KernelBlockGiven + def self.accept_block + Kernel.block_given? + end + + def self.accept_block_as_argument(&block) + Kernel.block_given? + end + + def self.accept_block_inside_block + yield_self { + Kernel.block_given? + } + end + + def self.accept_block_as_argument_inside_block(&block) + yield_self { + Kernel.block_given? + } + end + + class << self + define_method(:defined_block) do + Kernel.block_given? + end + + define_method(:defined_block_inside_block) do + yield_self { + Kernel.block_given? + } + end + end + end + + class EvalTest + def self.eval_yield_with_binding + eval("yield", binding) + end + def self.call_yield + yield + end + end + + module DuplicateM + def repr + self.class.name.to_s + end + end + + class Duplicate + attr_accessor :one, :two + + def initialize(one, two) + @one = one + @two = two + end + + def initialize_copy(other, **kw) + ScratchPad.record object_id + end + + # define to support calling #clone with optional :freeze keyword argument + def initialize_clone(other, **kw) + super(other) # to call #initialize_copy + end + end + + class Clone + def initialize_clone(other) + ScratchPad.record other + end + end + + class CloneFreeze + def initialize_clone(other, **kwargs) + ScratchPad.record([other, kwargs]) + end + end + + class Dup + def initialize_dup(other) + ScratchPad.record other.object_id + end + end + + module ParentMixin + def parent_mixin_method; end + end + + class Parent + include ParentMixin + def parent_method; end + def another_parent_method; end + def self.parent_class_method; :foo; end + end + + class Child < Parent + undef_method :parent_method + end + + class Grandchild < Child + undef_method :parent_mixin_method + end + + # for testing lambda + class Lambda + def outer + inner + end + + def mp(&b); b; end + + def inner + b = mp { return :good } + + pr = -> x { x.call } + + pr.call(b) + + # We shouldn't be here, b should have unwinded through + return :bad + end + end + + module LambdaSpecs + module ZSuper + def lambda + super + end + end + + class ForwardBlockWithZSuper + prepend(ZSuper) + end + + module Ampersand + def lambda(&block) + suppress_warning {super(&block)} + end + end + + class SuperAmpersand + prepend(Ampersand) + end + end + + class RespondViaMissing + def respond_to_missing?(method, priv=false) + case method + when :handled_publicly + true + when :handled_privately + priv + when :not_handled + false + else + raise "Typo in method name: #{method.inspect}" + end + end + + def method_missing(method, *args) + raise "the method name should be a Symbol" unless Symbol === method + "Done #{method}(#{args})" + end + end + + class InstanceVariable + def initialize + @greeting = "hello" + end + end + + class PrivateToAry + private + + def to_ary + [1, 2] + end + + def to_a + [3, 4] + end + end + + class PrivateToA + private + + def to_a + [3, 4] + end + end + + module AutoloadMethod + def setup_autoload(file) + autoload :AutoloadFromIncludedModule, file + end + end + + class AutoloadMethodIncluder + include AutoloadMethod + end + + module AutoloadMethod2 + def setup_autoload(file) + Kernel.autoload :AutoloadFromIncludedModule2, file + end + end + + class AutoloadMethodIncluder2 + include AutoloadMethod2 + end + + class WarnInNestedCall + def f4(s = "", n) + f3(s, n) + end + + def f3(s, n) + f2(s, n) + end + + def f2(s, n) + f1(s, n) + end + + def f1(s, n) + warn(s, uplevel: n) + end + + def warn_call_lineno; method(:f1).source_location[1] + 1; end + def f1_call_lineno; method(:f2).source_location[1] + 1; end + def f2_call_lineno; method(:f3).source_location[1] + 1; end + def f3_call_lineno; method(:f4).source_location[1] + 1; end + end + + CustomRangeInteger = Struct.new(:value) do + def to_int; value; end + def <=>(other); to_int <=> other.to_int; end + def -(other); self.class.new(to_int - other.to_int); end + def +(other); self.class.new(to_int + other.to_int); end + end + + CustomRangeFloat = Struct.new(:value) do + def to_f; value; end + def <=>(other); to_f <=> other.to_f; end + def -(other); to_f - other.to_f; end + def +(other); self.class.new(to_f + other.to_f); end + end +end + +class EvalSpecs + class A + eval "class B; end" + def c + eval "class C; end" + end + end + + class CoercedObject + def to_str + '2 + 3' + end + + def hash + nil + end + end + + def f + yield + end + + def self.call_eval + f = __FILE__ + eval "true", binding, "(eval)", 1 + return f + end +end diff --git a/spec/ruby/core/kernel/fixtures/eval_locals.rb b/spec/ruby/core/kernel/fixtures/eval_locals.rb new file mode 100644 index 0000000000..ca8b381806 --- /dev/null +++ b/spec/ruby/core/kernel/fixtures/eval_locals.rb @@ -0,0 +1,6 @@ +begin + eval("a = 2") + eval("p a") +rescue Object => e + puts e.class +end diff --git a/spec/ruby/core/kernel/fixtures/eval_return_with_lambda.rb b/spec/ruby/core/kernel/fixtures/eval_return_with_lambda.rb new file mode 100644 index 0000000000..9e2d045bf3 --- /dev/null +++ b/spec/ruby/core/kernel/fixtures/eval_return_with_lambda.rb @@ -0,0 +1,12 @@ +print "a," +x = -> do + print "b," + Proc.new do + print "c," + eval("return :eval") + print "d," + end.call + print "e," +end.call +print x, "," +print "f" diff --git a/spec/ruby/core/kernel/fixtures/eval_return_without_lambda.rb b/spec/ruby/core/kernel/fixtures/eval_return_without_lambda.rb new file mode 100644 index 0000000000..fc8b7f1d4a --- /dev/null +++ b/spec/ruby/core/kernel/fixtures/eval_return_without_lambda.rb @@ -0,0 +1,14 @@ +print "a," +begin + print "b," + x = Proc.new do + print "c," + eval("return :eval") + print "d," + end.call + print x, "," +rescue LocalJumpError => e + print "e," + print e.class, "," +end +print "f" diff --git a/spec/ruby/core/kernel/fixtures/singleton_methods.rb b/spec/ruby/core/kernel/fixtures/singleton_methods.rb new file mode 100644 index 0000000000..32ea0adf89 --- /dev/null +++ b/spec/ruby/core/kernel/fixtures/singleton_methods.rb @@ -0,0 +1,13 @@ +module SingletonMethodsSpecs + module Prepended + def mspec_test_kernel_singleton_methods + end + public :mspec_test_kernel_singleton_methods + end + + ::Module.prepend Prepended + + module SelfExtending + extend self + end +end diff --git a/spec/ruby/core/kernel/fixtures/test.rb b/spec/ruby/core/kernel/fixtures/test.rb new file mode 100644 index 0000000000..949948606f --- /dev/null +++ b/spec/ruby/core/kernel/fixtures/test.rb @@ -0,0 +1,362 @@ +def foo1 +end + +def foo2 +end + +def foo3 +end + +def foo4 +end + +def foo5 +end + +def foo6 +end + +def foo7 +end + +def foo8 +end + +def foo9 +end + +def foo10 +end + +def foo11 +end + +def foo12 +end + +def foo13 +end + +def foo14 +end + +def foo15 +end + +def foo16 +end + +def foo17 +end + +def foo18 +end + +def foo19 +end + +def foo20 +end + +def foo21 +end + +def foo22 +end + +def foo23 +end + +def foo24 +end + +def foo25 +end + +def foo26 +end + +def foo27 +end + +def foo28 +end + +def foo29 +end + +def foo30 +end + +def foo31 +end + +def foo32 +end + +def foo33 +end + +def foo34 +end + +def foo35 +end + +def foo36 +end + +def foo37 +end + +def foo38 +end + +def foo39 +end + +def foo40 +end + +def foo41 +end + +def foo42 +end + +def foo43 +end + +def foo44 +end + +def foo45 +end + +def foo46 +end + +def foo47 +end + +def foo48 +end + +def foo49 +end + +def foo50 +end + +def foo51 +end + +def foo52 +end + +def foo53 +end + +def foo54 +end + +def foo55 +end + +def foo56 +end + +def foo57 +end + +def foo58 +end + +def foo59 +end + +def foo60 +end + +def foo61 +end + +def foo62 +end + +def foo63 +end + +def foo64 +end + +def foo65 +end + +def foo66 +end + +def foo67 +end + +def foo68 +end + +def foo69 +end + +def foo70 +end + +def foo71 +end + +def foo72 +end + +def foo73 +end + +def foo74 +end + +def foo75 +end + +def foo76 +end + +def foo77 +end + +def foo78 +end + +def foo79 +end + +def foo80 +end + +def foo81 +end + +def foo82 +end + +def foo83 +end + +def foo84 +end + +def foo85 +end + +def foo86 +end + +def foo87 +end + +def foo88 +end + +def foo89 +end + +def foo90 +end + +def foo91 +end + +def foo92 +end + +def foo93 +end + +def foo94 +end + +def foo95 +end + +def foo96 +end + +def foo97 +end + +def foo98 +end + +def foo99 +end + +def foo100 +end + +def foo101 +end + +def foo102 +end + +def foo103 +end + +def foo104 +end + +def foo105 +end + +def foo106 +end + +def foo107 +end + +def foo108 +end + +def foo109 +end + +def foo110 +end + +def foo111 +end + +def foo112 +end + +def foo113 +end + +def foo114 +end + +def foo115 +end + +def foo116 +end + +def foo117 +end + +def foo118 +end + +def foo119 +end + +def foo120 +end + +def foo121 +end diff --git a/spec/ruby/core/kernel/fixtures/warn_core_method.rb b/spec/ruby/core/kernel/fixtures/warn_core_method.rb new file mode 100644 index 0000000000..fd82562404 --- /dev/null +++ b/spec/ruby/core/kernel/fixtures/warn_core_method.rb @@ -0,0 +1,14 @@ +raise 'should be run without RubyGems' if defined?(Gem) + +public def deprecated(n=1) + # puts nil, caller(0), nil + warn "use X instead", uplevel: n +end + +1.times do # to test with a non-empty stack above the reported locations + deprecated + tap(&:deprecated) + tap { deprecated(2) } + # eval sources with a <internal: file are also ignored + eval "tap(&:deprecated)", nil, "<internal:should-be-skipped-by-warn-uplevel>" +end diff --git a/spec/ruby/core/kernel/fixtures/warn_require.rb b/spec/ruby/core/kernel/fixtures/warn_require.rb new file mode 100644 index 0000000000..c4b0733233 --- /dev/null +++ b/spec/ruby/core/kernel/fixtures/warn_require.rb @@ -0,0 +1 @@ +warn 'warn-require-warning', uplevel: 1 diff --git a/spec/ruby/core/kernel/fixtures/warn_require_caller.rb b/spec/ruby/core/kernel/fixtures/warn_require_caller.rb new file mode 100644 index 0000000000..35a0f969f9 --- /dev/null +++ b/spec/ruby/core/kernel/fixtures/warn_require_caller.rb @@ -0,0 +1,2 @@ +# Use a different line than just 1 +require "#{__dir__}/warn_require" diff --git a/spec/ruby/core/kernel/fork_spec.rb b/spec/ruby/core/kernel/fork_spec.rb new file mode 100644 index 0000000000..b37f9980e0 --- /dev/null +++ b/spec/ruby/core/kernel/fork_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative '../../shared/process/fork' + +describe "Kernel#fork" do + it "is a private method" do + Kernel.should have_private_instance_method(:fork) + end + + it_behaves_like :process_fork, :fork, KernelSpecs::Method.new +end + +describe "Kernel.fork" do + it_behaves_like :process_fork, :fork, Kernel +end diff --git a/spec/ruby/core/kernel/format_spec.rb b/spec/ruby/core/kernel/format_spec.rb new file mode 100644 index 0000000000..1d0c000c15 --- /dev/null +++ b/spec/ruby/core/kernel/format_spec.rb @@ -0,0 +1,47 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +# NOTE: most specs are in sprintf_spec.rb, this is just an alias +describe "Kernel#format" do + it "is a private method" do + Kernel.should have_private_instance_method(:format) + end +end + +describe "Kernel.format" do + it "is accessible as a module function" do + Kernel.format("%s", "hello").should == "hello" + end + + describe "when $VERBOSE is true" do + it "warns if too many arguments are passed" do + code = <<~RUBY + $VERBOSE = true + format("test", 1) + RUBY + + ruby_exe(code, args: "2>&1").should include("warning: too many arguments for format string") + end + + it "does not warns if too many keyword arguments are passed" do + code = <<~RUBY + $VERBOSE = true + format("test %{test}", test: 1, unused: 2) + RUBY + + ruby_exe(code, args: "2>&1").should_not include("warning") + end + + ruby_bug "#20593", ""..."3.4" do + it "doesn't warns if keyword arguments are passed and none are used" do + code = <<~RUBY + $VERBOSE = true + format("test", test: 1) + format("test", {}) + RUBY + + ruby_exe(code, args: "2>&1").should_not include("warning") + end + end + end +end diff --git a/spec/ruby/core/kernel/freeze_spec.rb b/spec/ruby/core/kernel/freeze_spec.rb new file mode 100644 index 0000000000..fa32d321cf --- /dev/null +++ b/spec/ruby/core/kernel/freeze_spec.rb @@ -0,0 +1,91 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel#freeze" do + it "prevents self from being further modified" do + o = mock('o') + o.frozen?.should be_false + o.freeze + o.frozen?.should be_true + end + + it "returns self" do + o = Object.new + o.freeze.should equal(o) + end + + describe "on integers" do + it "has no effect since they are already frozen" do + 1.frozen?.should be_true + 1.freeze + + bignum = bignum_value + bignum.frozen?.should be_true + bignum.freeze + end + end + + describe "on a Float" do + it "has no effect since it is already frozen" do + 1.2.frozen?.should be_true + 1.2.freeze + end + end + + describe "on a Symbol" do + it "has no effect since it is already frozen" do + :sym.frozen?.should be_true + :sym.freeze + end + end + + describe "on true, false and nil" do + it "has no effect since they are already frozen" do + nil.frozen?.should be_true + true.frozen?.should be_true + false.frozen?.should be_true + + nil.freeze + true.freeze + false.freeze + end + end + + describe "on a Complex" do + it "has no effect since it is already frozen" do + c = Complex(1.3, 3.1) + c.frozen?.should be_true + c.freeze + end + end + + describe "on a Rational" do + it "has no effect since it is already frozen" do + r = Rational(1, 3) + r.frozen?.should be_true + r.freeze + end + end + + it "causes mutative calls to raise RuntimeError" do + o = Class.new do + def mutate; @foo = 1; end + end.new + o.freeze + -> {o.mutate}.should raise_error(RuntimeError) + end + + it "causes instance_variable_set to raise RuntimeError" do + o = Object.new + o.freeze + -> {o.instance_variable_set(:@foo, 1)}.should raise_error(RuntimeError) + end + + it "freezes an object's singleton class" do + o = Object.new + c = o.singleton_class + c.frozen?.should == false + o.freeze + c.frozen?.should == true + end +end diff --git a/spec/ruby/core/kernel/frozen_spec.rb b/spec/ruby/core/kernel/frozen_spec.rb new file mode 100644 index 0000000000..a4cec4263d --- /dev/null +++ b/spec/ruby/core/kernel/frozen_spec.rb @@ -0,0 +1,76 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel#frozen?" do + it "returns true if self is frozen" do + o = mock('o') + p = mock('p') + p.freeze + o.should_not.frozen? + p.should.frozen? + end + + describe "on true, false and nil" do + it "returns true" do + true.frozen?.should be_true + false.frozen?.should be_true + nil.frozen?.should be_true + end + end + + describe "on integers" do + before :each do + @fixnum = 1 + @bignum = bignum_value + end + + it "returns true" do + @fixnum.frozen?.should be_true + @bignum.frozen?.should be_true + end + end + + describe "on a Float" do + before :each do + @float = 0.1 + end + + it "returns true" do + @float.frozen?.should be_true + end + end + + describe "on a Symbol" do + before :each do + @symbol = :symbol + end + + it "returns true" do + @symbol.frozen?.should be_true + end + end + + describe "on a Complex" do + it "returns true" do + c = Complex(1.3, 3.1) + c.frozen?.should be_true + end + + it "literal returns true" do + c = eval "1.3i" + c.frozen?.should be_true + end + end + + describe "on a Rational" do + it "returns true" do + r = Rational(1, 3) + r.frozen?.should be_true + end + + it "literal returns true" do + r = eval "1/3r" + r.frozen?.should be_true + end + end +end diff --git a/spec/ruby/core/kernel/gets_spec.rb b/spec/ruby/core/kernel/gets_spec.rb new file mode 100644 index 0000000000..104613dbfa --- /dev/null +++ b/spec/ruby/core/kernel/gets_spec.rb @@ -0,0 +1,17 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel#gets" do + it "is a private method" do + Kernel.should have_private_instance_method(:gets) + end + + it "calls ARGF.gets" do + ARGF.should_receive(:gets).and_return("spec") + gets.should == "spec" + end +end + +describe "Kernel.gets" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/kernel/global_variables_spec.rb b/spec/ruby/core/kernel/global_variables_spec.rb new file mode 100644 index 0000000000..8bce8e25b7 --- /dev/null +++ b/spec/ruby/core/kernel/global_variables_spec.rb @@ -0,0 +1,26 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel.global_variables" do + it "is a private method" do + Kernel.should have_private_instance_method(:global_variables) + end + + before :all do + @i = 0 + end + + it "finds subset starting with std" do + global_variables.grep(/std/).should include(:$stderr, :$stdin, :$stdout) + a = global_variables.size + gvar_name = "$foolish_global_var#{@i += 1}" + global_variables.include?(gvar_name.to_sym).should == false + eval("#{gvar_name} = 1") + global_variables.size.should == a+1 + global_variables.should include(gvar_name.to_sym) + end +end + +describe "Kernel#global_variables" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/kernel/gsub_spec.rb b/spec/ruby/core/kernel/gsub_spec.rb new file mode 100644 index 0000000000..a0cb9f2a70 --- /dev/null +++ b/spec/ruby/core/kernel/gsub_spec.rb @@ -0,0 +1,96 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +# FIXME: These methods exist only when the -n or -p option is passed to +# ruby, but we currently don't have a way of specifying that. +ruby_version_is ""..."1.9" do + describe "Kernel#gsub" do + it "is a private method" do + Kernel.should have_private_instance_method(:gsub) + end + + it "raises a TypeError if $_ is not a String" do + -> { + $_ = 123 + gsub(/./, "!") + }.should raise_error(TypeError) + end + + it "when matches sets $_ to a new string, leaving the former value unaltered" do + orig_value = $_ = "hello" + gsub("ello", "ola") + $_.should_not equal(orig_value) + $_.should == "hola" + orig_value.should == "hello" + end + + it "returns a string with the same contents as $_ after the operation" do + $_ = "bye" + gsub("non-match", "?").should == "bye" + + orig_value = $_ = "bye" + gsub(/$/, "!").should == "bye!" + orig_value.should == "bye" + end + + it "accepts Regexps as patterns" do + $_ = "food" + gsub(/.$/, "l") + $_.should == "fool" + end + + it "accepts Strings as patterns, treated literally" do + $_ = "hello, world." + gsub(".", "!") + $_.should == "hello, world!" + end + + it "accepts objects which respond to #to_str as patterns and treats them as strings" do + $_ = "hello, world." + stringlike = mock(".") + stringlike.should_receive(:to_str).and_return(".") + gsub(stringlike, "!") + $_.should == "hello, world!" + end + end + + describe "Kernel#gsub with a pattern and replacement" do + it "accepts strings for replacement" do + $_ = "hello" + gsub(/./, ".") + $_.should == "....." + end + + it "accepts objects which respond to #to_str for replacement" do + o = mock("o") + o.should_receive(:to_str).and_return("o") + $_ = "ping" + gsub("i", o) + $_.should == "pong" + end + + it "replaces \\1 sequences with the regexp's corresponding capture" do + $_ = "hello!" + gsub(/(.)(.)/, '\2\1') + $_.should == "ehll!o" + end + end + + describe "Kernel#gsub with pattern and block" do + it "acts similarly to using $_.gsub" do + $_ = "olleh dlrow" + gsub(/(\w+)/){ $1.reverse } + $_.should == "hello world" + end + end + + describe "Kernel#gsub!" do + it "is a private method" do + Kernel.should have_private_instance_method(:gsub!) + end + end + + describe "Kernel.gsub!" do + it "needs to be reviewed for spec completeness" + end +end diff --git a/spec/ruby/core/kernel/initialize_clone_spec.rb b/spec/ruby/core/kernel/initialize_clone_spec.rb new file mode 100644 index 0000000000..21a90c19f0 --- /dev/null +++ b/spec/ruby/core/kernel/initialize_clone_spec.rb @@ -0,0 +1,26 @@ +require_relative '../../spec_helper' + +describe "Kernel#initialize_clone" do + it "is a private instance method" do + Kernel.should have_private_instance_method(:initialize_clone) + end + + it "returns the receiver" do + a = Object.new + b = Object.new + a.send(:initialize_clone, b).should == a + end + + it "calls #initialize_copy" do + a = Object.new + b = Object.new + a.should_receive(:initialize_copy).with(b) + a.send(:initialize_clone, b) + end + + it "accepts a :freeze keyword argument for obj.clone(freeze: value)" do + a = Object.new + b = Object.new + a.send(:initialize_clone, b, freeze: true).should == a + end +end diff --git a/spec/ruby/core/kernel/initialize_copy_spec.rb b/spec/ruby/core/kernel/initialize_copy_spec.rb new file mode 100644 index 0000000000..d71ca9f60f --- /dev/null +++ b/spec/ruby/core/kernel/initialize_copy_spec.rb @@ -0,0 +1,36 @@ +require_relative '../../spec_helper' + +describe "Kernel#initialize_copy" do + it "returns self" do + obj = Object.new + obj.send(:initialize_copy, obj).should.equal?(obj) + end + + it "does nothing if the argument is the same as the receiver" do + obj = Object.new + obj.send(:initialize_copy, obj).should.equal?(obj) + + obj = Object.new.freeze + obj.send(:initialize_copy, obj).should.equal?(obj) + + 1.send(:initialize_copy, 1).should.equal?(1) + end + + it "raises FrozenError if the receiver is frozen" do + -> { Object.new.freeze.send(:initialize_copy, Object.new) }.should raise_error(FrozenError) + -> { 1.send(:initialize_copy, Object.new) }.should raise_error(FrozenError) + end + + it "raises TypeError if the objects are of different class" do + klass = Class.new + sub = Class.new(klass) + a = klass.new + b = sub.new + message = 'initialize_copy should take same class object' + -> { a.send(:initialize_copy, b) }.should raise_error(TypeError, message) + -> { b.send(:initialize_copy, a) }.should raise_error(TypeError, message) + + -> { a.send(:initialize_copy, 1) }.should raise_error(TypeError, message) + -> { a.send(:initialize_copy, 1.0) }.should raise_error(TypeError, message) + end +end diff --git a/spec/ruby/core/kernel/initialize_dup_spec.rb b/spec/ruby/core/kernel/initialize_dup_spec.rb new file mode 100644 index 0000000000..6dff34b7ad --- /dev/null +++ b/spec/ruby/core/kernel/initialize_dup_spec.rb @@ -0,0 +1,20 @@ +require_relative '../../spec_helper' + +describe "Kernel#initialize_dup" do + it "is a private instance method" do + Kernel.should have_private_instance_method(:initialize_dup) + end + + it "returns the receiver" do + a = Object.new + b = Object.new + a.send(:initialize_dup, b).should == a + end + + it "calls #initialize_copy" do + a = Object.new + b = Object.new + a.should_receive(:initialize_copy).with(b) + a.send(:initialize_dup, b) + end +end diff --git a/spec/ruby/core/kernel/inspect_spec.rb b/spec/ruby/core/kernel/inspect_spec.rb new file mode 100644 index 0000000000..1fa66cab98 --- /dev/null +++ b/spec/ruby/core/kernel/inspect_spec.rb @@ -0,0 +1,90 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel#inspect" do + it "returns a String" do + Object.new.inspect.should be_an_instance_of(String) + end + + it "does not call #to_s if it is defined" do + # We must use a bare Object here + obj = Object.new + inspected = obj.inspect + + obj.stub!(:to_s).and_return("to_s'd") + + obj.inspect.should == inspected + end + + it "returns a String with the object class and object_id encoded" do + obj = Object.new + obj.inspect.should =~ /^#<Object:0x[0-9a-f]+>$/ + end + + it "returns a String for an object without #class method" do + obj = Object.new + class << obj + undef_method :class + end + obj.inspect.should be_kind_of(String) + end + + ruby_version_is "4.0" do + it "calls #instance_variables_to_inspect private method to know which variables to display" do + obj = Object.new + obj.instance_eval do + @host = "localhost" + @user = "root" + @password = "hunter2" + end + obj.singleton_class.class_eval do + private def instance_variables_to_inspect = %i[@host @user @does_not_exist] + end + + inspected = obj.inspect.sub(/^#<Object:0x[0-9a-f]+/, '#<Object:0x00') + inspected.should == '#<Object:0x00 @host="localhost", @user="root">' + + obj = Object.new + obj.instance_eval do + @host = "localhost" + @user = "root" + @password = "hunter2" + end + obj.singleton_class.class_eval do + private def instance_variables_to_inspect = [] + end + + inspected = obj.inspect.sub(/^#<Object:0x[0-9a-f]+/, '#<Object:0x00') + inspected.should == "#<Object:0x00>" + end + + it "displays all instance variables if #instance_variables_to_inspect returns nil" do + obj = Object.new + obj.instance_eval do + @host = "localhost" + @user = "root" + @password = "hunter2" + end + obj.singleton_class.class_eval do + private def instance_variables_to_inspect = nil + end + + inspected = obj.inspect.sub(/^#<Object:0x[0-9a-f]+/, '#<Object:0x00') + inspected.should == %{#<Object:0x00 @host="localhost", @user="root", @password="hunter2">} + end + + it "raises an error if #instance_variables_to_inspect returns an invalid value" do + obj = Object.new + obj.instance_eval do + @host = "localhost" + @user = "root" + @password = "hunter2" + end + obj.singleton_class.class_eval do + private def instance_variables_to_inspect = {} + end + + ->{ obj.inspect }.should raise_error(TypeError, "Expected #instance_variables_to_inspect to return an Array or nil, but it returned Hash") + end + end +end diff --git a/spec/ruby/core/kernel/instance_of_spec.rb b/spec/ruby/core/kernel/instance_of_spec.rb new file mode 100644 index 0000000000..d1170d5047 --- /dev/null +++ b/spec/ruby/core/kernel/instance_of_spec.rb @@ -0,0 +1,40 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel#instance_of?" do + before :each do + @o = KernelSpecs::InstanceClass.new + end + + it "returns true if given class is object's class" do + @o.instance_of?(KernelSpecs::InstanceClass).should == true + [].instance_of?(Array).should == true + ''.instance_of?(String).should == true + end + + it "returns false if given class is object's ancestor class" do + @o.instance_of?(KernelSpecs::AncestorClass).should == false + end + + it "returns false if given class is not object's class nor object's ancestor class" do + @o.instance_of?(Array).should == false + end + + it "returns false if given a Module that is included in object's class" do + @o.instance_of?(KernelSpecs::MyModule).should == false + end + + it "returns false if given a Module that is included one of object's ancestors only" do + @o.instance_of?(KernelSpecs::AncestorModule).should == false + end + + it "returns false if given a Module that is not included in object's class" do + @o.instance_of?(KernelSpecs::SomeOtherModule).should == false + end + + it "raises a TypeError if given an object that is not a Class nor a Module" do + -> { @o.instance_of?(Object.new) }.should raise_error(TypeError) + -> { @o.instance_of?('KernelSpecs::InstanceClass') }.should raise_error(TypeError) + -> { @o.instance_of?(1) }.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/kernel/instance_variable_defined_spec.rb b/spec/ruby/core/kernel/instance_variable_defined_spec.rb new file mode 100644 index 0000000000..2ebb582b43 --- /dev/null +++ b/spec/ruby/core/kernel/instance_variable_defined_spec.rb @@ -0,0 +1,41 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel#instance_variable_defined?" do + before do + @instance = KernelSpecs::InstanceVariable.new + end + + describe "when passed a String" do + it "returns false if the instance variable is not defined" do + @instance.instance_variable_defined?("@goodbye").should be_false + end + + it "returns true if the instance variable is defined" do + @instance.instance_variable_defined?("@greeting").should be_true + end + end + + describe "when passed a Symbol" do + it "returns false if the instance variable is not defined" do + @instance.instance_variable_defined?(:@goodbye).should be_false + end + + it "returns true if the instance variable is defined" do + @instance.instance_variable_defined?(:@greeting).should be_true + end + end + + it "raises a TypeError if passed an Object not defining #to_str" do + -> do + obj = mock("kernel instance_variable_defined?") + @instance.instance_variable_defined? obj + end.should raise_error(TypeError) + end + + it "returns false if the instance variable is not defined for different types" do + [nil, false, true, 1, 2.0, :test, "test"].each do |obj| + obj.instance_variable_defined?("@goodbye").should be_false + end + end +end diff --git a/spec/ruby/core/kernel/instance_variable_get_spec.rb b/spec/ruby/core/kernel/instance_variable_get_spec.rb new file mode 100644 index 0000000000..f1d2a45df8 --- /dev/null +++ b/spec/ruby/core/kernel/instance_variable_get_spec.rb @@ -0,0 +1,111 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel#instance_variable_get" do + before :each do + @obj = Object.new + @obj.instance_variable_set("@test", :test) + end + + it "tries to convert the passed argument to a String using #to_str" do + obj = mock("to_str") + obj.should_receive(:to_str).and_return("@test") + @obj.instance_variable_get(obj) + end + + it "returns the value of the passed instance variable that is referred to by the conversion result" do + obj = mock("to_str") + obj.stub!(:to_str).and_return("@test") + @obj.instance_variable_get(obj).should == :test + end + + it "returns nil when the referred instance variable does not exist" do + @obj.instance_variable_get(:@does_not_exist).should be_nil + end + + it "raises a TypeError when the passed argument does not respond to #to_str" do + -> { @obj.instance_variable_get(Object.new) }.should raise_error(TypeError) + end + + it "raises a TypeError when the passed argument can't be converted to a String" do + obj = mock("to_str") + obj.stub!(:to_str).and_return(123) + -> { @obj.instance_variable_get(obj) }.should raise_error(TypeError) + end + + it "raises a NameError when the conversion result does not start with an '@'" do + obj = mock("to_str") + obj.stub!(:to_str).and_return("test") + -> { @obj.instance_variable_get(obj) }.should raise_error(NameError) + end + + it "raises a NameError when passed just '@'" do + obj = mock("to_str") + obj.stub!(:to_str).and_return('@') + -> { @obj.instance_variable_get(obj) }.should raise_error(NameError) + end +end + +describe "Kernel#instance_variable_get when passed Symbol" do + before :each do + @obj = Object.new + @obj.instance_variable_set("@test", :test) + end + + it "returns the value of the instance variable that is referred to by the passed Symbol" do + @obj.instance_variable_get(:@test).should == :test + end + + it "raises a NameError when passed :@ as an instance variable name" do + -> { @obj.instance_variable_get(:"@") }.should raise_error(NameError) + end + + it "raises a NameError when the passed Symbol does not start with an '@'" do + -> { @obj.instance_variable_get(:test) }.should raise_error(NameError) + end + + it "raises a NameError when the passed Symbol is an invalid instance variable name" do + -> { @obj.instance_variable_get(:"@0") }.should raise_error(NameError) + end + + it "returns nil or raises for frozen objects" do + nil.instance_variable_get(:@foo).should == nil + -> { nil.instance_variable_get(:foo) }.should raise_error(NameError) + :foo.instance_variable_get(:@foo).should == nil + end +end + +describe "Kernel#instance_variable_get when passed String" do + before :each do + @obj = Object.new + @obj.instance_variable_set("@test", :test) + end + + it "returns the value of the instance variable that is referred to by the passed String" do + @obj.instance_variable_get("@test").should == :test + end + + it "raises a NameError when the passed String does not start with an '@'" do + -> { @obj.instance_variable_get("test") }.should raise_error(NameError) + end + + it "raises a NameError when the passed String is an invalid instance variable name" do + -> { @obj.instance_variable_get("@0") }.should raise_error(NameError) + end + + it "raises a NameError when passed '@' as an instance variable name" do + -> { @obj.instance_variable_get("@") }.should raise_error(NameError) + end +end + +describe "Kernel#instance_variable_get when passed Integer" do + before :each do + @obj = Object.new + @obj.instance_variable_set("@test", :test) + end + + it "raises a TypeError" do + -> { @obj.instance_variable_get(10) }.should raise_error(TypeError) + -> { @obj.instance_variable_get(-10) }.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/kernel/instance_variable_set_spec.rb b/spec/ruby/core/kernel/instance_variable_set_spec.rb new file mode 100644 index 0000000000..2c25f4366f --- /dev/null +++ b/spec/ruby/core/kernel/instance_variable_set_spec.rb @@ -0,0 +1,105 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel#instance_variable_set" do + it "sets the value of the specified instance variable" do + dog = Class.new do + def initialize(p1, p2) + @a, @b = p1, p2 + end + end + dog.new('cat', 99).instance_variable_set(:@a, 'dog').should == "dog" + end + + it "sets the value of the instance variable when no instance variables exist yet" do + no_variables = Class.new + no_variables.new.instance_variable_set(:@a, "new").should == "new" + end + + it "raises a NameError exception if the argument is not of form '@x'" do + no_dog = Class.new + -> { no_dog.new.instance_variable_set(:c, "cat") }.should raise_error(NameError) + end + + it "raises a NameError exception if the argument is an invalid instance variable name" do + digit_dog = Class.new + -> { digit_dog.new.instance_variable_set(:"@0", "cat") }.should raise_error(NameError) + end + + it "raises a NameError when the argument is '@'" do + dog_at = Class.new + -> { dog_at.new.instance_variable_set(:"@", "cat") }.should raise_error(NameError) + end + + it "raises a TypeError if the instance variable name is an Integer" do + -> { "".instance_variable_set(1, 2) }.should raise_error(TypeError) + end + + it "raises a TypeError if the instance variable name is an object that does not respond to to_str" do + class KernelSpecs::A; end + -> { "".instance_variable_set(KernelSpecs::A.new, 3) }.should raise_error(TypeError) + end + + it "raises a NameError if the passed object, when coerced with to_str, does not start with @" do + class KernelSpecs::B + def to_str + ":c" + end + end + -> { "".instance_variable_set(KernelSpecs::B.new, 4) }.should raise_error(NameError) + end + + it "raises a NameError if pass an object that cannot be a symbol" do + -> { "".instance_variable_set(:c, 1) }.should raise_error(NameError) + end + + it "accepts as instance variable name any instance of a class that responds to to_str" do + class KernelSpecs::C + def initialize + @a = 1 + end + def to_str + "@a" + end + end + KernelSpecs::C.new.instance_variable_set(KernelSpecs::C.new, 2).should == 2 + end + + describe "on frozen objects" do + before :each do + klass = Class.new do + attr_reader :ivar + def initialize + @ivar = :origin + end + end + + @frozen = klass.new.freeze + end + + it "keeps stored object after any exceptions" do + -> { @frozen.instance_variable_set(:@ivar, :replacement) }.should raise_error(Exception) + @frozen.ivar.should equal(:origin) + end + + it "raises a FrozenError when passed replacement is identical to stored object" do + -> { @frozen.instance_variable_set(:@ivar, :origin) }.should raise_error(FrozenError) + end + + it "raises a FrozenError when passed replacement is different from stored object" do + -> { @frozen.instance_variable_set(:@ivar, :replacement) }.should raise_error(FrozenError) + end + + it "accepts unicode instance variable names" do + o = Object.new + o.instance_variable_set(:@💙, 42) + o.instance_variable_get(:@💙).should == 42 + end + + it "raises for frozen objects" do + -> { nil.instance_variable_set(:@foo, 42) }.should raise_error(FrozenError) + -> { nil.instance_variable_set(:foo, 42) }.should raise_error(NameError) + -> { :foo.instance_variable_set(:@foo, 42) }.should raise_error(FrozenError) + end + end +end diff --git a/spec/ruby/core/kernel/instance_variables_spec.rb b/spec/ruby/core/kernel/instance_variables_spec.rb new file mode 100644 index 0000000000..677d8bb7b2 --- /dev/null +++ b/spec/ruby/core/kernel/instance_variables_spec.rb @@ -0,0 +1,40 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel#instance_variables" do + describe "immediate values" do + it "returns an empty array if no instance variables are defined" do + [0, 0.5, true, false, nil].each do |value| + value.instance_variables.should == [] + end + end + + it "returns the correct array if an instance variable is added" do + a = 0 + ->{ a.instance_variable_set("@test", 1) }.should raise_error(RuntimeError) + end + end + + describe "regular objects" do + it "returns an empty array if no instance variables are defined" do + Object.new.instance_variables.should == [] + end + + it "returns the correct array if an instance variable is added" do + a = Object.new + a.instance_variable_set("@test", 1) + a.instance_variables.should == [:@test] + end + + it "returns the instances variables in the order declared" do + c = Class.new do + def initialize + @c = 1 + @a = 2 + @b = 3 + end + end + c.new.instance_variables.should == [:@c, :@a, :@b] + end + end +end diff --git a/spec/ruby/core/kernel/is_a_spec.rb b/spec/ruby/core/kernel/is_a_spec.rb new file mode 100644 index 0000000000..bd8c96529a --- /dev/null +++ b/spec/ruby/core/kernel/is_a_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/kind_of' + +describe "Kernel#is_a?" do + it_behaves_like :kernel_kind_of, :is_a? +end diff --git a/spec/ruby/core/kernel/itself_spec.rb b/spec/ruby/core/kernel/itself_spec.rb new file mode 100644 index 0000000000..c906d7c3e8 --- /dev/null +++ b/spec/ruby/core/kernel/itself_spec.rb @@ -0,0 +1,9 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel#itself" do + it "returns the receiver itself" do + foo = Object.new + foo.itself.should equal foo + end +end diff --git a/spec/ruby/core/kernel/kind_of_spec.rb b/spec/ruby/core/kernel/kind_of_spec.rb new file mode 100644 index 0000000000..c988edccb5 --- /dev/null +++ b/spec/ruby/core/kernel/kind_of_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/kind_of' + +describe "Kernel#kind_of?" do + it_behaves_like :kernel_kind_of, :kind_of? +end diff --git a/spec/ruby/core/kernel/lambda_spec.rb b/spec/ruby/core/kernel/lambda_spec.rb new file mode 100644 index 0000000000..565536ac0d --- /dev/null +++ b/spec/ruby/core/kernel/lambda_spec.rb @@ -0,0 +1,158 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/lambda' + +# The functionality of lambdas is specified in core/proc + +describe "Kernel.lambda" do + it_behaves_like :kernel_lambda, :lambda + + it "is a private method" do + Kernel.should have_private_instance_method(:lambda) + end + + it "creates a lambda-style Proc if given a literal block" do + l = lambda { 42 } + l.lambda?.should be_true + end + + it "creates a lambda-style Proc if given a literal block via #send" do + l = send(:lambda) { 42 } + l.lambda?.should be_true + end + + it "creates a lambda-style Proc if given a literal block via #__send__" do + l = __send__(:lambda) { 42 } + l.lambda?.should be_true + end + + ruby_version_is ""..."3.3" do + it "creates a lambda-style Proc if given a literal block via Kernel.public_send" do + suppress_warning do + l = Kernel.public_send(:lambda) { 42 } + l.lambda?.should be_true + end + end + + it "returns the passed Proc if given an existing Proc" do + some_proc = proc {} + l = suppress_warning {lambda(&some_proc)} + l.should equal(some_proc) + l.lambda?.should be_false + end + + it "creates a lambda-style Proc when called with zsuper" do + suppress_warning do + l = KernelSpecs::LambdaSpecs::ForwardBlockWithZSuper.new.lambda { 42 } + l.lambda?.should be_true + l.call.should == 42 + + lambda { l.call(:extra) }.should raise_error(ArgumentError) + end + end + + it "returns the passed Proc if given an existing Proc through super" do + some_proc = proc { } + l = KernelSpecs::LambdaSpecs::SuperAmpersand.new.lambda(&some_proc) + l.should equal(some_proc) + l.lambda?.should be_false + end + + it "does not create lambda-style Procs when captured with #method" do + kernel_lambda = method(:lambda) + l = suppress_warning {kernel_lambda.call { 42 }} + l.lambda?.should be_false + l.call(:extra).should == 42 + end + end + + it "checks the arity of the call when no args are specified" do + l = lambda { :called } + l.call.should == :called + + lambda { l.call(1) }.should raise_error(ArgumentError) + lambda { l.call(1, 2) }.should raise_error(ArgumentError) + end + + it "checks the arity when 1 arg is specified" do + l = lambda { |a| :called } + l.call(1).should == :called + + lambda { l.call }.should raise_error(ArgumentError) + lambda { l.call(1, 2) }.should raise_error(ArgumentError) + end + + it "does not check the arity when passing a Proc with &" do + l = lambda { || :called } + p = proc { || :called } + + lambda { l.call(1) }.should raise_error(ArgumentError) + p.call(1).should == :called + end + + it "accepts 0 arguments when used with ||" do + lambda { + lambda { || }.call(1) + }.should raise_error(ArgumentError) + end + + it "strictly checks the arity when 0 or 2..inf args are specified" do + l = lambda { |a,b| } + + lambda { + l.call + }.should raise_error(ArgumentError) + + lambda { + l.call(1) + }.should raise_error(ArgumentError) + + lambda { + l.call(1,2) + }.should_not raise_error(ArgumentError) + end + + it "returns from the lambda itself, not the creation site of the lambda" do + @reached_end_of_method = nil + def test + send(:lambda) { return }.call + @reached_end_of_method = true + end + test + @reached_end_of_method.should be_true + end + + it "allows long returns to flow through it" do + KernelSpecs::Lambda.new.outer.should == :good + end + + it "treats the block as a Proc when lambda is re-defined" do + klass = Class.new do + def lambda (&block); block; end + def ret + lambda { return 1 }.call + 2 + end + end + klass.new.lambda { 42 }.should be_an_instance_of Proc + klass.new.ret.should == 1 + end + + context "when called without a literal block" do + ruby_version_is ""..."3.3" do + it "warns when proc isn't a lambda" do + -> { lambda(&proc{}) }.should complain("#{__FILE__}:#{__LINE__}: warning: lambda without a literal block is deprecated; use the proc without lambda instead\n") + end + end + + ruby_version_is "3.3" do + it "raises when proc isn't a lambda" do + -> { lambda(&proc{}) }.should raise_error(ArgumentError, /the lambda method requires a literal block/) + end + end + + it "doesn't warn when proc is lambda" do + -> { lambda(&lambda{}) }.should_not complain(verbose: true) + end + end +end diff --git a/spec/ruby/core/kernel/load_spec.rb b/spec/ruby/core/kernel/load_spec.rb new file mode 100644 index 0000000000..a165cc4acd --- /dev/null +++ b/spec/ruby/core/kernel/load_spec.rb @@ -0,0 +1,40 @@ +require_relative '../../spec_helper' +require_relative '../../fixtures/code_loading' +require_relative 'shared/load' +require_relative 'shared/require' + +describe "Kernel#load" do + before :each do + CodeLoadingSpecs.spec_setup + end + + after :each do + CodeLoadingSpecs.spec_cleanup + end + + it "is a private method" do + Kernel.should have_private_instance_method(:load) + end + + it_behaves_like :kernel_require_basic, :load, CodeLoadingSpecs::Method.new +end + +describe "Kernel#load" do + it_behaves_like :kernel_load, :load, CodeLoadingSpecs::Method.new +end + +describe "Kernel.load" do + before :each do + CodeLoadingSpecs.spec_setup + end + + after :each do + CodeLoadingSpecs.spec_cleanup + end + + it_behaves_like :kernel_require_basic, :load, Kernel +end + +describe "Kernel.load" do + it_behaves_like :kernel_load, :load, Kernel +end diff --git a/spec/ruby/core/kernel/local_variables_spec.rb b/spec/ruby/core/kernel/local_variables_spec.rb new file mode 100644 index 0000000000..f6f1e15f52 --- /dev/null +++ b/spec/ruby/core/kernel/local_variables_spec.rb @@ -0,0 +1,48 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel#local_variables" do + after :each do + ScratchPad.clear + end + + it "is a private method" do + Kernel.should have_private_instance_method(:local_variables) + end + + it "contains locals as they are added" do + a = 1 + b = 2 + local_variables.should include(:a, :b) + local_variables.length.should == 2 + end + + it "is accessible from bindings" do + def local_var_foo + a = 1 + b = 2 + binding + end + foo_binding = local_var_foo() + res = eval("local_variables",foo_binding) + res.should include(:a, :b) + res.length.should == 2 + end + + it "is accessible in eval" do + eval "a=1; b=2; ScratchPad.record local_variables" + ScratchPad.recorded.should include(:a, :b) + ScratchPad.recorded.length.should == 2 + end + + it "includes only unique variable names" do + def local_var_method + a = 1 + 1.times do |;a| + return local_variables + end + end + + local_var_method.should == [:a] + end +end diff --git a/spec/ruby/core/kernel/loop_spec.rb b/spec/ruby/core/kernel/loop_spec.rb new file mode 100644 index 0000000000..7c76c7d28e --- /dev/null +++ b/spec/ruby/core/kernel/loop_spec.rb @@ -0,0 +1,79 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel.loop" do + it "is a private method" do + Kernel.should have_private_instance_method(:loop) + end + + it "calls block until it is terminated by a break" do + i = 0 + loop do + i += 1 + break if i == 10 + end + + i.should == 10 + end + + it "returns value passed to break" do + loop do + break 123 + end.should == 123 + end + + it "returns nil if no value passed to break" do + loop do + break + end.should == nil + end + + it "returns an enumerator if no block given" do + enum = loop + enum.instance_of?(Enumerator).should be_true + cnt = 0 + enum.each do |*args| + raise "Args should be empty #{args.inspect}" unless args.empty? + cnt += 1 + break cnt if cnt >= 42 + end.should == 42 + end + + it "rescues StopIteration" do + loop do + raise StopIteration + end + 42.should == 42 + end + + it "rescues StopIteration's subclasses" do + finish = Class.new StopIteration + loop do + raise finish + end + 42.should == 42 + end + + it "does not rescue other errors" do + ->{ loop do raise StandardError end }.should raise_error( StandardError ) + end + + it "returns StopIteration#result, the result value of a finished iterator" do + e = Enumerator.new { |y| + y << 1 + y << 2 + :stopped + } + loop { e.next }.should == :stopped + end + + describe "when no block is given" do + describe "returned Enumerator" do + describe "size" do + it "returns Float::INFINITY" do + loop.size.should == Float::INFINITY + end + end + end + end +end diff --git a/spec/ruby/core/kernel/match_spec.rb b/spec/ruby/core/kernel/match_spec.rb new file mode 100644 index 0000000000..cd6330fe91 --- /dev/null +++ b/spec/ruby/core/kernel/match_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' + +describe "Kernel#=~" do + it "is no longer defined" do + Object.new.should_not.respond_to?(:=~) + end +end diff --git a/spec/ruby/core/kernel/method_spec.rb b/spec/ruby/core/kernel/method_spec.rb new file mode 100644 index 0000000000..3fc566d6a6 --- /dev/null +++ b/spec/ruby/core/kernel/method_spec.rb @@ -0,0 +1,80 @@ +require_relative '../../spec_helper' +require_relative 'shared/method' +require_relative 'fixtures/classes' + +describe "Kernel#method" do + it_behaves_like :kernel_method, :method + + before :each do + @obj = KernelSpecs::A.new + end + + it "can be called on a private method" do + @obj.send(:private_method).should == :private_method + @obj.method(:private_method).should be_an_instance_of(Method) + end + + it "can be called on a protected method" do + @obj.send(:protected_method).should == :protected_method + @obj.method(:protected_method).should be_an_instance_of(Method) + end + + it "will see an alias of the original method as == when in a derived class" do + obj = KernelSpecs::B.new + obj.method(:aliased_pub_method).should == obj.method(:pub_method) + end + + it "can call methods created with define_method" do + m = @obj.method(:defined_method) + m.call.should == :defined + end + + it "can be called even if we only respond_to_missing? method, true" do + m = KernelSpecs::RespondViaMissing.new.method(:handled_privately) + m.should be_an_instance_of(Method) + m.call(1, 2, 3).should == "Done handled_privately([1, 2, 3])" + end + + it "can call a #method_missing accepting zero or one arguments" do + cls = Class.new do + def respond_to_missing?(name, *) + name == :foo or super + end + def method_missing + :no_args + end + end + m = cls.new.method(:foo) + -> { m.call }.should raise_error(ArgumentError) + + cls = Class.new do + def respond_to_missing?(name, *) + name == :bar or super + end + def method_missing(m) + m + end + end + m = cls.new.method(:bar) + m.call.should == :bar + end + + describe "converts the given name to a String using #to_str" do + it "calls #to_str to convert the given name to a String" do + name = mock("method-name") + name.should_receive(:to_str).and_return("hash") + Object.method(name).should == Object.method(:hash) + end + + it "raises a TypeError if the given name can't be converted to a String" do + -> { Object.method(nil) }.should raise_error(TypeError) + -> { Object.method([]) }.should raise_error(TypeError) + end + + it "raises a NoMethodError if the given argument raises a NoMethodError during type coercion to a String" do + name = mock("method-name") + name.should_receive(:to_str).and_raise(NoMethodError) + -> { Object.method(name) }.should raise_error(NoMethodError) + end + end +end diff --git a/spec/ruby/core/kernel/methods_spec.rb b/spec/ruby/core/kernel/methods_spec.rb new file mode 100644 index 0000000000..fb7a7e8be9 --- /dev/null +++ b/spec/ruby/core/kernel/methods_spec.rb @@ -0,0 +1,101 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative '../../fixtures/reflection' + +# TODO: rewrite +describe "Kernel#methods" do + it "returns singleton methods defined by obj.meth" do + KernelSpecs::Methods.methods(false).should include(:ichi) + end + + it "returns singleton methods defined in 'class << self'" do + KernelSpecs::Methods.methods(false).should include(:san) + end + + it "returns private singleton methods defined by obj.meth" do + KernelSpecs::Methods.methods(false).should include(:shi) + end + + it "returns singleton methods defined in 'class << self' when it follows 'private'" do + KernelSpecs::Methods.methods(false).should include(:roku) + end + + it "does not return private singleton methods defined in 'class << self'" do + KernelSpecs::Methods.methods(false).should_not include(:shichi) + end + + it "returns the publicly accessible methods of the object" do + meths = KernelSpecs::Methods.methods(false) + meths.should include(:hachi, :ichi, :juu, :juu_ichi, + :juu_ni, :roku, :san, :shi) + + KernelSpecs::Methods.new.methods(false).should == [] + end + + it "returns the publicly accessible methods in the object, its ancestors and mixed-in modules" do + meths = KernelSpecs::Methods.methods(false) & KernelSpecs::Methods.methods + meths.should include(:hachi, :ichi, :juu, :juu_ichi, + :juu_ni, :roku, :san, :shi) + + KernelSpecs::Methods.new.methods.should include(:ku, :ni, :juu_san) + end + + it "returns methods added to the metaclass through extend" do + meth = KernelSpecs::Methods.new + meth.methods.should_not include(:peekaboo) + meth.extend(KernelSpecs::Methods::MetaclassMethods) + meth.methods.should include(:peekaboo) + end + + it "does not return undefined singleton methods defined by obj.meth" do + o = KernelSpecs::Child.new + def o.single; end + o.methods.should include(:single) + + class << o; self; end.send :undef_method, :single + o.methods.should_not include(:single) + end + + it "does not return superclass methods undefined in the object's class" do + KernelSpecs::Child.new.methods.should_not include(:parent_method) + end + + it "does not return superclass methods undefined in a superclass" do + KernelSpecs::Grandchild.new.methods.should_not include(:parent_method) + end + + it "does not return included module methods undefined in the object's class" do + KernelSpecs::Grandchild.new.methods.should_not include(:parent_mixin_method) + end +end + +describe :kernel_methods_supers, shared: true do + before :all do + @ms = [:pro, :pub] + end + + it "returns a unique list for an object extended by a module" do + m = ReflectSpecs.oed.methods(*@object) + m.select { |x| @ms.include? x }.sort.should == @ms + end + + it "returns a unique list for a class including a module" do + m = ReflectSpecs::D.new.methods(*@object) + m.select { |x| @ms.include? x }.sort.should == @ms + end + + it "returns a unique list for a subclass of a class that includes a module" do + m = ReflectSpecs::E.new.methods(*@object) + m.select { |x| @ms.include? x }.sort.should == @ms + end +end + +describe "Kernel#methods" do + describe "when not passed an argument" do + it_behaves_like :kernel_methods_supers, nil, [] + end + + describe "when passed true" do + it_behaves_like :kernel_methods_supers, nil, true + end +end diff --git a/spec/ruby/core/kernel/nil_spec.rb b/spec/ruby/core/kernel/nil_spec.rb new file mode 100644 index 0000000000..7418245f26 --- /dev/null +++ b/spec/ruby/core/kernel/nil_spec.rb @@ -0,0 +1,12 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe 'Kernel#nil?' do + it 'returns false' do + Object.should_not.nil? + Object.new.should_not.nil? + ''.should_not.nil? + [].should_not.nil? + {}.should_not.nil? + end +end diff --git a/spec/ruby/core/kernel/not_match_spec.rb b/spec/ruby/core/kernel/not_match_spec.rb new file mode 100644 index 0000000000..082e56fed7 --- /dev/null +++ b/spec/ruby/core/kernel/not_match_spec.rb @@ -0,0 +1,25 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel#!~" do + class KernelSpecs::NotMatch + def !~(obj) + :foo + end + end + + it 'calls =~ internally and negates the result' do + obj = Object.new + obj.should_receive(:=~).and_return(true) + (obj !~ :foo).should == false + end + + it "raises NoMethodError if self does not respond to #=~" do + -> { Object.new !~ :foo }.should raise_error(NoMethodError) + end + + it 'can be overridden in subclasses' do + obj = KernelSpecs::NotMatch.new + (obj !~ :bar).should == :foo + end +end diff --git a/spec/ruby/core/kernel/object_id_spec.rb b/spec/ruby/core/kernel/object_id_spec.rb new file mode 100644 index 0000000000..ef9e80c831 --- /dev/null +++ b/spec/ruby/core/kernel/object_id_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/kernel/object_id' + +describe "Kernel#object_id" do + it_behaves_like :object_id, :object_id, Object +end diff --git a/spec/ruby/core/kernel/open_spec.rb b/spec/ruby/core/kernel/open_spec.rb new file mode 100644 index 0000000000..b967d5044b --- /dev/null +++ b/spec/ruby/core/kernel/open_spec.rb @@ -0,0 +1,194 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel#open" do + before :each do + @name = tmp("kernel_open.txt") + @content = "This is a test" + touch(@name) { |f| f.write @content } + @file = nil + end + + after :each do + @file.close if @file + rm_r @name + end + + it "is a private method" do + Kernel.should have_private_instance_method(:open) + end + + it "opens a file when given a valid filename" do + @file = open(@name) + @file.should be_kind_of(File) + end + + it "opens a file when called with a block" do + open(@name, "r") { |f| f.gets }.should == @content + end + + ruby_version_is ""..."4.0" do + platform_is_not :windows, :wasi do + it "opens an io when path starts with a pipe" do + suppress_warning do # https://bugs.ruby-lang.org/issues/19630 + @io = open("|date") + end + begin + @io.should be_kind_of(IO) + @io.read + ensure + @io.close + end + end + + it "opens an io when called with a block" do + suppress_warning do # https://bugs.ruby-lang.org/issues/19630 + @output = open("|date") { |f| f.read } + end + @output.should_not == '' + end + + it "opens an io for writing" do + suppress_warning do # https://bugs.ruby-lang.org/issues/19630 + -> { + bytes = open("|cat", "w") { |io| io.write(".") } + bytes.should == 1 + }.should output_to_fd(".") + end + end + end + + platform_is :windows do + it "opens an io when path starts with a pipe" do + suppress_warning do # https://bugs.ruby-lang.org/issues/19630 + @io = open("|date /t") + end + begin + @io.should be_kind_of(IO) + @io.read + ensure + @io.close + end + end + + it "opens an io when called with a block" do + suppress_warning do # https://bugs.ruby-lang.org/issues/19630 + @output = open("|date /t") { |f| f.read } + end + @output.should_not == '' + end + end + + ruby_version_is "3.3" do + # https://bugs.ruby-lang.org/issues/19630 + it "warns about deprecation given a path with a pipe" do + cmd = "|echo ok" + -> { + open(cmd) { |f| f.read } + }.should complain(/Kernel#open with a leading '\|'/) + end + end + end + + it "raises an ArgumentError if not passed one argument" do + -> { open }.should raise_error(ArgumentError) + end + + it "accepts options as keyword arguments" do + @file = open(@name, "r", 0666, flags: File::CREAT) + @file.should be_kind_of(File) + + -> { + open(@name, "r", 0666, {flags: File::CREAT}) + }.should raise_error(ArgumentError, "wrong number of arguments (given 4, expected 1..3)") + end + + describe "when given an object that responds to to_open" do + before :each do + ScratchPad.clear + end + + it "calls #to_path to convert the argument to a String before calling #to_str" do + obj = mock("open to_path") + obj.should_receive(:to_path).at_least(1).times.and_return(@name) + obj.should_not_receive(:to_str) + + open(obj, "r") { |f| f.gets }.should == @content + end + + it "calls #to_str to convert the argument to a String" do + obj = mock("open to_str") + obj.should_receive(:to_str).at_least(1).times.and_return(@name) + + open(obj, "r") { |f| f.gets }.should == @content + end + + it "calls #to_open on argument" do + obj = mock('fileish') + @file = File.open(@name) + obj.should_receive(:to_open).and_return(@file) + @file = open(obj) + @file.should be_kind_of(File) + end + + it "returns the value from #to_open" do + obj = mock('to_open') + obj.should_receive(:to_open).and_return(:value) + + open(obj).should == :value + end + + it "passes its arguments onto #to_open" do + obj = mock('to_open') + obj.should_receive(:to_open).with(1, 2, 3) + open(obj, 1, 2, 3) + end + + it "passes keyword arguments onto #to_open as keyword arguments if to_open accepts them" do + obj = Object.new + def obj.to_open(*args, **kw) + ScratchPad << {args: args, kw: kw} + end + + ScratchPad.record [] + open(obj, 1, 2, 3, a: "b") + ScratchPad.recorded.should == [args: [1, 2, 3], kw: {a: "b"}] + end + + it "passes the return value from #to_open to a block" do + obj = mock('to_open') + obj.should_receive(:to_open).and_return(:value) + + open(obj) do |mock| + ScratchPad.record(mock) + end + + ScratchPad.recorded.should == :value + end + end + + it "raises a TypeError if passed a non-String that does not respond to #to_open" do + obj = mock('non-fileish') + -> { open(obj) }.should raise_error(TypeError) + -> { open(nil) }.should raise_error(TypeError) + -> { open(7) }.should raise_error(TypeError) + end + + it "accepts nil for mode and permission" do + open(@name, nil, nil) { |f| f.gets }.should == @content + end + + it "is not redefined by open-uri" do + code = <<~RUBY + before = Kernel.instance_method(:open) + require 'open-uri' + after = Kernel.instance_method(:open) + p before == after + RUBY + ruby_exe(code, args: "2>&1").should == "true\n" + end +end + +describe "Kernel.open" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/kernel/p_spec.rb b/spec/ruby/core/kernel/p_spec.rb new file mode 100644 index 0000000000..eae191aa54 --- /dev/null +++ b/spec/ruby/core/kernel/p_spec.rb @@ -0,0 +1,85 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel#p" do + before :all do + @rs_f, @rs_b, @rs_c = $/, $\, $, + end + + after :each do + suppress_warning { + $/, $\, $, = @rs_f, @rs_b, @rs_c + } + end + + it "is a private method" do + Kernel.should have_private_instance_method(:p) + end + + # TODO: fix + it "flushes output if receiver is a File" do + filename = tmp("Kernel_p_flush") + $$.to_s + begin + File.open(filename, "w") do |f| + begin + old_stdout = $stdout + $stdout = f + p("abcde") + ensure + $stdout = old_stdout + end + + File.open(filename) do |f2| + f2.read(7).should == "\"abcde\"" + end + end + ensure + rm_r filename + end + end + + it "prints obj.inspect followed by system record separator for each argument given" do + o = mock("Inspector Gadget") + o.should_receive(:inspect).any_number_of_times.and_return "Next time, Gadget, NEXT TIME!" + + -> { p(o) }.should output("Next time, Gadget, NEXT TIME!\n") + -> { p(*[o]) }.should output("Next time, Gadget, NEXT TIME!\n") + -> { p(*[o, o]) }.should output("Next time, Gadget, NEXT TIME!\nNext time, Gadget, NEXT TIME!\n") + -> { p([o])}.should output("[#{o.inspect}]\n") + end + + it "is not affected by setting $\\, $/ or $," do + o = mock("Inspector Gadget") + o.should_receive(:inspect).any_number_of_times.and_return "Next time, Gadget, NEXT TIME!" + + suppress_warning { + $, = " *helicopter sound*\n" + } + -> { p(o) }.should output_to_fd("Next time, Gadget, NEXT TIME!\n") + + suppress_warning { + $\ = " *helicopter sound*\n" + } + -> { p(o) }.should output_to_fd("Next time, Gadget, NEXT TIME!\n") + + suppress_warning { + $/ = " *helicopter sound*\n" + } + -> { p(o) }.should output_to_fd("Next time, Gadget, NEXT TIME!\n") + end + + it "prints nothing if no argument is given" do + -> { p }.should output("") + end + + it "prints nothing if called splatting an empty Array" do + -> { p(*[]) }.should output("") + end + + # Not sure how to spec this, but wanted to note the behavior here + it "does not flush if receiver is not a TTY or a File" +end + +describe "Kernel.p" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/kernel/pp_spec.rb b/spec/ruby/core/kernel/pp_spec.rb new file mode 100644 index 0000000000..b5b1c98d38 --- /dev/null +++ b/spec/ruby/core/kernel/pp_spec.rb @@ -0,0 +1,9 @@ +require_relative '../../spec_helper' + +describe "Kernel#pp" do + it "lazily loads the 'pp' library and delegates the call to that library" do + # Run in child process to ensure 'pp' hasn't been loaded yet. + output = ruby_exe("pp [1, 2, 3]") + output.should == "[1, 2, 3]\n" + end +end diff --git a/spec/ruby/core/kernel/print_spec.rb b/spec/ruby/core/kernel/print_spec.rb new file mode 100644 index 0000000000..7e7c9b822d --- /dev/null +++ b/spec/ruby/core/kernel/print_spec.rb @@ -0,0 +1,24 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel#print" do + it "is a private method" do + Kernel.should have_private_instance_method(:print) + end + + it "delegates to $stdout" do + -> { print :arg }.should output("arg") + end + + it "prints $_ when no arguments are given" do + orig_value = $_ + $_ = 'foo' + -> { print }.should output("foo") + ensure + $_ = orig_value + end +end + +describe "Kernel.print" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/kernel/printf_spec.rb b/spec/ruby/core/kernel/printf_spec.rb new file mode 100644 index 0000000000..61bf955c25 --- /dev/null +++ b/spec/ruby/core/kernel/printf_spec.rb @@ -0,0 +1,70 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/sprintf' + +describe "Kernel#printf" do + it "is a private method" do + Kernel.should have_private_instance_method(:printf) + end +end + +describe "Kernel.printf" do + before :each do + @stdout = $stdout + @name = tmp("kernel_puts.txt") + $stdout = new_io @name + end + + after :each do + $stdout.close + $stdout = @stdout + rm_r @name + end + + it "writes to stdout when a string is the first argument" do + $stdout.should_receive(:write).with("string") + Kernel.printf("%s", "string") + end + + it "calls write on the first argument when it is not a string" do + object = mock('io') + object.should_receive(:write).with("string") + Kernel.printf(object, "%s", "string") + end + + it "calls #to_str to convert the format object to a String" do + object = mock('format string') + object.should_receive(:to_str).and_return("to_str: %i") + $stdout.should_receive(:write).with("to_str: 42") + Kernel.printf($stdout, object, 42) + end +end + +describe "Kernel.printf" do + describe "formatting" do + before :each do + require "stringio" + end + + context "io is specified" do + it_behaves_like :kernel_sprintf, -> format, *args { + io = StringIO.new(+"") + Kernel.printf(io, format, *args) + io.string + } + end + + context "io is not specified" do + it_behaves_like :kernel_sprintf, -> format, *args { + stdout = $stdout + begin + $stdout = io = StringIO.new(+"") + Kernel.printf(format, *args) + io.string + ensure + $stdout = stdout + end + } + end + end +end diff --git a/spec/ruby/core/kernel/private_methods_spec.rb b/spec/ruby/core/kernel/private_methods_spec.rb new file mode 100644 index 0000000000..041634d1e5 --- /dev/null +++ b/spec/ruby/core/kernel/private_methods_spec.rb @@ -0,0 +1,69 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative '../../fixtures/reflection' + +# TODO: rewrite +describe "Kernel#private_methods" do + it "returns a list of the names of privately accessible methods in the object" do + m = KernelSpecs::Methods.private_methods(false) + m.should include(:shichi) + m = KernelSpecs::Methods.new.private_methods(false) + m.should include(:juu_shi) + end + + it "returns a list of the names of privately accessible methods in the object and its ancestors and mixed-in modules" do + m = (KernelSpecs::Methods.private_methods(false) & KernelSpecs::Methods.private_methods) + + m.should include(:shichi) + m = KernelSpecs::Methods.new.private_methods + m.should include(:juu_shi) + end + + it "returns private methods mixed in to the metaclass" do + m = KernelSpecs::Methods.new + m.extend(KernelSpecs::Methods::MetaclassMethods) + m.private_methods.should include(:shoo) + end +end + +describe :kernel_private_methods_supers, shared: true do + it "returns a unique list for an object extended by a module" do + m = ReflectSpecs.oed.private_methods(*@object) + m.select { |x| x == :pri }.sort.should == [:pri] + end + + it "returns a unique list for a class including a module" do + m = ReflectSpecs::D.new.private_methods(*@object) + m.select { |x| x == :pri }.sort.should == [:pri] + end + + it "returns a unique list for a subclass of a class that includes a module" do + m = ReflectSpecs::E.new.private_methods(*@object) + m.select { |x| x == :pri }.sort.should == [:pri] + end +end + +describe :kernel_private_methods_with_falsy, shared: true do + it "returns a list of private methods in without its ancestors" do + ReflectSpecs::F.private_methods(@object).select{|m|/_pri\z/ =~ m}.sort.should == [:ds_pri, :fs_pri] + ReflectSpecs::F.new.private_methods(@object).should == [:f_pri] + end +end + +describe "Kernel#private_methods" do + describe "when not passed an argument" do + it_behaves_like :kernel_private_methods_supers, nil, [] + end + + describe "when passed true" do + it_behaves_like :kernel_private_methods_supers, nil, true + end + + describe "when passed false" do + it_behaves_like :kernel_private_methods_with_falsy, nil, false + end + + describe "when passed nil" do + it_behaves_like :kernel_private_methods_with_falsy, nil, nil + end +end diff --git a/spec/ruby/core/kernel/proc_spec.rb b/spec/ruby/core/kernel/proc_spec.rb new file mode 100644 index 0000000000..6553b8fd04 --- /dev/null +++ b/spec/ruby/core/kernel/proc_spec.rb @@ -0,0 +1,48 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/lambda' + +# The functionality of Proc objects is specified in core/proc + +describe "Kernel.proc" do + it "is a private method" do + Kernel.should have_private_instance_method(:proc) + end + + it "creates a proc-style Proc if given a literal block" do + l = proc { 42 } + l.lambda?.should be_false + end + + it "returned the passed Proc if given an existing Proc" do + some_lambda = -> {} + some_lambda.lambda?.should be_true + l = proc(&some_lambda) + l.should equal(some_lambda) + l.lambda?.should be_true + end + + it_behaves_like :kernel_lambda, :proc + + it "returns from the creation site of the proc, not just the proc itself" do + @reached_end_of_method = nil + def test + proc { return }.call + @reached_end_of_method = true + end + test + @reached_end_of_method.should be_nil + end +end + +describe "Kernel#proc" do + def some_method + proc + end + + it "raises an ArgumentError when passed no block" do + -> { + some_method { "hello" } + }.should raise_error(ArgumentError, 'tried to create Proc object without a block') + end +end diff --git a/spec/ruby/core/kernel/protected_methods_spec.rb b/spec/ruby/core/kernel/protected_methods_spec.rb new file mode 100644 index 0000000000..d3334e886b --- /dev/null +++ b/spec/ruby/core/kernel/protected_methods_spec.rb @@ -0,0 +1,69 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative '../../fixtures/reflection' + +# TODO: rewrite + +# The reason why having include() is to show the specification explicitly. +# You should use have_protected_method() with the exception of this spec. +describe "Kernel#protected_methods" do + it "returns a list of the names of protected methods accessible in the object" do + KernelSpecs::Methods.protected_methods(false).sort.should include(:juu_ichi) + KernelSpecs::Methods.new.protected_methods(false).should include(:ku) + end + + it "returns a list of the names of protected methods accessible in the object and from its ancestors and mixed-in modules" do + l1 = KernelSpecs::Methods.protected_methods(false) + l2 = KernelSpecs::Methods.protected_methods + (l1 & l2).should include(:juu_ichi) + KernelSpecs::Methods.new.protected_methods.should include(:ku) + end + + it "returns methods mixed in to the metaclass" do + m = KernelSpecs::Methods.new + m.extend(KernelSpecs::Methods::MetaclassMethods) + m.protected_methods.should include(:nopeeking) + end +end + +describe :kernel_protected_methods_supers, shared: true do + it "returns a unique list for an object extended by a module" do + m = ReflectSpecs.oed.protected_methods(*@object) + m.select { |x| x == :pro }.sort.should == [:pro] + end + + it "returns a unique list for a class including a module" do + m = ReflectSpecs::D.new.protected_methods(*@object) + m.select { |x| x == :pro }.sort.should == [:pro] + end + + it "returns a unique list for a subclass of a class that includes a module" do + m = ReflectSpecs::E.new.protected_methods(*@object) + m.select { |x| x == :pro }.sort.should == [:pro] + end +end + +describe :kernel_protected_methods_with_falsy, shared: true do + it "returns a list of protected methods in without its ancestors" do + ReflectSpecs::F.protected_methods(@object).select{|m|/_pro\z/ =~ m}.sort.should == [:ds_pro, :fs_pro] + ReflectSpecs::F.new.protected_methods(@object).should == [:f_pro] + end +end + +describe "Kernel#protected_methods" do + describe "when not passed an argument" do + it_behaves_like :kernel_protected_methods_supers, nil, [] + end + + describe "when passed true" do + it_behaves_like :kernel_protected_methods_supers, nil, true + end + + describe "when passed false" do + it_behaves_like :kernel_protected_methods_with_falsy, nil, false + end + + describe "when passed nil" do + it_behaves_like :kernel_protected_methods_with_falsy, nil, nil + end +end diff --git a/spec/ruby/core/kernel/public_method_spec.rb b/spec/ruby/core/kernel/public_method_spec.rb new file mode 100644 index 0000000000..c5d54c777e --- /dev/null +++ b/spec/ruby/core/kernel/public_method_spec.rb @@ -0,0 +1,32 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/method' + +describe "Kernel#public_method" do + it_behaves_like :kernel_method, :public_method + + before :each do + @obj = KernelSpecs::A.new + end + + it "raises a NameError when called on a private method" do + @obj.send(:private_method).should == :private_method + -> do + @obj.public_method(:private_method) + end.should raise_error(NameError) + end + + it "raises a NameError when called on a protected method" do + @obj.send(:protected_method).should == :protected_method + -> { + @obj.public_method(:protected_method) + }.should raise_error(NameError) + end + + it "raises a NameError if we only repond_to_missing? method, true" do + obj = KernelSpecs::RespondViaMissing.new + -> do + obj.public_method(:handled_privately) + end.should raise_error(NameError) + end +end diff --git a/spec/ruby/core/kernel/public_methods_spec.rb b/spec/ruby/core/kernel/public_methods_spec.rb new file mode 100644 index 0000000000..a5512784fb --- /dev/null +++ b/spec/ruby/core/kernel/public_methods_spec.rb @@ -0,0 +1,76 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative '../../fixtures/reflection' + +# TODO: rewrite +describe "Kernel#public_methods" do + it "returns a list of the names of publicly accessible methods in the object" do + KernelSpecs::Methods.public_methods(false).sort.should include(:hachi, + :ichi, :juu, :juu_ni, :roku, :san, :shi) + KernelSpecs::Methods.new.public_methods(false).sort.should include(:juu_san, :ni) + end + + it "returns a list of names without protected accessible methods in the object" do + KernelSpecs::Methods.public_methods(false).sort.should_not include(:juu_ichi) + KernelSpecs::Methods.new.public_methods(false).sort.should_not include(:ku) + end + + it "returns a list of the names of publicly accessible methods in the object and its ancestors and mixed-in modules" do + (KernelSpecs::Methods.public_methods(false) & KernelSpecs::Methods.public_methods).sort.should include( + :hachi, :ichi, :juu, :juu_ni, :roku, :san, :shi) + m = KernelSpecs::Methods.new.public_methods + m.should include(:ni, :juu_san) + end + + it "returns methods mixed in to the metaclass" do + m = KernelSpecs::Methods.new + m.extend(KernelSpecs::Methods::MetaclassMethods) + m.public_methods.should include(:peekaboo) + end + + it "returns public methods for immediates" do + 10.public_methods.should include(:divmod) + end +end + +describe :kernel_public_methods_supers, shared: true do + it "returns a unique list for an object extended by a module" do + m = ReflectSpecs.oed.public_methods(*@object) + m.select { |x| x == :pub }.sort.should == [:pub] + end + + it "returns a unique list for a class including a module" do + m = ReflectSpecs::D.new.public_methods(*@object) + m.select { |x| x == :pub }.sort.should == [:pub] + end + + it "returns a unique list for a subclass of a class that includes a module" do + m = ReflectSpecs::E.new.public_methods(*@object) + m.select { |x| x == :pub }.sort.should == [:pub] + end +end + +describe :kernel_public_methods_with_falsy, shared: true do + it "returns a list of public methods in without its ancestors" do + ReflectSpecs::F.public_methods(@object).select{|m|/_pub\z/ =~ m}.sort.should == [:ds_pub, :fs_pub] + ReflectSpecs::F.new.public_methods(@object).should == [:f_pub] + end +end + +describe "Kernel#public_methods" do + describe "when not passed an argument" do + it_behaves_like :kernel_public_methods_supers, nil, [] + end + + describe "when passed true" do + it_behaves_like :kernel_public_methods_supers, nil, true + end + + describe "when passed false" do + it_behaves_like :kernel_public_methods_with_falsy, nil, false + end + + describe "when passed nil" do + it_behaves_like :kernel_public_methods_with_falsy, nil, nil + end +end diff --git a/spec/ruby/core/kernel/public_send_spec.rb b/spec/ruby/core/kernel/public_send_spec.rb new file mode 100644 index 0000000000..b684b1729c --- /dev/null +++ b/spec/ruby/core/kernel/public_send_spec.rb @@ -0,0 +1,116 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative '../../shared/basicobject/send' + +describe "Kernel#public_send" do + it "invokes the named public method" do + class KernelSpecs::Foo + def bar + 'done' + end + end + KernelSpecs::Foo.new.public_send(:bar).should == 'done' + end + + it "invokes the named alias of a public method" do + class KernelSpecs::Foo + def bar + 'done' + end + alias :aka :bar + end + KernelSpecs::Foo.new.public_send(:aka).should == 'done' + end + + it "raises a NoMethodError if the method is protected" do + class KernelSpecs::Foo + protected + def bar + 'done' + end + end + -> { KernelSpecs::Foo.new.public_send(:bar)}.should raise_error(NoMethodError) + end + + it "raises a NoMethodError if the named method is private" do + class KernelSpecs::Foo + private + def bar + 'done2' + end + end + -> { + KernelSpecs::Foo.new.public_send(:bar) + }.should raise_error(NoMethodError) + end + + context 'called from own public method' do + before do + class << @receiver = Object.new + def call_protected_method + public_send :protected_method + end + + def call_private_method + public_send :private_method + end + + protected + + def protected_method + raise 'Should not called' + end + + private + + def private_method + raise 'Should not called' + end + end + end + + it "raises a NoMethodError if the method is protected" do + -> { @receiver.call_protected_method }.should raise_error(NoMethodError) + end + + it "raises a NoMethodError if the method is private" do + -> { @receiver.call_private_method }.should raise_error(NoMethodError) + end + end + + it "raises a NoMethodError if the named method is an alias of a private method" do + class KernelSpecs::Foo + private + def bar + 'done2' + end + alias :aka :bar + end + -> { + KernelSpecs::Foo.new.public_send(:aka) + }.should raise_error(NoMethodError) + end + + it "raises a NoMethodError if the named method is an alias of a protected method" do + class KernelSpecs::Foo + protected + def bar + 'done2' + end + alias :aka :bar + end + -> { + KernelSpecs::Foo.new.public_send(:aka) + }.should raise_error(NoMethodError) + end + + it "includes `public_send` in the backtrace when passed not enough arguments" do + -> { public_send() }.should raise_error(ArgumentError) { |e| e.backtrace[0].should =~ /[`'](?:Kernel#)?public_send'/ } + end + + it "includes `public_send` in the backtrace when passed a single incorrect argument" do + -> { public_send(Object.new) }.should raise_error(TypeError) { |e| e.backtrace[0].should =~ /[`'](?:Kernel#)?public_send'/ } + end + + it_behaves_like :basicobject_send, :public_send +end diff --git a/spec/ruby/core/kernel/putc_spec.rb b/spec/ruby/core/kernel/putc_spec.rb new file mode 100644 index 0000000000..74bd3765db --- /dev/null +++ b/spec/ruby/core/kernel/putc_spec.rb @@ -0,0 +1,39 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative '../../shared/io/putc' + +describe "Kernel#putc" do + it "is a private instance method" do + Kernel.should have_private_instance_method(:putc) + end +end + +describe "Kernel.putc" do + before :each do + @name = tmp("kernel_putc.txt") + @io = new_io @name + @io_object = @object + @stdout, $stdout = $stdout, @io + end + + after :each do + $stdout = @stdout + end + + it_behaves_like :io_putc, :putc_method, KernelSpecs +end + +describe "Kernel#putc" do + before :each do + @name = tmp("kernel_putc.txt") + @io = new_io @name + @io_object = @object + @stdout, $stdout = $stdout, @io + end + + after :each do + $stdout = @stdout + end + + it_behaves_like :io_putc, :putc_function, KernelSpecs +end diff --git a/spec/ruby/core/kernel/puts_spec.rb b/spec/ruby/core/kernel/puts_spec.rb new file mode 100644 index 0000000000..6eb38e8fcf --- /dev/null +++ b/spec/ruby/core/kernel/puts_spec.rb @@ -0,0 +1,29 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel#puts" do + before :each do + @stdout = $stdout + @name = tmp("kernel_puts.txt") + $stdout = new_io @name + end + + after :each do + $stdout.close + $stdout = @stdout + rm_r @name + end + + it "is a private method" do + Kernel.should have_private_instance_method(:puts) + end + + it "delegates to $stdout.puts" do + $stdout.should_receive(:puts).with(:arg) + puts :arg + end +end + +describe "Kernel.puts" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/kernel/raise_spec.rb b/spec/ruby/core/kernel/raise_spec.rb new file mode 100644 index 0000000000..fcd011d4e6 --- /dev/null +++ b/spec/ruby/core/kernel/raise_spec.rb @@ -0,0 +1,292 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative '../../shared/kernel/raise' + +describe "Kernel#raise" do + it "is a private method" do + Kernel.should have_private_instance_method(:raise) + end + + it "re-raises the previously rescued exception if no exception is specified" do + ScratchPad.record nil + + -> do + begin + raise Exception, "outer" + ScratchPad.record :no_abort + rescue Exception + begin + raise StandardError, "inner" + rescue StandardError + end + + raise + ScratchPad.record :no_reraise + end + end.should raise_error(Exception, "outer") + + ScratchPad.recorded.should be_nil + end + + it "accepts a cause keyword argument that sets the cause" do + cause = StandardError.new + -> { raise("error", cause: cause) }.should raise_error(RuntimeError) { |e| e.cause.should == cause } + end + + it "accepts a cause keyword argument that overrides the last exception" do + begin + raise "first raise" + rescue => ignored + cause = StandardError.new + -> { raise("error", cause: cause) }.should raise_error(RuntimeError) { |e| e.cause.should == cause } + end + end + + it "raises an ArgumentError when only cause is given" do + cause = StandardError.new + -> { raise(cause: cause) }.should raise_error(ArgumentError, "only cause is given with no arguments") + end + + it "raises an ArgumentError when only cause is given even if it has nil value" do + -> { raise(cause: nil) }.should raise_error(ArgumentError, "only cause is given with no arguments") + end + + it "raises a TypeError when given cause is not an instance of Exception" do + -> { raise "message", cause: Object.new }.should raise_error(TypeError, "exception object expected") + end + + it "doesn't raise a TypeError when given cause is nil" do + -> { raise "message", cause: nil }.should raise_error(RuntimeError, "message") + end + + it "allows cause equal an exception" do + e = RuntimeError.new("message") + -> { raise e, cause: e }.should raise_error(e) + end + + it "doesn't set given cause when it equals an exception" do + e = RuntimeError.new("message") + + begin + raise e, cause: e + rescue + end + + e.cause.should == nil + end + + it "raises ArgumentError when exception is part of the cause chain" do + -> { + begin + raise "Error 1" + rescue => e1 + begin + raise "Error 2" + rescue => e2 + begin + raise "Error 3" + rescue => e3 + raise e1, cause: e3 + end + end + end + }.should raise_error(ArgumentError, "circular causes") + end + + it "re-raises a rescued exception" do + -> do + begin + raise StandardError, "aaa" + rescue Exception + begin + raise ArgumentError + rescue ArgumentError + end + + # should raise StandardError "aaa" + raise + end + end.should raise_error(StandardError, "aaa") + end + + it "re-raises a previously rescued exception without overwriting the cause" do + begin + begin + begin + begin + raise "Error 1" + rescue => e1 + raise "Error 2" + end + rescue => e2 + raise "Error 3" + end + rescue + e2.cause.should == e1 + raise e2 + end + rescue => e + e.cause.should == e1 + end + end + + it "re-raises a previously rescued exception with overwriting the cause when it's explicitly specified with :cause option" do + e4 = RuntimeError.new("Error 4") + + begin + begin + begin + begin + raise "Error 1" + rescue => e1 + raise "Error 2" + end + rescue => e2 + raise "Error 3" + end + rescue + e2.cause.should == e1 + raise e2, cause: e4 + end + rescue => e + e.cause.should == e4 + end + end + + it "re-raises a previously rescued exception without overwriting the cause when it's explicitly specified with :cause option and has nil value" do + begin + begin + begin + begin + raise "Error 1" + rescue => e1 + raise "Error 2" + end + rescue => e2 + raise "Error 3" + end + rescue + e2.cause.should == e1 + raise e2, cause: nil + end + rescue => e + e.cause.should == e1 + end + end + + it "re-raises a previously rescued exception without setting a cause implicitly" do + begin + begin + raise "Error 1" + rescue => e1 + raise + end + rescue => e + e.should == e1 + e.cause.should == nil + end + end + + it "re-raises a previously rescued exception that has a cause without setting a cause implicitly" do + begin + begin + raise "Error 1" + rescue => e1 + begin + raise "Error 2" + rescue => e2 + raise + end + end + rescue => e + e.should == e2 + e.cause.should == e1 + end + end + + it "re-raises a previously rescued exception that doesn't have a cause and isn't a cause of any other exception with setting a cause implicitly" do + begin + begin + raise "Error 1" + rescue => e1 + begin + raise "Error 2" + rescue => e2 + raise "Error 3" + end + end + rescue => e + e.message.should == "Error 3" + e.cause.should == e2 + end + end + + it "re-raises a previously rescued exception that doesn't have a cause and is a cause of other exception without setting a cause implicitly" do + begin + begin + raise "Error 1" + rescue => e1 + begin + raise "Error 2" + rescue => e2 + e1.cause.should == nil + e2.cause.should == e1 + raise e1 + end + end + rescue => e + e.should == e1 + e.cause.should == nil + end + end + + it "re-raises a previously rescued exception that doesn't have a cause and is a cause of other exception (that wasn't raised explicitly) without setting a cause implicitly" do + begin + begin + raise "Error 1" + rescue => e1 + begin + foo # raises NameError + rescue => e2 + e1.cause.should == nil + e2.cause.should == e1 + raise e1 + end + end + rescue => e + e.should == e1 + e.cause.should == nil + end + end + + it "re-raises a previously rescued exception that has a cause but isn't a cause of any other exception without setting a cause implicitly" do + begin + begin + raise "Error 1" + rescue => e1 + begin + raise "Error 2" + rescue => e2 + begin + raise "Error 3", cause: RuntimeError.new("Error 4") + rescue => e3 + e2.cause.should == e1 + e3.cause.should_not == e2 + raise e2 + end + end + end + rescue => e + e.should == e2 + e.cause.should == e1 + end + end +end + +describe "Kernel#raise" do + it_behaves_like :kernel_raise, :raise, Kernel +end + +describe "Kernel.raise" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/kernel/rand_spec.rb b/spec/ruby/core/kernel/rand_spec.rb new file mode 100644 index 0000000000..355e425792 --- /dev/null +++ b/spec/ruby/core/kernel/rand_spec.rb @@ -0,0 +1,197 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel#rand" do + it "is a private method" do + Kernel.should have_private_instance_method(:rand) + end + + it "returns a float if no argument is passed" do + rand.should be_kind_of(Float) + end + + it "returns an integer for an integer argument" do + rand(77).should be_kind_of(Integer) + end + + it "returns an integer for a float argument greater than 1" do + rand(1.3).should be_kind_of(Integer) + end + + it "returns a float for an argument between -1 and 1" do + rand(-0.999).should be_kind_of(Float) + rand(-0.01).should be_kind_of(Float) + rand(0).should be_kind_of(Float) + rand(0.01).should be_kind_of(Float) + rand(0.999).should be_kind_of(Float) + end + + it "ignores the sign of the argument" do + [0, 1, 2, 3].should include(rand(-4)) + end + + it "never returns a value greater or equal to 1.0 with no arguments" do + 1000.times do + (0...1.0).should include(rand) + end + end + + it "never returns a value greater or equal to any passed in max argument" do + 1000.times do + (0...100).to_a.should include(rand(100)) + end + end + + it "calls to_int on its argument" do + l = mock('limit') + l.should_receive(:to_int).and_return 7 + + rand l + end + + context "given an exclusive range" do + it "returns an Integer between the two Integers" do + 1000.times do + x = rand(4...6) + x.should be_kind_of(Integer) + (4...6).should include(x) + end + end + + it "returns a Float between the given Integer and Float" do + 1000.times do + x = rand(4...6.5) + x.should be_kind_of(Float) + (4...6.5).should include(x) + end + end + + it "returns a Float between the given Float and Integer" do + 1000.times do + x = rand(3.5...6) + x.should be_kind_of(Float) + (3.5...6).should include(x) + end + end + + it "returns a Float between the two given Floats" do + 1000.times do + x = rand(3.5...6.5) + x.should be_kind_of(Float) + (3.5...6.5).should include(x) + end + end + end + + context "given an inclusive range" do + it "returns an Integer between the two Integers" do + 1000.times do + x = rand(4..6) + x.should be_kind_of(Integer) + (4..6).should include(x) + end + end + + it "returns a Float between the given Integer and Float" do + 1000.times do + x = rand(4..6.5) + x.should be_kind_of(Float) + (4..6.5).should include(x) + end + end + + it "returns a Float between the given Float and Integer" do + 1000.times do + x = rand(3.5..6) + x.should be_kind_of(Float) + (3.5..6).should include(x) + end + end + + it "returns a Float between the two given Floats" do + 1000.times do + x = rand(3.5..6.5) + x.should be_kind_of(Float) + (3.5..6.5).should include(x) + end + end + end + + context "given an inclusive range between 0 and 1" do + it "returns an Integer between the two Integers" do + x = rand(0..1) + x.should be_kind_of(Integer) + (0..1).should include(x) + end + + it "returns a Float if at least one side is Float" do + seed = 42 + x1 = Random.new(seed).rand(0..1.0) + x2 = Random.new(seed).rand(0.0..1.0) + x3 = Random.new(seed).rand(0.0..1) + + x3.should be_kind_of(Float) + x1.should eql(x3) + x2.should eql(x3) + + (0.0..1.0).should include(x3) + end + end + + context "given an exclusive range between 0 and 1" do + it "returns zero as an Integer" do + x = rand(0...1) + x.should be_kind_of(Integer) + x.should eql(0) + end + + it "returns a Float if at least one side is Float" do + seed = 42 + x1 = Random.new(seed).rand(0...1.0) + x2 = Random.new(seed).rand(0.0...1.0) + x3 = Random.new(seed).rand(0.0...1) + + x3.should be_kind_of(Float) + x1.should eql(x3) + x2.should eql(x3) + + (0.0...1.0).should include(x3) + end + end + + it "returns a numeric for an range argument where max is < 1" do + rand(0.25..0.75).should be_kind_of(Numeric) + end + + it "returns nil when range is backwards" do + rand(1..0).should be_nil + end + + it "returns the range start/end when Float range is 0" do + rand(1.0..1.0).should eql(1.0) + end + + it "returns the range start/end when Integer range is 0" do + rand(42..42).should eql(42) + end + + it "supports custom object types" do + rand(KernelSpecs::CustomRangeInteger.new(1)..KernelSpecs::CustomRangeInteger.new(42)).should be_an_instance_of(KernelSpecs::CustomRangeInteger) + rand(KernelSpecs::CustomRangeFloat.new(1.0)..KernelSpecs::CustomRangeFloat.new(42.0)).should be_an_instance_of(KernelSpecs::CustomRangeFloat) + rand(Time.now..Time.now).should be_an_instance_of(Time) + end + + it "is random on boot" do + results = 2.times.map { + out = ruby_exe('p rand', options: '--disable-gems') + Float(out) + } + results.size.should == 2 + # this is technically flaky, but very unlikely in a good distribution + results[0].should_not == results[1] + end +end + +describe "Kernel.rand" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/kernel/readline_spec.rb b/spec/ruby/core/kernel/readline_spec.rb new file mode 100644 index 0000000000..dce7b03dc8 --- /dev/null +++ b/spec/ruby/core/kernel/readline_spec.rb @@ -0,0 +1,12 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel#readline" do + it "is a private method" do + Kernel.should have_private_instance_method(:readline) + end +end + +describe "Kernel.readline" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/kernel/readlines_spec.rb b/spec/ruby/core/kernel/readlines_spec.rb new file mode 100644 index 0000000000..2b6d65fff2 --- /dev/null +++ b/spec/ruby/core/kernel/readlines_spec.rb @@ -0,0 +1,12 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel#readlines" do + it "is a private method" do + Kernel.should have_private_instance_method(:readlines) + end +end + +describe "Kernel.readlines" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/kernel/remove_instance_variable_spec.rb b/spec/ruby/core/kernel/remove_instance_variable_spec.rb new file mode 100644 index 0000000000..4e5ba5e018 --- /dev/null +++ b/spec/ruby/core/kernel/remove_instance_variable_spec.rb @@ -0,0 +1,72 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe :kernel_remove_instance_variable, shared: true do + it "returns the instance variable's value" do + value = @instance.send :remove_instance_variable, @object + value.should == "hello" + end + + it "removes the instance variable" do + @instance.send :remove_instance_variable, @object + @instance.instance_variable_defined?(@object).should be_false + end +end + +describe "Kernel#remove_instance_variable" do + before do + @instance = KernelSpecs::InstanceVariable.new + end + + it "is a public method" do + Kernel.should have_public_instance_method(:remove_instance_variable, false) + end + + it "raises a NameError if the instance variable is not defined" do + -> do + @instance.send :remove_instance_variable, :@unknown + end.should raise_error(NameError) + end + + it "raises a NameError if the argument is not a valid instance variable name" do + -> do + @instance.send :remove_instance_variable, :"@0" + end.should raise_error(NameError) + end + + it "raises a TypeError if passed an Object not defining #to_str" do + -> do + obj = mock("kernel remove_instance_variable") + @instance.send :remove_instance_variable, obj + end.should raise_error(TypeError) + end + + it "raises a FrozenError if self is frozen" do + o = Object.new + o.freeze + -> { o.remove_instance_variable(:@foo) }.should raise_error(FrozenError) + -> { o.remove_instance_variable(:foo) }.should raise_error(NameError) + end + + it "raises for frozen objects" do + -> { nil.remove_instance_variable(:@foo) }.should raise_error(FrozenError) + -> { nil.remove_instance_variable(:foo) }.should raise_error(NameError) + -> { :foo.remove_instance_variable(:@foo) }.should raise_error(FrozenError) + end + + describe "when passed a String" do + it_behaves_like :kernel_remove_instance_variable, nil, "@greeting" + end + + describe "when passed a Symbol" do + it_behaves_like :kernel_remove_instance_variable, nil, :@greeting + end + + describe "when passed an Object" do + it "calls #to_str to convert the argument" do + name = mock("kernel remove_instance_variable") + name.should_receive(:to_str).and_return("@greeting") + @instance.send :remove_instance_variable, name + end + end +end diff --git a/spec/ruby/core/kernel/require_relative_spec.rb b/spec/ruby/core/kernel/require_relative_spec.rb new file mode 100644 index 0000000000..6188d13a4e --- /dev/null +++ b/spec/ruby/core/kernel/require_relative_spec.rb @@ -0,0 +1,437 @@ +require_relative '../../spec_helper' +require_relative '../../fixtures/code_loading' + +describe "Kernel#require_relative with a relative path" do + before :each do + CodeLoadingSpecs.spec_setup + @dir = "../../fixtures/code" + @abs_dir = File.realpath(@dir, __dir__) + @path = "#{@dir}/load_fixture.rb" + @abs_path = File.realpath(@path, __dir__) + end + + after :each do + CodeLoadingSpecs.spec_cleanup + end + + platform_is_not :windows do + describe "when file is a symlink" do + before :each do + @link = tmp("symlink.rb", false) + @real_path = "#{@abs_dir}/symlink/symlink1.rb" + File.symlink(@real_path, @link) + end + + after :each do + rm_r @link + end + + it "loads a path relative to current file" do + require_relative(@link).should be_true + ScratchPad.recorded.should == [:loaded] + end + end + end + + it "loads a path relative to the current file" do + require_relative(@path).should be_true + ScratchPad.recorded.should == [:loaded] + end + + describe "in an #instance_eval with a" do + + it "synthetic file base name loads a file base name relative to the working directory" do + Dir.chdir @abs_dir do + Object.new.instance_eval("require_relative(#{File.basename(@path).inspect})", "foo.rb").should be_true + end + ScratchPad.recorded.should == [:loaded] + end + + it "synthetic file path loads a relative path relative to the working directory plus the directory of the synthetic path" do + Dir.chdir @abs_dir do + Object.new.instance_eval("require_relative(File.join('..', #{File.basename(@path).inspect}))", "bar/foo.rb").should be_true + end + ScratchPad.recorded.should == [:loaded] + end + + platform_is_not :windows do + it "synthetic relative file path with a Windows path separator specified loads a relative path relative to the working directory" do + Dir.chdir @abs_dir do + Object.new.instance_eval("require_relative(#{File.basename(@path).inspect})", "bar\\foo.rb").should be_true + end + ScratchPad.recorded.should == [:loaded] + end + end + + it "absolute file path loads a path relative to the absolute path" do + Object.new.instance_eval("require_relative(#{@path.inspect})", __FILE__).should be_true + ScratchPad.recorded.should == [:loaded] + end + + it "absolute file path loads a path relative to the root directory" do + root = @abs_path + until File.dirname(root) == root + root = File.dirname(root) + end + root_relative = @abs_path[root.size..-1] + Object.new.instance_eval("require_relative(#{root_relative.inspect})", "/").should be_true + ScratchPad.recorded.should == [:loaded] + end + + end + + it "loads a file defining many methods" do + require_relative("#{@dir}/methods_fixture.rb").should be_true + ScratchPad.recorded.should == [:loaded] + end + + it "raises a LoadError if the file does not exist" do + -> { require_relative("#{@dir}/nonexistent.rb") }.should raise_error(LoadError) + ScratchPad.recorded.should == [] + end + + it "raises a LoadError that includes the missing path" do + missing_path = "#{@dir}/nonexistent.rb" + expanded_missing_path = File.expand_path(missing_path, __dir__) + -> { require_relative(missing_path) }.should raise_error(LoadError) { |e| + e.message.should include(expanded_missing_path) + e.path.should == expanded_missing_path + } + ScratchPad.recorded.should == [] + end + + it "raises a LoadError if basepath does not exist" do + -> { eval("require_relative('#{@dir}/nonexistent.rb')") }.should raise_error(LoadError) + end + + it "stores the missing path in a LoadError object" do + path = "#{@dir}/nonexistent.rb" + + -> { + require_relative(path) + }.should(raise_error(LoadError) { |e| + e.path.should == File.expand_path(path, @abs_dir) + }) + end + + it "calls #to_str on non-String objects" do + name = mock("load_fixture.rb mock") + name.should_receive(:to_str).and_return(@path) + require_relative(name).should be_true + ScratchPad.recorded.should == [:loaded] + end + + it "raises a TypeError if argument does not respond to #to_str" do + -> { require_relative(nil) }.should raise_error(TypeError) + -> { require_relative(42) }.should raise_error(TypeError) + -> { + require_relative([@path,@path]) + }.should raise_error(TypeError) + end + + it "raises a TypeError if passed an object that has #to_s but not #to_str" do + name = mock("load_fixture.rb mock") + name.stub!(:to_s).and_return(@path) + -> { require_relative(name) }.should raise_error(TypeError) + end + + it "raises a TypeError if #to_str does not return a String" do + name = mock("#to_str returns nil") + name.should_receive(:to_str).at_least(1).times.and_return(nil) + -> { require_relative(name) }.should raise_error(TypeError) + end + + it "calls #to_path on non-String objects" do + name = mock("load_fixture.rb mock") + name.should_receive(:to_path).and_return(@path) + require_relative(name).should be_true + ScratchPad.recorded.should == [:loaded] + end + + it "calls #to_str on non-String objects returned by #to_path" do + name = mock("load_fixture.rb mock") + to_path = mock("load_fixture_rb #to_path mock") + name.should_receive(:to_path).and_return(to_path) + to_path.should_receive(:to_str).and_return(@path) + require_relative(name).should be_true + ScratchPad.recorded.should == [:loaded] + end + + describe "(file extensions)" do + it "loads a .rb extensioned file when passed a non-extensioned path" do + require_relative("#{@dir}/load_fixture").should be_true + ScratchPad.recorded.should == [:loaded] + end + + it "loads a .rb extensioned file when a C-extension file of the same name is loaded" do + $LOADED_FEATURES << "#{@abs_dir}/load_fixture.bundle" + $LOADED_FEATURES << "#{@abs_dir}/load_fixture.dylib" + $LOADED_FEATURES << "#{@abs_dir}/load_fixture.so" + $LOADED_FEATURES << "#{@abs_dir}/load_fixture.dll" + require_relative(@path).should be_true + ScratchPad.recorded.should == [:loaded] + end + + it "does not load a C-extension file if a .rb extensioned file is already loaded" do + $LOADED_FEATURES << "#{@abs_dir}/load_fixture.rb" + require_relative("#{@dir}/load_fixture").should be_false + ScratchPad.recorded.should == [] + end + + it "loads a .rb extensioned file when passed a non-.rb extensioned path" do + require_relative("#{@dir}/load_fixture.ext").should be_true + ScratchPad.recorded.should == [:loaded] + $LOADED_FEATURES.should include "#{@abs_dir}/load_fixture.ext.rb" + end + + it "loads a .rb extensioned file when a complex-extensioned C-extension file of the same name is loaded" do + $LOADED_FEATURES << "#{@abs_dir}/load_fixture.ext.bundle" + $LOADED_FEATURES << "#{@abs_dir}/load_fixture.ext.dylib" + $LOADED_FEATURES << "#{@abs_dir}/load_fixture.ext.so" + $LOADED_FEATURES << "#{@abs_dir}/load_fixture.ext.dll" + require_relative("#{@dir}/load_fixture.ext").should be_true + ScratchPad.recorded.should == [:loaded] + $LOADED_FEATURES.should include "#{@abs_dir}/load_fixture.ext.rb" + end + + it "does not load a C-extension file if a complex-extensioned .rb file is already loaded" do + $LOADED_FEATURES << "#{@abs_dir}/load_fixture.ext.rb" + require_relative("#{@dir}/load_fixture.ext").should be_false + ScratchPad.recorded.should == [] + end + end + + describe "($LOADED_FEATURES)" do + it "stores an absolute path" do + require_relative(@path).should be_true + $LOADED_FEATURES.should include(@abs_path) + end + + platform_is_not :windows, :wasi do + describe "with symlinks" do + before :each do + @symlink_to_code_dir = tmp("codesymlink") + File.symlink(CODE_LOADING_DIR, @symlink_to_code_dir) + @symlink_basename = File.basename(@symlink_to_code_dir) + @requiring_file = tmp("requiring") + end + + after :each do + rm_r @symlink_to_code_dir, @requiring_file + end + + it "does not canonicalize the path and stores a path with symlinks" do + symlink_path = "#{@symlink_basename}/load_fixture.rb" + absolute_path = "#{tmp("")}#{symlink_path}" + canonical_path = "#{CODE_LOADING_DIR}/load_fixture.rb" + touch(@requiring_file) { |f| + f.puts "require_relative #{symlink_path.inspect}" + } + load(@requiring_file) + ScratchPad.recorded.should == [:loaded] + + features = $LOADED_FEATURES.select { |path| path.end_with?('load_fixture.rb') } + features.should include(absolute_path) + features.should_not include(canonical_path) + end + + it "stores the same path that __FILE__ returns in the required file" do + symlink_path = "#{@symlink_basename}/load_fixture_and__FILE__.rb" + touch(@requiring_file) { |f| + f.puts "require_relative #{symlink_path.inspect}" + } + load(@requiring_file) + loaded_feature = $LOADED_FEATURES.last + ScratchPad.recorded.should == [loaded_feature] + end + end + end + + it "does not store the path if the load fails" do + saved_loaded_features = $LOADED_FEATURES.dup + -> { require_relative("#{@dir}/raise_fixture.rb") }.should raise_error(RuntimeError) + $LOADED_FEATURES.should == saved_loaded_features + end + + it "does not load an absolute path that is already stored" do + $LOADED_FEATURES << @abs_path + require_relative(@path).should be_false + ScratchPad.recorded.should == [] + end + + it "adds the suffix of the resolved filename" do + require_relative("#{@dir}/load_fixture").should be_true + $LOADED_FEATURES.should include("#{@abs_dir}/load_fixture.rb") + end + + it "loads a path for a file already loaded with a relative path" do + $LOAD_PATH << File.expand_path(@dir) + $LOADED_FEATURES << "load_fixture.rb" << "load_fixture" + require_relative(@path).should be_true + $LOADED_FEATURES.should include(@abs_path) + ScratchPad.recorded.should == [:loaded] + end + end +end + +describe "Kernel#require_relative with an absolute path" do + before :each do + CodeLoadingSpecs.spec_setup + @dir = File.expand_path "../../fixtures/code", __dir__ + @abs_dir = @dir + @path = File.join @dir, "load_fixture.rb" + @abs_path = @path + end + + after :each do + CodeLoadingSpecs.spec_cleanup + end + + it "loads a path relative to the current file" do + require_relative(@path).should be_true + ScratchPad.recorded.should == [:loaded] + end + + it "loads a file defining many methods" do + require_relative("#{@dir}/methods_fixture.rb").should be_true + ScratchPad.recorded.should == [:loaded] + end + + it "raises a LoadError if the file does not exist" do + -> { require_relative("#{@dir}/nonexistent.rb") }.should raise_error(LoadError) + ScratchPad.recorded.should == [] + end + + it "raises a LoadError if basepath does not exist" do + -> { eval("require_relative('#{@dir}/nonexistent.rb')") }.should raise_error(LoadError) + end + + it "stores the missing path in a LoadError object" do + path = "#{@dir}/nonexistent.rb" + + -> { + require_relative(path) + }.should(raise_error(LoadError) { |e| + e.path.should == File.expand_path(path, @abs_dir) + }) + end + + it "calls #to_str on non-String objects" do + name = mock("load_fixture.rb mock") + name.should_receive(:to_str).and_return(@path) + require_relative(name).should be_true + ScratchPad.recorded.should == [:loaded] + end + + it "raises a TypeError if argument does not respond to #to_str" do + -> { require_relative(nil) }.should raise_error(TypeError) + -> { require_relative(42) }.should raise_error(TypeError) + -> { + require_relative([@path,@path]) + }.should raise_error(TypeError) + end + + it "raises a TypeError if passed an object that has #to_s but not #to_str" do + name = mock("load_fixture.rb mock") + name.stub!(:to_s).and_return(@path) + -> { require_relative(name) }.should raise_error(TypeError) + end + + it "raises a TypeError if #to_str does not return a String" do + name = mock("#to_str returns nil") + name.should_receive(:to_str).at_least(1).times.and_return(nil) + -> { require_relative(name) }.should raise_error(TypeError) + end + + it "calls #to_path on non-String objects" do + name = mock("load_fixture.rb mock") + name.should_receive(:to_path).and_return(@path) + require_relative(name).should be_true + ScratchPad.recorded.should == [:loaded] + end + + it "calls #to_str on non-String objects returned by #to_path" do + name = mock("load_fixture.rb mock") + to_path = mock("load_fixture_rb #to_path mock") + name.should_receive(:to_path).and_return(to_path) + to_path.should_receive(:to_str).and_return(@path) + require_relative(name).should be_true + ScratchPad.recorded.should == [:loaded] + end + + describe "(file extensions)" do + it "loads a .rb extensioned file when passed a non-extensioned path" do + require_relative("#{@dir}/load_fixture").should be_true + ScratchPad.recorded.should == [:loaded] + end + + it "loads a .rb extensioned file when a C-extension file of the same name is loaded" do + $LOADED_FEATURES << "#{@abs_dir}/load_fixture.bundle" + $LOADED_FEATURES << "#{@abs_dir}/load_fixture.dylib" + $LOADED_FEATURES << "#{@abs_dir}/load_fixture.so" + $LOADED_FEATURES << "#{@abs_dir}/load_fixture.dll" + require_relative(@path).should be_true + ScratchPad.recorded.should == [:loaded] + end + + it "does not load a C-extension file if a .rb extensioned file is already loaded" do + $LOADED_FEATURES << "#{@abs_dir}/load_fixture.rb" + require_relative("#{@dir}/load_fixture").should be_false + ScratchPad.recorded.should == [] + end + + it "loads a .rb extensioned file when passed a non-.rb extensioned path" do + require_relative("#{@dir}/load_fixture.ext").should be_true + ScratchPad.recorded.should == [:loaded] + $LOADED_FEATURES.should include "#{@abs_dir}/load_fixture.ext.rb" + end + + it "loads a .rb extensioned file when a complex-extensioned C-extension file of the same name is loaded" do + $LOADED_FEATURES << "#{@abs_dir}/load_fixture.ext.bundle" + $LOADED_FEATURES << "#{@abs_dir}/load_fixture.ext.dylib" + $LOADED_FEATURES << "#{@abs_dir}/load_fixture.ext.so" + $LOADED_FEATURES << "#{@abs_dir}/load_fixture.ext.dll" + require_relative("#{@dir}/load_fixture.ext").should be_true + ScratchPad.recorded.should == [:loaded] + $LOADED_FEATURES.should include "#{@abs_dir}/load_fixture.ext.rb" + end + + it "does not load a C-extension file if a complex-extensioned .rb file is already loaded" do + $LOADED_FEATURES << "#{@abs_dir}/load_fixture.ext.rb" + require_relative("#{@dir}/load_fixture.ext").should be_false + ScratchPad.recorded.should == [] + end + end + + describe "($LOAD_FEATURES)" do + it "stores an absolute path" do + require_relative(@path).should be_true + $LOADED_FEATURES.should include(@abs_path) + end + + it "does not store the path if the load fails" do + saved_loaded_features = $LOADED_FEATURES.dup + -> { require_relative("#{@dir}/raise_fixture.rb") }.should raise_error(RuntimeError) + $LOADED_FEATURES.should == saved_loaded_features + end + + it "does not load an absolute path that is already stored" do + $LOADED_FEATURES << @abs_path + require_relative(@path).should be_false + ScratchPad.recorded.should == [] + end + + it "adds the suffix of the resolved filename" do + require_relative("#{@dir}/load_fixture").should be_true + $LOADED_FEATURES.should include("#{@abs_dir}/load_fixture.rb") + end + + it "loads a path for a file already loaded with a relative path" do + $LOAD_PATH << File.expand_path(@dir) + $LOADED_FEATURES << "load_fixture.rb" << "load_fixture" + require_relative(@path).should be_true + $LOADED_FEATURES.should include(@abs_path) + ScratchPad.recorded.should == [:loaded] + end + end +end diff --git a/spec/ruby/core/kernel/require_spec.rb b/spec/ruby/core/kernel/require_spec.rb new file mode 100644 index 0000000000..60d17242fe --- /dev/null +++ b/spec/ruby/core/kernel/require_spec.rb @@ -0,0 +1,60 @@ +require_relative '../../spec_helper' +require_relative '../../fixtures/code_loading' +require_relative 'shared/require' + +describe "Kernel#require" do + before :each do + CodeLoadingSpecs.spec_setup + end + + after :each do + CodeLoadingSpecs.spec_cleanup + end + + # if this fails, update your rubygems + it "is a private method" do + Kernel.should have_private_instance_method(:require) + end + + provided = %w[complex enumerator fiber rational thread ruby2_keywords] + ruby_version_is "4.0" do + provided << "set" + provided << "pathname" + end + + it "#{provided.join(', ')} are already required" do + out = ruby_exe("puts $LOADED_FEATURES", options: '--disable-gems --disable-did-you-mean') + features = out.lines.map { |line| File.basename(line.chomp, '.*') } + + # Ignore CRuby internals + features -= %w[encdb transdb windows_1252 windows_31j] + features.reject! { |feature| feature.end_with?('-fake') } + + features.sort.should == provided.sort + + requires = provided + ruby_version_is "4.0" do + requires = requires.map { |f| f == "pathname" ? "pathname.so" : f } + end + + code = requires.map { |f| "puts require #{f.inspect}\n" }.join + required = ruby_exe(code, options: '--disable-gems') + required.should == "false\n" * requires.size + end + + it_behaves_like :kernel_require_basic, :require, CodeLoadingSpecs::Method.new + it_behaves_like :kernel_require, :require, CodeLoadingSpecs::Method.new +end + +describe "Kernel.require" do + before :each do + CodeLoadingSpecs.spec_setup + end + + after :each do + CodeLoadingSpecs.spec_cleanup + end + + it_behaves_like :kernel_require_basic, :require, Kernel + it_behaves_like :kernel_require, :require, Kernel +end diff --git a/spec/ruby/core/kernel/respond_to_missing_spec.rb b/spec/ruby/core/kernel/respond_to_missing_spec.rb new file mode 100644 index 0000000000..cc82031e26 --- /dev/null +++ b/spec/ruby/core/kernel/respond_to_missing_spec.rb @@ -0,0 +1,100 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel#respond_to_missing?" do + before :each do + @a = KernelSpecs::A.new + end + + it "is a private method" do + Kernel.should have_private_instance_method(:respond_to_missing?, false) + end + + it "is only an instance method" do + Kernel.method(:respond_to_missing?).owner.should == Kernel + end + + it "is not called when #respond_to? would return true" do + obj = mock('object') + obj.stub!(:glark) + obj.should_not_receive(:respond_to_missing?) + obj.respond_to?(:glark).should be_true + end + + it "is called with a 2nd argument of false when #respond_to? is" do + obj = mock('object') + obj.should_receive(:respond_to_missing?).with(:undefined_method, false) + obj.respond_to?(:undefined_method, false) + end + + it "is called a 2nd argument of false when #respond_to? is called with only 1 argument" do + obj = mock('object') + obj.should_receive(:respond_to_missing?).with(:undefined_method, false) + obj.respond_to?(:undefined_method) + end + + it "is called with true as the second argument when #respond_to? is" do + obj = mock('object') + obj.should_receive(:respond_to_missing?).with(:undefined_method, true) + obj.respond_to?(:undefined_method, true) + end + + it "is called when #respond_to? would return false" do + obj = mock('object') + obj.should_receive(:respond_to_missing?).with(:undefined_method, false) + obj.respond_to?(:undefined_method) + end + + it "causes #respond_to? to return true if called and not returning false" do + obj = mock('object') + obj.should_receive(:respond_to_missing?).with(:undefined_method, false).and_return(:glark) + obj.respond_to?(:undefined_method).should be_true + end + + it "causes #respond_to? to return false if called and returning false" do + obj = mock('object') + obj.should_receive(:respond_to_missing?).with(:undefined_method, false).and_return(false) + obj.respond_to?(:undefined_method).should be_false + end + + it "causes #respond_to? to return false if called and returning nil" do + obj = mock('object') + obj.should_receive(:respond_to_missing?).with(:undefined_method, false).and_return(nil) + obj.respond_to?(:undefined_method).should be_false + end + + it "isn't called when obj responds to the given public method" do + @a.should_not_receive(:respond_to_missing?) + @a.respond_to?(:pub_method).should be_true + end + + it "isn't called when obj responds to the given public method, include_private = true" do + @a.should_not_receive(:respond_to_missing?) + @a.respond_to?(:pub_method, true).should be_true + end + + it "is called when obj responds to the given protected method, include_private = false" do + @a.should_receive(:respond_to_missing?) + @a.respond_to?(:protected_method, false).should be_false + end + + it "isn't called when obj responds to the given protected method, include_private = true" do + @a.should_not_receive(:respond_to_missing?) + @a.respond_to?(:protected_method, true).should be_true + end + + it "is called when obj responds to the given private method, include_private = false" do + @a.should_receive(:respond_to_missing?).with(:private_method, false) + @a.respond_to?(:private_method) + end + + it "isn't called when obj responds to the given private method, include_private = true" do + @a.should_not_receive(:respond_to_missing?) + @a.respond_to?(:private_method, true).should be_true + end + + it "is called for missing class methods" do + @a.class.should_receive(:respond_to_missing?).with(:oOoOoO, false) + @a.class.respond_to?(:oOoOoO) + end +end diff --git a/spec/ruby/core/kernel/respond_to_spec.rb b/spec/ruby/core/kernel/respond_to_spec.rb new file mode 100644 index 0000000000..5b3ea3f651 --- /dev/null +++ b/spec/ruby/core/kernel/respond_to_spec.rb @@ -0,0 +1,72 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel#respond_to?" do + before :each do + @a = KernelSpecs::A.new + end + + it "is a public method" do + Kernel.should have_public_instance_method(:respond_to?, false) + end + + it "is only an instance method" do + Kernel.method(:respond_to?).owner.should == Kernel + end + + it "returns false if the given method was undefined" do + @a.respond_to?(:undefed_method).should == false + @a.respond_to?("undefed_method").should == false + end + + it "returns true if obj responds to the given public method" do + @a.respond_to?(:pub_method).should == true + @a.respond_to?("pub_method").should == true + end + + it "throws a type error if argument can't be coerced into a Symbol" do + -> { @a.respond_to?(Object.new) }.should raise_error(TypeError, /is not a symbol nor a string/) + end + + it "returns false if obj responds to the given protected method" do + @a.respond_to?(:protected_method).should == false + @a.respond_to?("protected_method").should == false + end + + it "returns false if obj responds to the given private method" do + @a.respond_to?(:private_method).should == false + @a.respond_to?("private_method").should == false + end + + it "returns true if obj responds to the given protected method (include_private = true)" do + @a.respond_to?(:protected_method, true).should == true + @a.respond_to?("protected_method", true).should == true + end + + it "returns false if obj responds to the given protected method (include_private = false)" do + @a.respond_to?(:protected_method, false).should == false + @a.respond_to?("protected_method", false).should == false + end + + it "returns false even if obj responds to the given private method (include_private = false)" do + @a.respond_to?(:private_method, false).should == false + @a.respond_to?("private_method", false).should == false + end + + it "returns true if obj responds to the given private method (include_private = true)" do + @a.respond_to?(:private_method, true).should == true + @a.respond_to?("private_method", true).should == true + end + + it "does not change method visibility when finding private method" do + KernelSpecs::VisibilityChange.respond_to?(:new, false).should == false + KernelSpecs::VisibilityChange.respond_to?(:new, true).should == true + -> { KernelSpecs::VisibilityChange.new }.should raise_error(NoMethodError) + end + + it "indicates if an object responds to a particular message" do + class KernelSpecs::Foo; def bar; 'done'; end; end + KernelSpecs::Foo.new.respond_to?(:bar).should == true + KernelSpecs::Foo.new.respond_to?(:invalid_and_silly_method_name).should == false + end +end diff --git a/spec/ruby/core/kernel/select_spec.rb b/spec/ruby/core/kernel/select_spec.rb new file mode 100644 index 0000000000..df23414b28 --- /dev/null +++ b/spec/ruby/core/kernel/select_spec.rb @@ -0,0 +1,18 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel#select" do + it "is a private method" do + Kernel.should have_private_instance_method(:select) + end +end + +describe "Kernel.select" do + it 'does not block when timeout is 0' do + IO.pipe do |read, write| + select([read], [], [], 0).should == nil + write.write 'data' + select([read], [], [], 0).should == [[read], [], []] + end + end +end diff --git a/spec/ruby/core/kernel/send_spec.rb b/spec/ruby/core/kernel/send_spec.rb new file mode 100644 index 0000000000..9a4d261964 --- /dev/null +++ b/spec/ruby/core/kernel/send_spec.rb @@ -0,0 +1,68 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative '../../shared/basicobject/send' + +describe "Kernel#send" do + it "invokes the named public method" do + class KernelSpecs::Foo + def bar + 'done' + end + end + KernelSpecs::Foo.new.send(:bar).should == 'done' + end + + it "invokes the named alias of a public method" do + class KernelSpecs::Foo + def bar + 'done' + end + alias :aka :bar + end + KernelSpecs::Foo.new.send(:aka).should == 'done' + end + + it "invokes the named protected method" do + class KernelSpecs::Foo + protected + def bar + 'done' + end + end + KernelSpecs::Foo.new.send(:bar).should == 'done' + end + + it "invokes the named private method" do + class KernelSpecs::Foo + private + def bar + 'done2' + end + end + KernelSpecs::Foo.new.send(:bar).should == 'done2' + end + + it "invokes the named alias of a private method" do + class KernelSpecs::Foo + private + def bar + 'done2' + end + alias :aka :bar + end + KernelSpecs::Foo.new.send(:aka).should == 'done2' + end + + it "invokes the named alias of a protected method" do + class KernelSpecs::Foo + protected + def bar + 'done2' + end + alias :aka :bar + end + KernelSpecs::Foo.new.send(:aka).should == 'done2' + end + + it_behaves_like :basicobject_send, :send +end diff --git a/spec/ruby/core/kernel/set_trace_func_spec.rb b/spec/ruby/core/kernel/set_trace_func_spec.rb new file mode 100644 index 0000000000..1f43e7a009 --- /dev/null +++ b/spec/ruby/core/kernel/set_trace_func_spec.rb @@ -0,0 +1,12 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel#set_trace_func" do + it "is a private method" do + Kernel.should have_private_instance_method(:set_trace_func) + end +end + +describe "Kernel.set_trace_func" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/kernel/shared/dup_clone.rb b/spec/ruby/core/kernel/shared/dup_clone.rb new file mode 100644 index 0000000000..4fac6006e1 --- /dev/null +++ b/spec/ruby/core/kernel/shared/dup_clone.rb @@ -0,0 +1,91 @@ +class ObjectSpecDup + def initialize() + @obj = :original + end + + attr_accessor :obj +end + +class ObjectSpecDupInitCopy + def initialize() + @obj = :original + end + + attr_accessor :obj, :original + + def initialize_copy(original) + @obj = :init_copy + @original = original + end + + private :initialize_copy +end + +describe :kernel_dup_clone, shared: true do + it "returns a new object duplicated from the original" do + o = ObjectSpecDup.new + o2 = ObjectSpecDup.new + + o.obj = 10 + + o3 = o.send(@method) + + o3.obj.should == 10 + o2.obj.should == :original + end + + it "produces a shallow copy, contained objects are not recursively dupped" do + o = ObjectSpecDup.new + array = [1, 2] + o.obj = array + + o2 = o.send(@method) + o2.obj.should equal(o.obj) + end + + it "calls #initialize_copy on the NEW object if available, passing in original object" do + o = ObjectSpecDupInitCopy.new + o2 = o.send(@method) + + o.obj.should == :original + o2.obj.should == :init_copy + o2.original.should equal(o) + end + + it "does not preserve the object_id" do + o1 = ObjectSpecDupInitCopy.new + old_object_id = o1.object_id + o2 = o1.send(@method) + o2.object_id.should_not == old_object_id + end + + it "returns nil for NilClass" do + nil.send(@method).should == nil + end + + it "returns true for TrueClass" do + true.send(@method).should == true + end + + it "returns false for FalseClass" do + false.send(@method).should == false + end + + it "returns the same Integer for Integer" do + 1.send(@method).should == 1 + end + + it "returns the same Symbol for Symbol" do + :my_symbol.send(@method).should == :my_symbol + end + + it "returns self for Complex" do + c = Complex(1.3, 3.1) + c.send(@method).should equal c + end + + it "returns self for Rational" do + r = Rational(1, 3) + r.send(@method).should equal r + end +end diff --git a/spec/ruby/core/kernel/shared/kind_of.rb b/spec/ruby/core/kernel/shared/kind_of.rb new file mode 100644 index 0000000000..aef6f1c1d8 --- /dev/null +++ b/spec/ruby/core/kernel/shared/kind_of.rb @@ -0,0 +1,55 @@ +require_relative '../fixtures/classes' + +describe :kernel_kind_of, shared: true do + before :each do + @o = KernelSpecs::KindaClass.new + end + + it "returns true if given class is the object's class" do + @o.send(@method, KernelSpecs::KindaClass).should == true + end + + it "returns true if given class is an ancestor of the object's class" do + @o.send(@method, KernelSpecs::AncestorClass).should == true + @o.send(@method, String).should == true + @o.send(@method, Object).should == true + end + + it "returns false if the given class is not object's class nor an ancestor" do + @o.send(@method, Array).should == false + end + + it "returns true if given a Module that is included in object's class" do + @o.send(@method, KernelSpecs::MyModule).should == true + end + + it "returns true if given a Module that is included one of object's ancestors only" do + @o.send(@method, KernelSpecs::AncestorModule).should == true + end + + it "returns true if given a Module that object has been extended with" do + @o.send(@method, KernelSpecs::MyExtensionModule).should == true + end + + it "returns true if given a Module that object has been prepended with" do + @o.send(@method, KernelSpecs::MyPrependedModule).should == true + end + + it "returns false if given a Module not included nor prepended in object's class nor ancestors" do + @o.send(@method, KernelSpecs::SomeOtherModule).should == false + end + + it "raises a TypeError if given an object that is not a Class nor a Module" do + -> { @o.send(@method, 1) }.should raise_error(TypeError) + -> { @o.send(@method, 'KindaClass') }.should raise_error(TypeError) + -> { @o.send(@method, :KindaClass) }.should raise_error(TypeError) + -> { @o.send(@method, Object.new) }.should raise_error(TypeError) + end + + it "does not take into account `class` method overriding" do + def @o.class; Integer; end + + @o.send(@method, Integer).should == false + @o.send(@method, KernelSpecs::KindaClass).should == true + end +end diff --git a/spec/ruby/core/kernel/shared/lambda.rb b/spec/ruby/core/kernel/shared/lambda.rb new file mode 100644 index 0000000000..c70640082a --- /dev/null +++ b/spec/ruby/core/kernel/shared/lambda.rb @@ -0,0 +1,11 @@ +describe :kernel_lambda, shared: true do + it "returns a Proc object" do + send(@method) { true }.kind_of?(Proc).should == true + end + + it "raises an ArgumentError when no block is given" do + suppress_warning do + -> { send(@method) }.should raise_error(ArgumentError) + end + end +end diff --git a/spec/ruby/core/kernel/shared/load.rb b/spec/ruby/core/kernel/shared/load.rb new file mode 100644 index 0000000000..62c5c7be9b --- /dev/null +++ b/spec/ruby/core/kernel/shared/load.rb @@ -0,0 +1,215 @@ +main = self + +# The big difference is Kernel#load does not attempt to add an extension to the passed path, unlike Kernel#require +describe :kernel_load, shared: true do + before :each do + CodeLoadingSpecs.spec_setup + @path = File.expand_path "load_fixture.rb", CODE_LOADING_DIR + end + + after :each do + CodeLoadingSpecs.spec_cleanup + end + + describe "(path resolution)" do + # This behavior is specific to Kernel#load, it differs for Kernel#require + it "loads a non-extensioned file as a Ruby source file" do + path = File.expand_path "load_fixture", CODE_LOADING_DIR + @object.load(path).should be_true + ScratchPad.recorded.should == [:no_ext] + end + + it "loads a non .rb extensioned file as a Ruby source file" do + path = File.expand_path "load_fixture.ext", CODE_LOADING_DIR + @object.load(path).should be_true + ScratchPad.recorded.should == [:no_rb_ext] + end + + it "loads from the current working directory" do + Dir.chdir CODE_LOADING_DIR do + @object.load("load_fixture.rb").should be_true + ScratchPad.recorded.should == [:loaded] + end + end + + # This behavior is specific to Kernel#load, it differs for Kernel#require + it "does not look for a c-extension file when passed a path without extension (when no .rb is present)" do + path = File.join CODE_LOADING_DIR, "a", "load_fixture" + -> { @object.send(@method, path) }.should raise_error(LoadError) + end + end + + it "loads a file that recursively requires itself" do + path = File.expand_path "recursive_require_fixture.rb", CODE_LOADING_DIR + -> { + @object.load(path).should be_true + }.should complain(/circular require considered harmful/, verbose: true) + ScratchPad.recorded.should == [:loaded, :loaded] + end + + it "loads a file that recursively loads itself" do + path = File.expand_path "recursive_load_fixture.rb", CODE_LOADING_DIR + @object.load(path).should be_true + ScratchPad.recorded.should == [:loaded, :loaded] + end + + it "loads a file each time the method is called" do + @object.load(@path).should be_true + @object.load(@path).should be_true + ScratchPad.recorded.should == [:loaded, :loaded] + end + + it "loads a file even when the name appears in $LOADED_FEATURES" do + $LOADED_FEATURES << @path + @object.load(@path).should be_true + ScratchPad.recorded.should == [:loaded] + end + + it "loads a file that has been loaded by #require" do + @object.require(@path).should be_true + @object.load(@path).should be_true + ScratchPad.recorded.should == [:loaded, :loaded] + end + + it "loads file even after $LOAD_PATH change" do + $LOAD_PATH << CODE_LOADING_DIR + @object.load("load_fixture.rb").should be_true + $LOAD_PATH.unshift CODE_LOADING_DIR + "/gem" + @object.load("load_fixture.rb").should be_true + ScratchPad.recorded.should == [:loaded, :loaded_gem] + end + + it "does not cause #require with the same path to fail" do + @object.load(@path).should be_true + @object.require(@path).should be_true + ScratchPad.recorded.should == [:loaded, :loaded] + end + + it "does not add the loaded path to $LOADED_FEATURES" do + saved_loaded_features = $LOADED_FEATURES.dup + @object.load(@path).should be_true + $LOADED_FEATURES.should == saved_loaded_features + end + + it "raises a LoadError if passed a non-extensioned path that does not exist but a .rb extensioned path does exist" do + path = File.expand_path "load_ext_fixture", CODE_LOADING_DIR + -> { @object.load(path) }.should raise_error(LoadError) + end + + describe "when passed true for 'wrap'" do + it "loads from an existing path" do + path = File.expand_path "load_wrap_fixture.rb", CODE_LOADING_DIR + @object.load(path, true).should be_true + end + + it "sets the enclosing scope to an anonymous module" do + path = File.expand_path "load_wrap_fixture.rb", CODE_LOADING_DIR + @object.load(path, true) + + Object.const_defined?(:LoadSpecWrap).should be_false + + wrap_module = ScratchPad.recorded[1] + wrap_module.should be_an_instance_of(Module) + end + + it "allows referencing outside namespaces" do + path = File.expand_path "load_wrap_fixture.rb", CODE_LOADING_DIR + @object.load(path, true) + + ScratchPad.recorded[0].should equal(String) + end + + it "sets self as a copy of the top-level main" do + path = File.expand_path "load_wrap_fixture.rb", CODE_LOADING_DIR + @object.load(path, true) + + top_level = ScratchPad.recorded[2] + top_level.to_s.should == "main" + top_level.method(:to_s).owner.should == top_level.singleton_class + top_level.should_not equal(main) + top_level.should be_an_instance_of(Object) + end + + it "includes modules included in main's singleton class in self's class" do + mod = Module.new + main.extend(mod) + + main_ancestors = main.singleton_class.ancestors[1..-1] + main_ancestors.first.should == mod + + path = File.expand_path "load_wrap_fixture.rb", CODE_LOADING_DIR + @object.load(path, true) + + top_level = ScratchPad.recorded[2] + top_level_ancestors = top_level.singleton_class.ancestors[-main_ancestors.size..-1] + top_level_ancestors.should == main_ancestors + + wrap_module = ScratchPad.recorded[1] + top_level.singleton_class.ancestors.should == [top_level.singleton_class, wrap_module, *main_ancestors] + end + + describe "with top-level methods" do + before :each do + path = File.expand_path "load_wrap_method_fixture.rb", CODE_LOADING_DIR + @object.load(path, true) + end + + it "allows calling top-level methods" do + ScratchPad.recorded.last.should == :load_wrap_loaded + end + + it "does not pollute the receiver" do + -> { @object.send(:top_level_method) }.should raise_error(NameError) + end + end + end + + describe "when passed a module for 'wrap'" do + it "sets the enclosing scope to the supplied module" do + path = File.expand_path "load_wrap_fixture.rb", CODE_LOADING_DIR + mod = Module.new + @object.load(path, mod) + + Object.const_defined?(:LoadSpecWrap).should be_false + mod.const_defined?(:LoadSpecWrap).should be_true + + wrap_module = ScratchPad.recorded[1] + wrap_module.should == mod + end + + it "makes constants and instance methods in the source file reachable with the supplied module" do + path = File.expand_path "load_wrap_fixture.rb", CODE_LOADING_DIR + mod = Module.new + @object.load(path, mod) + + mod::LOAD_WRAP_SPECS_TOP_LEVEL_CONSTANT.should == 1 + obj = Object.new + obj.extend(mod) + obj.send(:load_wrap_specs_top_level_method).should == :load_wrap_specs_top_level_method + end + + it "makes instance methods in the source file private" do + path = File.expand_path "load_wrap_fixture.rb", CODE_LOADING_DIR + mod = Module.new + @object.load(path, mod) + + mod.private_instance_methods.include?(:load_wrap_specs_top_level_method).should == true + end + end + + describe "(shell expansion)" do + before :each do + @env_home = ENV["HOME"] + ENV["HOME"] = CODE_LOADING_DIR + end + + after :each do + ENV["HOME"] = @env_home + end + + it "expands a tilde to the HOME environment variable as the path to load" do + @object.require("~/load_fixture.rb").should be_true + ScratchPad.recorded.should == [:loaded] + end + end +end diff --git a/spec/ruby/core/kernel/shared/method.rb b/spec/ruby/core/kernel/shared/method.rb new file mode 100644 index 0000000000..8b6ab23fd3 --- /dev/null +++ b/spec/ruby/core/kernel/shared/method.rb @@ -0,0 +1,56 @@ +require_relative '../../../spec_helper' + +describe :kernel_method, shared: true do + it "returns a method object for a valid method" do + class KernelSpecs::Foo; def bar; 'done'; end; end + m = KernelSpecs::Foo.new.send(@method, :bar) + m.should be_an_instance_of Method + m.call.should == 'done' + end + + it "returns a method object for a valid singleton method" do + class KernelSpecs::Foo; def self.bar; 'class done'; end; end + m = KernelSpecs::Foo.send(@method, :bar) + m.should be_an_instance_of Method + m.call.should == 'class done' + end + + it "returns a method object if respond_to_missing?(method) is true" do + m = KernelSpecs::RespondViaMissing.new.send(@method, :handled_publicly) + m.should be_an_instance_of Method + m.call(42).should == "Done handled_publicly([42])" + end + + it "the returned method object if respond_to_missing?(method) calls #method_missing with a Symbol name" do + m = KernelSpecs::RespondViaMissing.new.send(@method, "handled_publicly") + m.should be_an_instance_of Method + m.call(42).should == "Done handled_publicly([42])" + end + + it "raises a NameError for an invalid method name" do + class KernelSpecs::Foo; def bar; 'done'; end; end + -> { + KernelSpecs::Foo.new.send(@method, :invalid_and_silly_method_name) + }.should raise_error(NameError) + end + + it "raises a NameError for an invalid singleton method name" do + class KernelSpecs::Foo; def self.bar; 'done'; end; end + -> { KernelSpecs::Foo.send(@method, :baz) }.should raise_error(NameError) + end + + it "changes the method called for super on a target aliased method" do + c1 = Class.new do + def a; 'a'; end + def b; 'b'; end + end + c2 = Class.new(c1) do + def a; super; end + alias b a + end + + c2.new.a.should == 'a' + c2.new.b.should == 'a' + c2.new.send(@method, :b).call.should == 'a' + end +end diff --git a/spec/ruby/core/kernel/shared/require.rb b/spec/ruby/core/kernel/shared/require.rb new file mode 100644 index 0000000000..52f86f73e5 --- /dev/null +++ b/spec/ruby/core/kernel/shared/require.rb @@ -0,0 +1,848 @@ +describe :kernel_require_basic, shared: true do + describe "(path resolution)" do + it "loads an absolute path" do + path = File.expand_path "load_fixture.rb", CODE_LOADING_DIR + @object.send(@method, path).should be_true + ScratchPad.recorded.should == [:loaded] + end + + it "loads a non-canonical absolute path" do + path = File.join CODE_LOADING_DIR, "..", "code", "load_fixture.rb" + @object.send(@method, path).should be_true + ScratchPad.recorded.should == [:loaded] + end + + it "loads a file defining many methods" do + path = File.expand_path "methods_fixture.rb", CODE_LOADING_DIR + @object.send(@method, path).should be_true + ScratchPad.recorded.should == [:loaded] + end + + it "raises a LoadError if the file does not exist" do + path = File.expand_path "nonexistent.rb", CODE_LOADING_DIR + File.should_not.exist?(path) + -> { @object.send(@method, path) }.should raise_error(LoadError) + ScratchPad.recorded.should == [] + end + + # Can't make a file unreadable on these platforms + platform_is_not :windows, :cygwin do + as_user do + describe "with an unreadable file" do + before :each do + @path = tmp("unreadable_file.rb") + touch @path + File.chmod 0000, @path + end + + after :each do + File.chmod 0666, @path + rm_r @path + end + + it "raises a LoadError" do + File.should.exist?(@path) + -> { @object.send(@method, @path) }.should raise_error(LoadError) + end + end + end + end + + it "calls #to_str on non-String objects" do + path = File.expand_path "load_fixture.rb", CODE_LOADING_DIR + name = mock("load_fixture.rb mock") + name.should_receive(:to_str).and_return(path) + @object.send(@method, name).should be_true + ScratchPad.recorded.should == [:loaded] + end + + it "raises a TypeError if passed nil" do + -> { @object.send(@method, nil) }.should raise_error(TypeError) + end + + it "raises a TypeError if passed an Integer" do + -> { @object.send(@method, 42) }.should raise_error(TypeError) + end + + it "raises a TypeError if passed an Array" do + -> { @object.send(@method, []) }.should raise_error(TypeError) + end + + it "raises a TypeError if passed an object that does not provide #to_str" do + -> { @object.send(@method, mock("not a filename")) }.should raise_error(TypeError) + end + + it "raises a TypeError if passed an object that has #to_s but not #to_str" do + name = mock("load_fixture.rb mock") + name.stub!(:to_s).and_return("load_fixture.rb") + $LOAD_PATH << "." + Dir.chdir CODE_LOADING_DIR do + -> { @object.send(@method, name) }.should raise_error(TypeError) + end + end + + it "raises a TypeError if #to_str does not return a String" do + name = mock("#to_str returns nil") + name.should_receive(:to_str).at_least(1).times.and_return(nil) + -> { @object.send(@method, name) }.should raise_error(TypeError) + end + + it "calls #to_path on non-String objects" do + name = mock("load_fixture.rb mock") + name.stub!(:to_path).and_return("load_fixture.rb") + $LOAD_PATH << "." + Dir.chdir CODE_LOADING_DIR do + @object.send(@method, name).should be_true + end + ScratchPad.recorded.should == [:loaded] + end + + it "calls #to_path on a String" do + path = File.expand_path "load_fixture.rb", CODE_LOADING_DIR + str = mock("load_fixture.rb mock") + str.should_receive(:to_path).and_return(path) + @object.send(@method, str).should be_true + ScratchPad.recorded.should == [:loaded] + end + + it "calls #to_str on non-String objects returned by #to_path" do + path = File.expand_path "load_fixture.rb", CODE_LOADING_DIR + name = mock("load_fixture.rb mock") + to_path = mock("load_fixture_rb #to_path mock") + name.should_receive(:to_path).and_return(to_path) + to_path.should_receive(:to_str).and_return(path) + @object.send(@method, name).should be_true + ScratchPad.recorded.should == [:loaded] + end + + # "http://redmine.ruby-lang.org/issues/show/2578" + it "loads a ./ relative path from the current working directory with empty $LOAD_PATH" do + Dir.chdir CODE_LOADING_DIR do + @object.send(@method, "./load_fixture.rb").should be_true + end + ScratchPad.recorded.should == [:loaded] + end + + it "loads a ../ relative path from the current working directory with empty $LOAD_PATH" do + Dir.chdir CODE_LOADING_DIR do + @object.send(@method, "../code/load_fixture.rb").should be_true + end + ScratchPad.recorded.should == [:loaded] + end + + it "loads a ./ relative path from the current working directory with non-empty $LOAD_PATH" do + $LOAD_PATH << "an_irrelevant_dir" + Dir.chdir CODE_LOADING_DIR do + @object.send(@method, "./load_fixture.rb").should be_true + end + ScratchPad.recorded.should == [:loaded] + end + + it "loads a ../ relative path from the current working directory with non-empty $LOAD_PATH" do + $LOAD_PATH << "an_irrelevant_dir" + Dir.chdir CODE_LOADING_DIR do + @object.send(@method, "../code/load_fixture.rb").should be_true + end + ScratchPad.recorded.should == [:loaded] + end + + it "loads a non-canonical path from the current working directory with non-empty $LOAD_PATH" do + $LOAD_PATH << "an_irrelevant_dir" + Dir.chdir CODE_LOADING_DIR do + @object.send(@method, "../code/../code/load_fixture.rb").should be_true + end + ScratchPad.recorded.should == [:loaded] + end + + it "resolves a filename against $LOAD_PATH entries" do + $LOAD_PATH << CODE_LOADING_DIR + @object.send(@method, "load_fixture.rb").should be_true + ScratchPad.recorded.should == [:loaded] + end + + it "accepts an Object with #to_path in $LOAD_PATH" do + obj = mock("to_path") + obj.should_receive(:to_path).at_least(:once).and_return(CODE_LOADING_DIR) + $LOAD_PATH << obj + @object.send(@method, "load_fixture.rb").should be_true + ScratchPad.recorded.should == [:loaded] + end + + it "does not require file twice after $LOAD_PATH change" do + $LOAD_PATH << CODE_LOADING_DIR + @object.require("load_fixture.rb").should be_true + $LOAD_PATH.push CODE_LOADING_DIR + "/gem" + @object.require("load_fixture.rb").should be_false + ScratchPad.recorded.should == [:loaded] + end + + it "does not resolve a ./ relative path against $LOAD_PATH entries" do + $LOAD_PATH << CODE_LOADING_DIR + -> do + @object.send(@method, "./load_fixture.rb") + end.should raise_error(LoadError) + ScratchPad.recorded.should == [] + end + + it "does not resolve a ../ relative path against $LOAD_PATH entries" do + $LOAD_PATH << CODE_LOADING_DIR + -> do + @object.send(@method, "../code/load_fixture.rb") + end.should raise_error(LoadError) + ScratchPad.recorded.should == [] + end + + it "resolves a non-canonical path against $LOAD_PATH entries" do + $LOAD_PATH << File.dirname(CODE_LOADING_DIR) + @object.send(@method, "code/../code/load_fixture.rb").should be_true + ScratchPad.recorded.should == [:loaded] + end + + it "loads a path with duplicate path separators" do + $LOAD_PATH << "." + sep = File::Separator + File::Separator + path = ["..", "code", "load_fixture.rb"].join(sep) + Dir.chdir CODE_LOADING_DIR do + @object.send(@method, path).should be_true + end + ScratchPad.recorded.should == [:loaded] + end + end +end + +describe :kernel_require, shared: true do + describe "(path resolution)" do + it "loads .rb file when passed absolute path without extension" do + path = File.expand_path "load_fixture", CODE_LOADING_DIR + @object.send(@method, path).should be_true + # This should _not_ be [:no_ext] + ScratchPad.recorded.should == [:loaded] + end + + platform_is :linux, :darwin do + it "loads c-extension file when passed absolute path without extension when no .rb is present" do + # the error message is specific to what dlerror() returns + path = File.join CODE_LOADING_DIR, "a", "load_fixture" + -> { @object.send(@method, path) }.should raise_error(LoadError) + end + end + + platform_is :darwin do + it "loads .bundle file when passed absolute path with .so" do + # the error message is specific to what dlerror() returns + path = File.join CODE_LOADING_DIR, "a", "load_fixture.so" + -> { @object.send(@method, path) }.should raise_error(LoadError) + end + end + + it "does not try an extra .rb if the path already ends in .rb" do + path = File.join CODE_LOADING_DIR, "d", "load_fixture.rb" + -> { @object.send(@method, path) }.should raise_error(LoadError) + end + + # For reference see [ruby-core:24155] in which matz confirms this feature is + # intentional for security reasons. + it "does not load a bare filename unless the current working directory is in $LOAD_PATH" do + Dir.chdir CODE_LOADING_DIR do + -> { @object.require("load_fixture.rb") }.should raise_error(LoadError) + ScratchPad.recorded.should == [] + end + end + + it "does not load a relative path unless the current working directory is in $LOAD_PATH" do + Dir.chdir File.dirname(CODE_LOADING_DIR) do + -> do + @object.require("code/load_fixture.rb") + end.should raise_error(LoadError) + ScratchPad.recorded.should == [] + end + end + + it "loads a file that recursively requires itself" do + path = File.expand_path "recursive_require_fixture.rb", CODE_LOADING_DIR + -> { + @object.require(path).should be_true + }.should complain(/circular require considered harmful/, verbose: true) + ScratchPad.recorded.should == [:loaded] + end + + ruby_bug "#17340", ''...'3.3' do + it "loads a file concurrently" do + path = File.expand_path "concurrent_require_fixture.rb", CODE_LOADING_DIR + ScratchPad.record(@object) + -> { + @object.require(path) + }.should_not complain(/circular require considered harmful/, verbose: true) + ScratchPad.recorded.join + end + end + end + + describe "(non-extensioned path)" do + before :each do + a = File.expand_path "a", CODE_LOADING_DIR + b = File.expand_path "b", CODE_LOADING_DIR + $LOAD_PATH.replace [a, b] + end + + it "loads a .rb extensioned file when a C-extension file exists on an earlier load path" do + @object.require("load_fixture").should be_true + ScratchPad.recorded.should == [:loaded] + end + + it "does not load a feature twice when $LOAD_PATH has been modified" do + $LOAD_PATH.replace [CODE_LOADING_DIR] + @object.require("load_fixture").should be_true + $LOAD_PATH.replace [File.expand_path("b", CODE_LOADING_DIR), CODE_LOADING_DIR] + @object.require("load_fixture").should be_false + end + + it "stores the missing path in a LoadError object" do + path = "abcd1234" + + -> { + @object.send(@method, path) + }.should raise_error(LoadError) { |e| + e.path.should == path + } + end + end + + describe "(file extensions)" do + it "loads a .rb extensioned file when passed a non-extensioned path" do + path = File.expand_path "load_fixture", CODE_LOADING_DIR + File.should.exist?(path) + @object.require(path).should be_true + ScratchPad.recorded.should == [:loaded] + end + + it "loads a .rb extensioned file when a C-extension file of the same name is loaded" do + $LOADED_FEATURES << File.expand_path("load_fixture.bundle", CODE_LOADING_DIR) + $LOADED_FEATURES << File.expand_path("load_fixture.dylib", CODE_LOADING_DIR) + $LOADED_FEATURES << File.expand_path("load_fixture.so", CODE_LOADING_DIR) + $LOADED_FEATURES << File.expand_path("load_fixture.dll", CODE_LOADING_DIR) + path = File.expand_path "load_fixture", CODE_LOADING_DIR + @object.require(path).should be_true + ScratchPad.recorded.should == [:loaded] + end + + it "does not load a C-extension file if a .rb extensioned file is already loaded" do + $LOADED_FEATURES << File.expand_path("load_fixture.rb", CODE_LOADING_DIR) + path = File.expand_path "load_fixture", CODE_LOADING_DIR + @object.require(path).should be_false + ScratchPad.recorded.should == [] + end + + it "loads a .rb extensioned file when passed a non-.rb extensioned path" do + path = File.expand_path "load_fixture.ext", CODE_LOADING_DIR + File.should.exist?(path) + @object.require(path).should be_true + ScratchPad.recorded.should == [:loaded] + end + + it "loads a .rb extensioned file when a complex-extensioned C-extension file of the same name is loaded" do + $LOADED_FEATURES << File.expand_path("load_fixture.ext.bundle", CODE_LOADING_DIR) + $LOADED_FEATURES << File.expand_path("load_fixture.ext.dylib", CODE_LOADING_DIR) + $LOADED_FEATURES << File.expand_path("load_fixture.ext.so", CODE_LOADING_DIR) + $LOADED_FEATURES << File.expand_path("load_fixture.ext.dll", CODE_LOADING_DIR) + path = File.expand_path "load_fixture.ext", CODE_LOADING_DIR + @object.require(path).should be_true + ScratchPad.recorded.should == [:loaded] + end + + it "does not load a C-extension file if a complex-extensioned .rb file is already loaded" do + $LOADED_FEATURES << File.expand_path("load_fixture.ext.rb", CODE_LOADING_DIR) + path = File.expand_path "load_fixture.ext", CODE_LOADING_DIR + @object.require(path).should be_false + ScratchPad.recorded.should == [] + end + end + + describe "($LOADED_FEATURES)" do + before :each do + @path = File.expand_path("load_fixture.rb", CODE_LOADING_DIR) + end + + it "stores an absolute path" do + @object.require(@path).should be_true + $LOADED_FEATURES.should include(@path) + end + + platform_is_not :windows do + describe "with symlinks" do + before :each do + @symlink_to_code_dir = tmp("codesymlink") + File.symlink(CODE_LOADING_DIR, @symlink_to_code_dir) + + $LOAD_PATH.delete(CODE_LOADING_DIR) + $LOAD_PATH.unshift(@symlink_to_code_dir) + end + + after :each do + rm_r @symlink_to_code_dir + end + + it "does not canonicalize the path and stores a path with symlinks" do + symlink_path = "#{@symlink_to_code_dir}/load_fixture.rb" + canonical_path = "#{CODE_LOADING_DIR}/load_fixture.rb" + @object.require(symlink_path).should be_true + ScratchPad.recorded.should == [:loaded] + + features = $LOADED_FEATURES.select { |path| path.end_with?('load_fixture.rb') } + features.should include(symlink_path) + features.should_not include(canonical_path) + end + + it "stores the same path that __FILE__ returns in the required file" do + symlink_path = "#{@symlink_to_code_dir}/load_fixture_and__FILE__.rb" + @object.require(symlink_path).should be_true + loaded_feature = $LOADED_FEATURES.last + ScratchPad.recorded.should == [loaded_feature] + end + + it "requires only once when a new matching file added to path" do + @object.require('load_fixture').should be_true + ScratchPad.recorded.should == [:loaded] + + symlink_to_code_dir_two = tmp("codesymlinktwo") + File.symlink("#{CODE_LOADING_DIR}/b", symlink_to_code_dir_two) + begin + $LOAD_PATH.unshift(symlink_to_code_dir_two) + + @object.require('load_fixture').should be_false + ensure + rm_r symlink_to_code_dir_two + end + end + end + + describe "with symlinks in the required feature and $LOAD_PATH" do + before :each do + @dir = tmp("realdir") + mkdir_p @dir + @file = "#{@dir}/realfile.rb" + touch(@file) { |f| f.puts 'ScratchPad << __FILE__' } + + @symlink_to_dir = tmp("symdir").freeze + File.symlink(@dir, @symlink_to_dir) + @symlink_to_file = "#{@dir}/symfile.rb" + File.symlink("realfile.rb", @symlink_to_file) + end + + after :each do + rm_r @dir, @symlink_to_dir + end + + it "canonicalizes the entry in $LOAD_PATH but not the filename passed to #require" do + $LOAD_PATH.unshift(@symlink_to_dir) + @object.require("symfile").should be_true + loaded_feature = "#{@dir}/symfile.rb" + ScratchPad.recorded.should == [loaded_feature] + $".last.should == loaded_feature + $LOAD_PATH[0].should == @symlink_to_dir + end + end + end + + it "does not store the path if the load fails" do + $LOAD_PATH << CODE_LOADING_DIR + saved_loaded_features = $LOADED_FEATURES.dup + -> { @object.require("raise_fixture.rb") }.should raise_error(RuntimeError) + $LOADED_FEATURES.should == saved_loaded_features + end + + it "does not load an absolute path that is already stored" do + $LOADED_FEATURES << @path + @object.require(@path).should be_false + ScratchPad.recorded.should == [] + end + + it "does not load a ./ relative path that is already stored" do + $LOADED_FEATURES << "./load_fixture.rb" + Dir.chdir CODE_LOADING_DIR do + @object.require("./load_fixture.rb").should be_false + end + ScratchPad.recorded.should == [] + end + + it "does not load a ../ relative path that is already stored" do + $LOADED_FEATURES << "../load_fixture.rb" + Dir.chdir CODE_LOADING_DIR do + @object.require("../load_fixture.rb").should be_false + end + ScratchPad.recorded.should == [] + end + + it "does not load a non-canonical path that is already stored" do + $LOADED_FEATURES << "code/../code/load_fixture.rb" + $LOAD_PATH << File.dirname(CODE_LOADING_DIR) + @object.require("code/../code/load_fixture.rb").should be_false + ScratchPad.recorded.should == [] + end + + it "respects being replaced with a new array" do + prev = $LOADED_FEATURES.dup + + @object.require(@path).should be_true + $LOADED_FEATURES.should include(@path) + + $LOADED_FEATURES.replace(prev) + + $LOADED_FEATURES.should_not include(@path) + @object.require(@path).should be_true + $LOADED_FEATURES.should include(@path) + end + + it "does not load twice the same file with and without extension" do + $LOAD_PATH << CODE_LOADING_DIR + @object.require("load_fixture.rb").should be_true + @object.require("load_fixture").should be_false + end + + describe "when a non-extensioned file is in $LOADED_FEATURES" do + before :each do + $LOADED_FEATURES << "load_fixture" + end + + it "loads a .rb extensioned file when a non extensioned file is in $LOADED_FEATURES" do + $LOAD_PATH << CODE_LOADING_DIR + @object.require("load_fixture").should be_true + ScratchPad.recorded.should == [:loaded] + end + + it "loads a .rb extensioned file from a subdirectory" do + $LOAD_PATH << File.dirname(CODE_LOADING_DIR) + @object.require("code/load_fixture").should be_true + ScratchPad.recorded.should == [:loaded] + end + + it "returns false if the file is not found" do + Dir.chdir File.dirname(CODE_LOADING_DIR) do + @object.require("load_fixture").should be_false + ScratchPad.recorded.should == [] + end + end + + it "returns false when passed a path and the file is not found" do + $LOADED_FEATURES << "code/load_fixture" + Dir.chdir CODE_LOADING_DIR do + @object.require("code/load_fixture").should be_false + ScratchPad.recorded.should == [] + end + end + end + + it "stores ../ relative paths as absolute paths" do + Dir.chdir CODE_LOADING_DIR do + @object.require("../code/load_fixture.rb").should be_true + end + $LOADED_FEATURES.should include(@path) + end + + it "stores ./ relative paths as absolute paths" do + Dir.chdir CODE_LOADING_DIR do + @object.require("./load_fixture.rb").should be_true + end + $LOADED_FEATURES.should include(@path) + end + + it "collapses duplicate path separators" do + $LOAD_PATH << "." + sep = File::Separator + File::Separator + path = ["..", "code", "load_fixture.rb"].join(sep) + Dir.chdir CODE_LOADING_DIR do + @object.require(path).should be_true + end + $LOADED_FEATURES.should include(@path) + end + + it "expands absolute paths containing .." do + path = File.join CODE_LOADING_DIR, "..", "code", "load_fixture.rb" + @object.require(path).should be_true + $LOADED_FEATURES.should include(@path) + end + + it "adds the suffix of the resolved filename" do + $LOAD_PATH << CODE_LOADING_DIR + @object.require("load_fixture").should be_true + $LOADED_FEATURES.should include(@path) + end + + it "does not load a non-canonical path for a file already loaded" do + $LOADED_FEATURES << @path + $LOAD_PATH << File.dirname(CODE_LOADING_DIR) + @object.require("code/../code/load_fixture.rb").should be_false + ScratchPad.recorded.should == [] + end + + it "does not load a ./ relative path for a file already loaded" do + $LOADED_FEATURES << @path + $LOAD_PATH << "an_irrelevant_dir" + Dir.chdir CODE_LOADING_DIR do + @object.require("./load_fixture.rb").should be_false + end + ScratchPad.recorded.should == [] + end + + it "does not load a ../ relative path for a file already loaded" do + $LOADED_FEATURES << @path + $LOAD_PATH << "an_irrelevant_dir" + Dir.chdir CODE_LOADING_DIR do + @object.require("../code/load_fixture.rb").should be_false + end + ScratchPad.recorded.should == [] + end + + it "unicode_normalize is part of core and not $LOADED_FEATURES" do + features = ruby_exe("puts $LOADED_FEATURES", options: '--disable-gems') + features.lines.each { |feature| + feature.should_not include("unicode_normalize") + } + + -> { @object.require("unicode_normalize") }.should raise_error(LoadError) + end + + it "does not load a file earlier on the $LOAD_PATH when other similar features were already loaded" do + Dir.chdir CODE_LOADING_DIR do + @object.send(@method, "../code/load_fixture").should be_true + end + ScratchPad.recorded.should == [:loaded] + + $LOAD_PATH.unshift "#{CODE_LOADING_DIR}/b" + # This loads because the above load was not on the $LOAD_PATH + @object.send(@method, "load_fixture").should be_true + ScratchPad.recorded.should == [:loaded, :loaded] + + $LOAD_PATH.unshift "#{CODE_LOADING_DIR}/c" + # This does not load because the above load was on the $LOAD_PATH + @object.send(@method, "load_fixture").should be_false + ScratchPad.recorded.should == [:loaded, :loaded] + end + end + + describe "(shell expansion)" do + before :each do + @path = File.expand_path("load_fixture.rb", CODE_LOADING_DIR) + @env_home = ENV["HOME"] + ENV["HOME"] = CODE_LOADING_DIR + end + + after :each do + ENV["HOME"] = @env_home + end + + # "#3171" + it "performs tilde expansion on a .rb file before storing paths in $LOADED_FEATURES" do + @object.require("~/load_fixture.rb").should be_true + $LOADED_FEATURES.should include(@path) + end + + it "performs tilde expansion on a non-extensioned file before storing paths in $LOADED_FEATURES" do + @object.require("~/load_fixture").should be_true + $LOADED_FEATURES.should include(@path) + end + end + + describe "(concurrently)" do + before :each do + ScratchPad.record [] + @path = File.expand_path "concurrent.rb", CODE_LOADING_DIR + @path2 = File.expand_path "concurrent2.rb", CODE_LOADING_DIR + @path3 = File.expand_path "concurrent3.rb", CODE_LOADING_DIR + end + + after :each do + ScratchPad.clear + $LOADED_FEATURES.delete @path + $LOADED_FEATURES.delete @path2 + $LOADED_FEATURES.delete @path3 + end + + # Quick note about these specs: + # + # The behavior we're spec'ing requires that t2 enter #require, see t1 is + # loading @path, grab a lock, and wait on it. + # + # We do make sure that t2 starts the require once t1 is in the middle + # of concurrent.rb, but we then need to get t2 to get far enough into #require + # to see t1's lock and try to lock it. + it "blocks a second thread from returning while the 1st is still requiring" do + fin = false + + t1_res = nil + t2_res = nil + + t2 = nil + t1 = Thread.new do + Thread.pass until t2 + Thread.current[:wait_for] = t2 + t1_res = @object.require(@path) + Thread.pass until fin + ScratchPad.recorded << :t1_post + end + + t2 = Thread.new do + Thread.pass until t1[:in_concurrent_rb] + $VERBOSE, @verbose = nil, $VERBOSE + begin + t2_res = @object.require(@path) + ScratchPad.recorded << :t2_post + ensure + $VERBOSE = @verbose + fin = true + end + end + + t1.join + t2.join + + t1_res.should be_true + t2_res.should be_false + + ScratchPad.recorded.should == [:con_pre, :con_post, :t2_post, :t1_post] + end + + it "blocks based on the path" do + t1_res = nil + t2_res = nil + + t2 = nil + t1 = Thread.new do + Thread.pass until t2 + Thread.current[:concurrent_require_thread] = t2 + t1_res = @object.require(@path2) + end + + t2 = Thread.new do + Thread.pass until t1[:in_concurrent_rb2] + t2_res = @object.require(@path3) + end + + t1.join + t2.join + + t1_res.should be_true + t2_res.should be_true + + ScratchPad.recorded.should == [:con2_pre, :con3, :con2_post] + end + + it "allows a 2nd require if the 1st raised an exception" do + fin = false + + t2_res = nil + + t2 = nil + t1 = Thread.new do + Thread.pass until t2 + Thread.current[:wait_for] = t2 + Thread.current[:con_raise] = true + + -> { + @object.require(@path) + }.should raise_error(RuntimeError) + + Thread.pass until fin + ScratchPad.recorded << :t1_post + end + + t2 = Thread.new do + Thread.pass until t1[:in_concurrent_rb] + $VERBOSE, @verbose = nil, $VERBOSE + begin + t2_res = @object.require(@path) + ScratchPad.recorded << :t2_post + ensure + $VERBOSE = @verbose + fin = true + end + end + + t1.join + t2.join + + t2_res.should be_true + + ScratchPad.recorded.should == [:con_pre, :con_pre, :con_post, :t2_post, :t1_post] + end + + # "redmine #5754" + it "blocks a 3rd require if the 1st raises an exception and the 2nd is still running" do + fin = false + + t1_res = nil + t2_res = nil + + raised = false + + t2 = nil + t1 = Thread.new do + Thread.current[:con_raise] = true + + -> { + @object.require(@path) + }.should raise_error(RuntimeError) + + raised = true + + # This hits the bug. Because MRI removes its internal lock from a table + # when the exception is raised, this #require doesn't see that t2 is in + # the middle of requiring the file, so this #require runs when it should not. + Thread.pass until t2 && t2[:in_concurrent_rb] + t1_res = @object.require(@path) + + Thread.pass until fin + ScratchPad.recorded << :t1_post + end + + t2 = Thread.new do + Thread.pass until raised + Thread.current[:wait_for] = t1 + begin + t2_res = @object.require(@path) + ScratchPad.recorded << :t2_post + ensure + fin = true + end + end + + t1.join + t2.join + + t1_res.should be_false + t2_res.should be_true + + ScratchPad.recorded.should == [:con_pre, :con_pre, :con_post, :t2_post, :t1_post] + end + end + + it "stores the missing path in a LoadError object" do + path = "abcd1234" + + -> { + @object.send(@method, path) + }.should raise_error(LoadError) { |e| + e.path.should == path + } + end + + platform_is :linux, :darwin do + it "does not store the missing path in a LoadError object when c-extension file exists but loading fails and passed absolute path without extension" do + # the error message is specific to what dlerror() returns + path = File.join CODE_LOADING_DIR, "a", "load_fixture" + -> { @object.send(@method, path) }.should raise_error(LoadError) { |e| + e.path.should == nil + } + end + end + + platform_is :darwin do + it "does not store the missing path in a LoadError object when c-extension file exists but loading fails and passed absolute path with extension" do + # the error message is specific to what dlerror() returns + path = File.join CODE_LOADING_DIR, "a", "load_fixture.bundle" + -> { @object.send(@method, path) }.should raise_error(LoadError) { |e| + e.path.should == nil + } + end + end +end diff --git a/spec/ruby/core/kernel/shared/sprintf.rb b/spec/ruby/core/kernel/shared/sprintf.rb new file mode 100644 index 0000000000..2b2c6c9b63 --- /dev/null +++ b/spec/ruby/core/kernel/shared/sprintf.rb @@ -0,0 +1,987 @@ +describe :kernel_sprintf, shared: true do + describe "integer formats" do + it "converts argument into Integer with to_int" do + obj = Object.new + def obj.to_i; 10; end + def obj.to_int; 10; end + + obj.should_receive(:to_int).and_return(10) + @method.call("%b", obj).should == "1010" + end + + it "converts argument into Integer with to_i if to_int isn't available" do + obj = Object.new + def obj.to_i; 10; end + + obj.should_receive(:to_i).and_return(10) + @method.call("%b", obj).should == "1010" + end + + it "converts String argument with Kernel#Integer" do + @method.call("%d", "0b1010").should == "10" + @method.call("%d", "112").should == "112" + @method.call("%d", "0127").should == "87" + @method.call("%d", "0xc4").should == "196" + @method.call("%d", "0").should == "0" + end + + it "raises TypeError exception if cannot convert to Integer" do + -> { + @method.call("%b", Object.new) + }.should raise_error(TypeError) + end + + ["b", "B"].each do |f| + describe f do + it "converts argument as a binary number" do + @method.call("%#{f}", 10).should == "1010" + end + + it "displays negative number as a two's complement prefixed with '..1'" do + @method.call("%#{f}", -10).should == "..1" + "0110" + end + + it "collapse negative number representation if it equals 1" do + @method.call("%#{f}", -1).should_not == "..11" + @method.call("%#{f}", -1).should == "..1" + end + end + end + + ["d", "i", "u"].each do |f| + describe f do + it "converts argument as a decimal number" do + @method.call("%#{f}", 112).should == "112" + @method.call("%#{f}", -112).should == "-112" + end + + it "works well with large numbers" do + @method.call("%#{f}", 1234567890987654321).should == "1234567890987654321" + end + + it "converts to the empty string if precision is 0 and value is 0" do + @method.call("%.#{f}", 0).should == "" + @method.call("%.0#{f}", 0).should == "" + end + end + end + + describe "o" do + it "converts argument as an octal number" do + @method.call("%o", 87).should == "127" + end + + it "displays negative number as a two's complement prefixed with '..7'" do + @method.call("%o", -87).should == "..7" + "651" + end + + it "collapse negative number representation if it equals 7" do + @method.call("%o", -1).should_not == "..77" + @method.call("%o", -1).should == "..7" + end + end + + describe "x" do + it "converts argument as a hexadecimal number" do + @method.call("%x", 196).should == "c4" + end + + it "displays negative number as a two's complement prefixed with '..f'" do + @method.call("%x", -196).should == "..f" + "3c" + end + + it "collapse negative number representation if it equals f" do + @method.call("%x", -1).should_not == "..ff" + @method.call("%x", -1).should == "..f" + end + end + + describe "X" do + it "converts argument as a hexadecimal number with uppercase letters" do + @method.call("%X", 196).should == "C4" + end + + it "displays negative number as a two's complement prefixed with '..f'" do + @method.call("%X", -196).should == "..F" + "3C" + end + + it "collapse negative number representation if it equals F" do + @method.call("%X", -1).should_not == "..FF" + @method.call("%X", -1).should == "..F" + end + end + end + + describe "float formats" do + it "converts argument into Float" do + obj = mock("float") + obj.should_receive(:to_f).and_return(9.6) + @method.call("%f", obj).should == "9.600000" + end + + it "raises TypeError exception if cannot convert to Float" do + -> { + @method.call("%f", Object.new) + }.should raise_error(TypeError) + end + + {"e" => "e", "E" => "E"}.each_pair do |f, exp| + describe f do + it "converts argument into exponential notation [-]d.dddddde[+-]dd" do + @method.call("%#{f}", 109.52).should == "1.095200#{exp}+02" + @method.call("%#{f}", -109.52).should == "-1.095200#{exp}+02" + @method.call("%#{f}", 0.10952).should == "1.095200#{exp}-01" + @method.call("%#{f}", -0.10952).should == "-1.095200#{exp}-01" + end + + it "cuts excessive digits and keeps only 6 ones" do + @method.call("%#{f}", 1.123456789).should == "1.123457#{exp}+00" + end + + it "rounds the last significant digit to the closest one" do + @method.call("%#{f}", 1.555555555).should == "1.555556#{exp}+00" + @method.call("%#{f}", -1.555555555).should == "-1.555556#{exp}+00" + @method.call("%#{f}", 1.444444444).should == "1.444444#{exp}+00" + end + + it "displays Float::INFINITY as Inf" do + @method.call("%#{f}", Float::INFINITY).should == "Inf" + @method.call("%#{f}", -Float::INFINITY).should == "-Inf" + end + + it "displays Float::NAN as NaN" do + @method.call("%#{f}", Float::NAN).should == "NaN" + @method.call("%#{f}", -Float::NAN).should == "NaN" + end + end + end + + describe "f" do + it "converts floating point argument as [-]ddd.dddddd" do + @method.call("%f", 10.952).should == "10.952000" + @method.call("%f", -10.952).should == "-10.952000" + end + + it "cuts excessive digits and keeps only 6 ones" do + @method.call("%f", 1.123456789).should == "1.123457" + end + + it "rounds the last significant digit to the closest one" do + @method.call("%f", 1.555555555).should == "1.555556" + @method.call("%f", -1.555555555).should == "-1.555556" + @method.call("%f", 1.444444444).should == "1.444444" + end + + it "displays Float::INFINITY as Inf" do + @method.call("%f", Float::INFINITY).should == "Inf" + @method.call("%f", -Float::INFINITY).should == "-Inf" + end + + it "displays Float::NAN as NaN" do + @method.call("%f", Float::NAN).should == "NaN" + @method.call("%f", -Float::NAN).should == "NaN" + end + end + + {"g" => "e", "G" => "E"}.each_pair do |f, exp| + describe f do + context "the exponent is less than -4" do + it "converts a floating point number using exponential form" do + @method.call("%#{f}", 0.0000123456).should == "1.23456#{exp}-05" + @method.call("%#{f}", -0.0000123456).should == "-1.23456#{exp}-05" + + @method.call("%#{f}", 0.000000000123456).should == "1.23456#{exp}-10" + @method.call("%#{f}", -0.000000000123456).should == "-1.23456#{exp}-10" + end + end + + context "the exponent is greater than or equal to the precision (6 by default)" do + it "converts a floating point number using exponential form" do + @method.call("%#{f}", 1234567).should == "1.23457#{exp}+06" + @method.call("%#{f}", 1234567890123).should == "1.23457#{exp}+12" + @method.call("%#{f}", -1234567).should == "-1.23457#{exp}+06" + end + end + + context "otherwise" do + it "converts a floating point number in dd.dddd form" do + @method.call("%#{f}", 0.0001).should == "0.0001" + @method.call("%#{f}", -0.0001).should == "-0.0001" + @method.call("%#{f}", 123456).should == "123456" + @method.call("%#{f}", -123456).should == "-123456" + end + + it "cuts excessive digits in fractional part and keeps only 4 ones" do + @method.call("%#{f}", 12.12341111).should == "12.1234" + @method.call("%#{f}", -12.12341111).should == "-12.1234" + end + + it "rounds the last significant digit to the closest one in fractional part" do + @method.call("%#{f}", 1.555555555).should == "1.55556" + @method.call("%#{f}", -1.555555555).should == "-1.55556" + @method.call("%#{f}", 1.444444444).should == "1.44444" + end + + it "cuts fraction part to have only 6 digits at all" do + @method.call("%#{f}", 1.1234567).should == "1.12346" + @method.call("%#{f}", 12.1234567).should == "12.1235" + @method.call("%#{f}", 123.1234567).should == "123.123" + @method.call("%#{f}", 1234.1234567).should == "1234.12" + @method.call("%#{f}", 12345.1234567).should == "12345.1" + @method.call("%#{f}", 123456.1234567).should == "123456" + end + end + + it "displays Float::INFINITY as Inf" do + @method.call("%#{f}", Float::INFINITY).should == "Inf" + @method.call("%#{f}", -Float::INFINITY).should == "-Inf" + end + + it "displays Float::NAN as NaN" do + @method.call("%#{f}", Float::NAN).should == "NaN" + @method.call("%#{f}", -Float::NAN).should == "NaN" + end + end + end + + describe "a" do + it "converts floating point argument as [-]0xh.hhhhp[+-]dd" do + @method.call("%a", 196).should == "0x1.88p+7" + @method.call("%a", -196).should == "-0x1.88p+7" + @method.call("%a", 196.1).should == "0x1.8833333333333p+7" + @method.call("%a", 0.01).should == "0x1.47ae147ae147bp-7" + @method.call("%a", -0.01).should == "-0x1.47ae147ae147bp-7" + end + + it "displays Float::INFINITY as Inf" do + @method.call("%a", Float::INFINITY).should == "Inf" + @method.call("%a", -Float::INFINITY).should == "-Inf" + end + + it "displays Float::NAN as NaN" do + @method.call("%a", Float::NAN).should == "NaN" + @method.call("%a", -Float::NAN).should == "NaN" + end + end + + describe "A" do + it "converts floating point argument as [-]0xh.hhhhp[+-]dd and use uppercase X and P" do + @method.call("%A", 196).should == "0X1.88P+7" + @method.call("%A", -196).should == "-0X1.88P+7" + @method.call("%A", 196.1).should == "0X1.8833333333333P+7" + @method.call("%A", 0.01).should == "0X1.47AE147AE147BP-7" + @method.call("%A", -0.01).should == "-0X1.47AE147AE147BP-7" + end + + it "displays Float::INFINITY as Inf" do + @method.call("%A", Float::INFINITY).should == "Inf" + @method.call("%A", -Float::INFINITY).should == "-Inf" + end + + it "displays Float::NAN as NaN" do + @method.call("%A", Float::NAN).should == "NaN" + @method.call("%A", -Float::NAN).should == "NaN" + end + end + end + + describe "other formats" do + describe "c" do + it "displays character if argument is a numeric code of character" do + @method.call("%c", 97).should == "a" + end + + it "displays character if argument is a single character string" do + @method.call("%c", "a").should == "a" + end + + it "displays only the first character if argument is a string of several characters" do + @method.call("%c", "abc").should == "a" + end + + it "displays no characters if argument is an empty string" do + @method.call("%c", "").should == "" + end + + it "raises TypeError if argument is not String or Integer and cannot be converted to them" do + -> { + @method.call("%c", []) + }.should raise_error(TypeError, /no implicit conversion of Array into Integer/) + end + + it "raises TypeError if argument is nil" do + -> { + @method.call("%c", nil) + }.should raise_error(TypeError, /no implicit conversion from nil to integer/) + end + + it "tries to convert argument to String with to_str" do + obj = BasicObject.new + def obj.to_str + "a" + end + + @method.call("%c", obj).should == "a" + end + + it "tries to convert argument to Integer with to_int" do + obj = BasicObject.new + def obj.to_int + 90 + end + + @method.call("%c", obj).should == "Z" + end + + it "raises TypeError if converting to String with to_str returns non-String" do + obj = BasicObject.new + def obj.to_str + :foo + end + + -> { + @method.call("%c", obj) + }.should raise_error(TypeError, /can't convert BasicObject to String/) + end + + it "raises TypeError if converting to Integer with to_int returns non-Integer" do + obj = BasicObject.new + def obj.to_int + :foo + end + + -> { + @method.call("%c", obj) + }.should raise_error(TypeError, /can't convert BasicObject to Integer/) + end + end + + describe "p" do + it "displays argument.inspect value" do + obj = mock("object") + obj.should_receive(:inspect).and_return("<inspect-result>") + @method.call("%p", obj).should == "<inspect-result>" + end + + it "substitutes 'nil' for nil" do + @method.call("%p", nil).should == "nil" + end + end + + describe "s" do + it "substitute argument passes as a string" do + @method.call("%s", "abc").should == "abc" + end + + it "substitutes '' for nil" do + @method.call("%s", nil).should == "" + end + + it "converts argument to string with to_s" do + obj = mock("string") + obj.should_receive(:to_s).and_return("abc") + @method.call("%s", obj).should == "abc" + end + + it "does not try to convert with to_str" do + obj = BasicObject.new + def obj.to_str + "abc" + end + + -> { + @method.call("%s", obj) + }.should raise_error(NoMethodError) + end + + it "formats a partial substring without including omitted characters" do + long_string = "aabbccddhelloddccbbaa" + sub_string = long_string[8, 5] + sprintf("%.#{1 * 3}s", sub_string).should == "hel" + end + + it "formats string with precision" do + Kernel.format("%.3s", "hello").should == "hel" + Kernel.format("%-3.3s", "hello").should == "hel" + end + + it "formats string with width" do + @method.call("%6s", "abc").should == " abc" + @method.call("%6s", "abcdefg").should == "abcdefg" + end + + it "formats string with width and precision" do + @method.call("%4.6s", "abc").should == " abc" + @method.call("%4.6s", "abcdefg").should == "abcdef" + end + + it "formats nil with width" do + @method.call("%6s", nil).should == " " + end + + it "formats nil with precision" do + @method.call("%.6s", nil).should == "" + end + + it "formats nil with width and precision" do + @method.call("%4.6s", nil).should == " " + end + + it "formats multibyte string with precision" do + Kernel.format("%.2s", "été").should == "ét" + end + + it "preserves encoding of the format string" do + str = format('%s'.encode(Encoding::UTF_8), 'foobar') + str.encoding.should == Encoding::UTF_8 + + str = format('%s'.encode(Encoding::US_ASCII), 'foobar') + str.encoding.should == Encoding::US_ASCII + end + end + + describe "%" do + it "alone raises an ArgumentError" do + -> { + @method.call("%") + }.should raise_error(ArgumentError) + end + + it "is escaped by %" do + @method.call("%%").should == "%" + @method.call("%%d").should == "%d" + end + end + end + + describe "flags" do + describe "space" do + context "applies to numeric formats bBdiouxXeEfgGaA" do + it "leaves a space at the start of non-negative numbers" do + @method.call("% b", 10).should == " 1010" + @method.call("% B", 10).should == " 1010" + @method.call("% d", 112).should == " 112" + @method.call("% i", 112).should == " 112" + @method.call("% o", 87).should == " 127" + @method.call("% u", 112).should == " 112" + @method.call("% x", 196).should == " c4" + @method.call("% X", 196).should == " C4" + + @method.call("% e", 109.52).should == " 1.095200e+02" + @method.call("% E", 109.52).should == " 1.095200E+02" + @method.call("% f", 10.952).should == " 10.952000" + @method.call("% g", 12.1234).should == " 12.1234" + @method.call("% G", 12.1234).should == " 12.1234" + @method.call("% a", 196).should == " 0x1.88p+7" + @method.call("% A", 196).should == " 0X1.88P+7" + end + + it "does not leave a space at the start of negative numbers" do + @method.call("% b", -10).should == "-1010" + @method.call("% B", -10).should == "-1010" + @method.call("% d", -112).should == "-112" + @method.call("% i", -112).should == "-112" + @method.call("% o", -87).should == "-127" + @method.call("% u", -112).should == "-112" + @method.call("% x", -196).should == "-c4" + @method.call("% X", -196).should == "-C4" + + @method.call("% e", -109.52).should == "-1.095200e+02" + @method.call("% E", -109.52).should == "-1.095200E+02" + @method.call("% f", -10.952).should == "-10.952000" + @method.call("% g", -12.1234).should == "-12.1234" + @method.call("% G", -12.1234).should == "-12.1234" + @method.call("% a", -196).should == "-0x1.88p+7" + @method.call("% A", -196).should == "-0X1.88P+7" + end + + it "prevents converting negative argument to two's complement form" do + @method.call("% b", -10).should == "-1010" + @method.call("% B", -10).should == "-1010" + @method.call("% o", -87).should == "-127" + @method.call("% x", -196).should == "-c4" + @method.call("% X", -196).should == "-C4" + end + + it "treats several white spaces as one" do + @method.call("% b", 10).should == " 1010" + @method.call("% B", 10).should == " 1010" + @method.call("% d", 112).should == " 112" + @method.call("% i", 112).should == " 112" + @method.call("% o", 87).should == " 127" + @method.call("% u", 112).should == " 112" + @method.call("% x", 196).should == " c4" + @method.call("% X", 196).should == " C4" + + @method.call("% e", 109.52).should == " 1.095200e+02" + @method.call("% E", 109.52).should == " 1.095200E+02" + @method.call("% f", 10.952).should == " 10.952000" + @method.call("% g", 12.1234).should == " 12.1234" + @method.call("% G", 12.1234).should == " 12.1234" + @method.call("% a", 196).should == " 0x1.88p+7" + @method.call("% A", 196).should == " 0X1.88P+7" + end + end + end + + describe "(digit)$" do + it "specifies the absolute argument number for this field" do + @method.call("%2$b", 0, 10).should == "1010" + @method.call("%2$B", 0, 10).should == "1010" + @method.call("%2$d", 0, 112).should == "112" + @method.call("%2$i", 0, 112).should == "112" + @method.call("%2$o", 0, 87).should == "127" + @method.call("%2$u", 0, 112).should == "112" + @method.call("%2$x", 0, 196).should == "c4" + @method.call("%2$X", 0, 196).should == "C4" + + @method.call("%2$e", 0, 109.52).should == "1.095200e+02" + @method.call("%2$E", 0, 109.52).should == "1.095200E+02" + @method.call("%2$f", 0, 10.952).should == "10.952000" + @method.call("%2$g", 0, 12.1234).should == "12.1234" + @method.call("%2$G", 0, 12.1234).should == "12.1234" + @method.call("%2$a", 0, 196).should == "0x1.88p+7" + @method.call("%2$A", 0, 196).should == "0X1.88P+7" + + @method.call("%2$c", 1, 97).should == "a" + @method.call("%2$p", "a", []).should == "[]" + @method.call("%2$s", "-", "abc").should == "abc" + end + + it "raises exception if argument number is bigger than actual arguments list" do + -> { + @method.call("%4$d", 1, 2, 3) + }.should raise_error(ArgumentError) + end + + it "ignores '-' sign" do + @method.call("%2$d", 1, 2, 3).should == "2" + @method.call("%-2$d", 1, 2, 3).should == "2" + end + + it "raises ArgumentError exception when absolute and relative argument numbers are mixed" do + -> { + @method.call("%1$d %d", 1, 2) + }.should raise_error(ArgumentError) + end + end + + describe "#" do + context "applies to format o" do + it "increases the precision until the first digit will be `0' if it is not formatted as complements" do + @method.call("%#o", 87).should == "0127" + end + + it "does nothing for negative argument" do + @method.call("%#o", -87).should == "..7651" + end + end + + context "applies to formats bBxX" do + it "prefixes the result with 0x, 0X, 0b and 0B respectively for non-zero argument" do + @method.call("%#b", 10).should == "0b1010" + @method.call("%#b", -10).should == "0b..10110" + @method.call("%#B", 10).should == "0B1010" + @method.call("%#B", -10).should == "0B..10110" + + @method.call("%#x", 196).should == "0xc4" + @method.call("%#x", -196).should == "0x..f3c" + @method.call("%#X", 196).should == "0XC4" + @method.call("%#X", -196).should == "0X..F3C" + end + + it "does nothing for zero argument" do + @method.call("%#b", 0).should == "0" + @method.call("%#B", 0).should == "0" + + @method.call("%#o", 0).should == "0" + + @method.call("%#x", 0).should == "0" + @method.call("%#X", 0).should == "0" + end + end + + context "applies to formats aAeEfgG" do + it "forces a decimal point to be added, even if no digits follow" do + @method.call("%#.0a", 16.25).should == "0x1.p+4" + @method.call("%#.0A", 16.25).should == "0X1.P+4" + + @method.call("%#.0e", 100).should == "1.e+02" + @method.call("%#.0E", 100).should == "1.E+02" + + @method.call("%#.0f", 123.4).should == "123." + + @method.call("%#g", 123456).should == "123456." + @method.call("%#G", 123456).should == "123456." + end + + it "changes format from dd.dddd to exponential form for gG" do + @method.call("%#.0g", 123.4).should_not == "123." + @method.call("%#.0g", 123.4).should == "1.e+02" + end + end + + context "applies to gG" do + it "does not remove trailing zeros" do + @method.call("%#g", 123.4).should == "123.400" + @method.call("%#g", 123.4).should == "123.400" + end + end + end + + describe "+" do + context "applies to numeric formats bBdiouxXaAeEfgG" do + it "adds a leading plus sign to non-negative numbers" do + @method.call("%+b", 10).should == "+1010" + @method.call("%+B", 10).should == "+1010" + @method.call("%+d", 112).should == "+112" + @method.call("%+i", 112).should == "+112" + @method.call("%+o", 87).should == "+127" + @method.call("%+u", 112).should == "+112" + @method.call("%+x", 196).should == "+c4" + @method.call("%+X", 196).should == "+C4" + + @method.call("%+e", 109.52).should == "+1.095200e+02" + @method.call("%+E", 109.52).should == "+1.095200E+02" + @method.call("%+f", 10.952).should == "+10.952000" + @method.call("%+g", 12.1234).should == "+12.1234" + @method.call("%+G", 12.1234).should == "+12.1234" + @method.call("%+a", 196).should == "+0x1.88p+7" + @method.call("%+A", 196).should == "+0X1.88P+7" + end + + it "does not use two's complement form for negative numbers for formats bBoxX" do + @method.call("%+b", -10).should == "-1010" + @method.call("%+B", -10).should == "-1010" + @method.call("%+o", -87).should == "-127" + @method.call("%+x", -196).should == "-c4" + @method.call("%+X", -196).should == "-C4" + end + end + end + + describe "-" do + it "left-justifies the result of conversion if width is specified" do + @method.call("%-10b", 10).should == "1010 " + @method.call("%-10B", 10).should == "1010 " + @method.call("%-10d", 112).should == "112 " + @method.call("%-10i", 112).should == "112 " + @method.call("%-10o", 87).should == "127 " + @method.call("%-10u", 112).should == "112 " + @method.call("%-10x", 196).should == "c4 " + @method.call("%-10X", 196).should == "C4 " + + @method.call("%-20e", 109.52).should == "1.095200e+02 " + @method.call("%-20E", 109.52).should == "1.095200E+02 " + @method.call("%-20f", 10.952).should == "10.952000 " + @method.call("%-20g", 12.1234).should == "12.1234 " + @method.call("%-20G", 12.1234).should == "12.1234 " + @method.call("%-20a", 196).should == "0x1.88p+7 " + @method.call("%-20A", 196).should == "0X1.88P+7 " + + @method.call("%-10c", 97).should == "a " + @method.call("%-10p", []).should == "[] " + @method.call("%-10s", "abc").should == "abc " + end + end + + describe "0 (zero)" do + context "applies to numeric formats bBdiouxXaAeEfgG and width is specified" do + it "pads with zeros, not spaces" do + @method.call("%010b", 10).should == "0000001010" + @method.call("%010B", 10).should == "0000001010" + @method.call("%010d", 112).should == "0000000112" + @method.call("%010i", 112).should == "0000000112" + @method.call("%010o", 87).should == "0000000127" + @method.call("%010u", 112).should == "0000000112" + @method.call("%010x", 196).should == "00000000c4" + @method.call("%010X", 196).should == "00000000C4" + + @method.call("%020e", 109.52).should == "000000001.095200e+02" + @method.call("%020E", 109.52).should == "000000001.095200E+02" + @method.call("%020f", 10.952).should == "0000000000010.952000" + @method.call("%020g", 12.1234).should == "000000000000012.1234" + @method.call("%020G", 12.1234).should == "000000000000012.1234" + @method.call("%020a", 196).should == "0x000000000001.88p+7" + @method.call("%020A", 196).should == "0X000000000001.88P+7" + end + + it "uses radix-1 when displays negative argument as a two's complement" do + @method.call("%010b", -10).should == "..11110110" + @method.call("%010B", -10).should == "..11110110" + @method.call("%010o", -87).should == "..77777651" + @method.call("%010x", -196).should == "..ffffff3c" + @method.call("%010X", -196).should == "..FFFFFF3C" + end + end + end + + describe "*" do + it "uses the previous argument as the field width" do + @method.call("%*b", 10, 10).should == " 1010" + @method.call("%*B", 10, 10).should == " 1010" + @method.call("%*d", 10, 112).should == " 112" + @method.call("%*i", 10, 112).should == " 112" + @method.call("%*o", 10, 87).should == " 127" + @method.call("%*u", 10, 112).should == " 112" + @method.call("%*x", 10, 196).should == " c4" + @method.call("%*X", 10, 196).should == " C4" + + @method.call("%*e", 20, 109.52).should == " 1.095200e+02" + @method.call("%*E", 20, 109.52).should == " 1.095200E+02" + @method.call("%*f", 20, 10.952).should == " 10.952000" + @method.call("%*g", 20, 12.1234).should == " 12.1234" + @method.call("%*G", 20, 12.1234).should == " 12.1234" + @method.call("%*a", 20, 196).should == " 0x1.88p+7" + @method.call("%*A", 20, 196).should == " 0X1.88P+7" + + @method.call("%*c", 10, 97).should == " a" + @method.call("%*p", 10, []).should == " []" + @method.call("%*s", 10, "abc").should == " abc" + end + + it "left-justifies the result if width is negative" do + @method.call("%*b", -10, 10).should == "1010 " + @method.call("%*B", -10, 10).should == "1010 " + @method.call("%*d", -10, 112).should == "112 " + @method.call("%*i", -10, 112).should == "112 " + @method.call("%*o", -10, 87).should == "127 " + @method.call("%*u", -10, 112).should == "112 " + @method.call("%*x", -10, 196).should == "c4 " + @method.call("%*X", -10, 196).should == "C4 " + + @method.call("%*e", -20, 109.52).should == "1.095200e+02 " + @method.call("%*E", -20, 109.52).should == "1.095200E+02 " + @method.call("%*f", -20, 10.952).should == "10.952000 " + @method.call("%*g", -20, 12.1234).should == "12.1234 " + @method.call("%*G", -20, 12.1234).should == "12.1234 " + @method.call("%*a", -20, 196).should == "0x1.88p+7 " + @method.call("%*A", -20, 196).should == "0X1.88P+7 " + + @method.call("%*c", -10, 97).should == "a " + @method.call("%*p", -10, []).should == "[] " + @method.call("%*s", -10, "abc").should == "abc " + end + + it "uses the specified argument as the width if * is followed by a number and $" do + @method.call("%1$*2$b", 10, 10).should == " 1010" + @method.call("%1$*2$B", 10, 10).should == " 1010" + @method.call("%1$*2$d", 112, 10).should == " 112" + @method.call("%1$*2$i", 112, 10).should == " 112" + @method.call("%1$*2$o", 87, 10).should == " 127" + @method.call("%1$*2$u", 112, 10).should == " 112" + @method.call("%1$*2$x", 196, 10).should == " c4" + @method.call("%1$*2$X", 196, 10).should == " C4" + + @method.call("%1$*2$e", 109.52, 20).should == " 1.095200e+02" + @method.call("%1$*2$E", 109.52, 20).should == " 1.095200E+02" + @method.call("%1$*2$f", 10.952, 20).should == " 10.952000" + @method.call("%1$*2$g", 12.1234, 20).should == " 12.1234" + @method.call("%1$*2$G", 12.1234, 20).should == " 12.1234" + @method.call("%1$*2$a", 196, 20).should == " 0x1.88p+7" + @method.call("%1$*2$A", 196, 20).should == " 0X1.88P+7" + + @method.call("%1$*2$c", 97, 10).should == " a" + @method.call("%1$*2$p", [], 10).should == " []" + @method.call("%1$*2$s", "abc", 10).should == " abc" + end + + it "left-justifies the result if specified with $ argument is negative" do + @method.call("%1$*2$b", 10, -10).should == "1010 " + @method.call("%1$*2$B", 10, -10).should == "1010 " + @method.call("%1$*2$d", 112, -10).should == "112 " + @method.call("%1$*2$i", 112, -10).should == "112 " + @method.call("%1$*2$o", 87, -10).should == "127 " + @method.call("%1$*2$u", 112, -10).should == "112 " + @method.call("%1$*2$x", 196, -10).should == "c4 " + @method.call("%1$*2$X", 196, -10).should == "C4 " + + @method.call("%1$*2$e", 109.52, -20).should == "1.095200e+02 " + @method.call("%1$*2$E", 109.52, -20).should == "1.095200E+02 " + @method.call("%1$*2$f", 10.952, -20).should == "10.952000 " + @method.call("%1$*2$g", 12.1234, -20).should == "12.1234 " + @method.call("%1$*2$G", 12.1234, -20).should == "12.1234 " + @method.call("%1$*2$a", 196, -20).should == "0x1.88p+7 " + @method.call("%1$*2$A", 196, -20).should == "0X1.88P+7 " + + @method.call("%1$*2$c", 97, -10).should == "a " + @method.call("%1$*2$p", [], -10).should == "[] " + @method.call("%1$*2$s", "abc", -10).should == "abc " + end + + it "raises ArgumentError when is mixed with width" do + -> { + @method.call("%*10d", 10, 112) + }.should raise_error(ArgumentError) + end + end + end + + describe "width" do + it "specifies the minimum number of characters that will be written to the result" do + @method.call("%10b", 10).should == " 1010" + @method.call("%10B", 10).should == " 1010" + @method.call("%10d", 112).should == " 112" + @method.call("%10i", 112).should == " 112" + @method.call("%10o", 87).should == " 127" + @method.call("%10u", 112).should == " 112" + @method.call("%10x", 196).should == " c4" + @method.call("%10X", 196).should == " C4" + + @method.call("%20e", 109.52).should == " 1.095200e+02" + @method.call("%20E", 109.52).should == " 1.095200E+02" + @method.call("%20f", 10.952).should == " 10.952000" + @method.call("%20g", 12.1234).should == " 12.1234" + @method.call("%20G", 12.1234).should == " 12.1234" + @method.call("%20a", 196).should == " 0x1.88p+7" + @method.call("%20A", 196).should == " 0X1.88P+7" + + @method.call("%10c", 97).should == " a" + @method.call("%10p", []).should == " []" + @method.call("%10s", "abc").should == " abc" + end + + it "is ignored if argument's actual length is greater" do + @method.call("%5d", 1234567890).should == "1234567890" + end + end + + describe "precision" do + context "integer types" do + it "controls the number of decimal places displayed" do + @method.call("%.6b", 10).should == "001010" + @method.call("%.6B", 10).should == "001010" + @method.call("%.5d", 112).should == "00112" + @method.call("%.5i", 112).should == "00112" + @method.call("%.5o", 87).should == "00127" + @method.call("%.5u", 112).should == "00112" + + @method.call("%.5x", 196).should == "000c4" + @method.call("%.5X", 196).should == "000C4" + end + end + + context "float types" do + it "controls the number of decimal places displayed in fraction part" do + @method.call("%.10e", 109.52).should == "1.0952000000e+02" + @method.call("%.10E", 109.52).should == "1.0952000000E+02" + @method.call("%.10f", 10.952).should == "10.9520000000" + @method.call("%.10a", 196).should == "0x1.8800000000p+7" + @method.call("%.10A", 196).should == "0X1.8800000000P+7" + end + + it "does not affect G format" do + @method.call("%.10g", 12.1234).should == "12.1234" + @method.call("%.10g", 123456789).should == "123456789" + end + end + + context "string formats" do + it "determines the maximum number of characters to be copied from the string" do + @method.call("%.1p", [1]).should == "[" + @method.call("%.2p", [1]).should == "[1" + @method.call("%.10p", [1]).should == "[1]" + @method.call("%.0p", [1]).should == "" + + @method.call("%.1s", "abc").should == "a" + @method.call("%.2s", "abc").should == "ab" + @method.call("%.10s", "abc").should == "abc" + @method.call("%.0s", "abc").should == "" + end + end + end + + describe "reference by name" do + describe "%<name>s style" do + it "uses value passed in a hash argument" do + @method.call("%<foo>d", foo: 123).should == "123" + end + + it "supports flags, width, precision and type" do + @method.call("%+20.10<foo>f", foo: 10.952).should == " +10.9520000000" + end + + it "allows to place name in any position" do + @method.call("%+15.5<foo>f", foo: 10.952).should == " +10.95200" + @method.call("%+15<foo>.5f", foo: 10.952).should == " +10.95200" + @method.call("%+<foo>15.5f", foo: 10.952).should == " +10.95200" + @method.call("%<foo>+15.5f", foo: 10.952).should == " +10.95200" + end + + it "cannot be mixed with unnamed style" do + -> { + @method.call("%d %<foo>d", 1, foo: "123") + }.should raise_error(ArgumentError) + end + end + + describe "%{name} style" do + it "uses value passed in a hash argument" do + @method.call("%{foo}", foo: 123).should == "123" + end + + it "does not support type style" do + @method.call("%{foo}d", foo: 123).should == "123d" + end + + it "supports flags, width and precision" do + @method.call("%-20.5{foo}", foo: "123456789").should == "12345 " + end + + it "cannot be mixed with unnamed style" do + -> { + @method.call("%d %{foo}", 1, foo: "123") + }.should raise_error(ArgumentError) + end + + it "raises KeyError when there is no matching key" do + -> { + @method.call("%{foo}", {}) + }.should raise_error(KeyError) + end + + it "converts value to String with to_s" do + obj = Object.new + def obj.to_s; end + def obj.to_str; end + + obj.should_receive(:to_s).and_return("42") + obj.should_not_receive(:to_str) + + @method.call("%{foo}", foo: obj).should == "42" + end + end + end + + describe "faulty key" do + before :each do + @object = { foooo: 1 } + end + + it "raises a KeyError" do + -> { + @method.call("%<foo>s", @object) + }.should raise_error(KeyError) + end + + it "sets the Hash as the receiver of KeyError" do + -> { + @method.call("%<foo>s", @object) + }.should raise_error(KeyError) { |err| + err.receiver.should equal(@object) + } + end + + it "sets the unmatched key as the key of KeyError" do + -> { + @method.call("%<foo>s", @object) + }.should raise_error(KeyError) { |err| + err.key.to_s.should == 'foo' + } + end + end + + it "does not raise error when passed more arguments than needed" do + sprintf("%s %d %c", "string", 2, "c", []).should == "string 2 c" + end +end diff --git a/spec/ruby/core/kernel/shared/sprintf_encoding.rb b/spec/ruby/core/kernel/shared/sprintf_encoding.rb new file mode 100644 index 0000000000..7ec0fe4c48 --- /dev/null +++ b/spec/ruby/core/kernel/shared/sprintf_encoding.rb @@ -0,0 +1,67 @@ +# Keep encoding-related specs in a separate shared example to be able to skip them in IO/File/StringIO specs. +# It's difficult to check result's encoding in the test after writing to a file/io buffer. +describe :kernel_sprintf_encoding, shared: true do + it "can produce a string with valid encoding" do + string = @method.call("good day %{valid}", valid: "e") + string.encoding.should == Encoding::UTF_8 + string.valid_encoding?.should be_true + end + + it "can produce a string with invalid encoding" do + string = @method.call("good day %{invalid}", invalid: "\x80") + string.encoding.should == Encoding::UTF_8 + string.valid_encoding?.should be_false + end + + it "returns a String in the same encoding as the format String if compatible" do + string = "%s".dup.force_encoding(Encoding::KOI8_U) + result = @method.call(string, "dogs") + result.encoding.should equal(Encoding::KOI8_U) + end + + it "returns a String in the argument's encoding if format encoding is more restrictive" do + string = "foo %s".dup.force_encoding(Encoding::US_ASCII) + argument = "b\303\274r".dup.force_encoding(Encoding::UTF_8) + + result = @method.call(string, argument) + result.encoding.should equal(Encoding::UTF_8) + end + + it "raises Encoding::CompatibilityError if both encodings are ASCII compatible and there are not ASCII characters" do + string = "Ä %s".encode('windows-1252') + argument = "Ђ".encode('windows-1251') + + -> { + @method.call(string, argument) + }.should raise_error(Encoding::CompatibilityError) + end + + describe "%c" do + it "supports Unicode characters" do + result = @method.call("%c", 1286) + result.should == "Ô†" + result.bytes.should == [212, 134] + + result = @method.call("%c", "Ø´") + result.should == "Ø´" + result.bytes.should == [216, 180] + end + + it "raises error when a codepoint isn't representable in an encoding of a format string" do + format = "%c".encode("ASCII") + + -> { + @method.call(format, 1286) + }.should raise_error(RangeError, /out of char range/) + end + + it "uses the encoding of the format string to interpret codepoints" do + format = "%c".dup.force_encoding("euc-jp") + result = @method.call(format, 9415601) + + result.encoding.should == Encoding::EUC_JP + result.should == "é".encode(Encoding::EUC_JP) + result.bytes.should == [143, 171, 177] + end + end +end diff --git a/spec/ruby/core/kernel/shared/then.rb b/spec/ruby/core/kernel/shared/then.rb new file mode 100644 index 0000000000..b52075371f --- /dev/null +++ b/spec/ruby/core/kernel/shared/then.rb @@ -0,0 +1,20 @@ +describe :kernel_then, shared: true do + it "yields self" do + object = Object.new + object.send(@method) { |o| o.should equal object } + end + + it "returns the block return value" do + object = Object.new + object.send(@method) { 42 }.should equal 42 + end + + it "returns a sized Enumerator when no block given" do + object = Object.new + enum = object.send(@method) + enum.should be_an_instance_of Enumerator + enum.size.should equal 1 + enum.peek.should equal object + enum.first.should equal object + end +end diff --git a/spec/ruby/core/kernel/singleton_class_spec.rb b/spec/ruby/core/kernel/singleton_class_spec.rb new file mode 100644 index 0000000000..23c400f9bd --- /dev/null +++ b/spec/ruby/core/kernel/singleton_class_spec.rb @@ -0,0 +1,74 @@ +# truffleruby_primitives: true +require_relative '../../spec_helper' + +describe "Kernel#singleton_class" do + it "returns class extended from an object" do + x = Object.new + xs = class << x; self; end + xs.should == x.singleton_class + end + + it "returns NilClass for nil" do + nil.singleton_class.should == NilClass + end + + it "returns TrueClass for true" do + true.singleton_class.should == TrueClass + end + + it "returns FalseClass for false" do + false.singleton_class.should == FalseClass + end + + it "raises TypeError for Integer" do + -> { 123.singleton_class }.should raise_error(TypeError, "can't define singleton") + end + + it "raises TypeError for Float" do + -> { 3.14.singleton_class }.should raise_error(TypeError, "can't define singleton") + end + + it "raises TypeError for Symbol" do + -> { :foo.singleton_class }.should raise_error(TypeError, "can't define singleton") + end + + it "raises TypeError for a frozen deduplicated String" do + -> { (-"string").singleton_class }.should raise_error(TypeError, "can't define singleton") + -> { a = -"string"; a.singleton_class }.should raise_error(TypeError, "can't define singleton") + -> { a = "string"; (-a).singleton_class }.should raise_error(TypeError, "can't define singleton") + end + + it "returns a frozen singleton class if object is frozen" do + obj = Object.new + obj.freeze + obj.singleton_class.frozen?.should be_true + end + + context "for an IO object with a replaced singleton class" do + it "looks up singleton methods from the fresh singleton class after an object instance got a new one" do + proxy = -> io { io.foo } + if RUBY_ENGINE == 'truffleruby' + # We need an inline cache with only this object seen, the best way to do that is to use a Primitive + sclass = -> io { Primitive.singleton_class(io) } + else + sclass = -> io { io.singleton_class } + end + + io = File.new(__FILE__) + io.define_singleton_method(:foo) { "old" } + sclass1 = sclass.call(io) + proxy.call(io).should == "old" + + # IO#reopen is the only method which can replace an object's singleton class + io2 = File.new(__FILE__) + io.reopen(io2) + io.define_singleton_method(:foo) { "new" } + sclass2 = sclass.call(io) + sclass2.should_not.equal?(sclass1) + proxy.call(io).should == "new" + ensure + io2.close + io.close + end + end +end diff --git a/spec/ruby/core/kernel/singleton_method_spec.rb b/spec/ruby/core/kernel/singleton_method_spec.rb new file mode 100644 index 0000000000..7d63fa7cc6 --- /dev/null +++ b/spec/ruby/core/kernel/singleton_method_spec.rb @@ -0,0 +1,85 @@ +require_relative '../../spec_helper' + +describe "Kernel#singleton_method" do + it "finds a method defined on the singleton class" do + obj = Object.new + def obj.foo; end + obj.singleton_method(:foo).should be_an_instance_of(Method) + end + + it "returns a Method which can be called" do + obj = Object.new + def obj.foo; 42; end + obj.singleton_method(:foo).call.should == 42 + end + + it "only looks at singleton methods and not at methods in the class" do + klass = Class.new do + def foo + 42 + end + end + obj = klass.new + obj.foo.should == 42 + -> { + obj.singleton_method(:foo) + }.should raise_error(NameError) { |e| + # a NameError and not a NoMethodError + e.class.should == NameError + } + end + + it "raises a NameError if there is no such method" do + obj = Object.new + -> { + obj.singleton_method(:not_existing) + }.should raise_error(NameError) { |e| + # a NameError and not a NoMethodError + e.class.should == NameError + } + end + + ruby_bug "#20620", ""..."3.4" do + it "finds a method defined in a module included in the singleton class" do + m = Module.new do + def foo + :foo + end + end + + obj = Object.new + obj.singleton_class.include(m) + + obj.singleton_method(:foo).should be_an_instance_of(Method) + obj.singleton_method(:foo).call.should == :foo + end + + it "finds a method defined in a module prepended in the singleton class" do + m = Module.new do + def foo + :foo + end + end + + obj = Object.new + obj.singleton_class.prepend(m) + + obj.singleton_method(:foo).should be_an_instance_of(Method) + obj.singleton_method(:foo).call.should == :foo + end + + it "finds a method defined in a module that an object is extended with" do + m = Module.new do + def foo + :foo + end + end + + obj = Object.new + obj.extend(m) + + obj.singleton_method(:foo).should be_an_instance_of(Method) + obj.singleton_method(:foo).call.should == :foo + end + end +end diff --git a/spec/ruby/core/kernel/singleton_methods_spec.rb b/spec/ruby/core/kernel/singleton_methods_spec.rb new file mode 100644 index 0000000000..a127a439de --- /dev/null +++ b/spec/ruby/core/kernel/singleton_methods_spec.rb @@ -0,0 +1,192 @@ +require_relative '../../spec_helper' +require_relative '../../fixtures/reflection' +require_relative 'fixtures/classes' + +describe :kernel_singleton_methods, shared: true do + it "returns an empty Array for an object with no singleton methods" do + ReflectSpecs.o.singleton_methods(*@object).should == [] + end + + it "returns the names of module methods for a module" do + ReflectSpecs::M.singleton_methods(*@object).should include(:ms_pro, :ms_pub) + end + + it "does not return private module methods for a module" do + ReflectSpecs::M.singleton_methods(*@object).should_not include(:ms_pri) + end + + it "returns the names of class methods for a class" do + ReflectSpecs::A.singleton_methods(*@object).should include(:as_pro, :as_pub) + end + + it "does not return private class methods for a class" do + ReflectSpecs::A.singleton_methods(*@object).should_not include(:as_pri) + end + + it "returns the names of singleton methods for an object" do + ReflectSpecs.os.singleton_methods(*@object).should include(:os_pro, :os_pub) + end +end + +describe :kernel_singleton_methods_modules, shared: true do + it "does not return any included methods for a module including a module" do + ReflectSpecs::N.singleton_methods(*@object).should include(:ns_pro, :ns_pub) + end + + it "does not return any included methods for a class including a module" do + ReflectSpecs::D.singleton_methods(*@object).should include(:ds_pro, :ds_pub) + end + + it "for a module does not return methods in a module prepended to Module itself" do + require_relative 'fixtures/singleton_methods' + mod = SingletonMethodsSpecs::SelfExtending + mod.method(:mspec_test_kernel_singleton_methods).owner.should == SingletonMethodsSpecs::Prepended + + ancestors = mod.singleton_class.ancestors + ancestors[0...2].should == [ mod.singleton_class, mod ] + ancestors.should include(SingletonMethodsSpecs::Prepended) + + # Do not search prepended modules of `Module`, as that's a non-singleton class + mod.singleton_methods.should == [] + end +end + +describe :kernel_singleton_methods_supers, shared: true do + it "returns the names of singleton methods for an object extended with a module" do + ReflectSpecs.oe.singleton_methods(*@object).should include(:m_pro, :m_pub) + end + + it "returns a unique list for an object extended with a module" do + m = ReflectSpecs.oed.singleton_methods(*@object) + r = m.select { |x| x == :pub or x == :pro }.sort + r.should == [:pro, :pub] + end + + it "returns the names of singleton methods for an object extended with two modules" do + ReflectSpecs.oee.singleton_methods(*@object).should include(:m_pro, :m_pub, :n_pro, :n_pub) + end + + it "returns the names of singleton methods for an object extended with a module including a module" do + ReflectSpecs.oei.singleton_methods(*@object).should include(:n_pro, :n_pub, :m_pro, :m_pub) + end + + it "returns the names of inherited singleton methods for a subclass" do + ReflectSpecs::B.singleton_methods(*@object).should include(:as_pro, :as_pub, :bs_pro, :bs_pub) + end + + it "returns a unique list for a subclass" do + m = ReflectSpecs::B.singleton_methods(*@object) + r = m.select { |x| x == :pub or x == :pro }.sort + r.should == [:pro, :pub] + end + + it "returns the names of inherited singleton methods for a subclass including a module" do + ReflectSpecs::C.singleton_methods(*@object).should include(:as_pro, :as_pub, :cs_pro, :cs_pub) + end + + it "returns a unique list for a subclass including a module" do + m = ReflectSpecs::C.singleton_methods(*@object) + r = m.select { |x| x == :pub or x == :pro }.sort + r.should == [:pro, :pub] + end + + it "returns the names of inherited singleton methods for a subclass of a class including a module" do + ReflectSpecs::E.singleton_methods(*@object).should include(:ds_pro, :ds_pub, :es_pro, :es_pub) + end + + it "returns the names of inherited singleton methods for a subclass of a class that includes a module, where the subclass also includes a module" do + ReflectSpecs::F.singleton_methods(*@object).should include(:ds_pro, :ds_pub, :fs_pro, :fs_pub) + end + + it "returns the names of inherited singleton methods for a class extended with a module" do + ReflectSpecs::P.singleton_methods(*@object).should include(:m_pro, :m_pub) + end +end + +describe :kernel_singleton_methods_private_supers, shared: true do + it "does not return private singleton methods for an object extended with a module" do + ReflectSpecs.oe.singleton_methods(*@object).should_not include(:m_pri) + end + + it "does not return private singleton methods for an object extended with two modules" do + ReflectSpecs.oee.singleton_methods(*@object).should_not include(:m_pri) + end + + it "does not return private singleton methods for an object extended with a module including a module" do + ReflectSpecs.oei.singleton_methods(*@object).should_not include(:n_pri, :m_pri) + end + + it "does not return private singleton methods for a class extended with a module" do + ReflectSpecs::P.singleton_methods(*@object).should_not include(:m_pri) + end + + it "does not return private inherited singleton methods for a module including a module" do + ReflectSpecs::N.singleton_methods(*@object).should_not include(:ns_pri) + end + + it "does not return private inherited singleton methods for a class including a module" do + ReflectSpecs::D.singleton_methods(*@object).should_not include(:ds_pri) + end + + it "does not return private inherited singleton methods for a subclass" do + ReflectSpecs::B.singleton_methods(*@object).should_not include(:as_pri, :bs_pri) + end + + it "does not return private inherited singleton methods for a subclass including a module" do + ReflectSpecs::C.singleton_methods(*@object).should_not include(:as_pri, :cs_pri) + end + + it "does not return private inherited singleton methods for a subclass of a class including a module" do + ReflectSpecs::E.singleton_methods(*@object).should_not include(:ds_pri, :es_pri) + end + + it "does not return private inherited singleton methods for a subclass of a class that includes a module, where the subclass also includes a module" do + ReflectSpecs::F.singleton_methods(*@object).should_not include(:ds_pri, :fs_pri) + end +end + +describe "Kernel#singleton_methods" do + describe "when not passed an argument" do + it_behaves_like :kernel_singleton_methods, nil, [] + it_behaves_like :kernel_singleton_methods_supers, nil, [] + it_behaves_like :kernel_singleton_methods_modules, nil, [] + it_behaves_like :kernel_singleton_methods_private_supers, nil, [] + end + + describe "when passed true" do + it_behaves_like :kernel_singleton_methods, nil, true + it_behaves_like :kernel_singleton_methods_supers, nil, true + it_behaves_like :kernel_singleton_methods_modules, nil, true + it_behaves_like :kernel_singleton_methods_private_supers, nil, true + end + + describe "when passed false" do + it_behaves_like :kernel_singleton_methods, nil, false + it_behaves_like :kernel_singleton_methods_modules, nil, false + it_behaves_like :kernel_singleton_methods_private_supers, nil, false + + it "returns an empty Array for an object extended with a module" do + ReflectSpecs.oe.singleton_methods(false).should == [] + end + + it "returns an empty Array for an object extended with two modules" do + ReflectSpecs.oee.singleton_methods(false).should == [] + end + + it "returns an empty Array for an object extended with a module including a module" do + ReflectSpecs.oei.singleton_methods(false).should == [] + end + + it "returns the names of singleton methods of the subclass" do + ReflectSpecs::B.singleton_methods(false).should include(:bs_pro, :bs_pub) + end + + it "does not return names of inherited singleton methods for a subclass" do + ReflectSpecs::B.singleton_methods(false).should_not include(:as_pro, :as_pub) + end + + it "does not return the names of inherited singleton methods for a class extended with a module" do + ReflectSpecs::P.singleton_methods(false).should_not include(:m_pro, :m_pub) + end + end +end diff --git a/spec/ruby/core/kernel/sleep_spec.rb b/spec/ruby/core/kernel/sleep_spec.rb new file mode 100644 index 0000000000..e9c600aac4 --- /dev/null +++ b/spec/ruby/core/kernel/sleep_spec.rb @@ -0,0 +1,126 @@ +require_relative '../../spec_helper' +require_relative '../fiber/fixtures/scheduler' + +describe "Kernel#sleep" do + it "is a private method" do + Kernel.should have_private_instance_method(:sleep) + end + + it "returns an Integer" do + sleep(0.001).should be_kind_of(Integer) + end + + it "accepts a Float" do + sleep(0.001).should >= 0 + end + + it "accepts an Integer" do + sleep(0).should >= 0 + end + + it "accepts a Rational" do + sleep(Rational(1, 999)).should >= 0 + end + + it "accepts any Object that responds to divmod" do + o = Object.new + def o.divmod(*); [0, 0.001]; end + sleep(o).should >= 0 + end + + it "raises an ArgumentError when passed a negative duration" do + -> { sleep(-0.1) }.should raise_error(ArgumentError) + -> { sleep(-1) }.should raise_error(ArgumentError) + end + + it "raises a TypeError when passed a String" do + -> { sleep('2') }.should raise_error(TypeError) + end + + it "pauses execution indefinitely if not given a duration" do + running = false + t = Thread.new do + running = true + sleep + 5 + end + + Thread.pass until running + Thread.pass while t.status and t.status != "sleep" + + t.wakeup + t.value.should == 5 + end + + it "sleeps with nanosecond precision" do + start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 100.times do + sleep(0.0001) + end + end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) + + actual_duration = end_time - start_time + actual_duration.should > 0.01 # 100 * 0.0001 => 0.01 + end + + ruby_version_is ""..."3.3" do + it "raises a TypeError when passed nil" do + -> { sleep(nil) }.should raise_error(TypeError) + end + end + + ruby_version_is "3.3" do + it "accepts a nil duration" do + running = false + t = Thread.new do + running = true + sleep(nil) + 5 + end + + Thread.pass until running + Thread.pass while t.status and t.status != "sleep" + + t.wakeup + t.value.should == 5 + end + end + + context "Kernel.sleep with Fiber scheduler" do + before :each do + Fiber.set_scheduler(FiberSpecs::LoggingScheduler.new) + end + + after :each do + Fiber.set_scheduler(nil) + end + + it "calls the scheduler without arguments when no duration is given" do + sleeper = Fiber.new(blocking: false) do + sleep + end + sleeper.resume + Fiber.scheduler.events.should == [{ event: :kernel_sleep, fiber: sleeper, args: [] }] + end + + it "calls the scheduler with the given duration" do + sleeper = Fiber.new(blocking: false) do + sleep(0.01) + end + sleeper.resume + Fiber.scheduler.events.should == [{ event: :kernel_sleep, fiber: sleeper, args: [0.01] }] + end + + it "does not call the scheduler if the fiber is blocking" do + sleeper = Fiber.new(blocking: true) do + sleep(0.01) + end + sleeper.resume + Fiber.scheduler.events.should == [] + end + end +end + +describe "Kernel.sleep" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/kernel/spawn_spec.rb b/spec/ruby/core/kernel/spawn_spec.rb new file mode 100644 index 0000000000..ba05b629d5 --- /dev/null +++ b/spec/ruby/core/kernel/spawn_spec.rb @@ -0,0 +1,25 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +# These specs only run a basic usage of #spawn. +# Process.spawn has more complete specs and they are not +# run here as it is redundant and takes too long for little gain. +describe "Kernel#spawn" do + it "is a private method" do + Kernel.should have_private_instance_method(:spawn) + end + + it "executes the given command" do + -> { + Process.wait spawn("echo spawn") + }.should output_to_fd("spawn\n") + end +end + +describe "Kernel.spawn" do + it "executes the given command" do + -> { + Process.wait Kernel.spawn("echo spawn") + }.should output_to_fd("spawn\n") + end +end diff --git a/spec/ruby/core/kernel/sprintf_spec.rb b/spec/ruby/core/kernel/sprintf_spec.rb new file mode 100644 index 0000000000..5a4a90ff7a --- /dev/null +++ b/spec/ruby/core/kernel/sprintf_spec.rb @@ -0,0 +1,64 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/sprintf' +require_relative 'shared/sprintf_encoding' + +describe :kernel_sprintf_to_str, shared: true do + it "calls #to_str to convert the format object to a String" do + obj = mock('format string') + obj.should_receive(:to_str).and_return("to_str: %i") + @method.call(obj, 42).should == "to_str: 42" + end +end + +describe "Kernel#sprintf" do + it_behaves_like :kernel_sprintf, -> format, *args { + r = nil + -> { + r = sprintf(format, *args) + }.should_not complain(verbose: true) + r + } + + it_behaves_like :kernel_sprintf_encoding, -> format, *args { + r = nil + -> { + r = sprintf(format, *args) + }.should_not complain(verbose: true) + r + } + + it_behaves_like :kernel_sprintf_to_str, -> format, *args { + r = nil + -> { + r = sprintf(format, *args) + }.should_not complain(verbose: true) + r + } +end + +describe "Kernel.sprintf" do + it_behaves_like :kernel_sprintf, -> format, *args { + r = nil + -> { + r = Kernel.sprintf(format, *args) + }.should_not complain(verbose: true) + r + } + + it_behaves_like :kernel_sprintf_encoding, -> format, *args { + r = nil + -> { + r = Kernel.sprintf(format, *args) + }.should_not complain(verbose: true) + r + } + + it_behaves_like :kernel_sprintf_to_str, -> format, *args { + r = nil + -> { + r = Kernel.sprintf(format, *args) + }.should_not complain(verbose: true) + r + } +end diff --git a/spec/ruby/core/kernel/srand_spec.rb b/spec/ruby/core/kernel/srand_spec.rb new file mode 100644 index 0000000000..95bb406f46 --- /dev/null +++ b/spec/ruby/core/kernel/srand_spec.rb @@ -0,0 +1,73 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel#srand" do + before :each do + @seed = srand + end + + after :each do + srand(@seed) + end + + it "is a private method" do + Kernel.should have_private_instance_method(:srand) + end + + it "returns the previous seed value" do + srand(10) + srand(20).should == 10 + end + + it "returns the system-initialized seed value on the first call" do + ruby_exe('print srand(10)', options: '--disable-gems').should =~ /\A\d+\z/ + end + + it "seeds the RNG correctly and repeatably" do + srand(10) + x = rand + srand(10) + rand.should == x + end + + it "defaults number to a random value" do + -> { srand }.should_not raise_error + srand.should_not == 0 + end + + it "accepts and uses a seed of 0" do + srand(0) + srand.should == 0 + end + + it "accepts a negative seed" do + srand(-17) + srand.should == -17 + end + + it "accepts an Integer as a seed" do + srand(0x12345678901234567890) + srand.should == 0x12345678901234567890 + end + + it "calls #to_int on seed" do + srand(3.8) + srand.should == 3 + + s = mock('seed') + s.should_receive(:to_int).and_return 0 + srand(s) + end + + it "raises a TypeError when passed nil" do + -> { srand(nil) }.should raise_error(TypeError) + end + + it "raises a TypeError when passed a String" do + -> { srand("7") }.should raise_error(TypeError) + end +end + +describe "Kernel.srand" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/kernel/sub_spec.rb b/spec/ruby/core/kernel/sub_spec.rb new file mode 100644 index 0000000000..9130bd159c --- /dev/null +++ b/spec/ruby/core/kernel/sub_spec.rb @@ -0,0 +1,26 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +# FIXME: These methods exist only when the -n or -p option is passed to +# ruby, but we currently don't have a way of specifying that. +ruby_version_is ""..."1.9" do + describe "Kernel#sub" do + it "is a private method" do + Kernel.should have_private_instance_method(:sub) + end + end + + describe "Kernel#sub!" do + it "is a private method" do + Kernel.should have_private_instance_method(:sub!) + end + end + + describe "Kernel.sub" do + it "needs to be reviewed for spec completeness" + end + + describe "Kernel.sub!" do + it "needs to be reviewed for spec completeness" + end +end diff --git a/spec/ruby/core/kernel/syscall_spec.rb b/spec/ruby/core/kernel/syscall_spec.rb new file mode 100644 index 0000000000..32d07b3ae2 --- /dev/null +++ b/spec/ruby/core/kernel/syscall_spec.rb @@ -0,0 +1,12 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel#syscall" do + it "is a private method" do + Kernel.should have_private_instance_method(:syscall) + end +end + +describe "Kernel.syscall" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/kernel/system_spec.rb b/spec/ruby/core/kernel/system_spec.rb new file mode 100644 index 0000000000..9bc03924dd --- /dev/null +++ b/spec/ruby/core/kernel/system_spec.rb @@ -0,0 +1,132 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe :kernel_system, shared: true do + it "executes the specified command in a subprocess" do + -> { @object.system("echo a") }.should output_to_fd("a\n") + + $?.should be_an_instance_of Process::Status + $?.should.success? + end + + it "returns true when the command exits with a zero exit status" do + @object.system(ruby_cmd('exit 0')).should == true + + $?.should be_an_instance_of Process::Status + $?.should.success? + $?.exitstatus.should == 0 + end + + it "returns false when the command exits with a non-zero exit status" do + @object.system(ruby_cmd('exit 1')).should == false + + $?.should be_an_instance_of Process::Status + $?.should_not.success? + $?.exitstatus.should == 1 + end + + it "raises RuntimeError when `exception: true` is given and the command exits with a non-zero exit status" do + -> { @object.system(ruby_cmd('exit 1'), exception: true) }.should raise_error(RuntimeError) + end + + it "raises Errno::ENOENT when `exception: true` is given and the specified command does not exist" do + -> { @object.system('feature_14386', exception: true) }.should raise_error(Errno::ENOENT) + end + + it "returns nil when command execution fails" do + @object.system("sad").should be_nil + + $?.should be_an_instance_of Process::Status + $?.pid.should be_kind_of(Integer) + $?.should_not.success? + end + + it "does not write to stderr when command execution fails" do + -> { @object.system("sad") }.should output_to_fd("", STDERR) + end + + platform_is_not :windows do + before :each do + @shell = ENV['SHELL'] + end + + after :each do + ENV['SHELL'] = @shell + end + + it "executes with `sh` if the command contains shell characters" do + -> { @object.system("echo $0") }.should output_to_fd("sh\n") + end + + it "ignores SHELL env var and always uses `sh`" do + ENV['SHELL'] = "/bin/fakeshell" + -> { @object.system("echo $0") }.should output_to_fd("sh\n") + end + end + + platform_is_not :windows do + before :each do + require 'tmpdir' + @shell_command = File.join(Dir.mktmpdir, "noshebang.cmd") + File.write(@shell_command, %[echo "$PATH"\n], perm: 0o700) + end + + after :each do + File.unlink(@shell_command) + Dir.rmdir(File.dirname(@shell_command)) + end + + it "executes with `sh` if the command is executable but not binary and there is no shebang" do + -> { @object.system(@shell_command) }.should output_to_fd(ENV['PATH'] + "\n") + end + end + + before :each do + ENV['TEST_SH_EXPANSION'] = 'foo' + @shell_var = '$TEST_SH_EXPANSION' + platform_is :windows do + @shell_var = '%TEST_SH_EXPANSION%' + end + end + + after :each do + ENV.delete('TEST_SH_EXPANSION') + end + + it "expands shell variables when given a single string argument" do + -> { @object.system("echo #{@shell_var}") }.should output_to_fd("foo\n") + end + + platform_is_not :windows do + it "does not expand shell variables when given multiples arguments" do + -> { @object.system("echo", @shell_var) }.should output_to_fd("#{@shell_var}\n") + end + end + + platform_is :windows do + it "does expand shell variables when given multiples arguments" do + # See https://bugs.ruby-lang.org/issues/12231 + -> { @object.system("echo", @shell_var) }.should output_to_fd("foo\n") + end + end + + platform_is :windows do + it "runs commands starting with any number of @ using shell" do + `#{ruby_cmd("p system 'does_not_exist'")} 2>NUL`.chomp.should == "nil" + @object.system('@does_not_exist 2>NUL').should == false + @object.system("@@@#{ruby_cmd('exit 0')}").should == true + end + end +end + +describe "Kernel#system" do + it "is a private method" do + Kernel.should have_private_instance_method(:system) + end + + it_behaves_like :kernel_system, :system, KernelSpecs::Method.new +end + +describe "Kernel.system" do + it_behaves_like :kernel_system, :system, Kernel +end diff --git a/spec/ruby/core/kernel/taint_spec.rb b/spec/ruby/core/kernel/taint_spec.rb new file mode 100644 index 0000000000..9a2efbaea0 --- /dev/null +++ b/spec/ruby/core/kernel/taint_spec.rb @@ -0,0 +1,8 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel#taint" do + it "has been removed" do + Object.new.should_not.respond_to?(:taint) + end +end diff --git a/spec/ruby/core/kernel/tainted_spec.rb b/spec/ruby/core/kernel/tainted_spec.rb new file mode 100644 index 0000000000..837eb1dafb --- /dev/null +++ b/spec/ruby/core/kernel/tainted_spec.rb @@ -0,0 +1,8 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel#tainted?" do + it "has been removed" do + Object.new.should_not.respond_to?(:tainted?) + end +end diff --git a/spec/ruby/core/kernel/tap_spec.rb b/spec/ruby/core/kernel/tap_spec.rb new file mode 100644 index 0000000000..f7720a6dc7 --- /dev/null +++ b/spec/ruby/core/kernel/tap_spec.rb @@ -0,0 +1,13 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel#tap" do + it "always yields self and returns self" do + a = KernelSpecs::A.new + a.tap{|o| o.should equal(a); 42}.should equal(a) + end + + it "raises a LocalJumpError when no block given" do + -> { 3.tap }.should raise_error(LocalJumpError) + end +end diff --git a/spec/ruby/core/kernel/test_spec.rb b/spec/ruby/core/kernel/test_spec.rb new file mode 100644 index 0000000000..d26dc06361 --- /dev/null +++ b/spec/ruby/core/kernel/test_spec.rb @@ -0,0 +1,109 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel#test" do + before :all do + @file = __dir__ + '/fixtures/classes.rb' + @dir = __dir__ + '/fixtures' + end + + it "is a private method" do + Kernel.should have_private_instance_method(:test) + end + + it "returns true when passed ?f if the argument is a regular file" do + Kernel.test(?f, @file).should == true + end + + it "returns true when passed ?e if the argument is a file" do + Kernel.test(?e, @file).should == true + end + + it "returns true when passed ?d if the argument is a directory" do + Kernel.test(?d, @dir).should == true + end + + platform_is_not :windows do + it "returns true when passed ?l if the argument is a symlink" do + link = tmp("file_symlink.lnk") + File.symlink(@file, link) + begin + Kernel.test(?l, link).should be_true + ensure + rm_r link + end + end + end + + it "returns true when passed ?r if the argument is readable by the effective uid" do + Kernel.test(?r, @file).should be_true + end + + it "returns true when passed ?R if the argument is readable by the real uid" do + Kernel.test(?R, @file).should be_true + end + + context "writable test" do + before do + @tmp_file = tmp("file.kernel.test") + touch(@tmp_file) + end + + after do + rm_r @tmp_file + end + + it "returns true when passed ?w if the argument is readable by the effective uid" do + Kernel.test(?w, @tmp_file).should be_true + end + + it "returns true when passed ?W if the argument is readable by the real uid" do + Kernel.test(?W, @tmp_file).should be_true + end + end + + context "time commands" do + before :each do + @tmp_file = File.new(tmp("file.kernel.test"), "w") + end + + after :each do + @tmp_file.close + rm_r @tmp_file + end + + it "returns the last access time for the provided file when passed ?A" do + Kernel.test(?A, @tmp_file).should == @tmp_file.atime + end + + it "returns the time at which the file was created when passed ?C" do + Kernel.test(?C, @tmp_file).should == @tmp_file.ctime + end + + it "returns the time at which the file was modified when passed ?M" do + Kernel.test(?M, @tmp_file).should == @tmp_file.mtime + end + end + + it "calls #to_path on second argument when passed ?f and a filename" do + p = mock('path') + p.should_receive(:to_path).and_return @file + Kernel.test(?f, p) + end + + it "calls #to_path on second argument when passed ?e and a filename" do + p = mock('path') + p.should_receive(:to_path).and_return @file + Kernel.test(?e, p) + end + + it "calls #to_path on second argument when passed ?d and a directory" do + p = mock('path') + p.should_receive(:to_path).and_return @dir + Kernel.test(?d, p) + end +end + +describe "Kernel.test" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/kernel/then_spec.rb b/spec/ruby/core/kernel/then_spec.rb new file mode 100644 index 0000000000..8109a2960a --- /dev/null +++ b/spec/ruby/core/kernel/then_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/then' + +describe "Kernel#then" do + it_behaves_like :kernel_then, :then +end diff --git a/spec/ruby/core/kernel/throw_spec.rb b/spec/ruby/core/kernel/throw_spec.rb new file mode 100644 index 0000000000..64bfccb413 --- /dev/null +++ b/spec/ruby/core/kernel/throw_spec.rb @@ -0,0 +1,80 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel.throw" do + it "transfers control to the end of the active catch block waiting for symbol" do + catch(:blah) do + :value + throw :blah + fail("throw didn't transfer the control") + end.should be_nil + end + + it "transfers control to the innermost catch block waiting for the same symbol" do + one = two = three = 0 + catch :duplicate do + catch :duplicate do + catch :duplicate do + one = 1 + throw :duplicate + end + two = 2 + throw :duplicate + end + three = 3 + throw :duplicate + end + [one, two, three].should == [1, 2, 3] + end + + it "sets the return value of the catch block to nil by default" do + res = catch :blah do + throw :blah + end + res.should == nil + end + + it "sets the return value of the catch block to a value specified as second parameter" do + res = catch :blah do + throw :blah, :return_value + end + res.should == :return_value + end + + it "raises an ArgumentError if there is no catch block for the symbol" do + -> { throw :blah }.should raise_error(ArgumentError) + end + + it "raises an UncaughtThrowError if there is no catch block for the symbol" do + -> { throw :blah }.should raise_error(UncaughtThrowError) + end + + it "raises ArgumentError if 3 or more arguments provided" do + -> { + catch :blah do + throw :blah, :return_value, 2 + end + }.should raise_error(ArgumentError) + + -> { + catch :blah do + throw :blah, :return_value, 2, 3, 4, 5 + end + }.should raise_error(ArgumentError) + end + + it "can throw an object" do + -> { + obj = Object.new + catch obj do + throw obj + end + }.should_not raise_error(NameError) + end +end + +describe "Kernel#throw" do + it "is a private method" do + Kernel.should have_private_instance_method(:throw) + end +end diff --git a/spec/ruby/core/kernel/to_enum_spec.rb b/spec/ruby/core/kernel/to_enum_spec.rb new file mode 100644 index 0000000000..9d9945450f --- /dev/null +++ b/spec/ruby/core/kernel/to_enum_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../spec_helper' + +describe "Kernel#to_enum" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/kernel/to_s_spec.rb b/spec/ruby/core/kernel/to_s_spec.rb new file mode 100644 index 0000000000..ea4b00151e --- /dev/null +++ b/spec/ruby/core/kernel/to_s_spec.rb @@ -0,0 +1,8 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel#to_s" do + it "returns a String containing the name of self's class" do + Object.new.to_s.should =~ /Object/ + end +end diff --git a/spec/ruby/core/kernel/trace_var_spec.rb b/spec/ruby/core/kernel/trace_var_spec.rb new file mode 100644 index 0000000000..3c84aa5e60 --- /dev/null +++ b/spec/ruby/core/kernel/trace_var_spec.rb @@ -0,0 +1,54 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel#trace_var" do + before :each do + $Kernel_trace_var_global = nil + end + + after :each do + untrace_var :$Kernel_trace_var_global + + $Kernel_trace_var_global = nil + $Kernel_trace_var_extra = nil + end + + it "is a private method" do + Kernel.should have_private_instance_method(:trace_var) + end + + it "hooks assignments to a global variable" do + captured = nil + + trace_var :$Kernel_trace_var_global do |value| + captured = value + end + + $Kernel_trace_var_global = 'foo' + captured.should == 'foo' + end + + it "accepts a proc argument instead of a block" do + captured = nil + + trace_var :$Kernel_trace_var_global, proc {|value| captured = value} + + $Kernel_trace_var_global = 'foo' + captured.should == 'foo' + end + + # String arguments should be evaluated in the context of the caller. + it "accepts a String argument instead of a Proc or block" do + trace_var :$Kernel_trace_var_global, '$Kernel_trace_var_extra = true' + + $Kernel_trace_var_global = 'foo' + + $Kernel_trace_var_extra.should == true + end + + it "raises ArgumentError if no block or proc is provided" do + -> do + trace_var :$Kernel_trace_var_global + end.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/kernel/trap_spec.rb b/spec/ruby/core/kernel/trap_spec.rb new file mode 100644 index 0000000000..4c801a7215 --- /dev/null +++ b/spec/ruby/core/kernel/trap_spec.rb @@ -0,0 +1,9 @@ +require_relative '../../spec_helper' + +describe "Kernel#trap" do + it "is a private method" do + Kernel.should have_private_instance_method(:trap) + end + + # Behaviour is specified for Signal.trap +end diff --git a/spec/ruby/core/kernel/trust_spec.rb b/spec/ruby/core/kernel/trust_spec.rb new file mode 100644 index 0000000000..ef3fa9a3e1 --- /dev/null +++ b/spec/ruby/core/kernel/trust_spec.rb @@ -0,0 +1,8 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel#trust" do + it "has been removed" do + Object.new.should_not.respond_to?(:trust) + end +end diff --git a/spec/ruby/core/kernel/untaint_spec.rb b/spec/ruby/core/kernel/untaint_spec.rb new file mode 100644 index 0000000000..47e8544bd4 --- /dev/null +++ b/spec/ruby/core/kernel/untaint_spec.rb @@ -0,0 +1,8 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel#untaint" do + it "has been removed" do + Object.new.should_not.respond_to?(:untaint) + end +end diff --git a/spec/ruby/core/kernel/untrace_var_spec.rb b/spec/ruby/core/kernel/untrace_var_spec.rb new file mode 100644 index 0000000000..1925a3a836 --- /dev/null +++ b/spec/ruby/core/kernel/untrace_var_spec.rb @@ -0,0 +1,12 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel#untrace_var" do + it "is a private method" do + Kernel.should have_private_instance_method(:untrace_var) + end +end + +describe "Kernel.untrace_var" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/kernel/untrust_spec.rb b/spec/ruby/core/kernel/untrust_spec.rb new file mode 100644 index 0000000000..8787ab3fc9 --- /dev/null +++ b/spec/ruby/core/kernel/untrust_spec.rb @@ -0,0 +1,8 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel#untrust" do + it "has been removed" do + Object.new.should_not.respond_to?(:untrust) + end +end diff --git a/spec/ruby/core/kernel/untrusted_spec.rb b/spec/ruby/core/kernel/untrusted_spec.rb new file mode 100644 index 0000000000..29261be9c4 --- /dev/null +++ b/spec/ruby/core/kernel/untrusted_spec.rb @@ -0,0 +1,8 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel#untrusted?" do + it "has been removed" do + Object.new.should_not.respond_to?(:untrusted?) + end +end diff --git a/spec/ruby/core/kernel/warn_spec.rb b/spec/ruby/core/kernel/warn_spec.rb new file mode 100644 index 0000000000..e03498c6dc --- /dev/null +++ b/spec/ruby/core/kernel/warn_spec.rb @@ -0,0 +1,298 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Kernel#warn" do + before :each do + @before_verbose = $VERBOSE + @before_separator = $/ + end + + after :each do + $VERBOSE = nil + $/ = @before_separator + $VERBOSE = @before_verbose + end + + it "is a private method" do + Kernel.should have_private_instance_method(:warn) + end + + it "accepts multiple arguments" do + Kernel.method(:warn).arity.should < 0 + end + + it "does not append line-end if last character is line-end" do + -> { + $VERBOSE = true + warn("this is some simple text with line-end\n") + }.should output(nil, "this is some simple text with line-end\n") + end + + it "calls #write on $stderr if $VERBOSE is true" do + -> { + $VERBOSE = true + warn("this is some simple text") + }.should output(nil, "this is some simple text\n") + end + + it "calls #write on $stderr if $VERBOSE is false" do + -> { + $VERBOSE = false + warn("this is some simple text") + }.should output(nil, "this is some simple text\n") + end + + it "does not call #write on $stderr if $VERBOSE is nil" do + -> { + $VERBOSE = nil + warn("this is some simple text") + }.should output(nil, "") + end + + it "writes each argument on a line when passed multiple arguments" do + -> { + $VERBOSE = true + warn("line 1", "line 2") + }.should output(nil, "line 1\nline 2\n") + end + + it "writes each array element on a line when passes an array" do + -> { + $VERBOSE = true + warn(["line 1", "line 2"]) + }.should output(nil, "line 1\nline 2\n") + end + + it "does not write strings when passed no arguments" do + -> { + $VERBOSE = true + warn + }.should output("", "") + end + + it "writes the default record separator and NOT $/ to $stderr after the warning message" do + -> { + $VERBOSE = true + $/ = 'rs' + warn("") + }.should output(nil, /\n/) + end + + it "writes to_s representation if passed a non-string" do + obj = mock("obj") + obj.should_receive(:to_s).and_return("to_s called") + -> { + $VERBOSE = true + warn(obj) + }.should output(nil, "to_s called\n") + end + + describe ":uplevel keyword argument" do + before :each do + $VERBOSE = true + end + + it "prepends a message with specified line from the backtrace" do + w = KernelSpecs::WarnInNestedCall.new + + -> { w.f4("foo", 0) }.should output(nil, %r|core/kernel/fixtures/classes.rb:#{w.warn_call_lineno}: warning: foo|) + -> { w.f4("foo", 1) }.should output(nil, %r|core/kernel/fixtures/classes.rb:#{w.f1_call_lineno}: warning: foo|) + -> { w.f4("foo", 2) }.should output(nil, %r|core/kernel/fixtures/classes.rb:#{w.f2_call_lineno}: warning: foo|) + -> { w.f4("foo", 3) }.should output(nil, %r|core/kernel/fixtures/classes.rb:#{w.f3_call_lineno}: warning: foo|) + end + + # Test both explicitly without and with RubyGems as RubyGems overrides Kernel#warn + it "shows the caller of #require and not #require itself without RubyGems" do + file = fixture(__FILE__ , "warn_require_caller.rb") + ruby_exe(file, options: "--disable-gems", args: "2>&1").should == "#{file}:2: warning: warn-require-warning\n" + end + + it "shows the caller of #require and not #require itself with RubyGems loaded" do + file = fixture(__FILE__ , "warn_require_caller.rb") + ruby_exe(file, options: "-rrubygems", args: "2>&1").should == "#{file}:2: warning: warn-require-warning\n" + end + + it "doesn't show the caller when the uplevel is `nil`" do + w = KernelSpecs::WarnInNestedCall.new + + -> { w.f4("foo", nil) }.should output(nil, "foo\n") + end + + guard -> { Kernel.instance_method(:tap).source_location } do + it "skips <internal: core library methods defined in Ruby" do + file, line = Kernel.instance_method(:tap).source_location + file.should.start_with?('<internal:') + + file = fixture(__FILE__ , "warn_core_method.rb") + n = 9 + ruby_exe(file, options: "--disable-gems", args: "2>&1").lines.should == [ + "#{file}:#{n+0}: warning: use X instead\n", + "#{file}:#{n+1}: warning: use X instead\n", + "#{file}:#{n+2}: warning: use X instead\n", + "#{file}:#{n+4}: warning: use X instead\n", + ] + end + end + + it "accepts :category keyword with a symbol" do + -> { + $VERBOSE = true + warn("message", category: :deprecated) + }.should output(nil, "message\n") + end + + it "accepts :category keyword with nil" do + -> { + $VERBOSE = true + warn("message", category: nil) + }.should output(nil, "message\n") + end + + it "accepts :category keyword with object convertible to symbol" do + o = Object.new + def o.to_sym; :deprecated; end + -> { + $VERBOSE = true + warn("message", category: o) + }.should output(nil, "message\n") + end + + it "raises if :category keyword is not nil and not convertible to symbol" do + -> { + $VERBOSE = true + warn("message", category: Object.new) + }.should raise_error(TypeError) + end + + it "converts first arg using to_s" do + w = KernelSpecs::WarnInNestedCall.new + + -> { w.f4(false, 0) }.should output(nil, %r|core/kernel/fixtures/classes.rb:#{w.warn_call_lineno}: warning: false|) + -> { w.f4(nil, 1) }.should output(nil, %r|core/kernel/fixtures/classes.rb:#{w.f1_call_lineno}: warning: |) + obj = mock("obj") + obj.should_receive(:to_s).and_return("to_s called") + -> { w.f4(obj, 2) }.should output(nil, %r|core/kernel/fixtures/classes.rb:#{w.f2_call_lineno}: warning: to_s called|) + end + + it "does not prepend caller information if the uplevel argument is too large" do + w = KernelSpecs::WarnInNestedCall.new + -> { w.f4("foo", 100) }.should output(nil, "warning: foo\n") + end + + it "prepends even if a message is empty or nil" do + w = KernelSpecs::WarnInNestedCall.new + + -> { w.f4("", 0) }.should output(nil, %r|core/kernel/fixtures/classes.rb:#{w.warn_call_lineno}: warning: \n$|) + -> { w.f4(nil, 0) }.should output(nil, %r|core/kernel/fixtures/classes.rb:#{w.warn_call_lineno}: warning: \n$|) + end + + it "converts value to Integer" do + w = KernelSpecs::WarnInNestedCall.new + + -> { w.f4(0.1) }.should output(nil, %r|classes.rb:#{w.warn_call_lineno}:|) + -> { w.f4(Rational(1, 2)) }.should output(nil, %r|classes.rb:#{w.warn_call_lineno}:|) + end + + it "raises ArgumentError if passed negative value" do + -> { warn "", uplevel: -2 }.should raise_error(ArgumentError) + -> { warn "", uplevel: -100 }.should raise_error(ArgumentError) + end + + it "raises ArgumentError if passed -1" do + -> { warn "", uplevel: -1 }.should raise_error(ArgumentError) + end + + it "raises TypeError if passed not Integer" do + -> { warn "", uplevel: "" }.should raise_error(TypeError) + -> { warn "", uplevel: [] }.should raise_error(TypeError) + -> { warn "", uplevel: {} }.should raise_error(TypeError) + -> { warn "", uplevel: Object.new }.should raise_error(TypeError) + end + end + + it "treats empty hash as no keyword argument" do + h = {} + -> { warn(**h) }.should_not complain(verbose: true) + -> { warn('foo', **h) }.should complain("foo\n") + end + + it "calls Warning.warn without keyword arguments if Warning.warn does not accept keyword arguments" do + verbose = $VERBOSE + $VERBOSE = false + class << Warning + alias_method :_warn, :warn + def warn(message) + ScratchPad.record(message) + end + end + + begin + ScratchPad.clear + Kernel.warn("Chunky bacon!") + ScratchPad.recorded.should == "Chunky bacon!\n" + + Kernel.warn("Deprecated bacon!", category: :deprecated) + ScratchPad.recorded.should == "Deprecated bacon!\n" + ensure + class << Warning + remove_method :warn + alias_method :warn, :_warn + remove_method :_warn + end + $VERBOSE = verbose + end + end + + it "calls Warning.warn with category: nil if Warning.warn accepts keyword arguments" do + Warning.should_receive(:warn).with("Chunky bacon!\n", category: nil) + verbose = $VERBOSE + $VERBOSE = false + begin + Kernel.warn("Chunky bacon!") + ensure + $VERBOSE = verbose + end + end + + it "calls Warning.warn with given category keyword converted to a symbol" do + Warning.should_receive(:warn).with("Chunky bacon!\n", category: :deprecated) + verbose = $VERBOSE + $VERBOSE = false + begin + Kernel.warn("Chunky bacon!", category: 'deprecated') + ensure + $VERBOSE = verbose + end + end + + it "does not call Warning.warn if self is the Warning module" do + # RubyGems redefines Kernel#warn so we need to use a subprocess and disable RubyGems here + code = <<-RUBY + def Warning.warn(*args, **kwargs) + raise 'should not be called' + end + Kernel.instance_method(:warn).bind(Warning).call('Kernel#warn spec edge case') + RUBY + out = ruby_exe(code, args: "2>&1", options: "--disable-gems") + out.should == "Kernel#warn spec edge case\n" + $?.should.success? + end + + it "avoids recursion if Warning#warn is redefined and calls super" do + # This works because of the spec above, which is the workaround for it. + # Note that redefining Warning#warn is a mistake which would naturally end in infinite recursion, + # Warning.extend Module.new { def warn } should be used instead. + # RubyGems redefines Kernel#warn so we need to use a subprocess and disable RubyGems here + code = <<-RUBY + module Warning + def warn(*args, **kwargs) + super + end + end + warn "avoid infinite recursion" + RUBY + out = ruby_exe(code, args: "2>&1", options: "--disable-gems") + out.should == "avoid infinite recursion\n" + $?.should.success? + end +end diff --git a/spec/ruby/core/kernel/yield_self_spec.rb b/spec/ruby/core/kernel/yield_self_spec.rb new file mode 100644 index 0000000000..e311dcee47 --- /dev/null +++ b/spec/ruby/core/kernel/yield_self_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/then' + +describe "Kernel#yield_self" do + it_behaves_like :kernel_then, :yield_self +end diff --git a/spec/ruby/core/main/define_method_spec.rb b/spec/ruby/core/main/define_method_spec.rb new file mode 100644 index 0000000000..d85c5e8119 --- /dev/null +++ b/spec/ruby/core/main/define_method_spec.rb @@ -0,0 +1,28 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +script_binding = binding + +describe "main#define_method" do + before :each do + @code = 'define_method(:boom) { :bam }' + end + + after :each do + Object.send :remove_method, :boom + end + + it 'creates a public method in TOPLEVEL_BINDING' do + eval @code, TOPLEVEL_BINDING + Object.should have_method :boom + end + + it 'creates a public method in script binding' do + eval @code, script_binding + Object.should have_method :boom + end + + it 'returns the method name as symbol' do + eval(@code, TOPLEVEL_BINDING).should equal :boom + end +end diff --git a/spec/ruby/core/main/fixtures/classes.rb b/spec/ruby/core/main/fixtures/classes.rb new file mode 100644 index 0000000000..757cee4e4a --- /dev/null +++ b/spec/ruby/core/main/fixtures/classes.rb @@ -0,0 +1,26 @@ +module MainSpecs + module Module + end + + module WrapIncludeModule + end + + DATA = {} +end + + +def main_public_method +end +public :main_public_method + +def main_public_method2 +end +public :main_public_method2 + +def main_private_method +end +private :main_private_method + +def main_private_method2 +end +private :main_private_method2 diff --git a/spec/ruby/core/main/fixtures/string_refinement.rb b/spec/ruby/core/main/fixtures/string_refinement.rb new file mode 100644 index 0000000000..2dc6de52ca --- /dev/null +++ b/spec/ruby/core/main/fixtures/string_refinement.rb @@ -0,0 +1,7 @@ +module StringRefinement + refine(String) do + def foo + 'foo' + end + end +end diff --git a/spec/ruby/core/main/fixtures/string_refinement_user.rb b/spec/ruby/core/main/fixtures/string_refinement_user.rb new file mode 100644 index 0000000000..48620c325f --- /dev/null +++ b/spec/ruby/core/main/fixtures/string_refinement_user.rb @@ -0,0 +1,11 @@ +using StringRefinement + +module MainSpecs + DATA[:in_module] = 'hello'.foo + + def self.call_foo(x) + x.foo + end +end + +MainSpecs::DATA[:toplevel] = 'hello'.foo diff --git a/spec/ruby/core/main/fixtures/using.rb b/spec/ruby/core/main/fixtures/using.rb new file mode 100644 index 0000000000..30713ef309 --- /dev/null +++ b/spec/ruby/core/main/fixtures/using.rb @@ -0,0 +1 @@ +using Module.new diff --git a/spec/ruby/core/main/fixtures/using_in_main.rb b/spec/ruby/core/main/fixtures/using_in_main.rb new file mode 100644 index 0000000000..a4a71c89cc --- /dev/null +++ b/spec/ruby/core/main/fixtures/using_in_main.rb @@ -0,0 +1,5 @@ +MAIN = self + +module X + MAIN.send(:using, Module.new) +end diff --git a/spec/ruby/core/main/fixtures/using_in_method.rb b/spec/ruby/core/main/fixtures/using_in_method.rb new file mode 100644 index 0000000000..d9ea2e9ef0 --- /dev/null +++ b/spec/ruby/core/main/fixtures/using_in_method.rb @@ -0,0 +1,5 @@ +def foo + using Module.new +end + +foo diff --git a/spec/ruby/core/main/fixtures/wrapped_include.rb b/spec/ruby/core/main/fixtures/wrapped_include.rb new file mode 100644 index 0000000000..307c98b419 --- /dev/null +++ b/spec/ruby/core/main/fixtures/wrapped_include.rb @@ -0,0 +1 @@ +include MainSpecs::WrapIncludeModule diff --git a/spec/ruby/core/main/include_spec.rb b/spec/ruby/core/main/include_spec.rb new file mode 100644 index 0000000000..9f5a5f54ea --- /dev/null +++ b/spec/ruby/core/main/include_spec.rb @@ -0,0 +1,16 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "main#include" do + it "includes the given Module in Object" do + eval "include MainSpecs::Module", TOPLEVEL_BINDING + Object.ancestors.should include(MainSpecs::Module) + end + + context "in a file loaded with wrapping" do + it "includes the given Module in the load wrapper" do + load(File.expand_path("../fixtures/wrapped_include.rb", __FILE__), true) + Object.ancestors.should_not include(MainSpecs::WrapIncludeModule) + end + end +end diff --git a/spec/ruby/core/main/private_spec.rb b/spec/ruby/core/main/private_spec.rb new file mode 100644 index 0000000000..76895fd649 --- /dev/null +++ b/spec/ruby/core/main/private_spec.rb @@ -0,0 +1,42 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "main#private" do + after :each do + Object.send(:public, :main_public_method) + Object.send(:public, :main_public_method2) + end + + context "when single argument is passed and it is not an array" do + it "sets the visibility of the given methods to private" do + eval "private :main_public_method", TOPLEVEL_BINDING + Object.should have_private_method(:main_public_method) + end + end + + context "when multiple arguments are passed" do + it "sets the visibility of the given methods to private" do + eval "private :main_public_method, :main_public_method2", TOPLEVEL_BINDING + Object.should have_private_method(:main_public_method) + Object.should have_private_method(:main_public_method2) + end + end + + context "when single argument is passed and is an array" do + it "sets the visibility of the given methods to private" do + eval "private [:main_public_method, :main_public_method2]", TOPLEVEL_BINDING + Object.should have_private_method(:main_public_method) + Object.should have_private_method(:main_public_method2) + end + end + + it "returns argument" do + eval("private :main_public_method", TOPLEVEL_BINDING).should equal(:main_public_method) + end + + it "raises a NameError when at least one of given method names is undefined" do + -> do + eval "private :main_public_method, :main_undefined_method", TOPLEVEL_BINDING + end.should raise_error(NameError) + end +end diff --git a/spec/ruby/core/main/public_spec.rb b/spec/ruby/core/main/public_spec.rb new file mode 100644 index 0000000000..3c77798fbc --- /dev/null +++ b/spec/ruby/core/main/public_spec.rb @@ -0,0 +1,43 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "main#public" do + after :each do + Object.send(:private, :main_private_method) + Object.send(:private, :main_private_method2) + end + + context "when single argument is passed and it is not an array" do + it "sets the visibility of the given methods to public" do + eval "public :main_private_method", TOPLEVEL_BINDING + Object.should_not have_private_method(:main_private_method) + end + end + + context "when multiple arguments are passed" do + it "sets the visibility of the given methods to public" do + eval "public :main_private_method, :main_private_method2", TOPLEVEL_BINDING + Object.should_not have_private_method(:main_private_method) + Object.should_not have_private_method(:main_private_method2) + end + end + + context "when single argument is passed and is an array" do + it "sets the visibility of the given methods to public" do + eval "public [:main_private_method, :main_private_method2]", TOPLEVEL_BINDING + Object.should_not have_private_method(:main_private_method) + Object.should_not have_private_method(:main_private_method2) + end + end + + it "returns argument" do + eval("public :main_private_method", TOPLEVEL_BINDING).should equal(:main_private_method) + end + + + it "raises a NameError when given an undefined name" do + -> do + eval "public :main_undefined_method", TOPLEVEL_BINDING + end.should raise_error(NameError) + end +end diff --git a/spec/ruby/core/main/ruby2_keywords_spec.rb b/spec/ruby/core/main/ruby2_keywords_spec.rb new file mode 100644 index 0000000000..27ceae3253 --- /dev/null +++ b/spec/ruby/core/main/ruby2_keywords_spec.rb @@ -0,0 +1,9 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "main.ruby2_keywords" do + it "is the same as Object.ruby2_keywords" do + main = TOPLEVEL_BINDING.receiver + main.should have_private_method(:ruby2_keywords) + end +end diff --git a/spec/ruby/core/main/to_s_spec.rb b/spec/ruby/core/main/to_s_spec.rb new file mode 100644 index 0000000000..642cfa4433 --- /dev/null +++ b/spec/ruby/core/main/to_s_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' + +describe "main#to_s" do + it "returns 'main'" do + eval('to_s', TOPLEVEL_BINDING).should == "main" + end +end diff --git a/spec/ruby/core/main/using_spec.rb b/spec/ruby/core/main/using_spec.rb new file mode 100644 index 0000000000..5b9a751595 --- /dev/null +++ b/spec/ruby/core/main/using_spec.rb @@ -0,0 +1,150 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "main.using" do + it "requires one Module argument" do + -> do + eval('using', TOPLEVEL_BINDING) + end.should raise_error(ArgumentError) + + -> do + eval('using "foo"', TOPLEVEL_BINDING) + end.should raise_error(TypeError) + end + + it "uses refinements from the given module only in the target file" do + require_relative 'fixtures/string_refinement' + load File.expand_path('../fixtures/string_refinement_user.rb', __FILE__) + MainSpecs::DATA[:in_module].should == 'foo' + MainSpecs::DATA[:toplevel].should == 'foo' + -> do + 'hello'.foo + end.should raise_error(NoMethodError) + end + + it "uses refinements from the given module for method calls in the target file" do + require_relative 'fixtures/string_refinement' + load File.expand_path('../fixtures/string_refinement_user.rb', __FILE__) + -> do + 'hello'.foo + end.should raise_error(NoMethodError) + MainSpecs.call_foo('hello').should == 'foo' + end + + it "uses refinements from the given module in the eval string" do + cls = MainSpecs::DATA[:cls] = Class.new {def foo; 'foo'; end} + MainSpecs::DATA[:mod] = Module.new do + refine(cls) do + def foo; 'bar'; end + end + end + eval(<<-EOS, TOPLEVEL_BINDING).should == 'bar' + using MainSpecs::DATA[:mod] + MainSpecs::DATA[:cls].new.foo + EOS + end + + it "does not affect methods defined before it is called" do + cls = Class.new {def foo; 'foo'; end} + MainSpecs::DATA[:mod] = Module.new do + refine(cls) do + def foo; 'bar'; end + end + end + x = MainSpecs::DATA[:x] = Object.new + eval <<-EOS, TOPLEVEL_BINDING + x = MainSpecs::DATA[:x] + def x.before_using(obj) + obj.foo + end + using MainSpecs::DATA[:mod] + def x.after_using(obj) + obj.foo + end + EOS + + obj = cls.new + x.before_using(obj).should == 'foo' + x.after_using(obj).should == 'bar' + end + + it "propagates refinements added to existing modules after it is called" do + cls = Class.new {def foo; 'foo'; end} + mod = MainSpecs::DATA[:mod] = Module.new do + refine(cls) do + def foo; 'quux'; end + end + end + x = MainSpecs::DATA[:x] = Object.new + eval <<-EOS, TOPLEVEL_BINDING + using MainSpecs::DATA[:mod] + x = MainSpecs::DATA[:x] + def x.call_foo(obj) + obj.foo + end + def x.call_bar(obj) + obj.bar + end + EOS + + obj = cls.new + x.call_foo(obj).should == 'quux' + + mod.module_eval do + refine(cls) do + def bar; 'quux'; end + end + end + + x.call_bar(obj).should == 'quux' + end + + it "does not propagate refinements of new modules added after it is called" do + cls = Class.new {def foo; 'foo'; end} + cls2 = Class.new {def bar; 'bar'; end} + mod = MainSpecs::DATA[:mod] = Module.new do + refine(cls) do + def foo; 'quux'; end + end + end + x = MainSpecs::DATA[:x] = Object.new + eval <<-EOS, TOPLEVEL_BINDING + using MainSpecs::DATA[:mod] + x = MainSpecs::DATA[:x] + def x.call_foo(obj) + obj.foo + end + def x.call_bar(obj) + obj.bar + end + EOS + + x.call_foo(cls.new).should == 'quux' + + mod.module_eval do + refine(cls2) do + def bar; 'quux'; end + end + end + + x.call_bar(cls2.new).should == 'bar' + end + + it "raises error when called from method in wrapped script" do + -> do + load File.expand_path('../fixtures/using_in_method.rb', __FILE__), true + end.should raise_error(RuntimeError) + end + + it "raises error when called on toplevel from module" do + -> do + load File.expand_path('../fixtures/using_in_main.rb', __FILE__), true + end.should raise_error(RuntimeError) + end + + it "does not raise error when wrapped with module" do + -> do + load File.expand_path('../fixtures/using.rb', __FILE__), true + end.should_not raise_error + end +end diff --git a/spec/ruby/core/marshal/dump_spec.rb b/spec/ruby/core/marshal/dump_spec.rb new file mode 100644 index 0000000000..ff9b9214fa --- /dev/null +++ b/spec/ruby/core/marshal/dump_spec.rb @@ -0,0 +1,1070 @@ +# encoding: binary +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'fixtures/marshal_data' + +describe "Marshal.dump" do + it "dumps nil" do + Marshal.dump(nil).should == "\004\b0" + end + + it "dumps true" do + Marshal.dump(true).should == "\004\bT" + end + + it "dumps false" do + Marshal.dump(false).should == "\004\bF" + end + + describe "with a Fixnum" do + it "dumps a Fixnum" do + [ [Marshal, 0, "\004\bi\000"], + [Marshal, 5, "\004\bi\n"], + [Marshal, 8, "\004\bi\r"], + [Marshal, 122, "\004\bi\177"], + [Marshal, 123, "\004\bi\001{"], + [Marshal, 1234, "\004\bi\002\322\004"], + [Marshal, -8, "\004\bi\363"], + [Marshal, -123, "\004\bi\200"], + [Marshal, -124, "\004\bi\377\204"], + [Marshal, -1234, "\004\bi\376.\373"], + [Marshal, -4516727, "\004\bi\375\211\024\273"], + [Marshal, 2**8, "\004\bi\002\000\001"], + [Marshal, 2**16, "\004\bi\003\000\000\001"], + [Marshal, 2**24, "\004\bi\004\000\000\000\001"], + [Marshal, -2**8, "\004\bi\377\000"], + [Marshal, -2**16, "\004\bi\376\000\000"], + [Marshal, -2**24, "\004\bi\375\000\000\000"], + ].should be_computed_by(:dump) + end + + platform_is c_long_size: 64 do + it "dumps a positive Fixnum > 31 bits as a Bignum" do + Marshal.dump(2**31 + 1).should == "\x04\bl+\a\x01\x00\x00\x80" + end + + it "dumps a negative Fixnum > 31 bits as a Bignum" do + Marshal.dump(-2**31 - 1).should == "\x04\bl-\a\x01\x00\x00\x80" + end + end + + it "does not use object links for objects repeatedly dumped" do + Marshal.dump([0, 0]).should == "\x04\b[\ai\x00i\x00" + Marshal.dump([2**16, 2**16]).should == "\x04\b[\ai\x03\x00\x00\x01i\x03\x00\x00\x01" + end + end + + describe "with a Symbol" do + it "dumps a Symbol" do + Marshal.dump(:symbol).should == "\004\b:\vsymbol" + end + + it "dumps a big Symbol" do + Marshal.dump(('big' * 100).to_sym).should == "\004\b:\002,\001#{'big' * 100}" + end + + it "dumps an encoded Symbol" do + s = "\u2192" + [ [Marshal, s.encode("utf-8").to_sym, + "\x04\bI:\b\xE2\x86\x92\x06:\x06ET"], + [Marshal, s.encode("utf-16").to_sym, + "\x04\bI:\t\xFE\xFF!\x92\x06:\rencoding\"\vUTF-16"], + [Marshal, s.encode("utf-16le").to_sym, + "\x04\bI:\a\x92!\x06:\rencoding\"\rUTF-16LE"], + [Marshal, s.encode("utf-16be").to_sym, + "\x04\bI:\a!\x92\x06:\rencoding\"\rUTF-16BE"], + [Marshal, s.encode("euc-jp").to_sym, + "\x04\bI:\a\xA2\xAA\x06:\rencoding\"\vEUC-JP"], + [Marshal, s.encode("sjis").to_sym, + "\x04\bI:\a\x81\xA8\x06:\rencoding\"\x10Windows-31J"] + ].should be_computed_by(:dump) + end + + it "dumps a binary encoded Symbol" do + s = "\u2192".dup.force_encoding("binary").to_sym + Marshal.dump(s).should == "\x04\b:\b\xE2\x86\x92" + end + + it "dumps multiple Symbols sharing the same encoding" do + # Note that the encoding is a link for the second Symbol + symbol1 = "I:\t\xE2\x82\xACa\x06:\x06ET" + symbol2 = "I:\t\xE2\x82\xACb\x06;\x06T" + value = [ + "€a".dup.force_encoding(Encoding::UTF_8).to_sym, + "€b".dup.force_encoding(Encoding::UTF_8).to_sym + ] + Marshal.dump(value).should == "\x04\b[\a#{symbol1}#{symbol2}" + + value = [*value, value[0]] + Marshal.dump(value).should == "\x04\b[\b#{symbol1}#{symbol2};\x00" + end + + it "uses symbol links for objects repeatedly dumped" do + symbol = :foo + Marshal.dump([symbol, symbol]).should == "\x04\b[\a:\bfoo;\x00" # ;\x00 is a link to the symbol object + end + end + + describe "with an object responding to #marshal_dump" do + it "dumps the object returned by #marshal_dump" do + Marshal.dump(UserMarshal.new).should == "\x04\bU:\x10UserMarshal:\tdata" + end + + it "does not use Class#name" do + UserMarshal.should_not_receive(:name) + Marshal.dump(UserMarshal.new) + end + + it "raises TypeError if an Object is an instance of an anonymous class" do + -> { Marshal.dump(Class.new(UserMarshal).new) }.should raise_error(TypeError, /can't dump anonymous class/) + end + + it "uses object links for objects repeatedly dumped" do + obj = UserMarshal.new + Marshal.dump([obj, obj]).should == "\x04\b[\aU:\x10UserMarshal:\tdata@\x06" # @\x06 is a link to the object + end + + it "adds instance variables of a dumped object after the object itself into the objects table" do + value = "<foo>" + obj = MarshalSpec::UserMarshalDumpWithIvar.new("string", value) + + # expect a link to the object (@\x06, that means Integer 1) is smaller than a link + # to the instance variable value (@\t, that means Integer 4) + Marshal.dump([obj, obj, value]).should == "\x04\b[\bU:)MarshalSpec::UserMarshalDumpWithIvarI[\x06\"\vstring\x06:\t@foo\"\n<foo>@\x06@\t" + end + end + + describe "with an object responding to #_dump" do + it "dumps the String returned by #_dump" do + Marshal.dump(UserDefined.new).should == "\004\bu:\020UserDefined\022\004\b[\a:\nstuff;\000" + end + + it "dumps the String in non US-ASCII and non UTF-8 encoding" do + object = UserDefinedString.new("a".encode("windows-1251")) + Marshal.dump(object).should == "\x04\bIu:\x16UserDefinedString\x06a\x06:\rencoding\"\x11Windows-1251" + end + + it "dumps the String in multibyte encoding" do + object = UserDefinedString.new("a".encode("utf-32le")) + Marshal.dump(object).should == "\x04\bIu:\x16UserDefinedString\ta\x00\x00\x00\x06:\rencoding\"\rUTF-32LE" + end + + it "ignores overridden name method" do + obj = MarshalSpec::UserDefinedWithOverriddenName.new + Marshal.dump(obj).should == "\x04\bu:/MarshalSpec::UserDefinedWithOverriddenName\x12\x04\b[\a:\nstuff;\x00" + end + + it "raises a TypeError if _dump returns a non-string" do + m = mock("marshaled") + m.should_receive(:_dump).and_return(0) + -> { Marshal.dump(m) }.should raise_error(TypeError) + end + + it "raises TypeError if an Object is an instance of an anonymous class" do + -> { Marshal.dump(Class.new(UserDefined).new) }.should raise_error(TypeError, /can't dump anonymous class/) + end + + it "favors marshal_dump over _dump" do + m = mock("marshaled") + m.should_receive(:marshal_dump).and_return(0) + m.should_not_receive(:_dump) + Marshal.dump(m) + end + + it "indexes instance variables of a String returned by #_dump at first and then indexes the object itself" do + class MarshalSpec::M1::A + def _dump(level) + s = +"<dump>" + s.instance_variable_set(:@foo, "bar") + s + end + end + + a = MarshalSpec::M1::A.new + + # 0-based index of the object a = 2, that is encoded as \x07 and printed as "\a" character. + # Objects are serialized in the following order: Array, a, "bar". + # But they are indexed in different order: Array (index=0), "bar" (index=1), a (index=2) + # So the second occurenc of the object a is encoded as an index 2. + reference = "@\a" + Marshal.dump([a, a]).should == "\x04\b[\aIu:\x17MarshalSpec::M1::A\v<dump>\x06:\t@foo\"\bbar#{reference}" + end + + it "uses object links for objects repeatedly dumped" do + obj = UserDefined.new + Marshal.dump([obj, obj]).should == "\x04\b[\au:\x10UserDefined\x12\x04\b[\a:\nstuff;\x00@\x06" # @\x06 is a link to the object + end + + it "adds instance variables of a dumped String before the object itself into the objects table" do + value = "<foo>" + obj = MarshalSpec::UserDefinedDumpWithIVars.new(+"string", value) + + # expect a link to the object (@\a, that means Integer 2) is greater than a link + # to the instance variable value (@\x06, that means Integer 1) + Marshal.dump([obj, obj, value]).should == "\x04\b[\bIu:*MarshalSpec::UserDefinedDumpWithIVars\vstring\x06:\t@foo\"\n<foo>@\a@\x06" + end + + describe "Core library classes with #_dump returning a String with instance variables" do + it "indexes instance variables and then a Time object itself" do + t = Time.utc(2022) + reference = "@\a" + + Marshal.dump([t, t]).should == "\x04\b[\aIu:\tTime\r \x80\x1E\xC0\x00\x00\x00\x00\x06:\tzoneI\"\bUTC\x06:\x06EF#{reference}" + end + end + end + + describe "with a Class" do + it "dumps a builtin Class" do + Marshal.dump(String).should == "\004\bc\vString" + end + + it "dumps a user Class" do + Marshal.dump(UserDefined).should == "\x04\bc\x10UserDefined" + end + + it "dumps a nested Class" do + Marshal.dump(UserDefined::Nested).should == "\004\bc\030UserDefined::Nested" + end + + it "ignores overridden name method" do + Marshal.dump(MarshalSpec::ClassWithOverriddenName).should == "\x04\bc)MarshalSpec::ClassWithOverriddenName" + end + + ruby_version_is "4.0" do + it "dumps a class with multibyte characters in name" do + source_object = eval("MarshalSpec::Multibyteãã‚ãƒã„Class".dup.force_encoding(Encoding::UTF_8)) + Marshal.dump(source_object).should == "\x04\bIc,MarshalSpec::Multibyte\xE3\x81\x81\xE3\x81\x82\xE3\x81\x83\xE3\x81\x84Class\x06:\x06ET" + Marshal.load(Marshal.dump(source_object)) == source_object + end + end + + it "uses object links for objects repeatedly dumped" do + Marshal.dump([String, String]).should == "\x04\b[\ac\vString@\x06" # @\x06 is a link to the object + end + + it "raises TypeError with an anonymous Class" do + -> { Marshal.dump(Class.new) }.should raise_error(TypeError, /can't dump anonymous class/) + end + + it "raises TypeError with a singleton Class" do + -> { Marshal.dump(class << self; self end) }.should raise_error(TypeError) + end + end + + describe "with a Module" do + it "dumps a builtin Module" do + Marshal.dump(Marshal).should == "\004\bm\fMarshal" + end + + it "ignores overridden name method" do + Marshal.dump(MarshalSpec::ModuleWithOverriddenName).should == "\x04\bc*MarshalSpec::ModuleWithOverriddenName" + end + + ruby_version_is "4.0" do + it "dumps a module with multibyte characters in name" do + source_object = eval("MarshalSpec::Multibyteã‘ã’ã“ã”Module".dup.force_encoding(Encoding::UTF_8)) + Marshal.dump(source_object).should == "\x04\bIm-MarshalSpec::Multibyte\xE3\x81\x91\xE3\x81\x92\xE3\x81\x93\xE3\x81\x94Module\x06:\x06ET" + Marshal.load(Marshal.dump(source_object)) == source_object + end + end + + it "uses object links for objects repeatedly dumped" do + Marshal.dump([Marshal, Marshal]).should == "\x04\b[\am\fMarshal@\x06" # @\x06 is a link to the object + end + + it "raises TypeError with an anonymous Module" do + -> { Marshal.dump(Module.new) }.should raise_error(TypeError, /can't dump anonymous module/) + end + end + + describe "with a Float" do + it "dumps a Float" do + [ [Marshal, 0.0, "\004\bf\0060"], + [Marshal, -0.0, "\004\bf\a-0"], + [Marshal, 1.0, "\004\bf\0061"], + [Marshal, 123.4567, "\004\bf\r123.4567"], + [Marshal, -0.841, "\x04\bf\v-0.841"], + [Marshal, -9876.345, "\x04\bf\x0E-9876.345"], + [Marshal, infinity_value, "\004\bf\binf"], + [Marshal, -infinity_value, "\004\bf\t-inf"], + [Marshal, nan_value, "\004\bf\bnan"], + ].should be_computed_by(:dump) + end + + it "may or may not use object links for objects repeatedly dumped" do + # it's an MRI implementation detail - on x86 architecture object links + # aren't used for Float values but on amd64 - object links are used + + dump = Marshal.dump([0.0, 0.0]) + ["\x04\b[\af\x060@\x06", "\x04\b[\af\x060f\x060"].should.include?(dump) + + # if object links aren't used - entries in the objects table are still + # occupied by Float values + if dump == "\x04\b[\af\x060f\x060" + s = "string" + # an index of "string" ("@\b") in the object table equals 3 (`"\b".ord - 5`), + # so `0.0, 0,0` elements occupied indices 1 and 2 + Marshal.dump([0.0, 0.0, s, s]).should == "\x04\b[\tf\x060f\x060\"\vstring@\b" + end + end + end + + describe "with a Bignum" do + it "dumps a Bignum" do + [ [Marshal, -4611686018427387903, "\004\bl-\t\377\377\377\377\377\377\377?"], + [Marshal, -2361183241434822606847, "\004\bl-\n\377\377\377\377\377\377\377\377\177\000"], + ].should be_computed_by(:dump) + end + + it "dumps a Bignum" do + [ [Marshal, 2**64, "\004\bl+\n\000\000\000\000\000\000\000\000\001\000"], + [Marshal, 2**90, "\004\bl+\v#{"\000" * 11}\004"], + [Marshal, -2**63, "\004\bl-\t\000\000\000\000\000\000\000\200"], + [Marshal, -2**64, "\004\bl-\n\000\000\000\000\000\000\000\000\001\000"], + ].should be_computed_by(:dump) + end + + it "uses object links for objects repeatedly dumped" do + n = 2**64 + Marshal.dump([n, n]).should == "\x04\b[\al+\n\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00@\x06" # @\x06 is a link to the object + end + + it "increases the object links counter" do + obj = Object.new + object_1_link = "\x06" # representing of (0-based) index=1 (by adding 5 for small Integers) + object_2_link = "\x07" # representing of index=2 + + # objects: Array, Object, Object + Marshal.dump([obj, obj]).should == "\x04\b[\ao:\vObject\x00@#{object_1_link}" + + # objects: Array, Bignum, Object, Object + Marshal.dump([2**64, obj, obj]).should == "\x04\b[\bl+\n\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00o:\vObject\x00@#{object_2_link}" + Marshal.dump([2**48, obj, obj]).should == "\x04\b[\bl+\t\x00\x00\x00\x00\x00\x00\x01\x00o:\vObject\x00@#{object_2_link}" + Marshal.dump([2**32, obj, obj]).should == "\x04\b[\bl+\b\x00\x00\x00\x00\x01\x00o:\vObject\x00@#{object_2_link}" + end + end + + describe "with a Rational" do + it "dumps a Rational" do + Marshal.dump(Rational(2, 3)).should == "\x04\bU:\rRational[\ai\ai\b" + end + + it "uses object links for objects repeatedly dumped" do + r = Rational(2, 3) + Marshal.dump([r, r]).should == "\x04\b[\aU:\rRational[\ai\ai\b@\x06" # @\x06 is a link to the object + end + end + + describe "with a Complex" do + it "dumps a Complex" do + Marshal.dump(Complex(2, 3)).should == "\x04\bU:\fComplex[\ai\ai\b" + end + + it "uses object links for objects repeatedly dumped" do + c = Complex(2, 3) + Marshal.dump([c, c]).should == "\x04\b[\aU:\fComplex[\ai\ai\b@\x06" # @\x06 is a link to the object + end + end + + describe "with a Data" do + it "dumps a Data" do + Marshal.dump(MarshalSpec::DataSpec::Measure.new(100, 'km')).should == "\x04\bS:#MarshalSpec::DataSpec::Measure\a:\vamountii:\tunit\"\akm" + end + + it "dumps an extended Data" do + obj = MarshalSpec::DataSpec::MeasureExtended.new(100, "km") + Marshal.dump(obj).should == "\x04\bS:+MarshalSpec::DataSpec::MeasureExtended\a:\vamountii:\tunit\"\akm" + end + + it "ignores overridden name method" do + obj = MarshalSpec::DataSpec::MeasureWithOverriddenName.new(100, "km") + Marshal.dump(obj).should == "\x04\bS:5MarshalSpec::DataSpec::MeasureWithOverriddenName\a:\vamountii:\tunit\"\akm" + end + + it "uses object links for objects repeatedly dumped" do + d = MarshalSpec::DataSpec::Measure.new(100, 'km') + Marshal.dump([d, d]).should == "\x04\b[\aS:#MarshalSpec::DataSpec::Measure\a:\vamountii:\tunit\"\akm@\x06" # @\x06 is a link to the object + end + + it "raises TypeError with an anonymous Struct" do + -> { Marshal.dump(Data.define(:a).new(1)) }.should raise_error(TypeError, /can't dump anonymous class/) + end + end + + describe "with a String" do + it "dumps a blank String" do + Marshal.dump("".dup.force_encoding("binary")).should == "\004\b\"\000" + end + + it "dumps a short String" do + Marshal.dump("short".dup.force_encoding("binary")).should == "\004\b\"\012short" + end + + it "dumps a long String" do + Marshal.dump(("big" * 100).force_encoding("binary")).should == "\004\b\"\002,\001#{"big" * 100}" + end + + it "dumps a String extended with a Module" do + Marshal.dump("".dup.extend(Meths).force_encoding("binary")).should == "\004\be:\nMeths\"\000" + end + + it "dumps a String subclass" do + Marshal.dump(UserString.new.force_encoding("binary")).should == "\004\bC:\017UserString\"\000" + end + + it "dumps a String subclass extended with a Module" do + Marshal.dump(UserString.new.extend(Meths).force_encoding("binary")).should == "\004\be:\nMethsC:\017UserString\"\000" + end + + it "ignores overridden name method when dumps a String subclass" do + obj = MarshalSpec::StringWithOverriddenName.new + Marshal.dump(obj).should == "\x04\bC:*MarshalSpec::StringWithOverriddenName\"\x00" + end + + it "dumps a String with instance variables" do + str = +"" + str.instance_variable_set("@foo", "bar") + Marshal.dump(str.force_encoding("binary")).should == "\x04\bI\"\x00\x06:\t@foo\"\bbar" + end + + it "dumps a US-ASCII String" do + str = "abc".dup.force_encoding("us-ascii") + Marshal.dump(str).should == "\x04\bI\"\babc\x06:\x06EF" + end + + it "dumps a UTF-8 String" do + str = "\x6d\xc3\xb6\x68\x72\x65".dup.force_encoding("utf-8") + Marshal.dump(str).should == "\x04\bI\"\vm\xC3\xB6hre\x06:\x06ET" + end + + it "dumps a String in another encoding" do + str = "\x6d\x00\xf6\x00\x68\x00\x72\x00\x65\x00".dup.force_encoding("utf-16le") + result = "\x04\bI\"\x0Fm\x00\xF6\x00h\x00r\x00e\x00\x06:\rencoding\"\rUTF-16LE" + Marshal.dump(str).should == result + end + + it "dumps multiple strings using symlinks for the :E (encoding) symbol" do + Marshal.dump(["".encode("us-ascii"), "".encode("utf-8")]).should == "\x04\b[\aI\"\x00\x06:\x06EFI\"\x00\x06;\x00T" + end + + it "uses object links for objects repeatedly dumped" do + s = "string" + Marshal.dump([s, s]).should == "\x04\b[\a\"\vstring@\x06" # @\x06 is a link to the object + end + + it "adds instance variables after the object itself into the objects table" do + obj = +"string" + value = "<foo>" + obj.instance_variable_set :@foo, value + + # expect a link to the object (@\x06, that means Integer 1) is smaller than a link + # to the instance variable value (@\a, that means Integer 2) + Marshal.dump([obj, obj, value]).should == "\x04\b[\bI\"\vstring\x06:\t@foo\"\n<foo>@\x06@\a" + end + end + + describe "with a Regexp" do + it "dumps a Regexp" do + Marshal.dump(/\A.\Z/).should == "\x04\bI/\n\\A.\\Z\x00\x06:\x06EF" + end + + it "dumps a Regexp with flags" do + Marshal.dump(//im).should == "\x04\bI/\x00\x05\x06:\x06EF" + end + + it "dumps a Regexp with instance variables" do + o = Regexp.new("") + o.instance_variable_set(:@ivar, :ivar) + Marshal.dump(o).should == "\x04\bI/\x00\x00\a:\x06EF:\n@ivar:\tivar" + end + + it "dumps an extended Regexp" do + Marshal.dump(Regexp.new("").extend(Meths)).should == "\x04\bIe:\nMeths/\x00\x00\x06:\x06EF" + end + + it "dumps a Regexp subclass" do + Marshal.dump(UserRegexp.new("")).should == "\x04\bIC:\x0FUserRegexp/\x00\x00\x06:\x06EF" + end + + it "dumps a binary Regexp" do + o = Regexp.new("".dup.force_encoding("binary"), Regexp::FIXEDENCODING) + Marshal.dump(o).should == "\x04\b/\x00\x10" + end + + it "dumps an ascii-compatible Regexp" do + o = Regexp.new("a".encode("us-ascii"), Regexp::FIXEDENCODING) + Marshal.dump(o).should == "\x04\bI/\x06a\x10\x06:\x06EF" + + o = Regexp.new("a".encode("us-ascii")) + Marshal.dump(o).should == "\x04\bI/\x06a\x00\x06:\x06EF" + + o = Regexp.new("a".encode("windows-1251"), Regexp::FIXEDENCODING) + Marshal.dump(o).should == "\x04\bI/\x06a\x10\x06:\rencoding\"\x11Windows-1251" + + o = Regexp.new("a".encode("windows-1251")) + Marshal.dump(o).should == "\x04\bI/\x06a\x00\x06:\x06EF" + end + + it "dumps a UTF-8 Regexp" do + o = Regexp.new("".dup.force_encoding("utf-8"), Regexp::FIXEDENCODING) + Marshal.dump(o).should == "\x04\bI/\x00\x10\x06:\x06ET" + + o = Regexp.new("a".dup.force_encoding("utf-8"), Regexp::FIXEDENCODING) + Marshal.dump(o).should == "\x04\bI/\x06a\x10\x06:\x06ET" + + o = Regexp.new("\u3042".dup.force_encoding("utf-8"), Regexp::FIXEDENCODING) + Marshal.dump(o).should == "\x04\bI/\b\xE3\x81\x82\x10\x06:\x06ET" + end + + it "dumps a Regexp in another encoding" do + o = Regexp.new("".dup.force_encoding("utf-16le"), Regexp::FIXEDENCODING) + Marshal.dump(o).should == "\x04\bI/\x00\x10\x06:\rencoding\"\rUTF-16LE" + + o = Regexp.new("a".encode("utf-16le"), Regexp::FIXEDENCODING) + Marshal.dump(o).should == "\x04\bI/\aa\x00\x10\x06:\rencoding\"\rUTF-16LE" + end + + it "ignores overridden name method when dumps a Regexp subclass" do + obj = MarshalSpec::RegexpWithOverriddenName.new("") + Marshal.dump(obj).should == "\x04\bIC:*MarshalSpec::RegexpWithOverriddenName/\x00\x00\x06:\x06EF" + end + + it "uses object links for objects repeatedly dumped" do + r = /\A.\Z/ + Marshal.dump([r, r]).should == "\x04\b[\aI/\n\\A.\\Z\x00\x06:\x06EF@\x06" # @\x06 is a link to the object + end + end + + describe "with an Array" do + it "dumps an empty Array" do + Marshal.dump([]).should == "\004\b[\000" + end + + it "dumps a non-empty Array" do + Marshal.dump([:a, 1, 2]).should == "\004\b[\b:\006ai\006i\a" + end + + it "dumps an Array subclass" do + Marshal.dump(UserArray.new).should == "\004\bC:\016UserArray[\000" + end + + it "dumps a recursive Array" do + a = [] + a << a + Marshal.dump(a).should == "\x04\b[\x06@\x00" + end + + it "dumps an Array with instance variables" do + a = [] + a.instance_variable_set(:@ivar, 1) + Marshal.dump(a).should == "\004\bI[\000\006:\n@ivari\006" + end + + it "dumps an extended Array" do + Marshal.dump([].extend(Meths)).should == "\004\be:\nMeths[\000" + end + + it "ignores overridden name method when dumps an Array subclass" do + obj = MarshalSpec::ArrayWithOverriddenName.new + Marshal.dump(obj).should == "\x04\bC:)MarshalSpec::ArrayWithOverriddenName[\x00" + end + + it "uses object links for objects repeatedly dumped" do + a = [1] + Marshal.dump([a, a]).should == "\x04\b[\a[\x06i\x06@\x06" # @\x06 is a link to the object + end + + it "adds instance variables after the object itself into the objects table" do + obj = [] + value = "<foo>" + obj.instance_variable_set :@foo, value + + # expect a link to the object (@\x06, that means Integer 1) is smaller than a link + # to the instance variable value (@\a, that means Integer 2) + Marshal.dump([obj, obj, value]).should == "\x04\b[\bI[\x00\x06:\t@foo\"\n<foo>@\x06@\a" + end + end + + describe "with a Hash" do + it "dumps a Hash" do + Marshal.dump({}).should == "\004\b{\000" + end + + it "dumps a non-empty Hash" do + Marshal.dump({a: 1}).should == "\x04\b{\x06:\x06ai\x06" + end + + it "dumps a Hash subclass" do + Marshal.dump(UserHash.new).should == "\004\bC:\rUserHash{\000" + end + + it "dumps a Hash with a default value" do + Marshal.dump(Hash.new(1)).should == "\004\b}\000i\006" + end + + it "dumps a Hash with compare_by_identity" do + h = {} + h.compare_by_identity + + Marshal.dump(h).should == "\004\bC:\tHash{\x00" + end + + it "dumps a Hash subclass with compare_by_identity" do + h = UserHash.new + h.compare_by_identity + + Marshal.dump(h).should == "\x04\bC:\rUserHashC:\tHash{\x00" + end + + it "raises a TypeError with hash having default proc" do + -> { Marshal.dump(Hash.new {}) }.should raise_error(TypeError, "can't dump hash with default proc") + end + + it "dumps a Hash with instance variables" do + a = {} + a.instance_variable_set(:@ivar, 1) + Marshal.dump(a).should == "\004\bI{\000\006:\n@ivari\006" + end + + it "dumps an extended Hash" do + Marshal.dump({}.extend(Meths)).should == "\004\be:\nMeths{\000" + end + + it "dumps an Hash subclass with a parameter to initialize" do + Marshal.dump(UserHashInitParams.new(1)).should == "\004\bIC:\027UserHashInitParams{\000\006:\a@ai\006" + end + + it "ignores overridden name method when dumps a Hash subclass" do + obj = MarshalSpec::HashWithOverriddenName.new + Marshal.dump(obj).should == "\x04\bC:(MarshalSpec::HashWithOverriddenName{\x00" + end + + it "uses object links for objects repeatedly dumped" do + h = {a: 1} + Marshal.dump([h, h]).should == "\x04\b[\a{\x06:\x06ai\x06@\x06" # @\x06 is a link to the object + end + + it "adds instance variables after the object itself into the objects table" do + obj = {} + value = "<foo>" + obj.instance_variable_set :@foo, value + + # expect a link to the object (@\x06, that means Integer 1) is smaller than a link + # to the instance variable value (@\a, that means Integer 2) + Marshal.dump([obj, obj, value]).should == "\x04\b[\bI{\x00\x06:\t@foo\"\n<foo>@\x06@\a" + end + end + + describe "with a Struct" do + it "dumps a Struct" do + Marshal.dump(Struct::Pyramid.new).should == "\004\bS:\024Struct::Pyramid\000" + end + + it "dumps a Struct" do + Marshal.dump(Struct::Useful.new(1, 2)).should == "\004\bS:\023Struct::Useful\a:\006ai\006:\006bi\a" + end + + it "dumps a Struct with instance variables" do + st = Struct.new("Thick").new + st.instance_variable_set(:@ivar, 1) + Marshal.dump(st).should == "\004\bIS:\022Struct::Thick\000\006:\n@ivari\006" + Struct.send(:remove_const, :Thick) + end + + it "dumps an extended Struct" do + obj = Struct.new("Extended", :a, :b).new.extend(Meths) + Marshal.dump(obj).should == "\004\be:\nMethsS:\025Struct::Extended\a:\006a0:\006b0" + + s = 'hi' + obj.a = [:a, s] + obj.b = [:Meths, s] + Marshal.dump(obj).should == "\004\be:\nMethsS:\025Struct::Extended\a:\006a[\a;\a\"\ahi:\006b[\a;\000@\a" + Struct.send(:remove_const, :Extended) + end + + it "ignores overridden name method" do + obj = MarshalSpec::StructWithOverriddenName.new("member") + Marshal.dump(obj).should == "\x04\bS:*MarshalSpec::StructWithOverriddenName\x06:\x06a\"\vmember" + end + + it "uses object links for objects repeatedly dumped" do + s = Struct::Pyramid.new + Marshal.dump([s, s]).should == "\x04\b[\aS:\x14Struct::Pyramid\x00@\x06" # @\x06 is a link to the object + end + + it "raises TypeError with an anonymous Struct" do + -> { Marshal.dump(Struct.new(:a).new(1)) }.should raise_error(TypeError, /can't dump anonymous class/) + end + + it "adds instance variables after the object itself into the objects table" do + obj = Struct::Pyramid.new + value = "<foo>" + obj.instance_variable_set :@foo, value + + # expect a link to the object (@\x06, that means Integer 1) is smaller than a link + # to the instance variable value (@\a, that means Integer 2) + Marshal.dump([obj, obj, value]).should == "\x04\b[\bIS:\x14Struct::Pyramid\x00\x06:\t@foo\"\n<foo>@\x06@\a" + end + end + + describe "with an Object" do + it "dumps an Object" do + Marshal.dump(Object.new).should == "\004\bo:\x0BObject\x00" + end + + it "dumps an extended Object" do + Marshal.dump(Object.new.extend(Meths)).should == "\004\be:\x0AMethso:\x0BObject\x00" + end + + it "dumps an Object with an instance variable" do + obj = Object.new + obj.instance_variable_set(:@ivar, 1) + Marshal.dump(obj).should == "\004\bo:\vObject\006:\n@ivari\006" + end + + it "dumps an Object with a non-US-ASCII instance variable" do + obj = Object.new + ivar = "@é".dup.force_encoding(Encoding::UTF_8).to_sym + obj.instance_variable_set(ivar, 1) + Marshal.dump(obj).should == "\x04\bo:\vObject\x06I:\b@\xC3\xA9\x06:\x06ETi\x06" + end + + it "dumps an Object that has had an instance variable added and removed as though it was never set" do + obj = Object.new + obj.instance_variable_set(:@ivar, 1) + obj.send(:remove_instance_variable, :@ivar) + Marshal.dump(obj).should == "\004\bo:\x0BObject\x00" + end + + it "dumps an Object if it has a singleton class but no singleton methods and no singleton instance variables" do + obj = Object.new + obj.singleton_class + Marshal.dump(obj).should == "\004\bo:\x0BObject\x00" + end + + it "ignores overridden name method" do + obj = MarshalSpec::ClassWithOverriddenName.new + Marshal.dump(obj).should == "\x04\bo:)MarshalSpec::ClassWithOverriddenName\x00" + end + + it "raises TypeError if an Object has a singleton class and singleton methods" do + obj = Object.new + def obj.foo; end + -> { + Marshal.dump(obj) + }.should raise_error(TypeError, "singleton can't be dumped") + end + + it "raises TypeError if an Object has a singleton class and singleton instance variables" do + obj = Object.new + class << obj + @v = 1 + end + + -> { + Marshal.dump(obj) + }.should raise_error(TypeError, "singleton can't be dumped") + end + + it "raises TypeError if an Object is an instance of an anonymous class" do + anonymous_class = Class.new + obj = anonymous_class.new + + -> { Marshal.dump(obj) }.should raise_error(TypeError, /can't dump anonymous class/) + end + + it "raises TypeError if an Object extends an anonymous module" do + anonymous_module = Module.new + obj = Object.new + obj.extend(anonymous_module) + + -> { Marshal.dump(obj) }.should raise_error(TypeError, /can't dump anonymous class/) + end + + it "dumps a BasicObject subclass if it defines respond_to?" do + obj = MarshalSpec::BasicObjectSubWithRespondToFalse.new + Marshal.dump(obj).should == "\x04\bo:2MarshalSpec::BasicObjectSubWithRespondToFalse\x00" + end + + it "dumps without marshaling any attached finalizer" do + obj = Object.new + finalizer = Object.new + def finalizer.noop(_) + end + ObjectSpace.define_finalizer(obj, finalizer.method(:noop)) + Marshal.load(Marshal.dump(obj)).class.should == Object + end + + it "uses object links for objects repeatedly dumped" do + obj = Object.new + Marshal.dump([obj, obj]).should == "\x04\b[\ao:\vObject\x00@\x06" # @\x06 is a link to the object + end + + it "adds instance variables after the object itself into the objects table" do + obj = Object.new + value = "<foo>" + obj.instance_variable_set :@foo, value + + # expect a link to the object (@\x06, that means Integer 1) is smaller than a link + # to the instance variable value (@\a, that means Integer 2) + Marshal.dump([obj, obj, value]).should == "\x04\b[\bo:\vObject\x06:\t@foo\"\n<foo>@\x06@\a" + end + end + + describe "with a Range" do + it "dumps a Range inclusive of end" do + dump = Marshal.dump(1..2) + dump.should == "\x04\bo:\nRange\b:\texclF:\nbegini\x06:\bendi\a" + + load = Marshal.load(dump) + load.should == (1..2) + end + + it "dumps a Range exclusive of end" do + dump = Marshal.dump(1...2) + dump.should == "\x04\bo:\nRange\b:\texclT:\nbegini\x06:\bendi\a" + + load = Marshal.load(dump) + load.should == (1...2) + end + + it "uses object links for objects repeatedly dumped" do + r = 1..2 + Marshal.dump([r, r]).should == "\x04\b[\ao:\nRange\b:\texclF:\nbegini\x06:\bendi\a@\x06" # @\x06 is a link to the object + end + + it "raises TypeError with an anonymous Range subclass" do + -> { Marshal.dump(Class.new(Range).new(1, 2)) }.should raise_error(TypeError, /can't dump anonymous class/) + end + end + + describe "with a Time" do + before :each do + @internal = Encoding.default_internal + Encoding.default_internal = Encoding::UTF_8 + + @utc = Time.utc(2012, 1, 1) + @utc_dump = @utc.send(:_dump) + + with_timezone 'AST', 3 do + @t = Time.local(2012, 1, 1) + @fract = Time.local(2012, 1, 1, 1, 59, 56.2) + @t_dump = @t.send(:_dump) + @fract_dump = @fract.send(:_dump) + end + end + + after :each do + Encoding.default_internal = @internal + end + + it "dumps the zone and the offset" do + with_timezone 'AST', 3 do + dump = Marshal.dump(@t) + base = "\x04\bIu:\tTime\r#{@t_dump}\a" + offset = ":\voffseti\x020*" + zone = ":\tzoneI\"\bAST\x06:\x06EF" # Last is 'F' (US-ASCII) + [ "#{base}#{offset}#{zone}", "#{base}#{zone}#{offset}" ].should include(dump) + end + end + + it "dumps the zone, but not the offset if zone is UTC" do + dump = Marshal.dump(@utc) + zone = ":\tzoneI\"\bUTC\x06:\x06EF" # Last is 'F' (US-ASCII) + dump.should == "\x04\bIu:\tTime\r#{@utc_dump}\x06#{zone}" + end + + it "ignores overridden name method" do + obj = MarshalSpec::TimeWithOverriddenName.new + Marshal.dump(obj).should include("MarshalSpec::TimeWithOverriddenName") + end + + ruby_version_is "4.0" do + it "dumps a Time subclass with multibyte characters in name" do + source_object = eval("MarshalSpec::Multibyteãã‚ãƒã„Time".dup.force_encoding(Encoding::UTF_8)) + Marshal.dump(source_object).should == "\x04\bIc+MarshalSpec::Multibyte\xE3\x81\x81\xE3\x81\x82\xE3\x81\x83\xE3\x81\x84Time\x06:\x06ET" + Marshal.load(Marshal.dump(source_object)) == source_object + end + end + + it "uses object links for objects repeatedly dumped" do + # order of the offset and zone instance variables is a subject to change + # and may be different on different CRuby versions + base = Regexp.quote("\x04\b[\aIu:\tTime\r\xF5\xEF\e\x80\x00\x00\x00\x00\a") + offset = Regexp.quote(":\voffseti\x020*:\tzoneI\"\bAST\x06:\x06EF") + zone = Regexp.quote(":\tzoneI\"\bAST\x06:\x06EF:\voffseti\x020*") + instance_variables = /#{offset}|#{zone}/ + Marshal.dump([@t, @t]).should =~ /\A#{base}#{instance_variables}@\a\Z/ # @\a is a link to the object + end + + it "adds instance variables before the object itself into the objects table" do + obj = @utc + value = "<foo>" + obj.instance_variable_set :@foo, value + + # expect a link to the object (@\b, that means Integer 3) is greater than a link + # to the instance variable value (@\x06, that means Integer 1) + Marshal.dump([obj, obj, value]).should == "\x04\b[\bIu:\tTime\r \x00\x1C\xC0\x00\x00\x00\x00\a:\t@foo\"\n<foo>:\tzoneI\"\bUTC\x06:\x06EF@\b@\x06" + end + + it "raises TypeError with an anonymous Time subclass" do + -> { Marshal.dump(Class.new(Time).now) }.should raise_error(TypeError) + end + end + + describe "with an Exception" do + it "dumps an empty Exception" do + Marshal.dump(Exception.new).should == "\x04\bo:\x0EException\a:\tmesg0:\abt0" + end + + it "dumps the message for the exception" do + Marshal.dump(Exception.new("foo")).should == "\x04\bo:\x0EException\a:\tmesg\"\bfoo:\abt0" + end + + it "contains the filename in the backtrace" do + obj = Exception.new("foo") + obj.set_backtrace(["foo/bar.rb:10"]) + Marshal.dump(obj).should == "\x04\bo:\x0EException\a:\tmesg\"\bfoo:\abt[\x06\"\x12foo/bar.rb:10" + end + + it "dumps instance variables if they exist" do + obj = Exception.new("foo") + obj.instance_variable_set(:@ivar, 1) + Marshal.dump(obj).should == "\x04\bo:\x0EException\b:\tmesg\"\bfoo:\abt0:\n@ivari\x06" + end + + it "dumps the cause for the exception" do + exc = nil + begin + raise StandardError, "the cause" + rescue StandardError => cause + begin + raise RuntimeError, "the consequence" + rescue RuntimeError => e + e.cause.should equal(cause) + exc = e + end + end + + reloaded = Marshal.load(Marshal.dump(exc)) + reloaded.cause.should be_an_instance_of(StandardError) + reloaded.cause.message.should == "the cause" + end + + # NoMethodError uses an exception formatter on TruffleRuby and computes a message lazily + it "dumps the message for the raised NoMethodError exception" do + begin + "".foo + rescue => e + end + + Marshal.dump(e).should =~ /undefined method [`']foo' for ("":String|an instance of String)/ + end + + it "uses object links for objects repeatedly dumped" do + e = Exception.new + Marshal.dump([e, e]).should == "\x04\b[\ao:\x0EException\a:\tmesg0:\abt0@\x06" # @\x\a is a link to the object + end + + it "adds instance variables after the object itself into the objects table" do + obj = Exception.new + value = "<foo>" + obj.instance_variable_set :@foo, value + + # expect a link to the object (@\x06, that means Integer 1) is smaller than a link + # to the instance variable value (@\a, that means Integer 2) + Marshal.dump([obj, obj, value]).should == "\x04\b[\bo:\x0EException\b:\tmesg0:\abt0:\t@foo\"\n<foo>@\x06@\a" + end + + it "raises TypeError if an Object is an instance of an anonymous class" do + anonymous_class = Class.new(Exception) + obj = anonymous_class.new + + -> { Marshal.dump(obj) }.should raise_error(TypeError, /can't dump anonymous class/) + end + end + + it "dumps subsequent appearances of a symbol as a link" do + Marshal.dump([:a, :a]).should == "\004\b[\a:\006a;\000" + end + + it "dumps subsequent appearances of an object as a link" do + o = Object.new + Marshal.dump([o, o]).should == "\004\b[\ao:\vObject\000@\006" + end + + MarshalSpec::DATA_19.each do |description, (object, marshal, attributes)| + it "#{description} returns a binary string" do + Marshal.dump(object).encoding.should == Encoding::BINARY + end + end + + it "raises an ArgumentError when the recursion limit is exceeded" do + h = {'one' => {'two' => {'three' => 0}}} + -> { Marshal.dump(h, 3) }.should raise_error(ArgumentError) + -> { Marshal.dump([h], 4) }.should raise_error(ArgumentError) + -> { Marshal.dump([], 0) }.should raise_error(ArgumentError) + -> { Marshal.dump([[[]]], 1) }.should raise_error(ArgumentError) + end + + it "ignores the recursion limit if the limit is negative" do + Marshal.dump([], -1).should == "\004\b[\000" + Marshal.dump([[]], -1).should == "\004\b[\006[\000" + Marshal.dump([[[]]], -1).should == "\004\b[\006[\006[\000" + end + + describe "when passed an IO" do + it "writes the serialized data to the IO-Object" do + (obj = mock('test')).should_receive(:write).at_least(1) + Marshal.dump("test", obj) + end + + it "returns the IO-Object" do + (obj = mock('test')).should_receive(:write).at_least(1) + Marshal.dump("test", obj).should == obj + end + + it "raises an Error when the IO-Object does not respond to #write" do + obj = mock('test') + -> { Marshal.dump("test", obj) }.should raise_error(TypeError) + end + + + it "calls binmode when it's defined" do + obj = mock('test') + obj.should_receive(:write).at_least(1) + obj.should_receive(:binmode).at_least(1) + Marshal.dump("test", obj) + end + end + + describe "when passed a StringIO" do + it "should raise an error" do + require "stringio" + -> { Marshal.dump(StringIO.new) }.should raise_error(TypeError) + end + end + + it "raises a TypeError if marshalling a Method instance" do + -> { Marshal.dump(Marshal.method(:dump)) }.should raise_error(TypeError) + end + + it "raises a TypeError if marshalling a Proc" do + -> { Marshal.dump(proc {}) }.should raise_error(TypeError) + end + + it "raises a TypeError if dumping a IO/File instance" do + -> { Marshal.dump(STDIN) }.should raise_error(TypeError) + -> { File.open(__FILE__) { |f| Marshal.dump(f) } }.should raise_error(TypeError) + end + + it "raises a TypeError if dumping a MatchData instance" do + -> { Marshal.dump(/(.)/.match("foo")) }.should raise_error(TypeError) + end + + it "raises a TypeError if dumping a Mutex instance" do + m = Mutex.new + -> { Marshal.dump(m) }.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/marshal/fixtures/classes.rb b/spec/ruby/core/marshal/fixtures/classes.rb new file mode 100644 index 0000000000..7c81c64927 --- /dev/null +++ b/spec/ruby/core/marshal/fixtures/classes.rb @@ -0,0 +1,4 @@ +module MarshalSpec + # empty modules + module M1 end +end diff --git a/spec/ruby/core/marshal/fixtures/marshal_data.rb b/spec/ruby/core/marshal/fixtures/marshal_data.rb new file mode 100644 index 0000000000..c16d9e4bb6 --- /dev/null +++ b/spec/ruby/core/marshal/fixtures/marshal_data.rb @@ -0,0 +1,567 @@ +# encoding: binary + +require_relative 'marshal_multibyte_data' + +class UserDefined + class Nested + def ==(other) + other.kind_of? self.class + end + end + + attr_reader :a, :b + + def initialize + @a = 'stuff' + @b = @a + end + + def _dump(depth) + Marshal.dump [:stuff, :stuff] + end + + def self._load(data) + a, b = Marshal.load data + + obj = allocate + obj.instance_variable_set :@a, a + obj.instance_variable_set :@b, b + + obj + end + + def ==(other) + self.class === other and + @a == other.a and + @b == other.b + end +end + +class UserDefinedWithIvar + attr_reader :a, :b, :c + + def initialize + @a = +'stuff' + @a.instance_variable_set :@foo, :UserDefinedWithIvar + @b = 'more' + @c = @b + end + + def _dump(depth) + Marshal.dump [:stuff, :more, :more] + end + + def self._load(data) + a, b, c = Marshal.load data + + obj = allocate + obj.instance_variable_set :@a, a + obj.instance_variable_set :@b, b + obj.instance_variable_set :@c, c + + obj + end + + def ==(other) + self.class === other and + @a == other.a and + @b == other.b and + @c == other.c and + @a.instance_variable_get(:@foo) == other.a.instance_variable_get(:@foo) + end +end + +class UserDefinedImmediate + def _dump(depth) + '' + end + + def self._load(data) + nil + end +end + +class UserDefinedString + attr_reader :string + + def initialize(string) + @string = string + end + + def _dump(depth) + @string + end + + def self._load(data) + new(data) + end +end + +module MarshalSpec + class UserDefinedDumpWithIVars + attr_reader :string + + def initialize(string, ivar_value) + @string = string + @string.instance_variable_set(:@foo, ivar_value) + end + + def _dump(depth) + @string + end + + def self._load(data) + new(data) + end + end +end + +class UserPreviouslyDefinedWithInitializedIvar + attr_accessor :field1, :field2 +end + +class UserMarshal + attr_accessor :data + + def initialize + @data = 'stuff' + end + def marshal_dump() :data end + def marshal_load(data) @data = data end + def ==(other) self.class === other and @data == other.data end +end + +class UserMarshalWithClassName < UserMarshal + def self.name + "Never::A::Real::Class" + end +end + +class UserMarshalWithIvar + attr_reader :data + + def initialize + @data = 'my data' + end + + def marshal_dump + [:data] + end + + def marshal_load(o) + @data = o.first + end + + def ==(other) + self.class === other and + @data = other.data + end +end + +module MarshalSpec + class UserMarshalDumpWithIvar + attr_reader :data + + def initialize(data, ivar_value) + @data = data + @ivar_value = ivar_value + end + + def marshal_dump + obj = [data] + obj.instance_variable_set(:@foo, @ivar_value) + obj + end + + def marshal_load(o) + @data = o[0] + end + + def ==(other) + self.class === other and + @data = other.data + end + end +end + +class UserArray < Array +end + +class UserHash < Hash +end + +class UserHashInitParams < Hash + def initialize(a) + @a = a + end +end + +class UserObject +end + +class UserRegexp < Regexp +end + +class UserString < String +end + +class UserCustomConstructorString < String + def initialize(arg1, arg2) + end +end + +module Meths + def meths_method() end +end + +module MethsMore + def meths_more_method() end +end + +Struct.new "Pyramid" +Struct.new "Useful", :a, :b + +module MarshalSpec + class StructWithUserInitialize < Struct.new(:a) + THREADLOCAL_KEY = :marshal_load_struct_args + def initialize(*args) + # using thread-local to avoid ivar marshaling + Thread.current[THREADLOCAL_KEY] = args + super(*args) + end + end + + StructToDump = Struct.new(:a, :b) + + class BasicObjectSubWithRespondToFalse < BasicObject + def respond_to?(method_name, include_all=false) + false + end + end + + module ModuleToExtendBy + end + + def self.random_data + randomizer = Random.new(42) + 1000.times{randomizer.rand} # Make sure we exhaust his first state of 624 random words + dump_data = File.binread(fixture(__FILE__, 'random.dump')) + [randomizer, dump_data] + rescue => e + ["Error when building Random marshal data #{e}", ""] + end + + SwappedClass = nil + def self.set_swapped_class(cls) + remove_const(:SwappedClass) + const_set(:SwappedClass, cls) + end + + def self.reset_swapped_class + set_swapped_class(nil) + end + + class ClassWithOverriddenName + def self.name + "Foo" + end + end + + class ModuleWithOverriddenName + def self.name + "Foo" + end + end + + class TimeWithOverriddenName < Time + def self.name + "Foo" + end + end + + class StructWithOverriddenName < Struct.new(:a) + def self.name + "Foo" + end + end + + class UserDefinedWithOverriddenName < UserDefined + def self.name + "Foo" + end + end + + class StringWithOverriddenName < String + def self.name + "Foo" + end + end + + class ArrayWithOverriddenName < Array + def self.name + "Foo" + end + end + + class HashWithOverriddenName < Hash + def self.name + "Foo" + end + end + + class RegexpWithOverriddenName < Regexp + def self.name + "Foo" + end + end + + class ObjectWithFreezeRaisingException < Object + def freeze + raise + end + end + + class ObjectWithoutFreeze < Object + undef freeze + end + + DATA = { + "nil" => [nil, "\004\b0"], + "1..2" => [(1..2), + "\004\bo:\nRange\b:\nbegini\006:\texclF:\bendi\a", + { begin: 1, end: 2, :exclude_end? => false }], + "1...2" => [(1...2), + "\004\bo:\nRange\b:\nbegini\006:\texclT:\bendi\a", + { begin: 1, end: 2, :exclude_end? => true }], + "'a'..'b'" => [('a'..'b'), + "\004\bo:\nRange\b:\nbegin\"\006a:\texclF:\bend\"\006b", + { begin: 'a', end: 'b', :exclude_end? => false }], + "Struct" => [Struct::Useful.new(1, 2), + "\004\bS:\023Struct::Useful\a:\006ai\006:\006bi\a"], + "Symbol" => [:symbol, + "\004\b:\vsymbol"], + "true" => [true, + "\004\bT"], + "false" => [false, + "\004\bF"], + "String empty" => ['', + "\004\b\"\000"], + "String small" => ['small', + "\004\b\"\012small"], + "String big" => ['big' * 100, + "\004\b\"\002,\001#{'big' * 100}"], + "String extended" => [''.dup.extend(Meths), # TODO: check for module on load + "\004\be:\nMeths\"\000"], + "String subclass" => [UserString.new, + "\004\bC:\017UserString\"\000"], + "String subclass extended" => [UserString.new.extend(Meths), + "\004\be:\nMethsC:\017UserString\"\000"], + "Symbol small" => [:big, + "\004\b:\010big"], + "Symbol big" => [('big' * 100).to_sym, + "\004\b:\002,\001#{'big' * 100}"], + "Integer -2**64" => [-2**64, + "\004\bl-\n\000\000\000\000\000\000\000\000\001\000"], + "Integer -2**63" => [-2**63, + "\004\bl-\t\000\000\000\000\000\000\000\200"], + "Integer -2**24" => [-2**24, + "\004\bi\375\000\000\000"], + "Integer -4516727" => [-4516727, + "\004\bi\375\211\024\273"], + "Integer -2**16" => [-2**16, + "\004\bi\376\000\000"], + "Integer -2**8" => [-2**8, + "\004\bi\377\000"], + "Integer -123" => [-123, + "\004\bi\200"], + "Integer -124" => [-124, "\004\bi\377\204"], + "Integer 0" => [0, + "\004\bi\000"], + "Integer 5" => [5, + "\004\bi\n"], + "Integer 122" => [122, "\004\bi\177"], + "Integer 123" => [123, "\004\bi\001{"], + "Integer 2**8" => [2**8, + "\004\bi\002\000\001"], + "Integer 2**16" => [2**16, + "\004\bi\003\000\000\001"], + "Integer 2**24" => [2**24, + "\004\bi\004\000\000\000\001"], + "Integer 2**64" => [2**64, + "\004\bl+\n\000\000\000\000\000\000\000\000\001\000"], + "Integer 2**90" => [2**90, + "\004\bl+\v#{"\000" * 11}\004"], + "Class String" => [String, + "\004\bc\vString"], + "Module Marshal" => [Marshal, + "\004\bm\fMarshal"], + "Module nested" => [UserDefined::Nested.new, + "\004\bo:\030UserDefined::Nested\000"], + "_dump object" => [UserDefinedWithIvar.new, + "\004\bu:\030UserDefinedWithIvar5\004\b[\bI\"\nstuff\006:\t@foo:\030UserDefinedWithIvar\"\tmore@\a"], + "_dump object extended" => [UserDefined.new.extend(Meths), + "\004\bu:\020UserDefined\022\004\b[\a\"\nstuff@\006"], + "marshal_dump object" => [UserMarshalWithIvar.new, + "\004\bU:\030UserMarshalWithIvar[\006\"\fmy data"], + "Regexp" => [/\A.\Z/, + "\004\b/\n\\A.\\Z\000"], + "Regexp subclass /i" => [UserRegexp.new('', Regexp::IGNORECASE), + "\004\bC:\017UserRegexp/\000\001"], + "Float 0.0" => [0.0, + "\004\bf\0060"], + "Float -0.0" => [-0.0, + "\004\bf\a-0"], + "Float Infinity" => [(1.0 / 0.0), + "\004\bf\binf"], + "Float -Infinity" => [(-1.0 / 0.0), + "\004\bf\t-inf"], + "Float 1.0" => [1.0, + "\004\bf\0061"], + "Float 8323434.342" => [8323434.342, + "\004\bf\0328323434.3420000002\000S\370"], + "Float 1.0799999999999912" => [1.0799999999999912, + "\004\bf\0321.0799999999999912\000\341 "], + "Hash" => [Hash.new, + "\004\b{\000"], + "Hash subclass" => [UserHash.new, + "\004\bC:\rUserHash{\000"], + "Array" => [Array.new, + "\004\b[\000"], + "Array subclass" => [UserArray.new, + "\004\bC:\016UserArray[\000"], + "Struct Pyramid" => [Struct::Pyramid.new, + "\004\bS:\024Struct::Pyramid\000"], + } + DATA_19 = { + "nil" => [nil, "\004\b0"], + "1..2" => [(1..2), + "\004\bo:\nRange\b:\nbegini\006:\texclF:\bendi\a", + { begin: 1, end: 2, :exclude_end? => false }], + "1...2" => [(1...2), + "\004\bo:\nRange\b:\nbegini\006:\texclT:\bendi\a", + { begin: 1, end: 2, :exclude_end? => true }], + "'a'..'b'" => [('a'..'b'), + "\004\bo:\nRange\b:\nbegin\"\006a:\texclF:\bend\"\006b", + { begin: 'a', end: 'b', :exclude_end? => false }], + "Struct" => [Struct::Useful.new(1, 2), + "\004\bS:\023Struct::Useful\a:\006ai\006:\006bi\a"], + "Symbol" => [:symbol, + "\004\b:\vsymbol"], + "true" => [true, + "\004\bT"], + "false" => [false, + "\004\bF"], + "String empty" => ['', + "\x04\bI\"\x00\x06:\x06EF"], + "String small" => ['small', + "\x04\bI\"\nsmall\x06:\x06EF"], + "String big" => ['big' * 100, + "\x04\bI\"\x02,\x01bigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbig\x06:\x06EF"], + "String extended" => [''.dup.extend(Meths), # TODO: check for module on load + "\x04\bIe:\nMeths\"\x00\x06:\x06EF"], + "String subclass" => [UserString.new, + "\004\bC:\017UserString\"\000"], + "String subclass extended" => [UserString.new.extend(Meths), + "\004\be:\nMethsC:\017UserString\"\000"], + "Symbol small" => [:big, + "\004\b:\010big"], + "Symbol big" => [('big' * 100).to_sym, + "\004\b:\002,\001#{'big' * 100}"], + "Integer -2**64" => [-2**64, + "\004\bl-\n\000\000\000\000\000\000\000\000\001\000"], + "Integer -2**63" => [-2**63, + "\004\bl-\t\000\000\000\000\000\000\000\200"], + "Integer -2**24" => [-2**24, + "\004\bi\375\000\000\000"], + "Integer -2**16" => [-2**16, + "\004\bi\376\000\000"], + "Integer -2**8" => [-2**8, + "\004\bi\377\000"], + "Integer -123" => [-123, + "\004\bi\200"], + "Integer 0" => [0, + "\004\bi\000"], + "Integer 5" => [5, + "\004\bi\n"], + "Integer 2**8" => [2**8, + "\004\bi\002\000\001"], + "Integer 2**16" => [2**16, + "\004\bi\003\000\000\001"], + "Integer 2**24" => [2**24, + "\004\bi\004\000\000\000\001"], + "Integer 2**64" => [2**64, + "\004\bl+\n\000\000\000\000\000\000\000\000\001\000"], + "Integer 2**90" => [2**90, + "\004\bl+\v#{"\000" * 11}\004"], + "Class String" => [String, + "\004\bc\vString"], + "Module Marshal" => [Marshal, + "\004\bm\fMarshal"], + "Module nested" => [UserDefined::Nested.new, + "\004\bo:\030UserDefined::Nested\000"], + "_dump object" => [UserDefinedWithIvar.new, + "\x04\bu:\x18UserDefinedWithIvar>\x04\b[\bI\"\nstuff\a:\x06EF:\t@foo:\x18UserDefinedWithIvarI\"\tmore\x06;\x00F@\a"], + "_dump object extended" => [UserDefined.new.extend(Meths), + "\x04\bu:\x10UserDefined\x18\x04\b[\aI\"\nstuff\x06:\x06EF@\x06"], + "marshal_dump object" => [UserMarshalWithIvar.new, + "\x04\bU:\x18UserMarshalWithIvar[\x06I\"\fmy data\x06:\x06EF"], + "Regexp" => [/\A.\Z/, + "\x04\bI/\n\\A.\\Z\x00\x06:\x06EF"], + "Regexp subclass /i" => [UserRegexp.new('', Regexp::IGNORECASE), + "\x04\bIC:\x0FUserRegexp/\x00\x01\x06:\x06EF"], + "Float 0.0" => [0.0, + "\004\bf\0060"], + "Float -0.0" => [-0.0, + "\004\bf\a-0"], + "Float Infinity" => [(1.0 / 0.0), + "\004\bf\binf"], + "Float -Infinity" => [(-1.0 / 0.0), + "\004\bf\t-inf"], + "Float 1.0" => [1.0, + "\004\bf\0061"], + "Hash" => [Hash.new, + "\004\b{\000"], + "Hash subclass" => [UserHash.new, + "\004\bC:\rUserHash{\000"], + "Array" => [Array.new, + "\004\b[\000"], + "Array subclass" => [UserArray.new, + "\004\bC:\016UserArray[\000"], + "Struct Pyramid" => [Struct::Pyramid.new, + "\004\bS:\024Struct::Pyramid\000"], + "Random" => random_data, + } + + module DataSpec + Measure = Data.define(:amount, :unit) + Empty = Data.define + + MeasureExtended = Class.new(Measure) + MeasureExtended.extend(Enumerable) + + class MeasureWithOverriddenName < Measure + def self.name + "Foo" + end + end + end +end + +class ArraySub < Array + def initialize(*args) + super(args) + end +end + +class ArraySubPush < Array + def << value + raise 'broken' + end + alias_method :push, :<< +end + +class SameName +end + +module NamespaceTest +end diff --git a/spec/ruby/core/marshal/fixtures/marshal_multibyte_data.rb b/spec/ruby/core/marshal/fixtures/marshal_multibyte_data.rb new file mode 100644 index 0000000000..98a0d43392 --- /dev/null +++ b/spec/ruby/core/marshal/fixtures/marshal_multibyte_data.rb @@ -0,0 +1,12 @@ +# -*- encoding: utf-8 -*- + +module MarshalSpec + class Multibyteãã‚ãƒã„Class + end + + module Multibyteã‘ã’ã“ã”Module + end + + class Multibyteãã‚ãƒã„Time < Time + end +end diff --git a/spec/ruby/core/marshal/fixtures/random.dump b/spec/ruby/core/marshal/fixtures/random.dump Binary files differnew file mode 100644 index 0000000000..0af56120aa --- /dev/null +++ b/spec/ruby/core/marshal/fixtures/random.dump diff --git a/spec/ruby/core/marshal/float_spec.rb b/spec/ruby/core/marshal/float_spec.rb new file mode 100644 index 0000000000..5793bbd564 --- /dev/null +++ b/spec/ruby/core/marshal/float_spec.rb @@ -0,0 +1,77 @@ +require_relative '../../spec_helper' + +describe "Marshal.dump with Float" do + it "represents NaN" do + Marshal.dump(nan_value).should == "\004\bf\bnan" + end + + it "represents +Infinity" do + Marshal.dump(infinity_value).should == "\004\bf\binf" + end + + it "represents -Infinity" do + Marshal.dump(-infinity_value).should == "\004\bf\t-inf" + end + + it "represents zero" do + Marshal.dump(0.0).should == "\004\bf\0060" + end + + it "represents a Float less than 1" do + Marshal.dump(0.666666667).should == "\x04\bf\x100.666666667" + end + + it "represents a Float much less than 1" do + Marshal.dump(0.000000001234697).should == "\x04\bf\x101.234697e-9" + end + + it "represents a Float greater than 1" do + Marshal.dump(42.666666667).should == "\x04\bf\x1142.666666667" + end + + it "represents a Float much greater than 1" do + Marshal.dump(98743561239011.0).should == "\x04\bf\x1398743561239011" + end + + it "represents a Float much greater than 1 with a very small fractional part" do + Marshal.dump(799346183459.0000000002999312541).should == "\x04\bf\x11799346183459" + end +end + +describe "Marshal.load with Float" do + it "loads NaN" do + Marshal.load("\004\bf\bnan").should be_nan + end + + it "loads +Infinity" do + Marshal.load("\004\bf\binf").should == infinity_value + end + + it "loads -Infinity" do + Marshal.load("\004\bf\t-inf").should == -infinity_value + end + + it "loads zero" do + Marshal.load("\004\bf\0060").should == 0.0 + end + + it "loads a Float less than 1" do + Marshal.load("\x04\bf\x100.666666667").should == 0.666666667 + end + + it "loads a Float much less than 1" do + Marshal.load("\x04\bf\x101.234697e-9").should == 0.000000001234697 + end + + it "loads a Float greater than 1" do + Marshal.load("\x04\bf\x1142.666666667").should == 42.666666667 + end + + it "loads a Float much greater than 1" do + Marshal.load("\x04\bf\x1398743561239011").should == 98743561239011.0 + end + + it "loads a Float much greater than 1 with a very small fractional part" do + Marshal.load("\x04\bf\x16793468359.0002999").should == 793468359.0002999 + end +end diff --git a/spec/ruby/core/marshal/load_spec.rb b/spec/ruby/core/marshal/load_spec.rb new file mode 100644 index 0000000000..a5bdfbf520 --- /dev/null +++ b/spec/ruby/core/marshal/load_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/load' + +describe "Marshal.load" do + it_behaves_like :marshal_load, :load +end diff --git a/spec/ruby/core/marshal/major_version_spec.rb b/spec/ruby/core/marshal/major_version_spec.rb new file mode 100644 index 0000000000..931f1f6c27 --- /dev/null +++ b/spec/ruby/core/marshal/major_version_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' + +describe "Marshal::MAJOR_VERSION" do + it "is 4" do + Marshal::MAJOR_VERSION.should == 4 + end +end diff --git a/spec/ruby/core/marshal/minor_version_spec.rb b/spec/ruby/core/marshal/minor_version_spec.rb new file mode 100644 index 0000000000..f19210c4e1 --- /dev/null +++ b/spec/ruby/core/marshal/minor_version_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' + +describe "Marshal::MINOR_VERSION" do + it "is 8" do + Marshal::MINOR_VERSION.should == 8 + end +end diff --git a/spec/ruby/core/marshal/restore_spec.rb b/spec/ruby/core/marshal/restore_spec.rb new file mode 100644 index 0000000000..7e75d7dea6 --- /dev/null +++ b/spec/ruby/core/marshal/restore_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/load' + +describe "Marshal.restore" do + it_behaves_like :marshal_load, :restore +end diff --git a/spec/ruby/core/marshal/shared/load.rb b/spec/ruby/core/marshal/shared/load.rb new file mode 100644 index 0000000000..204a4d34e3 --- /dev/null +++ b/spec/ruby/core/marshal/shared/load.rb @@ -0,0 +1,1270 @@ +# encoding: binary +require_relative '../fixtures/marshal_data' + +describe :marshal_load, shared: true do + before :all do + @num_self_class = 1 + end + + it "raises an ArgumentError when the dumped data is truncated" do + obj = {first: 1, second: 2, third: 3} + -> { Marshal.send(@method, Marshal.dump(obj)[0, 5]) }.should raise_error(ArgumentError, "marshal data too short") + end + + it "raises an ArgumentError when the argument is empty String" do + -> { Marshal.send(@method, "") }.should raise_error(ArgumentError, "marshal data too short") + end + + it "raises an ArgumentError when the dumped class is missing" do + Object.send(:const_set, :KaBoom, Class.new) + kaboom = Marshal.dump(KaBoom.new) + Object.send(:remove_const, :KaBoom) + + -> { Marshal.send(@method, kaboom) }.should raise_error(ArgumentError) + end + + describe "when called with freeze: true" do + it "returns frozen strings" do + string = Marshal.send(@method, Marshal.dump("foo"), freeze: true) + string.should == "foo" + string.should.frozen? + + utf8_string = "foo".encode(Encoding::UTF_8) + string = Marshal.send(@method, Marshal.dump(utf8_string), freeze: true) + string.should == utf8_string + string.should.frozen? + end + + it "returns frozen arrays" do + array = Marshal.send(@method, Marshal.dump([1, 2, 3]), freeze: true) + array.should == [1, 2, 3] + array.should.frozen? + end + + it "returns frozen hashes" do + hash = Marshal.send(@method, Marshal.dump({foo: 42}), freeze: true) + hash.should == {foo: 42} + hash.should.frozen? + end + + it "returns frozen regexps" do + regexp = Marshal.send(@method, Marshal.dump(/foo/), freeze: true) + regexp.should == /foo/ + regexp.should.frozen? + end + + it "returns frozen structs" do + struct = Marshal.send(@method, Marshal.dump(MarshalSpec::StructToDump.new(1, 2)), freeze: true) + struct.should == MarshalSpec::StructToDump.new(1, 2) + struct.should.frozen? + end + + it "returns frozen objects" do + source_object = Object.new + + object = Marshal.send(@method, Marshal.dump(source_object), freeze: true) + object.should.frozen? + end + + describe "deep freezing" do + it "returns hashes with frozen keys and values" do + key = Object.new + value = Object.new + source_object = {key => value} + + hash = Marshal.send(@method, Marshal.dump(source_object), freeze: true) + hash.size.should == 1 + hash.keys[0].should.frozen? + hash.values[0].should.frozen? + end + + it "returns arrays with frozen elements" do + object = Object.new + source_object = [object] + + array = Marshal.send(@method, Marshal.dump(source_object), freeze: true) + array.size.should == 1 + array[0].should.frozen? + end + + it "returns structs with frozen members" do + object1 = Object.new + object2 = Object.new + source_object = MarshalSpec::StructToDump.new(object1, object2) + + struct = Marshal.send(@method, Marshal.dump(source_object), freeze: true) + struct.a.should.frozen? + struct.b.should.frozen? + end + + it "returns objects with frozen instance variables" do + source_object = Object.new + instance_variable = Object.new + source_object.instance_variable_set(:@a, instance_variable) + + object = Marshal.send(@method, Marshal.dump(source_object), freeze: true) + object.instance_variable_get(:@a).should != nil + object.instance_variable_get(:@a).should.frozen? + end + + it "deduplicates frozen strings" do + source_object = ["foo" + "bar", "foobar"] + object = Marshal.send(@method, Marshal.dump(source_object), freeze: true) + + object[0].should equal(object[1]) + end + end + + it "does not freeze modules" do + object = Marshal.send(@method, Marshal.dump(Kernel), freeze: true) + object.should_not.frozen? + Kernel.should_not.frozen? + end + + it "does not freeze classes" do + object = Marshal.send(@method, Marshal.dump(Object), freeze: true) + object.should_not.frozen? + Object.should_not.frozen? + end + + ruby_bug "#19427", ""..."3.3" do + it "does freeze extended objects" do + object = Marshal.load("\x04\be:\x0FEnumerableo:\vObject\x00", freeze: true) + object.should.frozen? + end + + it "does freeze extended objects with instance variables" do + object = Marshal.load("\x04\be:\x0FEnumerableo:\vObject\x06:\n@ivarT", freeze: true) + object.should.frozen? + end + end + + ruby_bug "#19427", ""..."3.3" do + it "returns frozen object having #_dump method" do + object = Marshal.send(@method, Marshal.dump(UserDefined.new), freeze: true) + object.should.frozen? + end + + it "returns frozen object responding to #marshal_dump and #marshal_load" do + object = Marshal.send(@method, Marshal.dump(UserMarshal.new), freeze: true) + object.should.frozen? + end + + it "returns frozen object extended by a module" do + object = Object.new + object.extend(MarshalSpec::ModuleToExtendBy) + + object = Marshal.send(@method, Marshal.dump(object), freeze: true) + object.should.frozen? + end + end + + it "does not call freeze method" do + object = MarshalSpec::ObjectWithFreezeRaisingException.new + object = Marshal.send(@method, Marshal.dump(object), freeze: true) + object.should.frozen? + end + + it "returns frozen object even if object does not respond to freeze method" do + object = MarshalSpec::ObjectWithoutFreeze.new + object = Marshal.send(@method, Marshal.dump(object), freeze: true) + object.should.frozen? + end + + it "returns a frozen object when is an instance of String/Array/Regexp/Hash subclass and has instance variables" do + source_object = UserString.new + source_object.instance_variable_set(:@foo, "bar") + + object = Marshal.send(@method, Marshal.dump(source_object), freeze: true) + object.should.frozen? + end + + describe "when called with a proc" do + it "call the proc with frozen objects" do + arr = [] + s = +'hi' + s.instance_variable_set(:@foo, 5) + st = Struct.new("Brittle", :a).new + st.instance_variable_set(:@clue, 'none') + st.a = 0.0 + h = Hash.new('def') + h['nine'] = 9 + a = [:a, :b, :c] + a.instance_variable_set(:@two, 2) + obj = [s, 10, s, s, st, a] + obj.instance_variable_set(:@zoo, 'ant') + proc = Proc.new { |o| arr << o; o} + + Marshal.send( + @method, + "\x04\bI[\vI\"\ahi\a:\x06EF:\t@fooi\ni\x0F@\x06@\x06IS:\x14Struct::Brittle\x06:\x06af\x060\x06:\n@clueI\"\tnone\x06;\x00FI[\b;\b:\x06b:\x06c\x06:\t@twoi\a\x06:\t@zooI\"\bant\x06;\x00F", + proc, + freeze: true, + ) + + arr.should == [ + false, 5, "hi", 10, "hi", "hi", 0.0, false, "none", st, + :b, :c, 2, a, false, "ant", ["hi", 10, "hi", "hi", st, [:a, :b, :c]], + ] + + arr.each do |v| + v.should.frozen? + end + + Struct.send(:remove_const, :Brittle) + end + + it "does not freeze the object returned by the proc" do + string = Marshal.send(@method, Marshal.dump("foo"), proc { |o| o.upcase }, freeze: true) + string.should == "FOO" + string.should_not.frozen? + end + end + end + + describe "when called with a proc" do + it "call the proc with fully initialized strings" do + utf8_string = "foo".encode(Encoding::UTF_8) + Marshal.send(@method, Marshal.dump(utf8_string), proc { |arg| + if arg.is_a?(String) + arg.should == utf8_string + arg.encoding.should == Encoding::UTF_8 + end + arg + }) + end + + it "no longer mutate the object after it was passed to the proc" do + string = Marshal.load(Marshal.dump("foo"), :freeze.to_proc) + string.should.frozen? + end + + ruby_bug "#19427", ""..."3.3" do + it "call the proc with extended objects" do + objs = [] + obj = Marshal.load("\x04\be:\x0FEnumerableo:\vObject\x00", Proc.new { |o| objs << o; o }) + objs.should == [obj] + end + end + + it "returns the value of the proc" do + Marshal.send(@method, Marshal.dump([1,2]), proc { [3,4] }).should == [3,4] + end + + it "calls the proc for recursively visited data" do + a = [1] + a << a + ret = [] + Marshal.send(@method, Marshal.dump(a), proc { |arg| ret << arg.inspect; arg }) + ret[0].should == 1.inspect + ret[1].should == a.inspect + ret.size.should == 2 + end + + it "loads an Array with proc" do + arr = [] + s = +'hi' + s.instance_variable_set(:@foo, 5) + st = Struct.new("Brittle", :a).new + st.instance_variable_set(:@clue, 'none') + st.a = 0.0 + h = Hash.new('def') + h['nine'] = 9 + a = [:a, :b, :c] + a.instance_variable_set(:@two, 2) + obj = [s, 10, s, s, st, a] + obj.instance_variable_set(:@zoo, 'ant') + proc = Proc.new { |o| arr << o.dup; o} + + Marshal.send(@method, "\x04\bI[\vI\"\ahi\a:\x06EF:\t@fooi\ni\x0F@\x06@\x06IS:\x14Struct::Brittle\x06:\x06af\x060\x06:\n@clueI\"\tnone\x06;\x00FI[\b;\b:\x06b:\x06c\x06:\t@twoi\a\x06:\t@zooI\"\bant\x06;\x00F", proc) + + arr.should == [ + false, 5, "hi", 10, "hi", "hi", 0.0, false, "none", st, + :b, :c, 2, a, false, "ant", ["hi", 10, "hi", "hi", st, [:a, :b, :c]], + ] + Struct.send(:remove_const, :Brittle) + end + end + + describe "when called with nil for the proc argument" do + it "behaves as if no proc argument was passed" do + a = [1] + a << a + b = Marshal.send(@method, Marshal.dump(a), nil) + b.should == a + end + end + + describe "when called on objects with custom _dump methods" do + it "does not set instance variables of an object with user-defined _dump/_load" do + # this string represents: <#UserPreviouslyDefinedWithInitializedIvar @field2=7 @field1=6> + dump_str = "\004\bu:-UserPreviouslyDefinedWithInitializedIvar\a:\f@field2i\f:\f@field1i\v" + + UserPreviouslyDefinedWithInitializedIvar.should_receive(:_load).and_return(UserPreviouslyDefinedWithInitializedIvar.new) + marshaled_obj = Marshal.send(@method, dump_str) + + marshaled_obj.should be_an_instance_of(UserPreviouslyDefinedWithInitializedIvar) + marshaled_obj.field1.should be_nil + marshaled_obj.field2.should be_nil + end + + it "loads the String in non US-ASCII and non UTF-8 encoding" do + source_object = UserDefinedString.new("a".encode("windows-1251")) + object = Marshal.send(@method, Marshal.dump(source_object)) + object.string.should == "a".encode("windows-1251") + end + + it "loads the String in multibyte encoding" do + source_object = UserDefinedString.new("a".encode("utf-32le")) + object = Marshal.send(@method, Marshal.dump(source_object)) + object.string.should == "a".encode("utf-32le") + end + + describe "that returns an immediate value" do + it "loads an array containing an instance of the object, followed by multiple instances of another object" do + str = "string" + + # this string represents: [<#UserDefinedImmediate A>, <#String "string">, <#String "string">] + marshaled_obj = Marshal.send(@method, "\004\b[\bu:\031UserDefinedImmediate\000\"\vstring@\a") + + marshaled_obj.should == [nil, str, str] + end + + it "loads any structure with multiple references to the same object, followed by multiple instances of another object" do + str = "string" + + # this string represents: {a: <#UserDefinedImmediate A>, b: <#UserDefinedImmediate A>, c: <#String "string">, d: <#String "string">} + hash_dump = "\x04\b{\t:\x06aIu:\x19UserDefinedImmediate\x00\x06:\x06ET:\x06b@\x06:\x06cI\"\vstring\x06;\aT:\x06d@\a" + + marshaled_obj = Marshal.send(@method, hash_dump) + marshaled_obj.should == {a: nil, b: nil, c: str, d: str} + + # this string represents: [<#UserDefinedImmediate A>, <#UserDefinedImmediate A>, <#String "string">, <#String "string">] + array_dump = "\x04\b[\tIu:\x19UserDefinedImmediate\x00\x06:\x06ET@\x06I\"\vstring\x06;\x06T@\a" + + marshaled_obj = Marshal.send(@method, array_dump) + marshaled_obj.should == [nil, nil, str, str] + end + + it "loads an array containing references to multiple instances of the object, followed by multiple instances of another object" do + str = "string" + + # this string represents: [<#UserDefinedImmediate A>, <#UserDefinedImmediate B>, <#String "string">, <#String "string">] + array_dump = "\x04\b[\tIu:\x19UserDefinedImmediate\x00\x06:\x06ETIu;\x00\x00\x06;\x06TI\"\vstring\x06;\x06T@\b" + + marshaled_obj = Marshal.send(@method, array_dump) + marshaled_obj.should == [nil, nil, str, str] + end + end + end + + it "loads an array containing objects having _dump method, and with proc" do + arr = [] + myproc = Proc.new { |o| arr << o.dup; o } + o1 = UserDefined.new; + o2 = UserDefinedWithIvar.new + obj = [o1, o2, o1, o2] + + Marshal.send(@method, "\x04\b[\tu:\x10UserDefined\x18\x04\b[\aI\"\nstuff\x06:\x06EF@\x06u:\x18UserDefinedWithIvar>\x04\b[\bI\"\nstuff\a:\x06EF:\t@foo:\x18UserDefinedWithIvarI\"\tmore\x06;\x00F@\a@\x06@\a", myproc) + + arr[0].should == o1 + arr[1].should == o2 + arr[2].should == obj + arr.size.should == 3 + end + + it "loads an array containing objects having marshal_dump method, and with proc" do + arr = [] + proc = Proc.new { |o| arr << o.dup; o } + o1 = UserMarshal.new + o2 = UserMarshalWithIvar.new + + Marshal.send(@method, "\004\b[\tU:\020UserMarshal\"\nstuffU:\030UserMarshalWithIvar[\006\"\fmy data@\006@\b", proc) + + arr[0].should == 'stuff' + arr[1].should == o1 + arr[2].should == 'my data' + arr[3].should == ['my data'] + arr[4].should == o2 + arr[5].should == [o1, o2, o1, o2] + + arr.size.should == 6 + end + + it "assigns classes to nested subclasses of Array correctly" do + arr = ArraySub.new(ArraySub.new) + arr_dump = Marshal.dump(arr) + Marshal.send(@method, arr_dump).class.should == ArraySub + end + + it "loads subclasses of Array with overridden << and push correctly" do + arr = ArraySubPush.new + arr[0] = '1' + arr_dump = Marshal.dump(arr) + Marshal.send(@method, arr_dump).should == arr + end + + it "raises a TypeError with bad Marshal version" do + marshal_data = +'\xff\xff' + marshal_data[0] = (Marshal::MAJOR_VERSION).chr + marshal_data[1] = (Marshal::MINOR_VERSION + 1).chr + + -> { Marshal.send(@method, marshal_data) }.should raise_error(TypeError) + + marshal_data = +'\xff\xff' + marshal_data[0] = (Marshal::MAJOR_VERSION - 1).chr + marshal_data[1] = (Marshal::MINOR_VERSION).chr + + -> { Marshal.send(@method, marshal_data) }.should raise_error(TypeError) + end + + it "raises EOFError on loading an empty file" do + temp_file = tmp("marshal.rubyspec.tmp.#{Process.pid}") + file = File.new(temp_file, "w+") + begin + -> { Marshal.send(@method, file) }.should raise_error(EOFError) + ensure + file.close + rm_r temp_file + end + end + + # Note: Ruby 1.9 should be compatible with older marshal format + MarshalSpec::DATA.each do |description, (object, marshal, attributes)| + it "loads a #{description}" do + Marshal.send(@method, marshal).should == object + end + end + + MarshalSpec::DATA_19.each do |description, (object, marshal, attributes)| + it "loads a #{description}" do + Marshal.send(@method, marshal).should == object + end + end + + describe "for an Array" do + it "loads an array containing the same objects" do + s = 'oh' + b = 'hi' + r = // + d = [b, :no, s, :go] + c = String + f = 1.0 + + o1 = UserMarshalWithIvar.new; o2 = UserMarshal.new + + obj = [:so, 'hello', 100, :so, :so, d, :so, o2, :so, :no, o2, + :go, c, nil, Struct::Pyramid.new, f, :go, :no, s, b, r, + :so, 'huh', o1, true, b, b, 99, r, b, s, :so, f, c, :no, o1, d] + + Marshal.send(@method, "\004\b[*:\aso\"\nhelloii;\000;\000[\t\"\ahi:\ano\"\aoh:\ago;\000U:\020UserMarshal\"\nstuff;\000;\006@\n;\ac\vString0S:\024Struct::Pyramid\000f\0061;\a;\006@\t@\b/\000\000;\000\"\bhuhU:\030UserMarshalWithIvar[\006\"\fmy dataT@\b@\bih@\017@\b@\t;\000@\016@\f;\006@\021@\a").should == + obj + end + + it "loads an array having ivar" do + s = +'well' + s.instance_variable_set(:@foo, 10) + obj = ['5', s, 'hi'].extend(Meths, MethsMore) + obj.instance_variable_set(:@mix, s) + new_obj = Marshal.send(@method, "\004\bI[\b\"\0065I\"\twell\006:\t@fooi\017\"\ahi\006:\t@mix@\a") + new_obj.should == obj + new_obj.instance_variable_get(:@mix).should equal new_obj[1] + new_obj[1].instance_variable_get(:@foo).should == 10 + end + + it "loads an extended Array object containing a user-marshaled object" do + obj = [UserMarshal.new, UserMarshal.new].extend(Meths) + dump = "\x04\be:\nMeths[\ao:\x10UserMarshal\x06:\n@dataI\"\nstuff\x06:\x06ETo;\x06\x06;\aI\"\nstuff\x06;\bT" + new_obj = Marshal.send(@method, dump) + + new_obj.should == obj + obj_ancestors = class << obj; ancestors[1..-1]; end + new_obj_ancestors = class << new_obj; ancestors[1..-1]; end + obj_ancestors.should == new_obj_ancestors + end + end + + describe "for a Hash" do + it "loads an extended_user_hash with a parameter to initialize" do + obj = UserHashInitParams.new(:abc).extend(Meths) + + new_obj = Marshal.send(@method, "\004\bIe:\nMethsC:\027UserHashInitParams{\000\006:\a@a:\babc") + + new_obj.should == obj + new_obj_metaclass_ancestors = class << new_obj; ancestors; end + new_obj_metaclass_ancestors[@num_self_class].should == Meths + new_obj_metaclass_ancestors[@num_self_class+1].should == UserHashInitParams + end + + it "loads an extended hash object containing a user-marshaled object" do + obj = {a: UserMarshal.new}.extend(Meths) + + new_obj = Marshal.send(@method, "\004\be:\nMeths{\006:\006aU:\020UserMarshal\"\nstuff") + + new_obj.should == obj + new_obj_metaclass_ancestors = class << new_obj; ancestors; end + new_obj_metaclass_ancestors[@num_self_class].should == Meths + new_obj_metaclass_ancestors[@num_self_class+1].should == Hash + end + + it "preserves hash ivars when hash contains a string having ivar" do + s = +'string' + s.instance_variable_set :@string_ivar, 'string ivar' + h = { key: s } + h.instance_variable_set :@hash_ivar, 'hash ivar' + + unmarshalled = Marshal.send(@method, Marshal.dump(h)) + unmarshalled.instance_variable_get(:@hash_ivar).should == 'hash ivar' + unmarshalled[:key].instance_variable_get(:@string_ivar).should == 'string ivar' + end + + it "preserves compare_by_identity behaviour" do + h = { a: 1 } + h.compare_by_identity + unmarshalled = Marshal.send(@method, Marshal.dump(h)) + unmarshalled.should.compare_by_identity? + + h = { a: 1 } + unmarshalled = Marshal.send(@method, Marshal.dump(h)) + unmarshalled.should_not.compare_by_identity? + end + + it "preserves compare_by_identity behaviour for a Hash subclass" do + h = UserHash.new({ a: 1 }) + h.compare_by_identity + unmarshalled = Marshal.send(@method, Marshal.dump(h)) + unmarshalled.should.compare_by_identity? + + h = UserHash.new({ a: 1 }) + unmarshalled = Marshal.send(@method, Marshal.dump(h)) + unmarshalled.should_not.compare_by_identity? + end + + it "allocates an instance of the proper class when Hash subclass with compare_by_identity behaviour" do + h = UserHash.new({ a: 1 }) + h.compare_by_identity + + unmarshalled = Marshal.send(@method, Marshal.dump(h)) + unmarshalled.should.kind_of?(UserHash) + end + end + + describe "for a Symbol" do + it "loads a Symbol" do + sym = Marshal.send(@method, "\004\b:\vsymbol") + sym.should == :symbol + sym.encoding.should == Encoding::US_ASCII + end + + it "loads a big Symbol" do + sym = ('big' * 100).to_sym + Marshal.send(@method, "\004\b:\002,\001#{'big' * 100}").should == sym + end + + it "loads an encoded Symbol" do + s = "\u2192" + + sym = Marshal.send(@method, "\x04\bI:\b\xE2\x86\x92\x06:\x06ET") + sym.should == s.encode("utf-8").to_sym + sym.encoding.should == Encoding::UTF_8 + + sym = Marshal.send(@method, "\x04\bI:\t\xFE\xFF!\x92\x06:\rencoding\"\vUTF-16") + sym.should == s.encode("utf-16").to_sym + sym.encoding.should == Encoding::UTF_16 + + sym = Marshal.send(@method, "\x04\bI:\a\x92!\x06:\rencoding\"\rUTF-16LE") + sym.should == s.encode("utf-16le").to_sym + sym.encoding.should == Encoding::UTF_16LE + + sym = Marshal.send(@method, "\x04\bI:\a!\x92\x06:\rencoding\"\rUTF-16BE") + sym.should == s.encode("utf-16be").to_sym + sym.encoding.should == Encoding::UTF_16BE + + sym = Marshal.send(@method, "\x04\bI:\a\xA2\xAA\x06:\rencoding\"\vEUC-JP") + sym.should == s.encode("euc-jp").to_sym + sym.encoding.should == Encoding::EUC_JP + + sym = Marshal.send(@method, "\x04\bI:\a\x81\xA8\x06:\rencoding\"\x10Windows-31J") + sym.should == s.encode("sjis").to_sym + sym.encoding.should == Encoding::SJIS + end + + it "loads a binary encoded Symbol" do + s = "\u2192".dup.force_encoding("binary").to_sym + sym = Marshal.send(@method, "\x04\b:\b\xE2\x86\x92") + sym.should == s + sym.encoding.should == Encoding::BINARY + end + + it "loads multiple Symbols sharing the same encoding" do + # Note that the encoding is a link for the second Symbol + symbol1 = "I:\t\xE2\x82\xACa\x06:\x06ET" + symbol2 = "I:\t\xE2\x82\xACb\x06;\x06T" + dump = "\x04\b[\a#{symbol1}#{symbol2}" + value = Marshal.send(@method, dump) + value.map(&:encoding).should == [Encoding::UTF_8, Encoding::UTF_8] + expected = [ + "€a".dup.force_encoding(Encoding::UTF_8).to_sym, + "€b".dup.force_encoding(Encoding::UTF_8).to_sym + ] + value.should == expected + + value = Marshal.send(@method, "\x04\b[\b#{symbol1}#{symbol2};\x00") + value.map(&:encoding).should == [Encoding::UTF_8, Encoding::UTF_8, Encoding::UTF_8] + value.should == [*expected, expected[0]] + end + + it "raises ArgumentError when end of byte sequence reached before symbol characters end" do + Marshal.dump(:hello).should == "\x04\b:\nhello" + + -> { + Marshal.send(@method, "\x04\b:\nhel") + }.should raise_error(ArgumentError, "marshal data too short") + end + end + + describe "for a String" do + it "loads a string having ivar with ref to self" do + obj = +'hi' + obj.instance_variable_set(:@self, obj) + Marshal.send(@method, "\004\bI\"\ahi\006:\n@self@\000").should == obj + end + + it "loads a string through StringIO stream" do + require 'stringio' + obj = "This is a string which should be unmarshalled through StringIO stream!" + Marshal.send(@method, StringIO.new(Marshal.dump(obj))).should == obj + end + + it "sets binmode if it is loading through StringIO stream" do + io = StringIO.new("\004\b:\vsymbol") + def io.binmode; raise "binmode"; end + -> { Marshal.load(io) }.should raise_error(RuntimeError, "binmode") + end + + it "loads a string with an ivar" do + str = Marshal.send(@method, "\x04\bI\"\x00\x06:\t@fooI\"\bbar\x06:\x06EF") + str.instance_variable_get("@foo").should == "bar" + end + + it "loads a String subclass with custom constructor" do + str = Marshal.send(@method, "\x04\bC: UserCustomConstructorString\"\x00") + str.should be_an_instance_of(UserCustomConstructorString) + end + + it "loads a US-ASCII String" do + str = "abc".dup.force_encoding("us-ascii") + data = "\x04\bI\"\babc\x06:\x06EF" + result = Marshal.send(@method, data) + result.should == str + result.encoding.should equal(Encoding::US_ASCII) + end + + it "loads a UTF-8 String" do + str = "\x6d\xc3\xb6\x68\x72\x65".dup.force_encoding("utf-8") + data = "\x04\bI\"\vm\xC3\xB6hre\x06:\x06ET" + result = Marshal.send(@method, data) + result.should == str + result.encoding.should equal(Encoding::UTF_8) + end + + it "loads a String in another encoding" do + str = "\x6d\x00\xf6\x00\x68\x00\x72\x00\x65\x00".dup.force_encoding("utf-16le") + data = "\x04\bI\"\x0Fm\x00\xF6\x00h\x00r\x00e\x00\x06:\rencoding\"\rUTF-16LE" + result = Marshal.send(@method, data) + result.should == str + result.encoding.should equal(Encoding::UTF_16LE) + end + + it "loads a String as BINARY if no encoding is specified at the end" do + str = "\xC3\xB8".dup.force_encoding("BINARY") + data = "\x04\b\"\a\xC3\xB8".dup.force_encoding("UTF-8") + result = Marshal.send(@method, data) + result.encoding.should == Encoding::BINARY + result.should == str + end + + it "raises ArgumentError when end of byte sequence reached before string characters end" do + Marshal.dump("hello").should == "\x04\b\"\nhello" + + -> { + Marshal.send(@method, "\x04\b\"\nhel") + }.should raise_error(ArgumentError, "marshal data too short") + end + end + + describe "for a Struct" do + it "loads a extended_struct having fields with same objects" do + s = 'hi' + obj = Struct.new("Extended", :a, :b).new.extend(Meths) + dump = "\004\be:\nMethsS:\025Struct::Extended\a:\006a0:\006b0" + Marshal.send(@method, dump).should == obj + + obj.a = [:a, s] + obj.b = [:Meths, s] + dump = "\004\be:\nMethsS:\025Struct::Extended\a:\006a[\a;\a\"\ahi:\006b[\a;\000@\a" + Marshal.send(@method, dump).should == obj + Struct.send(:remove_const, :Extended) + end + + it "loads a struct having ivar" do + obj = Struct.new("Thick").new + obj.instance_variable_set(:@foo, 5) + reloaded = Marshal.send(@method, "\004\bIS:\022Struct::Thick\000\006:\t@fooi\n") + reloaded.should == obj + reloaded.instance_variable_get(:@foo).should == 5 + Struct.send(:remove_const, :Thick) + end + + it "loads a struct having fields" do + obj = Struct.new("Ure1", :a, :b).new + Marshal.send(@method, "\004\bS:\021Struct::Ure1\a:\006a0:\006b0").should == obj + Struct.send(:remove_const, :Ure1) + end + + it "does not call initialize on the unmarshaled struct" do + threadlocal_key = MarshalSpec::StructWithUserInitialize::THREADLOCAL_KEY + + s = MarshalSpec::StructWithUserInitialize.new('foo') + Thread.current[threadlocal_key].should == ['foo'] + s.a.should == 'foo' + + Thread.current[threadlocal_key] = nil + + dumped = Marshal.dump(s) + loaded = Marshal.send(@method, dumped) + + Thread.current[threadlocal_key].should == nil + loaded.a.should == 'foo' + end + end + + describe "for a Data" do + it "loads a Data" do + obj = MarshalSpec::DataSpec::Measure.new(100, 'km') + dumped = "\x04\bS:#MarshalSpec::DataSpec::Measure\a:\vamountii:\tunit\"\akm" + Marshal.dump(obj).should == dumped + + Marshal.send(@method, dumped).should == obj + end + + it "loads an extended Data" do + obj = MarshalSpec::DataSpec::MeasureExtended.new(100, "km") + dumped = "\x04\bS:+MarshalSpec::DataSpec::MeasureExtended\a:\vamountii:\tunit\"\akm" + Marshal.dump(obj).should == dumped + + Marshal.send(@method, dumped).should == obj + end + + it "returns a frozen object" do + obj = MarshalSpec::DataSpec::Measure.new(100, 'km') + dumped = "\x04\bS:#MarshalSpec::DataSpec::Measure\a:\vamountii:\tunit\"\akm" + Marshal.dump(obj).should == dumped + + Marshal.send(@method, dumped).should.frozen? + end + end + + describe "for an Exception" do + it "loads a marshalled exception with no message" do + obj = Exception.new + loaded = Marshal.send(@method, "\004\bo:\016Exception\a:\abt0:\tmesg0") + loaded.message.should == obj.message + loaded.backtrace.should == obj.backtrace + loaded = Marshal.send(@method, "\x04\bo:\x0EException\a:\tmesg0:\abt0") + loaded.message.should == obj.message + loaded.backtrace.should == obj.backtrace + end + + it "loads a marshalled exception with a message" do + obj = Exception.new("foo") + loaded = Marshal.send(@method, "\004\bo:\016Exception\a:\abt0:\tmesg\"\bfoo") + loaded.message.should == obj.message + loaded.backtrace.should == obj.backtrace + loaded = Marshal.send(@method, "\x04\bo:\x0EException\a:\tmesgI\"\bfoo\x06:\x06EF:\abt0") + loaded.message.should == obj.message + loaded.backtrace.should == obj.backtrace + end + + it "loads a marshalled exception with a backtrace" do + obj = Exception.new("foo") + obj.set_backtrace(["foo/bar.rb:10"]) + loaded = Marshal.send(@method, "\004\bo:\016Exception\a:\abt[\006\"\022foo/bar.rb:10:\tmesg\"\bfoo") + loaded.message.should == obj.message + loaded.backtrace.should == obj.backtrace + loaded = Marshal.send(@method, "\x04\bo:\x0EException\a:\tmesgI\"\bfoo\x06:\x06EF:\abt[\x06I\"\x12foo/bar.rb:10\x06;\aF") + loaded.message.should == obj.message + loaded.backtrace.should == obj.backtrace + end + + it "loads an marshalled exception with ivars" do + s = 'hi' + arr = [:so, :so, s, s] + obj = Exception.new("foo") + obj.instance_variable_set :@arr, arr + + loaded = Marshal.send(@method, "\x04\bo:\x0EException\b:\tmesg\"\bfoo:\abt0:\t@arr[\t:\aso;\t\"\ahi@\b") + new_arr = loaded.instance_variable_get :@arr + + loaded.message.should == obj.message + new_arr.should == arr + end + end + + describe "for an Object" do + it "loads an object" do + Marshal.send(@method, "\004\bo:\vObject\000").should be_kind_of(Object) + end + + it "loads an extended Object" do + obj = Object.new.extend(Meths) + + new_obj = Marshal.send(@method, "\004\be:\nMethso:\vObject\000") + + new_obj.class.should == obj.class + new_obj_metaclass_ancestors = class << new_obj; ancestors; end + new_obj_metaclass_ancestors[@num_self_class, 2].should == [Meths, Object] + end + + it "loads an object having ivar" do + s = 'hi' + arr = [:so, :so, s, s] + obj = Object.new + obj.instance_variable_set :@str, arr + + new_obj = Marshal.send(@method, "\004\bo:\vObject\006:\t@str[\t:\aso;\a\"\ahi@\a") + new_str = new_obj.instance_variable_get :@str + + new_str.should == arr + end + + it "loads an Object with a non-US-ASCII instance variable" do + ivar = "@é".dup.force_encoding(Encoding::UTF_8).to_sym + obj = Marshal.send(@method, "\x04\bo:\vObject\x06I:\b@\xC3\xA9\x06:\x06ETi\x06") + obj.instance_variables.should == [ivar] + obj.instance_variables[0].encoding.should == Encoding::UTF_8 + obj.instance_variable_get(ivar).should == 1 + end + + it "raises ArgumentError if the object from an 'o' stream is not dumpable as 'o' type user class" do + -> do + Marshal.send(@method, "\x04\bo:\tFile\001\001:\001\005@path\"\x10/etc/passwd") + end.should raise_error(ArgumentError) + end + + it "raises ArgumentError when end of byte sequence reached before class name end" do + Marshal.dump(Object.new).should == "\x04\bo:\vObject\x00" + + -> { + Marshal.send(@method, "\x04\bo:\vObj") + }.should raise_error(ArgumentError, "marshal data too short") + end + end + + describe "for an object responding to #marshal_dump and #marshal_load" do + it "loads a user-marshaled object" do + obj = UserMarshal.new + obj.data = :data + value = [obj, :data] + dump = Marshal.dump(value) + dump.should == "\x04\b[\aU:\x10UserMarshal:\tdata;\x06" + reloaded = Marshal.load(dump) + reloaded.should == value + end + end + + describe "for a user object" do + it "loads a user-marshaled extended object" do + obj = UserMarshal.new.extend(Meths) + + new_obj = Marshal.send(@method, "\004\bU:\020UserMarshal\"\nstuff") + + new_obj.should == obj + new_obj_metaclass_ancestors = class << new_obj; ancestors; end + new_obj_metaclass_ancestors[@num_self_class].should == UserMarshal + end + + it "loads a UserObject" do + Marshal.send(@method, "\004\bo:\017UserObject\000").should be_kind_of(UserObject) + end + + describe "that extends a core type other than Object or BasicObject" do + after :each do + MarshalSpec.reset_swapped_class + end + + it "raises ArgumentError if the resulting class does not extend the same type" do + MarshalSpec.set_swapped_class(Class.new(Hash)) + data = Marshal.dump(MarshalSpec::SwappedClass.new) + + MarshalSpec.set_swapped_class(Class.new(Array)) + -> { Marshal.send(@method, data) }.should raise_error(ArgumentError) + + MarshalSpec.set_swapped_class(Class.new) + -> { Marshal.send(@method, data) }.should raise_error(ArgumentError) + end + end + end + + describe "for a Regexp" do + it "loads an extended Regexp" do + obj = /[a-z]/.dup.extend(Meths, MethsMore) + new_obj = Marshal.send(@method, "\004\be:\nMethse:\016MethsMore/\n[a-z]\000") + + new_obj.should == obj + new_obj_metaclass_ancestors = class << new_obj; ancestors; end + new_obj_metaclass_ancestors[@num_self_class, 3].should == + [Meths, MethsMore, Regexp] + end + + it "loads a Regexp subclass instance variables when it is extended with a module" do + obj = UserRegexp.new('').extend(Meths) + obj.instance_variable_set(:@noise, 'much') + + new_obj = Marshal.send(@method, "\004\bIe:\nMethsC:\017UserRegexp/\000\000\006:\v@noise\"\tmuch") + + new_obj.should == obj + new_obj.instance_variable_get(:@noise).should == 'much' + new_obj_metaclass_ancestors = class << new_obj; ancestors; end + new_obj_metaclass_ancestors[@num_self_class, 3].should == + [Meths, UserRegexp, Regexp] + end + + ruby_bug "#19439", ""..."3.3" do + it "restore the regexp instance variables" do + obj = Regexp.new("hello") + obj.instance_variable_set(:@regexp_ivar, [42]) + + new_obj = Marshal.send(@method, "\x04\bI/\nhello\x00\a:\x06EF:\x11@regexp_ivar[\x06i/") + new_obj.instance_variables.should == [:@regexp_ivar] + new_obj.instance_variable_get(:@regexp_ivar).should == [42] + end + end + + it "preserves Regexp encoding" do + source_object = Regexp.new("a".encode("utf-32le")) + regexp = Marshal.send(@method, Marshal.dump(source_object)) + + regexp.encoding.should == Encoding::UTF_32LE + regexp.source.should == "a".encode("utf-32le") + end + + it "raises ArgumentError when end of byte sequence reached before source string end" do + Marshal.dump(/hello world/).should == "\x04\bI/\x10hello world\x00\x06:\x06EF" + + -> { + Marshal.send(@method, "\x04\bI/\x10hel") + }.should raise_error(ArgumentError, "marshal data too short") + end + end + + describe "for a Float" do + it "loads a Float NaN" do + obj = 0.0 / 0.0 + Marshal.send(@method, "\004\bf\bnan").to_s.should == obj.to_s + end + + it "loads a Float 1.3" do + Marshal.send(@method, "\004\bf\v1.3\000\314\315").should == 1.3 + end + + it "loads a Float -5.1867345e-22" do + obj = -5.1867345e-22 + Marshal.send(@method, "\004\bf\037-5.1867345000000008e-22\000\203_").should be_close(obj, 1e-30) + end + + it "loads a Float 1.1867345e+22" do + obj = 1.1867345e+22 + Marshal.send(@method, "\004\bf\0361.1867344999999999e+22\000\344@").should == obj + end + + it "raises ArgumentError when end of byte sequence reached before float string representation end" do + Marshal.dump(1.3).should == "\x04\bf\b1.3" + + -> { + Marshal.send(@method, "\004\bf\v1") + }.should raise_error(ArgumentError, "marshal data too short") + end + end + + describe "for an Integer" do + it "loads 0" do + Marshal.send(@method, "\004\bi\000").should == 0 + Marshal.send(@method, "\004\bi\005").should == 0 + end + + it "loads an Integer 8" do + Marshal.send(@method, "\004\bi\r" ).should == 8 + end + + it "loads and Integer -8" do + Marshal.send(@method, "\004\bi\363" ).should == -8 + end + + it "loads an Integer 1234" do + Marshal.send(@method, "\004\bi\002\322\004").should == 1234 + end + + it "loads an Integer -1234" do + Marshal.send(@method, "\004\bi\376.\373").should == -1234 + end + + it "loads an Integer 4611686018427387903" do + Marshal.send(@method, "\004\bl+\t\377\377\377\377\377\377\377?").should == 4611686018427387903 + end + + it "loads an Integer -4611686018427387903" do + Marshal.send(@method, "\004\bl-\t\377\377\377\377\377\377\377?").should == -4611686018427387903 + end + + it "loads an Integer 2361183241434822606847" do + Marshal.send(@method, "\004\bl+\n\377\377\377\377\377\377\377\377\177\000").should == 2361183241434822606847 + end + + it "loads an Integer -2361183241434822606847" do + Marshal.send(@method, "\004\bl-\n\377\377\377\377\377\377\377\377\177\000").should == -2361183241434822606847 + end + + it "raises ArgumentError if the input is too short" do + ["\004\bi", + "\004\bi\001", + "\004\bi\002", + "\004\bi\002\0", + "\004\bi\003", + "\004\bi\003\0", + "\004\bi\003\0\0", + "\004\bi\004", + "\004\bi\004\0", + "\004\bi\004\0\0", + "\004\bi\004\0\0\0"].each do |invalid| + -> { Marshal.send(@method, invalid) }.should raise_error(ArgumentError) + end + end + + if 0.size == 8 # for platforms like x86_64 + it "roundtrips 4611686018427387903 from dump/load correctly" do + Marshal.send(@method, Marshal.dump(4611686018427387903)).should == 4611686018427387903 + end + end + end + + describe "for a Rational" do + it "loads" do + r = Marshal.send(@method, Marshal.dump(Rational(1, 3))) + r.should == Rational(1, 3) + r.should.frozen? + end + end + + describe "for a Complex" do + it "loads" do + c = Marshal.send(@method, Marshal.dump(Complex(4, 3))) + c.should == Complex(4, 3) + c.should.frozen? + end + end + + describe "for a Bignum" do + platform_is c_long_size: 64 do + context "that is Bignum on 32-bit platforms but Fixnum on 64-bit" do + it "dumps a Fixnum" do + val = Marshal.send(@method, "\004\bl+\ab:wU") + val.should == 1433877090 + val.class.should == Integer + end + + it "dumps an array containing multiple references to the Bignum as an array of Fixnum" do + arr = Marshal.send(@method, "\004\b[\al+\a\223BwU@\006") + arr.should == [1433879187, 1433879187] + arr.each { |v| v.class.should == Integer } + end + end + end + end + + describe "for a Time" do + it "loads" do + Marshal.send(@method, Marshal.dump(Time.at(1))).should == Time.at(1) + end + + it "loads serialized instance variables" do + t = Time.new + t.instance_variable_set(:@foo, 'bar') + + Marshal.send(@method, Marshal.dump(t)).instance_variable_get(:@foo).should == 'bar' + end + + it "loads Time objects stored as links" do + t = Time.new + + t1, t2 = Marshal.send(@method, Marshal.dump([t, t])) + t1.should equal t2 + end + + it "keeps the local zone" do + with_timezone 'AST', 3 do + t = Time.local(2012, 1, 1) + Marshal.send(@method, Marshal.dump(t)).zone.should == t.zone + end + end + + it "keeps UTC zone" do + t = Time.now.utc + t2 = Marshal.send(@method, Marshal.dump(t)) + t2.should.utc? + end + + it "keeps the zone" do + t = nil + + with_timezone 'AST', 4 do + t = Time.local(2012, 1, 1) + end + + with_timezone 'EET', -2 do + Marshal.send(@method, Marshal.dump(t)).zone.should == 'AST' + end + end + + it "keeps utc offset" do + t = Time.new(2007,11,1,15,25,0, "+09:00") + t2 = Marshal.send(@method, Marshal.dump(t)) + t2.utc_offset.should == 32400 + end + + it "keeps nanoseconds" do + t = Time.now + Marshal.send(@method, Marshal.dump(t)).nsec.should == t.nsec + end + + it "does not add any additional instance variable" do + t = Time.now + t2 = Marshal.send(@method, Marshal.dump(t)) + t2.instance_variables.should.empty? + end + end + + describe "for nil" do + it "loads" do + Marshal.send(@method, "\x04\b0").should be_nil + end + end + + describe "for true" do + it "loads" do + Marshal.send(@method, "\x04\bT").should be_true + end + end + + describe "for false" do + it "loads" do + Marshal.send(@method, "\x04\bF").should be_false + end + end + + describe "for a Class" do + it "loads" do + Marshal.send(@method, "\x04\bc\vString").should == String + end + + it "raises ArgumentError if given the name of a non-Module" do + -> { Marshal.send(@method, "\x04\bc\vKernel") }.should raise_error(ArgumentError) + end + + it "raises ArgumentError if given a nonexistent class" do + -> { Marshal.send(@method, "\x04\bc\vStrung") }.should raise_error(ArgumentError) + end + + it "raises ArgumentError when end of byte sequence reached before class name end" do + Marshal.dump(String).should == "\x04\bc\vString" + + -> { + Marshal.send(@method, "\x04\bc\vStr") + }.should raise_error(ArgumentError, "marshal data too short") + end + end + + describe "for a Module" do + it "loads a module" do + Marshal.send(@method, "\x04\bm\vKernel").should == Kernel + end + + it "raises ArgumentError if given the name of a non-Class" do + -> { Marshal.send(@method, "\x04\bm\vString") }.should raise_error(ArgumentError) + end + + it "loads an old module" do + Marshal.send(@method, "\x04\bM\vKernel").should == Kernel + end + + it "raises ArgumentError when end of byte sequence reached before module name end" do + Marshal.dump(Kernel).should == "\x04\bm\vKernel" + + -> { + Marshal.send(@method, "\x04\bm\vKer") + }.should raise_error(ArgumentError, "marshal data too short") + end + end + + describe "for a wrapped C pointer" do + it "loads" do + class DumpableDir < Dir + def _dump_data + path + end + def _load_data path + initialize(path) + end + end + + data = "\x04\bd:\x10DumpableDirI\"\x06.\x06:\x06ET" + + dir = Marshal.send(@method, data) + begin + dir.path.should == '.' + ensure + dir.close + end + end + + it "raises TypeError when the local class is missing _load_data" do + class UnloadableDumpableDir < Dir + def _dump_data + path + end + # no _load_data + end + + data = "\x04\bd:\x1AUnloadableDumpableDirI\"\x06.\x06:\x06ET" + + -> { Marshal.send(@method, data) }.should raise_error(TypeError) + end + + it "raises ArgumentError when the local class is a regular object" do + data = "\004\bd:\020UserDefined\0" + + -> { Marshal.send(@method, data) }.should raise_error(ArgumentError) + end + end + + describe "when a class does not exist in the namespace" do + before :each do + NamespaceTest.send(:const_set, :SameName, Class.new) + @data = Marshal.dump(NamespaceTest::SameName.new) + NamespaceTest.send(:remove_const, :SameName) + end + + it "raises an ArgumentError" do + message = "undefined class/module NamespaceTest::SameName" + -> { Marshal.send(@method, @data) }.should raise_error(ArgumentError, message) + end + end + + it "raises an ArgumentError with full constant name when the dumped constant is missing" do + NamespaceTest.send(:const_set, :KaBoom, Class.new) + @data = Marshal.dump(NamespaceTest::KaBoom.new) + NamespaceTest.send(:remove_const, :KaBoom) + + -> { Marshal.send(@method, @data) }.should raise_error(ArgumentError, /NamespaceTest::KaBoom/) + end +end diff --git a/spec/ruby/core/matchdata/allocate_spec.rb b/spec/ruby/core/matchdata/allocate_spec.rb new file mode 100644 index 0000000000..142ce639c2 --- /dev/null +++ b/spec/ruby/core/matchdata/allocate_spec.rb @@ -0,0 +1,8 @@ +require_relative '../../spec_helper' + +describe "MatchData.allocate" do + it "is undefined" do + # https://bugs.ruby-lang.org/issues/16294 + -> { MatchData.allocate }.should raise_error(NoMethodError) + end +end diff --git a/spec/ruby/core/matchdata/begin_spec.rb b/spec/ruby/core/matchdata/begin_spec.rb new file mode 100644 index 0000000000..54b4e0a33f --- /dev/null +++ b/spec/ruby/core/matchdata/begin_spec.rb @@ -0,0 +1,132 @@ +# -*- encoding: utf-8 -*- + +require_relative '../../spec_helper' + +describe "MatchData#begin" do + context "when passed an integer argument" do + it "returns the character offset of the start of the nth element" do + match_data = /(.)(.)(\d+)(\d)/.match("THX1138.") + match_data.begin(0).should == 1 + match_data.begin(2).should == 2 + end + + it "returns nil when the nth match isn't found" do + match_data = /something is( not)? (right)/.match("something is right") + match_data.begin(1).should be_nil + end + + it "returns the character offset for multi-byte strings" do + match_data = /(.)(.)(\d+)(\d)/.match("TñX1138.") + match_data.begin(0).should == 1 + match_data.begin(2).should == 2 + end + + not_supported_on :opal do + it "returns the character offset for multi-byte strings with unicode regexp" do + match_data = /(.)(.)(\d+)(\d)/u.match("TñX1138.") + match_data.begin(0).should == 1 + match_data.begin(2).should == 2 + end + end + + it "tries to convert the passed argument to an Integer using #to_int" do + obj = mock('to_int') + obj.should_receive(:to_int).and_return(2) + + match_data = /(.)(.)(\d+)(\d)/.match("THX1138.") + match_data.begin(obj).should == 2 + end + + it "raises IndexError if index is out of bounds" do + match_data = /(?<f>foo)(?<b>bar)/.match("foobar") + + -> { + match_data.begin(-1) + }.should raise_error(IndexError, "index -1 out of matches") + + -> { + match_data.begin(3) + }.should raise_error(IndexError, "index 3 out of matches") + end + end + + context "when passed a String argument" do + it "return the character offset of the start of the named capture" do + match_data = /(?<a>.)(.)(?<b>\d+)(\d)/.match("THX1138.") + match_data.begin("a").should == 1 + match_data.begin("b").should == 3 + end + + it "returns the character offset for multi byte strings" do + match_data = /(?<a>.)(.)(?<b>\d+)(\d)/.match("TñX1138.") + match_data.begin("a").should == 1 + match_data.begin("b").should == 3 + end + + not_supported_on :opal do + it "returns the character offset for multi byte strings with unicode regexp" do + match_data = /(?<a>.)(.)(?<b>\d+)(\d)/u.match("TñX1138.") + match_data.begin("a").should == 1 + match_data.begin("b").should == 3 + end + end + + it "returns the character offset for the farthest match when multiple named captures use the same name" do + match_data = /(?<a>.)(.)(?<a>\d+)(\d)/.match("THX1138.") + match_data.begin("a").should == 3 + end + + it "returns the character offset for multi-byte names" do + match_data = /(?<æ>.)(.)(?<b>\d+)(\d)/.match("THX1138.") + match_data.begin("æ").should == 1 + end + + it "raises IndexError if there is no group with the provided name" do + match_data = /(?<f>foo)(?<b>bar)/.match("foobar") + + -> { + match_data.begin("y") + }.should raise_error(IndexError, "undefined group name reference: y") + end + end + + context "when passed a Symbol argument" do + it "return the character offset of the start of the named capture" do + match_data = /(?<a>.)(.)(?<b>\d+)(\d)/.match("THX1138.") + match_data.begin(:a).should == 1 + match_data.begin(:b).should == 3 + end + + it "returns the character offset for multi byte strings" do + match_data = /(?<a>.)(.)(?<b>\d+)(\d)/.match("TñX1138.") + match_data.begin(:a).should == 1 + match_data.begin(:b).should == 3 + end + + not_supported_on :opal do + it "returns the character offset for multi byte strings with unicode regexp" do + match_data = /(?<a>.)(.)(?<b>\d+)(\d)/u.match("TñX1138.") + match_data.begin(:a).should == 1 + match_data.begin(:b).should == 3 + end + end + + it "returns the character offset for the farthest match when multiple named captures use the same name" do + match_data = /(?<a>.)(.)(?<a>\d+)(\d)/.match("THX1138.") + match_data.begin(:a).should == 3 + end + + it "returns the character offset for multi-byte names" do + match_data = /(?<æ>.)(.)(?<b>\d+)(\d)/.match("THX1138.") + match_data.begin(:æ).should == 1 + end + + it "raises IndexError if there is no group with the provided name" do + match_data = /(?<f>foo)(?<b>bar)/.match("foobar") + + -> { + match_data.begin(:y) + }.should raise_error(IndexError, "undefined group name reference: y") + end + end +end diff --git a/spec/ruby/core/matchdata/bytebegin_spec.rb b/spec/ruby/core/matchdata/bytebegin_spec.rb new file mode 100644 index 0000000000..08c1fd6d1e --- /dev/null +++ b/spec/ruby/core/matchdata/bytebegin_spec.rb @@ -0,0 +1,132 @@ +require_relative '../../spec_helper' + +ruby_version_is "3.4" do + describe "MatchData#bytebegin" do + context "when passed an integer argument" do + it "returns the byte-based offset of the start of the nth element" do + match_data = /(.)(.)(\d+)(\d)/.match("THX1138.") + match_data.bytebegin(0).should == 1 + match_data.bytebegin(2).should == 2 + end + + it "returns nil when the nth match isn't found" do + match_data = /something is( not)? (right)/.match("something is right") + match_data.bytebegin(1).should be_nil + end + + it "returns the byte-based offset for multi-byte strings" do + match_data = /(.)(.)(\d+)(\d)/.match("TñX1138.") + match_data.bytebegin(0).should == 1 + match_data.bytebegin(2).should == 3 + end + + not_supported_on :opal do + it "returns the byte-based offset for multi-byte strings with unicode regexp" do + match_data = /(.)(.)(\d+)(\d)/u.match("TñX1138.") + match_data.bytebegin(0).should == 1 + match_data.bytebegin(2).should == 3 + end + end + + it "tries to convert the passed argument to an Integer using #to_int" do + obj = mock('to_int') + obj.should_receive(:to_int).and_return(2) + + match_data = /(.)(.)(\d+)(\d)/.match("THX1138.") + match_data.bytebegin(obj).should == 2 + end + + it "raises IndexError if index is out of bounds" do + match_data = /(?<f>foo)(?<b>bar)/.match("foobar") + + -> { + match_data.bytebegin(-1) + }.should raise_error(IndexError, "index -1 out of matches") + + -> { + match_data.bytebegin(3) + }.should raise_error(IndexError, "index 3 out of matches") + end + end + + context "when passed a String argument" do + it "return the byte-based offset of the start of the named capture" do + match_data = /(?<a>.)(.)(?<b>\d+)(\d)/.match("THX1138.") + match_data.bytebegin("a").should == 1 + match_data.bytebegin("b").should == 3 + end + + it "returns the byte-based offset for multi byte strings" do + match_data = /(?<a>.)(.)(?<b>\d+)(\d)/.match("TñX1138.") + match_data.bytebegin("a").should == 1 + match_data.bytebegin("b").should == 4 + end + + not_supported_on :opal do + it "returns the byte-based offset for multi byte strings with unicode regexp" do + match_data = /(?<a>.)(.)(?<b>\d+)(\d)/u.match("TñX1138.") + match_data.bytebegin("a").should == 1 + match_data.bytebegin("b").should == 4 + end + end + + it "returns the byte-based offset for the farthest match when multiple named captures use the same name" do + match_data = /(?<a>.)(.)(?<a>\d+)(\d)/.match("THX1138.") + match_data.bytebegin("a").should == 3 + end + + it "returns the byte-based offset for multi-byte names" do + match_data = /(?<æ>.)(.)(?<b>\d+)(\d)/.match("THX1138.") + match_data.bytebegin("æ").should == 1 + end + + it "raises IndexError if there is no group with the provided name" do + match_data = /(?<f>foo)(?<b>bar)/.match("foobar") + + -> { + match_data.bytebegin("y") + }.should raise_error(IndexError, "undefined group name reference: y") + end + end + + context "when passed a Symbol argument" do + it "return the byte-based offset of the start of the named capture" do + match_data = /(?<a>.)(.)(?<b>\d+)(\d)/.match("THX1138.") + match_data.bytebegin(:a).should == 1 + match_data.bytebegin(:b).should == 3 + end + + it "returns the byte-based offset for multi byte strings" do + match_data = /(?<a>.)(.)(?<b>\d+)(\d)/.match("TñX1138.") + match_data.bytebegin(:a).should == 1 + match_data.bytebegin(:b).should == 4 + end + + not_supported_on :opal do + it "returns the byte-based offset for multi byte strings with unicode regexp" do + match_data = /(?<a>.)(.)(?<b>\d+)(\d)/u.match("TñX1138.") + match_data.bytebegin(:a).should == 1 + match_data.bytebegin(:b).should == 4 + end + end + + it "returns the byte-based offset for the farthest match when multiple named captures use the same name" do + match_data = /(?<a>.)(.)(?<a>\d+)(\d)/.match("THX1138.") + match_data.bytebegin(:a).should == 3 + end + + it "returns the byte-based offset for multi-byte names" do + match_data = /(?<æ>.)(.)(?<b>\d+)(\d)/.match("THX1138.") + match_data.bytebegin(:æ).should == 1 + end + + it "raises IndexError if there is no group with the provided name" do + match_data = /(?<f>foo)(?<b>bar)/.match("foobar") + + -> { + match_data.bytebegin(:y) + }.should raise_error(IndexError, "undefined group name reference: y") + end + end + end +end diff --git a/spec/ruby/core/matchdata/byteend_spec.rb b/spec/ruby/core/matchdata/byteend_spec.rb new file mode 100644 index 0000000000..98015e287d --- /dev/null +++ b/spec/ruby/core/matchdata/byteend_spec.rb @@ -0,0 +1,104 @@ +require_relative '../../spec_helper' + +ruby_version_is "3.4" do + describe "MatchData#byteend" do + context "when passed an integer argument" do + it "returns the byte-based offset of the end of the nth element" do + match_data = /(.)(.)(\d+)(\d)/.match("THX1138.") + match_data.byteend(0).should == 7 + match_data.byteend(2).should == 3 + end + + it "returns nil when the nth match isn't found" do + match_data = /something is( not)? (right)/.match("something is right") + match_data.byteend(1).should be_nil + end + + it "returns the byte-based offset for multi-byte strings" do + match_data = /(.)(.)(\d+)(\d)/.match("TñX1138.") + match_data.byteend(0).should == 8 + match_data.byteend(2).should == 4 + end + + not_supported_on :opal do + it "returns the byte-based offset for multi-byte strings with unicode regexp" do + match_data = /(.)(.)(\d+)(\d)/u.match("TñX1138.") + match_data.byteend(0).should == 8 + match_data.byteend(2).should == 4 + end + end + + it "tries to convert the passed argument to an Integer using #to_int" do + obj = mock('to_int') + obj.should_receive(:to_int).and_return(2) + + match_data = /(.)(.)(\d+)(\d)/.match("THX1138.") + match_data.byteend(obj).should == 3 + end + end + + context "when passed a String argument" do + it "return the byte-based offset of the start of the named capture" do + match_data = /(?<a>.)(.)(?<b>\d+)(\d)/.match("THX1138.") + match_data.byteend("a").should == 2 + match_data.byteend("b").should == 6 + end + + it "returns the byte-based offset for multi byte strings" do + match_data = /(?<a>.)(.)(?<b>\d+)(\d)/.match("TñX1138.") + match_data.byteend("a").should == 3 + match_data.byteend("b").should == 7 + end + + not_supported_on :opal do + it "returns the byte-based offset for multi byte strings with unicode regexp" do + match_data = /(?<a>.)(.)(?<b>\d+)(\d)/u.match("TñX1138.") + match_data.byteend("a").should == 3 + match_data.byteend("b").should == 7 + end + end + + it "returns the byte-based offset for the farthest match when multiple named captures use the same name" do + match_data = /(?<a>.)(.)(?<a>\d+)(\d)/.match("THX1138.") + match_data.byteend("a").should == 6 + end + + it "returns the byte-based offset for multi-byte names" do + match_data = /(?<æ>.)(.)(?<b>\d+)(\d)/.match("THX1138.") + match_data.byteend("æ").should == 2 + end + end + + context "when passed a Symbol argument" do + it "return the byte-based offset of the start of the named capture" do + match_data = /(?<a>.)(.)(?<b>\d+)(\d)/.match("THX1138.") + match_data.byteend(:a).should == 2 + match_data.byteend(:b).should == 6 + end + + it "returns the byte-based offset for multi byte strings" do + match_data = /(?<a>.)(.)(?<b>\d+)(\d)/.match("TñX1138.") + match_data.byteend(:a).should == 3 + match_data.byteend(:b).should == 7 + end + + not_supported_on :opal do + it "returns the byte-based offset for multi byte strings with unicode regexp" do + match_data = /(?<a>.)(.)(?<b>\d+)(\d)/u.match("TñX1138.") + match_data.byteend(:a).should == 3 + match_data.byteend(:b).should == 7 + end + end + + it "returns the byte-based offset for the farthest match when multiple named captures use the same name" do + match_data = /(?<a>.)(.)(?<a>\d+)(\d)/.match("THX1138.") + match_data.byteend(:a).should == 6 + end + + it "returns the byte-based offset for multi-byte names" do + match_data = /(?<æ>.)(.)(?<b>\d+)(\d)/.match("THX1138.") + match_data.byteend(:æ).should == 2 + end + end + end +end diff --git a/spec/ruby/core/matchdata/byteoffset_spec.rb b/spec/ruby/core/matchdata/byteoffset_spec.rb new file mode 100644 index 0000000000..fb8f5fb67d --- /dev/null +++ b/spec/ruby/core/matchdata/byteoffset_spec.rb @@ -0,0 +1,93 @@ +require_relative '../../spec_helper' + +describe "MatchData#byteoffset" do + it "returns beginning and ending byte-based offset of whole matched substring for 0 element" do + m = /(.)(.)(\d+)(\d)/.match("THX1138.") + m.byteoffset(0).should == [1, 7] + end + + it "returns beginning and ending byte-based offset of n-th match, all the subsequent elements are capturing groups" do + m = /(.)(.)(\d+)(\d)/.match("THX1138.") + + m.byteoffset(2).should == [2, 3] + m.byteoffset(3).should == [3, 6] + m.byteoffset(4).should == [6, 7] + end + + it "accepts String as a reference to a named capture" do + m = /(?<f>foo)(?<b>bar)/.match("foobar") + + m.byteoffset("f").should == [0, 3] + m.byteoffset("b").should == [3, 6] + end + + it "accepts Symbol as a reference to a named capture" do + m = /(?<f>foo)(?<b>bar)/.match("foobar") + + m.byteoffset(:f).should == [0, 3] + m.byteoffset(:b).should == [3, 6] + end + + it "returns [nil, nil] if a capturing group is optional and doesn't match" do + m = /(?<x>q..)?/.match("foobarbaz") + + m.byteoffset("x").should == [nil, nil] + m.byteoffset(1).should == [nil, nil] + end + + it "returns correct beginning and ending byte-based offset for multi-byte strings" do + m = /\A\u3042(.)(.)?(.)\z/.match("\u3042\u3043\u3044") + + m.byteoffset(1).should == [3, 6] + m.byteoffset(3).should == [6, 9] + end + + it "returns [nil, nil] if a capturing group is optional and doesn't match for multi-byte string" do + m = /\A\u3042(.)(.)?(.)\z/.match("\u3042\u3043\u3044") + + m.byteoffset(2).should == [nil, nil] + end + + it "converts argument into integer if is not String nor Symbol" do + m = /(?<f>foo)(?<b>bar)/.match("foobar") + + obj = Object.new + def obj.to_int; 2; end + + m.byteoffset(1r).should == [0, 3] + m.byteoffset(1.1).should == [0, 3] + m.byteoffset(obj).should == [3, 6] + end + + it "raises IndexError if there is no group with the provided name" do + m = /(?<f>foo)(?<b>bar)/.match("foobar") + + -> { + m.byteoffset("y") + }.should raise_error(IndexError, "undefined group name reference: y") + + -> { + m.byteoffset(:y) + }.should raise_error(IndexError, "undefined group name reference: y") + end + + it "raises IndexError if index is out of bounds" do + m = /(?<f>foo)(?<b>bar)/.match("foobar") + + -> { + m.byteoffset(-1) + }.should raise_error(IndexError, "index -1 out of matches") + + -> { + m.byteoffset(3) + }.should raise_error(IndexError, "index 3 out of matches") + end + + it "raises TypeError if can't convert argument into Integer" do + m = /(?<f>foo)(?<b>bar)/.match("foobar") + + -> { + m.byteoffset([]) + }.should raise_error(TypeError, "no implicit conversion of Array into Integer") + end +end diff --git a/spec/ruby/core/matchdata/captures_spec.rb b/spec/ruby/core/matchdata/captures_spec.rb new file mode 100644 index 0000000000..f829a25481 --- /dev/null +++ b/spec/ruby/core/matchdata/captures_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/captures' + +describe "MatchData#captures" do + it_behaves_like :matchdata_captures, :captures +end diff --git a/spec/ruby/core/matchdata/deconstruct_keys_spec.rb b/spec/ruby/core/matchdata/deconstruct_keys_spec.rb new file mode 100644 index 0000000000..bf22bc33ff --- /dev/null +++ b/spec/ruby/core/matchdata/deconstruct_keys_spec.rb @@ -0,0 +1,63 @@ +require_relative '../../spec_helper' + +describe "MatchData#deconstruct_keys" do + it "returns whole hash for nil as an argument" do + m = /(?<f>foo)(?<b>bar)/.match("foobar") + + m.deconstruct_keys(nil).should == { f: "foo", b: "bar" } + end + + it "returns only specified keys" do + m = /(?<f>foo)(?<b>bar)/.match("foobar") + + m.deconstruct_keys([:f]).should == { f: "foo" } + end + + it "requires one argument" do + m = /l/.match("l") + + -> { + m.deconstruct_keys + }.should raise_error(ArgumentError, "wrong number of arguments (given 0, expected 1)") + end + + it "it raises error when argument is neither nil nor array" do + m = /(?<f>foo)(?<b>bar)/.match("foobar") + + -> { m.deconstruct_keys(1) }.should raise_error(TypeError, "wrong argument type Integer (expected Array)") + -> { m.deconstruct_keys("asd") }.should raise_error(TypeError, "wrong argument type String (expected Array)") + -> { m.deconstruct_keys(:x) }.should raise_error(TypeError, "wrong argument type Symbol (expected Array)") + -> { m.deconstruct_keys({}) }.should raise_error(TypeError, "wrong argument type Hash (expected Array)") + end + + it "returns {} when passed []" do + m = /(?<f>foo)(?<b>bar)/.match("foobar") + + m.deconstruct_keys([]).should == {} + end + + it "does not accept non-Symbol keys" do + m = /(?<f>foo)(?<b>bar)/.match("foobar") + + -> { + m.deconstruct_keys(['year', :foo]) + }.should raise_error(TypeError, "wrong argument type String (expected Symbol)") + end + + it "process keys till the first non-existing one" do + m = /(?<f>foo)(?<b>bar)(?<c>baz)/.match("foobarbaz") + + m.deconstruct_keys([:f, :a, :b]).should == { f: "foo" } + end + + it "returns {} when there are no named captured groups at all" do + m = /foo.+/.match("foobar") + + m.deconstruct_keys(nil).should == {} + end + + it "returns {} when passed more keys than named captured groups" do + m = /(?<f>foo)(?<b>bar)/.match("foobar") + m.deconstruct_keys([:f, :b, :c]).should == {} + end +end diff --git a/spec/ruby/core/matchdata/deconstruct_spec.rb b/spec/ruby/core/matchdata/deconstruct_spec.rb new file mode 100644 index 0000000000..c55095665d --- /dev/null +++ b/spec/ruby/core/matchdata/deconstruct_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/captures' + +describe "MatchData#deconstruct" do + it_behaves_like :matchdata_captures, :deconstruct +end diff --git a/spec/ruby/core/matchdata/dup_spec.rb b/spec/ruby/core/matchdata/dup_spec.rb new file mode 100644 index 0000000000..70877f07eb --- /dev/null +++ b/spec/ruby/core/matchdata/dup_spec.rb @@ -0,0 +1,14 @@ +require_relative '../../spec_helper' + +describe "MatchData#dup" do + it "duplicates the match data" do + original = /ll/.match("hello") + original.instance_variable_set(:@custom_ivar, 42) + duplicate = original.dup + + duplicate.instance_variable_get(:@custom_ivar).should == 42 + original.regexp.should == duplicate.regexp + original.string.should == duplicate.string + original.offset(0).should == duplicate.offset(0) + end +end diff --git a/spec/ruby/core/matchdata/element_reference_spec.rb b/spec/ruby/core/matchdata/element_reference_spec.rb new file mode 100644 index 0000000000..0924be0aae --- /dev/null +++ b/spec/ruby/core/matchdata/element_reference_spec.rb @@ -0,0 +1,124 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "MatchData#[]" do + it "acts as normal array indexing [index]" do + md = /(.)(.)(\d+)(\d)/.match("THX1138.") + + md[0].should == 'HX1138' + md[1].should == 'H' + md[2].should == 'X' + md[-3].should == 'X' + md[10000].should == nil + md[-10000].should == nil + end + + it "supports accessors [start, length]" do + /(.)(.)(\d+)(\d)/.match("THX1138.")[1, 2].should == %w|H X| + /(.)(.)(\d+)(\d)/.match("THX1138.")[-3, 2].should == %w|X 113| + + # negative index is larger than the number of match values + /(.)(.)(\d+)(\d)/.match("THX1138.")[-30, 2].should == nil + + # positive index larger than number of match values + /(.)(.)(\d+)(\d)/.match("THX1138.")[5, 2].should == [] + /(.)(.)(\d+)(\d)/.match("THX1138.")[6, 2].should == nil + /(.)(.)(\d+)(\d)/.match("THX1138.")[30, 2].should == nil + + # length argument larger than number of match values is capped to match value length + /(.)(.)(\d+)(\d)/.match("THX1138.")[3, 10].should == %w|113 8| + + /(.)(.)(\d+)(\d)/.match("THX1138.")[3, 0].should == [] + + /(.)(.)(\d+)(\d)/.match("THX1138.")[3, -1].should == nil + /(.)(.)(\d+)(\d)/.match("THX1138.")[3, -30].should == nil + end + + it "supports ranges [start..end]" do + /(.)(.)(\d+)(\d)/.match("THX1138.")[1..3].should == %w|H X 113| + /(.)(.)(\d+)(\d)/.match("THX1138.")[3..10].should == %w|113 8| + /(.)(.)(\d+)(\d)/.match("THX1138.")[-30..2].should == nil + /(.)(.)(\d+)(\d)/.match("THX1138.")[3..1].should == [] + end + + it "supports endless ranges [start..]" do + /(.)(.)(\d+)(\d)/.match("THX1138.")[3..].should == %w|113 8| + end + + it "supports beginningless ranges [..end]" do + /(.)(.)(\d+)(\d)/.match("THX1138.")[..1].should == %w|HX1138 H| + end + + it "supports beginningless endless ranges [nil..nil]" do + /(.)(.)(\d+)(\d)/.match("THX1138.")[nil..nil].should == %w|HX1138 H X 113 8| + end + + it "returns instances of String when given a String subclass" do + str = MatchDataSpecs::MyString.new("THX1138.") + /(.)(.)(\d+)(\d)/.match(str)[0..-1].each { |m| m.should be_an_instance_of(String) } + end +end + +describe "MatchData#[Symbol]" do + it "returns the corresponding named match when given a Symbol" do + md = 'haystack'.match(/(?<t>t(?<a>ack))/) + md[:a].should == 'ack' + md[:t].should == 'tack' + end + + it "returns the corresponding named match when given a String" do + md = 'haystack'.match(/(?<t>t(?<a>ack))/) + md['a'].should == 'ack' + md['t'].should == 'tack' + end + + it "returns the matching version of multiple corresponding named match" do + regexp = /(?: + A(?<word>\w+) + | + B(?<word>\w+) + )/x + md_a = regexp.match("Afoo") + md_b = regexp.match("Bfoo") + + md_a[:word].should == "foo" + md_b[:word].should == "foo" + + md_a['word'].should == "foo" + md_b['word'].should == "foo" + end + + it "returns the last match when multiple named matches exist with the same name" do + md = /(?<word>hay)(?<word>stack)/.match('haystack') + md[:word].should == "stack" + md['word'].should == "stack" + end + + it "returns nil on non-matching named matches" do + regexp = /(?<foo>foo )?(?<bar>bar)/ + full_match = regexp.match("foo bar") + partial_match = regexp.match("bar") + + full_match[:foo].should == "foo " + partial_match[:foo].should == nil + + full_match['foo'].should == "foo " + partial_match['foo'].should == nil + end + + it "raises an IndexError if there is no named match corresponding to the Symbol" do + md = 'haystack'.match(/(?<t>t(?<a>ack))/) + -> { md[:baz] }.should raise_error(IndexError, /baz/) + end + + it "raises an IndexError if there is no named match corresponding to the String" do + md = 'haystack'.match(/(?<t>t(?<a>ack))/) + -> { md['baz'] }.should raise_error(IndexError, /baz/) + end + + it "returns matches in the String's encoding" do + rex = /(?<t>t(?<a>ack))/u + md = 'haystack'.dup.force_encoding('euc-jp').match(rex) + md[:t].encoding.should == Encoding::EUC_JP + end +end diff --git a/spec/ruby/core/matchdata/end_spec.rb b/spec/ruby/core/matchdata/end_spec.rb new file mode 100644 index 0000000000..d01b0a8b30 --- /dev/null +++ b/spec/ruby/core/matchdata/end_spec.rb @@ -0,0 +1,104 @@ +# -*- encoding: utf-8 -*- + +require_relative '../../spec_helper' + +describe "MatchData#end" do + context "when passed an integer argument" do + it "returns the character offset of the end of the nth element" do + match_data = /(.)(.)(\d+)(\d)/.match("THX1138.") + match_data.end(0).should == 7 + match_data.end(2).should == 3 + end + + it "returns nil when the nth match isn't found" do + match_data = /something is( not)? (right)/.match("something is right") + match_data.end(1).should be_nil + end + + it "returns the character offset for multi-byte strings" do + match_data = /(.)(.)(\d+)(\d)/.match("TñX1138.") + match_data.end(0).should == 7 + match_data.end(2).should == 3 + end + + not_supported_on :opal do + it "returns the character offset for multi-byte strings with unicode regexp" do + match_data = /(.)(.)(\d+)(\d)/u.match("TñX1138.") + match_data.end(0).should == 7 + match_data.end(2).should == 3 + end + end + + it "tries to convert the passed argument to an Integer using #to_int" do + obj = mock('to_int') + obj.should_receive(:to_int).and_return(2) + + match_data = /(.)(.)(\d+)(\d)/.match("THX1138.") + match_data.end(obj).should == 3 + end + end + + context "when passed a String argument" do + it "return the character offset of the start of the named capture" do + match_data = /(?<a>.)(.)(?<b>\d+)(\d)/.match("THX1138.") + match_data.end("a").should == 2 + match_data.end("b").should == 6 + end + + it "returns the character offset for multi byte strings" do + match_data = /(?<a>.)(.)(?<b>\d+)(\d)/.match("TñX1138.") + match_data.end("a").should == 2 + match_data.end("b").should == 6 + end + + not_supported_on :opal do + it "returns the character offset for multi byte strings with unicode regexp" do + match_data = /(?<a>.)(.)(?<b>\d+)(\d)/u.match("TñX1138.") + match_data.end("a").should == 2 + match_data.end("b").should == 6 + end + end + + it "returns the character offset for the farthest match when multiple named captures use the same name" do + match_data = /(?<a>.)(.)(?<a>\d+)(\d)/.match("THX1138.") + match_data.end("a").should == 6 + end + + it "returns the character offset for multi-byte names" do + match_data = /(?<æ>.)(.)(?<b>\d+)(\d)/.match("THX1138.") + match_data.end("æ").should == 2 + end + end + + context "when passed a Symbol argument" do + it "return the character offset of the start of the named capture" do + match_data = /(?<a>.)(.)(?<b>\d+)(\d)/.match("THX1138.") + match_data.end(:a).should == 2 + match_data.end(:b).should == 6 + end + + it "returns the character offset for multi byte strings" do + match_data = /(?<a>.)(.)(?<b>\d+)(\d)/.match("TñX1138.") + match_data.end(:a).should == 2 + match_data.end(:b).should == 6 + end + + not_supported_on :opal do + it "returns the character offset for multi byte strings with unicode regexp" do + match_data = /(?<a>.)(.)(?<b>\d+)(\d)/u.match("TñX1138.") + match_data.end(:a).should == 2 + match_data.end(:b).should == 6 + end + end + + it "returns the character offset for the farthest match when multiple named captures use the same name" do + match_data = /(?<a>.)(.)(?<a>\d+)(\d)/.match("THX1138.") + match_data.end(:a).should == 6 + end + + it "returns the character offset for multi-byte names" do + match_data = /(?<æ>.)(.)(?<b>\d+)(\d)/.match("THX1138.") + match_data.end(:æ).should == 2 + end + end +end diff --git a/spec/ruby/core/matchdata/eql_spec.rb b/spec/ruby/core/matchdata/eql_spec.rb new file mode 100644 index 0000000000..1d9666ebe1 --- /dev/null +++ b/spec/ruby/core/matchdata/eql_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/eql' + +describe "MatchData#eql?" do + it_behaves_like :matchdata_eql, :eql? +end diff --git a/spec/ruby/core/matchdata/equal_value_spec.rb b/spec/ruby/core/matchdata/equal_value_spec.rb new file mode 100644 index 0000000000..a58f1277e4 --- /dev/null +++ b/spec/ruby/core/matchdata/equal_value_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/eql' + +describe "MatchData#==" do + it_behaves_like :matchdata_eql, :== +end diff --git a/spec/ruby/core/matchdata/fixtures/classes.rb b/spec/ruby/core/matchdata/fixtures/classes.rb new file mode 100644 index 0000000000..54795636e5 --- /dev/null +++ b/spec/ruby/core/matchdata/fixtures/classes.rb @@ -0,0 +1,3 @@ +module MatchDataSpecs + class MyString < String; end +end diff --git a/spec/ruby/core/matchdata/hash_spec.rb b/spec/ruby/core/matchdata/hash_spec.rb new file mode 100644 index 0000000000..cef18fdd20 --- /dev/null +++ b/spec/ruby/core/matchdata/hash_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../spec_helper' + +describe "MatchData#hash" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/matchdata/inspect_spec.rb b/spec/ruby/core/matchdata/inspect_spec.rb new file mode 100644 index 0000000000..5315257677 --- /dev/null +++ b/spec/ruby/core/matchdata/inspect_spec.rb @@ -0,0 +1,23 @@ +require_relative '../../spec_helper' + +describe "MatchData#inspect" do + before :each do + @match_data = /(.)(.)(\d+)(\d)/.match("THX1138.") + end + + it "returns a String" do + @match_data.inspect.should be_kind_of(String) + end + + it "returns a human readable representation that contains entire matched string and the captures" do + # yeah, hardcoding the inspect output is not ideal, but in this case + # it makes perfect sense. See JRUBY-4558 for example. + @match_data.inspect.should == '#<MatchData "HX1138" 1:"H" 2:"X" 3:"113" 4:"8">' + end + + it "returns a human readable representation of named captures" do + match_data = "abc def ghi".match(/(?<first>\w+)\s+(?<last>\w+)\s+(\w+)/) + + match_data.inspect.should == '#<MatchData "abc def ghi" first:"abc" last:"def">' + end +end diff --git a/spec/ruby/core/matchdata/length_spec.rb b/spec/ruby/core/matchdata/length_spec.rb new file mode 100644 index 0000000000..39df36df4b --- /dev/null +++ b/spec/ruby/core/matchdata/length_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/length' + +describe "MatchData#length" do + it_behaves_like :matchdata_length, :length +end diff --git a/spec/ruby/core/matchdata/match_length_spec.rb b/spec/ruby/core/matchdata/match_length_spec.rb new file mode 100644 index 0000000000..824f94a397 --- /dev/null +++ b/spec/ruby/core/matchdata/match_length_spec.rb @@ -0,0 +1,32 @@ +# -*- encoding: utf-8 -*- + +require_relative '../../spec_helper' + +describe "MatchData#match_length" do + it "returns the length of the corresponding match when given an Integer" do + md = /(.)(.)(\d+)(\d)/.match("THX1138.") + + md.match_length(0).should == 6 + md.match_length(1).should == 1 + md.match_length(2).should == 1 + md.match_length(3).should == 3 + md.match_length(4).should == 1 + end + + it "returns nil on non-matching index matches" do + md = /\d+(\w)?/.match("THX1138.") + md.match_length(1).should == nil + end + + it "returns the length of the corresponding named match when given a Symbol" do + md = 'haystack'.match(/(?<t>t(?<a>ack))/) + md.match_length(:a).should == 3 + md.match_length(:t).should == 4 + end + + it "returns nil on non-matching index matches" do + md = 'haystack'.match(/(?<t>t)(?<a>all)?/) + md.match_length(:t).should == 1 + md.match_length(:a).should == nil + end +end diff --git a/spec/ruby/core/matchdata/match_spec.rb b/spec/ruby/core/matchdata/match_spec.rb new file mode 100644 index 0000000000..a16914ea15 --- /dev/null +++ b/spec/ruby/core/matchdata/match_spec.rb @@ -0,0 +1,32 @@ +# -*- encoding: utf-8 -*- + +require_relative '../../spec_helper' + +describe "MatchData#match" do + it "returns the corresponding match when given an Integer" do + md = /(.)(.)(\d+)(\d)/.match("THX1138.") + + md.match(0).should == 'HX1138' + md.match(1).should == 'H' + md.match(2).should == 'X' + md.match(3).should == '113' + md.match(4).should == '8' + end + + it "returns nil on non-matching index matches" do + md = /\d+(\w)?/.match("THX1138.") + md.match(1).should == nil + end + + it "returns the corresponding named match when given a Symbol" do + md = 'haystack'.match(/(?<t>t(?<a>ack))/) + md.match(:a).should == 'ack' + md.match(:t).should == 'tack' + end + + it "returns nil on non-matching index matches" do + md = 'haystack'.match(/(?<t>t)(?<a>all)?/) + md.match(:t).should == 't' + md.match(:a).should == nil + end +end diff --git a/spec/ruby/core/matchdata/named_captures_spec.rb b/spec/ruby/core/matchdata/named_captures_spec.rb new file mode 100644 index 0000000000..5e4693d62d --- /dev/null +++ b/spec/ruby/core/matchdata/named_captures_spec.rb @@ -0,0 +1,27 @@ +require_relative '../../spec_helper' + +describe 'MatchData#named_captures' do + it 'returns a Hash that has captured name and the matched string pairs' do + /(?<a>.)(?<b>.)?/.match('0').named_captures.should == { 'a' => '0', 'b' => nil } + end + + it 'prefers later captures' do + /\A(?<a>.)(?<b>.)(?<b>.)(?<a>.)\z/.match('0123').named_captures.should == { 'a' => '3', 'b' => '2' } + end + + it 'returns the latest matched capture, even if a later one that does not match exists' do + /\A(?<a>.)(?<b>.)(?<b>.)(?<a>.)?\z/.match('012').named_captures.should == { 'a' => '0', 'b' => '2' } + end + + ruby_version_is "3.3" do + it 'returns a Hash with Symbol keys when symbolize_names is provided a true value' do + /(?<a>.)(?<b>.)?/.match('0').named_captures(symbolize_names: true).should == { a: '0', b: nil } + /(?<a>.)(?<b>.)?/.match('0').named_captures(symbolize_names: "truly").should == { a: '0', b: nil } + end + + it 'returns a Hash with String keys when symbolize_names is provided a false value' do + /(?<a>.)(?<b>.)?/.match('02').named_captures(symbolize_names: false).should == { 'a' => '0', 'b' => '2' } + /(?<a>.)(?<b>.)?/.match('02').named_captures(symbolize_names: nil).should == { 'a' => '0', 'b' => '2' } + end + end +end diff --git a/spec/ruby/core/matchdata/names_spec.rb b/spec/ruby/core/matchdata/names_spec.rb new file mode 100644 index 0000000000..25ca06ced9 --- /dev/null +++ b/spec/ruby/core/matchdata/names_spec.rb @@ -0,0 +1,33 @@ +require_relative '../../spec_helper' + +describe "MatchData#names" do + it "returns an Array" do + md = 'haystack'.match(/(?<yellow>hay)/) + md.names.should be_an_instance_of(Array) + end + + it "sets each element to a String" do + 'haystack'.match(/(?<yellow>hay)/).names.all? do |e| + e.should be_an_instance_of(String) + end + end + + it "returns the names of the named capture groups" do + md = 'haystack'.match(/(?<yellow>hay).(?<pin>tack)/) + md.names.should == ['yellow', 'pin'] + end + + it "returns [] if there were no named captures" do + 'haystack'.match(/(hay).(tack)/).names.should == [] + end + + it "returns each name only once" do + md = 'haystack'.match(/(?<hay>hay)(?<dot>.)(?<hay>tack)/) + md.names.should == ['hay', 'dot'] + end + + it "equals Regexp#names" do + r = /(?<hay>hay)(?<dot>.)(?<hay>tack)/ + 'haystack'.match(r).names.should == r.names + end +end diff --git a/spec/ruby/core/matchdata/offset_spec.rb b/spec/ruby/core/matchdata/offset_spec.rb new file mode 100644 index 0000000000..a03d58aad1 --- /dev/null +++ b/spec/ruby/core/matchdata/offset_spec.rb @@ -0,0 +1,102 @@ +require_relative '../../spec_helper' + +describe "MatchData#offset" do + it "returns beginning and ending character offset of whole matched substring for 0 element" do + m = /(.)(.)(\d+)(\d)/.match("THX1138.") + m.offset(0).should == [1, 7] + end + + it "returns beginning and ending character offset of n-th match, all the subsequent elements are capturing groups" do + m = /(.)(.)(\d+)(\d)/.match("THX1138.") + + m.offset(2).should == [2, 3] + m.offset(3).should == [3, 6] + m.offset(4).should == [6, 7] + end + + it "accepts String as a reference to a named capture" do + m = /(?<f>foo)(?<b>bar)/.match("foobar") + + m.offset("f").should == [0, 3] + m.offset("b").should == [3, 6] + end + + it "accepts Symbol as a reference to a named capture" do + m = /(?<f>foo)(?<b>bar)/.match("foobar") + + m.offset(:f).should == [0, 3] + m.offset(:b).should == [3, 6] + end + + it "returns [nil, nil] if a capturing group is optional and doesn't match" do + m = /(?<x>q..)?/.match("foobarbaz") + + m.offset("x").should == [nil, nil] + m.offset(1).should == [nil, nil] + end + + it "returns correct beginning and ending character offset for multi-byte strings" do + m = /\A\u3042(.)(.)?(.)\z/.match("\u3042\u3043\u3044") + + m.offset(1).should == [1, 2] + m.offset(3).should == [2, 3] + end + + not_supported_on :opal do + it "returns correct character offset for multi-byte strings with unicode regexp" do + m = /\A\u3042(.)(.)?(.)\z/u.match("\u3042\u3043\u3044") + + m.offset(1).should == [1, 2] + m.offset(3).should == [2, 3] + end + end + + it "returns [nil, nil] if a capturing group is optional and doesn't match for multi-byte string" do + m = /\A\u3042(.)(.)?(.)\z/.match("\u3042\u3043\u3044") + + m.offset(2).should == [nil, nil] + end + + it "converts argument into integer if is not String nor Symbol" do + m = /(?<f>foo)(?<b>bar)/.match("foobar") + + obj = Object.new + def obj.to_int; 2; end + + m.offset(1r).should == [0, 3] + m.offset(1.1).should == [0, 3] + m.offset(obj).should == [3, 6] + end + + it "raises IndexError if there is no group with the provided name" do + m = /(?<f>foo)(?<b>bar)/.match("foobar") + + -> { + m.offset("y") + }.should raise_error(IndexError, "undefined group name reference: y") + + -> { + m.offset(:y) + }.should raise_error(IndexError, "undefined group name reference: y") + end + + it "raises IndexError if index is out of bounds" do + m = /(?<f>foo)(?<b>bar)/.match("foobar") + + -> { + m.offset(-1) + }.should raise_error(IndexError, "index -1 out of matches") + + -> { + m.offset(3) + }.should raise_error(IndexError, "index 3 out of matches") + end + + it "raises TypeError if can't convert argument into Integer" do + m = /(?<f>foo)(?<b>bar)/.match("foobar") + + -> { + m.offset([]) + }.should raise_error(TypeError, "no implicit conversion of Array into Integer") + end +end diff --git a/spec/ruby/core/matchdata/post_match_spec.rb b/spec/ruby/core/matchdata/post_match_spec.rb new file mode 100644 index 0000000000..7bfe6df119 --- /dev/null +++ b/spec/ruby/core/matchdata/post_match_spec.rb @@ -0,0 +1,24 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "MatchData#post_match" do + it "returns the string after the match equiv. special var $'" do + /(.)(.)(\d+)(\d)/.match("THX1138: The Movie").post_match.should == ': The Movie' + $'.should == ': The Movie' + end + + it "sets the encoding to the encoding of the source String" do + str = "abc".dup.force_encoding Encoding::EUC_JP + str.match(/b/).post_match.encoding.should equal(Encoding::EUC_JP) + end + + it "sets an empty result to the encoding of the source String" do + str = "abc".dup.force_encoding Encoding::ISO_8859_1 + str.match(/c/).post_match.encoding.should equal(Encoding::ISO_8859_1) + end + + it "returns an instance of String when given a String subclass" do + str = MatchDataSpecs::MyString.new("THX1138: The Movie") + /(.)(.)(\d+)(\d)/.match(str).post_match.should be_an_instance_of(String) + end +end diff --git a/spec/ruby/core/matchdata/pre_match_spec.rb b/spec/ruby/core/matchdata/pre_match_spec.rb new file mode 100644 index 0000000000..2f1ba9b8f6 --- /dev/null +++ b/spec/ruby/core/matchdata/pre_match_spec.rb @@ -0,0 +1,24 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "MatchData#pre_match" do + it "returns the string before the match, equiv. special var $`" do + /(.)(.)(\d+)(\d)/.match("THX1138: The Movie").pre_match.should == 'T' + $`.should == 'T' + end + + it "sets the encoding to the encoding of the source String" do + str = "abc".dup.force_encoding Encoding::EUC_JP + str.match(/b/).pre_match.encoding.should equal(Encoding::EUC_JP) + end + + it "sets an empty result to the encoding of the source String" do + str = "abc".dup.force_encoding Encoding::ISO_8859_1 + str.match(/a/).pre_match.encoding.should equal(Encoding::ISO_8859_1) + end + + it "returns an instance of String when given a String subclass" do + str = MatchDataSpecs::MyString.new("THX1138: The Movie") + /(.)(.)(\d+)(\d)/.match(str).pre_match.should be_an_instance_of(String) + end +end diff --git a/spec/ruby/core/matchdata/regexp_spec.rb b/spec/ruby/core/matchdata/regexp_spec.rb new file mode 100644 index 0000000000..099b59c559 --- /dev/null +++ b/spec/ruby/core/matchdata/regexp_spec.rb @@ -0,0 +1,24 @@ +require_relative '../../spec_helper' + +describe "MatchData#regexp" do + it "returns a Regexp object" do + m = 'haystack'.match(/hay/) + m.regexp.should be_an_instance_of(Regexp) + end + + it "returns the pattern used in the match" do + m = 'haystack'.match(/hay/) + m.regexp.should == /hay/ + end + + it "returns the same Regexp used to match" do + r = /hay/ + m = 'haystack'.match(r) + m.regexp.object_id.should == r.object_id + end + + it "returns a Regexp for the result of gsub(String)" do + 'he[[o'.gsub('[', ']') + $~.regexp.should == /\[/ + end +end diff --git a/spec/ruby/core/matchdata/shared/captures.rb b/spec/ruby/core/matchdata/shared/captures.rb new file mode 100644 index 0000000000..33f834561a --- /dev/null +++ b/spec/ruby/core/matchdata/shared/captures.rb @@ -0,0 +1,13 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' + +describe :matchdata_captures, shared: true do + it "returns an array of the match captures" do + /(.)(.)(\d+)(\d)/.match("THX1138.").send(@method).should == ["H","X","113","8"] + end + + it "returns instances of String when given a String subclass" do + str = MatchDataSpecs::MyString.new("THX1138: The Movie") + /(.)(.)(\d+)(\d)/.match(str).send(@method).each { |c| c.should be_an_instance_of(String) } + end +end diff --git a/spec/ruby/core/matchdata/shared/eql.rb b/spec/ruby/core/matchdata/shared/eql.rb new file mode 100644 index 0000000000..e021baa178 --- /dev/null +++ b/spec/ruby/core/matchdata/shared/eql.rb @@ -0,0 +1,26 @@ +require_relative '../../../spec_helper' + +describe :matchdata_eql, shared: true do + it "returns true if both operands have equal target strings, patterns, and match positions" do + a = 'haystack'.match(/hay/) + b = 'haystack'.match(/hay/) + a.send(@method, b).should be_true + end + + it "returns false if the operands have different target strings" do + a = 'hay'.match(/hay/) + b = 'haystack'.match(/hay/) + a.send(@method, b).should be_false + end + + it "returns false if the operands have different patterns" do + a = 'haystack'.match(/h.y/) + b = 'haystack'.match(/hay/) + a.send(@method, b).should be_false + end + + it "returns false if the argument is not a MatchData object" do + a = 'haystack'.match(/hay/) + a.send(@method, Object.new).should be_false + end +end diff --git a/spec/ruby/core/matchdata/shared/length.rb b/spec/ruby/core/matchdata/shared/length.rb new file mode 100644 index 0000000000..6312a7ed4c --- /dev/null +++ b/spec/ruby/core/matchdata/shared/length.rb @@ -0,0 +1,5 @@ +describe :matchdata_length, shared: true do + it "length should return the number of elements in the match array" do + /(.)(.)(\d+)(\d)/.match("THX1138.").send(@method).should == 5 + end +end diff --git a/spec/ruby/core/matchdata/size_spec.rb b/spec/ruby/core/matchdata/size_spec.rb new file mode 100644 index 0000000000..b4965db3b8 --- /dev/null +++ b/spec/ruby/core/matchdata/size_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/length' + +describe "MatchData#size" do + it_behaves_like :matchdata_length, :size +end diff --git a/spec/ruby/core/matchdata/string_spec.rb b/spec/ruby/core/matchdata/string_spec.rb new file mode 100644 index 0000000000..952e953318 --- /dev/null +++ b/spec/ruby/core/matchdata/string_spec.rb @@ -0,0 +1,26 @@ +require_relative '../../spec_helper' + +describe "MatchData#string" do + it "returns a copy of the match string" do + str = /(.)(.)(\d+)(\d)/.match("THX1138.").string + str.should == "THX1138." + end + + it "returns a frozen copy of the match string" do + str = /(.)(.)(\d+)(\d)/.match("THX1138.").string + str.should == "THX1138." + str.should.frozen? + end + + it "returns the same frozen string for every call" do + md = /(.)(.)(\d+)(\d)/.match("THX1138.") + md.string.should equal(md.string) + end + + it "returns a frozen copy of the matched string for gsub!(String)" do + s = +'he[[o' + s.gsub!('[', ']') + $~.string.should == 'he[[o' + $~.string.should.frozen? + end +end diff --git a/spec/ruby/core/matchdata/to_a_spec.rb b/spec/ruby/core/matchdata/to_a_spec.rb new file mode 100644 index 0000000000..4fa11ff604 --- /dev/null +++ b/spec/ruby/core/matchdata/to_a_spec.rb @@ -0,0 +1,13 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "MatchData#to_a" do + it "returns an array of matches" do + /(.)(.)(\d+)(\d)/.match("THX1138.").to_a.should == ["HX1138", "H", "X", "113", "8"] + end + + it "returns instances of String when given a String subclass" do + str = MatchDataSpecs::MyString.new("THX1138.") + /(.)(.)(\d+)(\d)/.match(str)[0..-1].to_a.each { |m| m.should be_an_instance_of(String) } + end +end diff --git a/spec/ruby/core/matchdata/to_s_spec.rb b/spec/ruby/core/matchdata/to_s_spec.rb new file mode 100644 index 0000000000..cd1c4dbca2 --- /dev/null +++ b/spec/ruby/core/matchdata/to_s_spec.rb @@ -0,0 +1,13 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "MatchData#to_s" do + it "returns the entire matched string" do + /(.)(.)(\d+)(\d)/.match("THX1138.").to_s.should == "HX1138" + end + + it "returns an instance of String when given a String subclass" do + str = MatchDataSpecs::MyString.new("THX1138.") + /(.)(.)(\d+)(\d)/.match(str).to_s.should be_an_instance_of(String) + end +end diff --git a/spec/ruby/core/matchdata/values_at_spec.rb b/spec/ruby/core/matchdata/values_at_spec.rb new file mode 100644 index 0000000000..535719a2ee --- /dev/null +++ b/spec/ruby/core/matchdata/values_at_spec.rb @@ -0,0 +1,76 @@ +require_relative '../../spec_helper' + +describe "MatchData#values_at" do + # Should be synchronized with core/array/values_at_spec.rb and core/struct/values_at_spec.rb + # + # /(.)(.)(\d+)(\d)/.match("THX1138: The Movie").to_a # => ["HX1138", "H", "X", "113", "8"] + + context "when passed a list of Integers" do + it "returns an array containing each value given by one of integers" do + /(.)(.)(\d+)(\d)/.match("THX1138: The Movie").values_at(0, 2, -2).should == ["HX1138", "X", "113"] + end + + it "returns nil value for any integer that is out of range" do + /(.)(.)(\d+)(\d)/.match("THX1138: The Movie").values_at(5).should == [nil] + /(.)(.)(\d+)(\d)/.match("THX1138: The Movie").values_at(-6).should == [nil] + end + end + + context "when passed an integer Range" do + it "returns an array containing each value given by the elements of the range" do + /(.)(.)(\d+)(\d)/.match("THX1138: The Movie").values_at(0..2).should == ["HX1138", "H", "X"] + end + + it "fills with nil values for range elements larger than the captured values number" do + /(.)(.)(\d+)(\d)/.match("THX1138: The Movie").values_at(0..5).should == ["HX1138", "H", "X", "113", "8", nil] + end + + it "raises RangeError if any element of the range is negative and out of range" do + -> { /(.)(.)(\d+)(\d)/.match("THX1138: The Movie").values_at(-6..3) }.should raise_error(RangeError, "-6..3 out of range") + end + + it "supports endless Range" do + /(.)(.)(\d+)(\d)/.match("THX1138: The Movie").values_at(0..).should == ["HX1138", "H", "X", "113", "8"] + end + + it "supports beginningless Range" do + /(.)(.)(\d+)(\d)/.match("THX1138: The Movie").values_at(..2).should == ["HX1138", "H", "X"] + end + + it "returns an empty Array when Range is empty" do + /(.)(.)(\d+)(\d)/.match("THX1138: The Movie").values_at(2..0).should == [] + end + end + + context "when passed names" do + it 'slices captures with the given names' do + /(?<a>.)(?<b>.)(?<c>.)/.match('012').values_at(:c, :a).should == ['2', '0'] + end + + it 'slices captures with the given String names' do + /(?<a>.)(?<b>.)(?<c>.)/.match('012').values_at('c', 'a').should == ['2', '0'] + end + end + + it "supports multiple integer Ranges" do + /(.)(.)(\d+)(\d)/.match("THX1138: The Movie").values_at(1..2, 2..3).should == ["H", "X", "X", "113"] + end + + it "supports mixing integer Ranges and Integers" do + /(.)(.)(\d+)(\d)/.match("THX1138: The Movie").values_at(1..2, 4).should == ["H", "X", "8"] + end + + it 'supports mixing of names and indices' do + /\A(?<a>.)(?<b>.)\z/.match('01').values_at(0, 1, 2, :a, :b).should == ['01', '0', '1', '0', '1'] + end + + it "returns a new empty Array if no arguments given" do + /(.)(.)(\d+)(\d)/.match("THX1138: The Movie").values_at().should == [] + end + + it "fails when passed arguments of unsupported types" do + -> { + /(.)(.)(\d+)(\d)/.match("THX1138: The Movie").values_at(Object.new) + }.should raise_error(TypeError, "no implicit conversion of Object into Integer") + end +end diff --git a/spec/ruby/core/math/acos_spec.rb b/spec/ruby/core/math/acos_spec.rb new file mode 100644 index 0000000000..8b321ab29b --- /dev/null +++ b/spec/ruby/core/math/acos_spec.rb @@ -0,0 +1,56 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +# arccosine : (-1.0, 1.0) --> (0, PI) +describe "Math.acos" do + before :each do + ScratchPad.clear + end + + it "returns a float" do + Math.acos(1).should be_kind_of(Float ) + end + + it "returns the arccosine of the argument" do + Math.acos(1).should be_close(0.0, TOLERANCE) + Math.acos(0).should be_close(1.5707963267949, TOLERANCE) + Math.acos(-1).should be_close(Math::PI,TOLERANCE) + Math.acos(0.25).should be_close(1.31811607165282, TOLERANCE) + Math.acos(0.50).should be_close(1.0471975511966 , TOLERANCE) + Math.acos(0.75).should be_close(0.722734247813416, TOLERANCE) + end + + it "raises an Math::DomainError if the argument is greater than 1.0" do + -> { Math.acos(1.0001) }.should raise_error(Math::DomainError) + end + + it "raises an Math::DomainError if the argument is less than -1.0" do + -> { Math.acos(-1.0001) }.should raise_error(Math::DomainError) + end + + it "raises a TypeError if the string argument cannot be coerced with Float()" do + -> { Math.acos("test") }.should raise_error(TypeError) + end + + it "returns NaN given NaN" do + Math.acos(nan_value).nan?.should be_true + end + + it "raises a TypeError if the argument cannot be coerced with Float()" do + -> { Math.acos(MathSpecs::UserClass.new) }.should raise_error(TypeError) + end + + it "raises a TypeError if the argument is nil" do + -> { Math.acos(nil) }.should raise_error(TypeError) + end + + it "accepts any argument that can be coerced with Float()" do + Math.acos(MathSpecs::Float.new(0.5)).should be_close(Math.acos(0.5), TOLERANCE) + end +end + +describe "Math#acos" do + it "is accessible as a private instance method" do + IncludesMath.new.send(:acos, 0).should be_close(1.5707963267949, TOLERANCE) + end +end diff --git a/spec/ruby/core/math/acosh_spec.rb b/spec/ruby/core/math/acosh_spec.rb new file mode 100644 index 0000000000..6707de95d3 --- /dev/null +++ b/spec/ruby/core/math/acosh_spec.rb @@ -0,0 +1,41 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Math.acosh" do + it "returns a float" do + Math.acosh(1.0).should be_kind_of(Float) + end + + it "returns the principle value of the inverse hyperbolic cosine of the argument" do + Math.acosh(14.2).should be_close(3.345146999647, TOLERANCE) + Math.acosh(1.0).should be_close(0.0, TOLERANCE) + end + + it "raises Math::DomainError if the passed argument is less than -1.0 or greater than 1.0" do + -> { Math.acosh(1.0 - TOLERANCE) }.should raise_error(Math::DomainError) + -> { Math.acosh(0) }.should raise_error(Math::DomainError) + -> { Math.acosh(-1.0) }.should raise_error(Math::DomainError) + end + + it "raises a TypeError if the argument cannot be coerced with Float()" do + -> { Math.acosh("test") }.should raise_error(TypeError) + end + + it "returns NaN given NaN" do + Math.acosh(nan_value).nan?.should be_true + end + + it "raises a TypeError if the argument is nil" do + -> { Math.acosh(nil) }.should raise_error(TypeError) + end + + it "accepts any argument that can be coerced with Float()" do + Math.acosh(MathSpecs::Float.new).should == 0.0 + end +end + +describe "Math#acosh" do + it "is accessible as a private instance method" do + IncludesMath.new.send(:acosh, 1.0).should be_close(0.0, TOLERANCE) + end +end diff --git a/spec/ruby/core/math/asin_spec.rb b/spec/ruby/core/math/asin_spec.rb new file mode 100644 index 0000000000..3a674a1147 --- /dev/null +++ b/spec/ruby/core/math/asin_spec.rb @@ -0,0 +1,48 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +# arcsine : (-1.0, 1.0) --> (-PI/2, PI/2) +describe "Math.asin" do + it "returns a float" do + Math.asin(1).should be_kind_of(Float) + end + + it "returns the arcsine of the argument" do + Math.asin(1).should be_close(Math::PI/2, TOLERANCE) + Math.asin(0).should be_close(0.0, TOLERANCE) + Math.asin(-1).should be_close(-Math::PI/2, TOLERANCE) + Math.asin(0.25).should be_close(0.252680255142079, TOLERANCE) + Math.asin(0.50).should be_close(0.523598775598299, TOLERANCE) + Math.asin(0.75).should be_close(0.8480620789814816,TOLERANCE) + end + + it "raises an Math::DomainError if the argument is greater than 1.0" do + -> { Math.asin(1.0001) }.should raise_error( Math::DomainError) + end + + it "raises an Math::DomainError if the argument is less than -1.0" do + -> { Math.asin(-1.0001) }.should raise_error( Math::DomainError) + end + + it "raises a TypeError if the argument cannot be coerced with Float()" do + -> { Math.asin("test") }.should raise_error(TypeError) + end + + it "returns NaN given NaN" do + Math.asin(nan_value).nan?.should be_true + end + + it "raises a TypeError if the argument is nil" do + -> { Math.asin(nil) }.should raise_error(TypeError) + end + + it "accepts any argument that can be coerced with Float()" do + Math.asin(MathSpecs::Float.new).should be_close(1.5707963267949, TOLERANCE) + end +end + +describe "Math#asin" do + it "is accessible as a private instance method" do + IncludesMath.new.send(:asin, 0.5).should be_close(0.523598775598299, TOLERANCE) + end +end diff --git a/spec/ruby/core/math/asinh_spec.rb b/spec/ruby/core/math/asinh_spec.rb new file mode 100644 index 0000000000..ff8210df0a --- /dev/null +++ b/spec/ruby/core/math/asinh_spec.rb @@ -0,0 +1,42 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Math.asinh" do + it "returns a float" do + Math.asinh(1.5).should be_kind_of(Float) + end + + it "returns the inverse hyperbolic sin of the argument" do + Math.asinh(1.5).should be_close(1.19476321728711, TOLERANCE) + Math.asinh(-2.97).should be_close(-1.8089166921397, TOLERANCE) + Math.asinh(0.0).should == 0.0 + Math.asinh(-0.0).should == -0.0 + Math.asinh(1.05367e-08).should be_close(1.05367e-08, TOLERANCE) + Math.asinh(-1.05367e-08).should be_close(-1.05367e-08, TOLERANCE) + # Default tolerance does not scale right for these... + #Math.asinh(94906265.62).should be_close(19.0615, TOLERANCE) + #Math.asinh(-94906265.62).should be_close(-19.0615, TOLERANCE) + end + + it "raises a TypeError if the argument cannot be coerced with Float()" do + -> { Math.asinh("test") }.should raise_error(TypeError) + end + + it "returns NaN given NaN" do + Math.asinh(nan_value).nan?.should be_true + end + + it "raises a TypeError if the argument is nil" do + -> { Math.asinh(nil) }.should raise_error(TypeError) + end + + it "accepts any argument that can be coerced with Float()" do + Math.asinh(MathSpecs::Float.new).should be_close(0.881373587019543, TOLERANCE) + end +end + +describe "Math#asinh" do + it "is accessible as a private instance method" do + IncludesMath.new.send(:asinh, 19.275).should be_close(3.65262832292466, TOLERANCE) + end +end diff --git a/spec/ruby/core/math/atan2_spec.rb b/spec/ruby/core/math/atan2_spec.rb new file mode 100644 index 0000000000..d4ef369d2a --- /dev/null +++ b/spec/ruby/core/math/atan2_spec.rb @@ -0,0 +1,54 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Math.atan2" do + it "returns a float" do + Math.atan2(1.2, 0.5).should be_kind_of(Float) + end + + it "returns the arc tangent of y, x" do + Math.atan2(4.2, 0.3).should be_close(1.49948886200961, TOLERANCE) + Math.atan2(0.0, 1.0).should be_close(0.0, TOLERANCE) + Math.atan2(-9.1, 3.2).should be_close(-1.23265379809025, TOLERANCE) + Math.atan2(7.22, -3.3).should be_close(1.99950888779256, TOLERANCE) + end + + it "raises a TypeError if the argument cannot be coerced with Float()" do + -> { Math.atan2(1.0, "test") }.should raise_error(TypeError) + -> { Math.atan2("test", 0.0) }.should raise_error(TypeError) + -> { Math.atan2("test", "this") }.should raise_error(TypeError) + end + + it "raises a TypeError if the argument is nil" do + -> { Math.atan2(nil, 1.0) }.should raise_error(TypeError) + -> { Math.atan2(-1.0, nil) }.should raise_error(TypeError) + -> { Math.atan2(nil, nil) }.should raise_error(TypeError) + end + + it "accepts any argument that can be coerced with Float()" do + Math.atan2(MathSpecs::Float.new, MathSpecs::Float.new).should be_close(0.785398163397448, TOLERANCE) + end + + it "returns positive zero when passed 0.0, 0.0" do + Math.atan2(0.0, 0.0).should be_positive_zero + end + + it "returns negative zero when passed -0.0, 0.0" do + Math.atan2(-0.0, 0.0).should be_negative_zero + end + + it "returns Pi when passed 0.0, -0.0" do + Math.atan2(0.0, -0.0).should == Math::PI + end + + it "returns -Pi when passed -0.0, -0.0" do + Math.atan2(-0.0, -0.0).should == -Math::PI + end + +end + +describe "Math#atan2" do + it "is accessible as a private instance method" do + IncludesMath.new.send(:atan2, 1.1, 2.2).should be_close(0.463647609000806, TOLERANCE) + end +end diff --git a/spec/ruby/core/math/atan_spec.rb b/spec/ruby/core/math/atan_spec.rb new file mode 100644 index 0000000000..15edf68c05 --- /dev/null +++ b/spec/ruby/core/math/atan_spec.rb @@ -0,0 +1,40 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +# arctangent : (-Inf, Inf) --> (-PI/2, PI/2) +describe "Math.atan" do + it "returns a float" do + Math.atan(1).should be_kind_of(Float) + end + + it "returns the arctangent of the argument" do + Math.atan(1).should be_close(Math::PI/4, TOLERANCE) + Math.atan(0).should be_close(0.0, TOLERANCE) + Math.atan(-1).should be_close(-Math::PI/4, TOLERANCE) + Math.atan(0.25).should be_close(0.244978663126864, TOLERANCE) + Math.atan(0.50).should be_close(0.463647609000806, TOLERANCE) + Math.atan(0.75).should be_close(0.643501108793284, TOLERANCE) + end + + it "raises a TypeError if the argument cannot be coerced with Float()" do + -> { Math.atan("test") }.should raise_error(TypeError) + end + + it "returns NaN given NaN" do + Math.atan(nan_value).nan?.should be_true + end + + it "raises a TypeError if the argument is nil" do + -> { Math.atan(nil) }.should raise_error(TypeError) + end + + it "accepts any argument that can be coerced with Float()" do + Math.atan(MathSpecs::Float.new).should be_close(0.785398163397448, TOLERANCE) + end +end + +describe "Math#atan" do + it "is accessible as a private instance method" do + IncludesMath.new.send(:atan, 3.1415).should be_close(1.2626187313511, TOLERANCE) + end +end diff --git a/spec/ruby/core/math/atanh_spec.rb b/spec/ruby/core/math/atanh_spec.rb new file mode 100644 index 0000000000..edcb8f2e52 --- /dev/null +++ b/spec/ruby/core/math/atanh_spec.rb @@ -0,0 +1,14 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' +require_relative 'shared/atanh' + +describe "Math.atanh" do + it_behaves_like :math_atanh_base, :atanh, Math + it_behaves_like :math_atanh_no_complex, :atanh, Math +end + +describe "Math#atanh" do + it_behaves_like :math_atanh_private, :atanh + it_behaves_like :math_atanh_base, :atanh, IncludesMath.new + it_behaves_like :math_atanh_no_complex, :atanh, IncludesMath.new +end diff --git a/spec/ruby/core/math/cbrt_spec.rb b/spec/ruby/core/math/cbrt_spec.rb new file mode 100644 index 0000000000..01cf923c71 --- /dev/null +++ b/spec/ruby/core/math/cbrt_spec.rb @@ -0,0 +1,27 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Math.cbrt" do + it "returns a float" do + Math.cbrt(1).should be_an_instance_of(Float) + end + + it "returns the cubic root of the argument" do + Math.cbrt(1).should == 1.0 + Math.cbrt(8.0).should == 2.0 + Math.cbrt(-8.0).should == -2.0 + Math.cbrt(3).should be_close(1.44224957030741, TOLERANCE) + end + + it "raises a TypeError if the argument cannot be coerced with Float()" do + -> { Math.cbrt("foobar") }.should raise_error(TypeError) + end + + it "raises a TypeError if the argument is nil" do + -> { Math.cbrt(nil) }.should raise_error(TypeError) + end + + it "accepts any argument that can be coerced with Float()" do + Math.cbrt(MathSpecs::Float.new).should be_close(1.0, TOLERANCE) + end +end diff --git a/spec/ruby/core/math/constants_spec.rb b/spec/ruby/core/math/constants_spec.rb new file mode 100644 index 0000000000..b500b21a79 --- /dev/null +++ b/spec/ruby/core/math/constants_spec.rb @@ -0,0 +1,22 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Math::PI" do + it "approximates the value of pi" do + Math::PI.should be_close(3.14159_26535_89793_23846, TOLERANCE) + end + + it "is accessible to a class that includes Math" do + IncludesMath::PI.should == Math::PI + end +end + +describe "Math::E" do + it "approximates the value of Napier's constant" do + Math::E.should be_close(2.71828_18284_59045_23536, TOLERANCE) + end + + it "is accessible to a class that includes Math" do + IncludesMath::E.should == Math::E + end +end diff --git a/spec/ruby/core/math/cos_spec.rb b/spec/ruby/core/math/cos_spec.rb new file mode 100644 index 0000000000..006afeb2cc --- /dev/null +++ b/spec/ruby/core/math/cos_spec.rb @@ -0,0 +1,50 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +# cosine : (-Inf, Inf) --> (-1.0, 1.0) +describe "Math.cos" do + it "returns a float" do + Math.cos(Math::PI).should be_kind_of(Float) + end + + it "returns the cosine of the argument expressed in radians" do + Math.cos(Math::PI).should be_close(-1.0, TOLERANCE) + Math.cos(0).should be_close(1.0, TOLERANCE) + Math.cos(Math::PI/2).should be_close(0.0, TOLERANCE) + Math.cos(3*Math::PI/2).should be_close(0.0, TOLERANCE) + Math.cos(2*Math::PI).should be_close(1.0, TOLERANCE) + end + + it "raises a TypeError unless the argument is Numeric and has #to_f" do + -> { Math.cos("test") }.should raise_error(TypeError) + end + + it "returns NaN given NaN" do + Math.cos(nan_value).nan?.should be_true + end + + describe "coerces its argument with #to_f" do + it "coerces its argument with #to_f" do + f = mock_numeric('8.2') + f.should_receive(:to_f).and_return(8.2) + Math.cos(f).should == Math.cos(8.2) + end + + it "raises a TypeError if the given argument can't be converted to a Float" do + -> { Math.cos(nil) }.should raise_error(TypeError) + -> { Math.cos(:abc) }.should raise_error(TypeError) + end + + it "raises a NoMethodError if the given argument raises a NoMethodError during type coercion to a Float" do + object = mock_numeric('mock-float') + object.should_receive(:to_f).and_raise(NoMethodError) + -> { Math.cos(object) }.should raise_error(NoMethodError) + end + end +end + +describe "Math#cos" do + it "is accessible as a private instance method" do + IncludesMath.new.send(:cos, 3.1415).should be_close(-0.999999995707656, TOLERANCE) + end +end diff --git a/spec/ruby/core/math/cosh_spec.rb b/spec/ruby/core/math/cosh_spec.rb new file mode 100644 index 0000000000..049e117e56 --- /dev/null +++ b/spec/ruby/core/math/cosh_spec.rb @@ -0,0 +1,37 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Math.cosh" do + it "returns a float" do + Math.cosh(1.0).should be_kind_of(Float) + end + + it "returns the hyperbolic cosine of the argument" do + Math.cosh(0.0).should == 1.0 + Math.cosh(-0.0).should == 1.0 + Math.cosh(1.5).should be_close(2.35240961524325, TOLERANCE) + Math.cosh(-2.99).should be_close(9.96798496414416, TOLERANCE) + end + + it "raises a TypeError if the argument cannot be coerced with Float()" do + -> { Math.cosh("test") }.should raise_error(TypeError) + end + + it "returns NaN given NaN" do + Math.cosh(nan_value).nan?.should be_true + end + + it "raises a TypeError if the argument is nil" do + -> { Math.cosh(nil) }.should raise_error(TypeError) + end + + it "accepts any argument that can be coerced with Float()" do + Math.cosh(MathSpecs::Float.new).should be_close(1.54308063481524, TOLERANCE) + end +end + +describe "Math#cosh" do + it "is accessible as a private instance method" do + IncludesMath.new.send(:cos, 3.1415).should be_close(-0.999999995707656, TOLERANCE) + end +end diff --git a/spec/ruby/core/math/erf_spec.rb b/spec/ruby/core/math/erf_spec.rb new file mode 100644 index 0000000000..b4e6248e43 --- /dev/null +++ b/spec/ruby/core/math/erf_spec.rb @@ -0,0 +1,44 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +# erf method is the "error function" encountered in integrating the normal +# distribution (which is a normalized form of the Gaussian function). +describe "Math.erf" do + it "returns a float" do + Math.erf(1).should be_kind_of(Float) + end + + it "returns the error function of the argument" do + Math.erf(0).should be_close(0.0, TOLERANCE) + Math.erf(1).should be_close(0.842700792949715, TOLERANCE) + Math.erf(-1).should be_close(-0.842700792949715, TOLERANCE) + Math.erf(0.5).should be_close(0.520499877813047, TOLERANCE) + Math.erf(-0.5).should be_close(-0.520499877813047, TOLERANCE) + Math.erf(10000).should be_close(1.0, TOLERANCE) + Math.erf(-10000).should be_close(-1.0, TOLERANCE) + Math.erf(0.00000000000001).should be_close(0.0, TOLERANCE) + Math.erf(-0.00000000000001).should be_close(0.0, TOLERANCE) + end + + it "raises a TypeError if the argument cannot be coerced with Float()" do + -> { Math.erf("test") }.should raise_error(TypeError) + end + + it "returns NaN given NaN" do + Math.erf(nan_value).nan?.should be_true + end + + it "raises a TypeError if the argument is nil" do + -> { Math.erf(nil) }.should raise_error(TypeError) + end + + it "accepts any argument that can be coerced with Float()" do + Math.erf(MathSpecs::Float.new).should be_close(0.842700792949715, TOLERANCE) + end +end + +describe "Math#erf" do + it "is accessible as a private instance method" do + IncludesMath.new.send(:erf, 3.1415).should be_close(0.999991118444483, TOLERANCE) + end +end diff --git a/spec/ruby/core/math/erfc_spec.rb b/spec/ruby/core/math/erfc_spec.rb new file mode 100644 index 0000000000..e465f5cf58 --- /dev/null +++ b/spec/ruby/core/math/erfc_spec.rb @@ -0,0 +1,43 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +# erfc is the complementary error function +describe "Math.erfc" do + it "returns a float" do + Math.erf(1).should be_kind_of(Float) + end + + it "returns the complementary error function of the argument" do + Math.erfc(0).should be_close(1.0, TOLERANCE) + Math.erfc(1).should be_close(0.157299207050285, TOLERANCE) + Math.erfc(-1).should be_close(1.84270079294971, TOLERANCE) + Math.erfc(0.5).should be_close(0.479500122186953, TOLERANCE) + Math.erfc(-0.5).should be_close(1.52049987781305, TOLERANCE) + Math.erfc(10000).should be_close(0.0, TOLERANCE) + Math.erfc(-10000).should be_close(2.0, TOLERANCE) + Math.erfc(0.00000000000001).should be_close(0.999999999999989, TOLERANCE) + Math.erfc(-0.00000000000001).should be_close(1.00000000000001, TOLERANCE) + end + + it "raises a TypeError if the argument cannot be coerced with Float()" do + -> { Math.erfc("test") }.should raise_error(TypeError) + end + + it "returns NaN given NaN" do + Math.erfc(nan_value).nan?.should be_true + end + + it "raises a TypeError if the argument is nil" do + -> { Math.erfc(nil) }.should raise_error(TypeError) + end + + it "accepts any argument that can be coerced with Float()" do + Math.erfc(MathSpecs::Float.new).should be_close(0.157299207050285, TOLERANCE) + end +end + +describe "Math#erfc" do + it "is accessible as a private instance method" do + IncludesMath.new.send(:erf, 3.1415).should be_close(0.999991118444483, TOLERANCE) + end +end diff --git a/spec/ruby/core/math/exp_spec.rb b/spec/ruby/core/math/exp_spec.rb new file mode 100644 index 0000000000..36eb49a8c7 --- /dev/null +++ b/spec/ruby/core/math/exp_spec.rb @@ -0,0 +1,37 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Math.exp" do + it "returns a float" do + Math.exp(1.0).should be_kind_of(Float) + end + + it "returns the base-e exponential of the argument" do + Math.exp(0.0).should == 1.0 + Math.exp(-0.0).should == 1.0 + Math.exp(-1.8).should be_close(0.165298888221587, TOLERANCE) + Math.exp(1.25).should be_close(3.49034295746184, TOLERANCE) + end + + it "raises a TypeError if the argument cannot be coerced with Float()" do + -> { Math.exp("test") }.should raise_error(TypeError) + end + + it "returns NaN given NaN" do + Math.exp(nan_value).nan?.should be_true + end + + it "raises a TypeError if the argument is nil" do + -> { Math.exp(nil) }.should raise_error(TypeError) + end + + it "accepts any argument that can be coerced with Float()" do + Math.exp(MathSpecs::Float.new).should be_close(Math::E, TOLERANCE) + end +end + +describe "Math#exp" do + it "is accessible as a private instance method" do + IncludesMath.new.send(:exp, 23.1415).should be_close(11226018484.0012, TOLERANCE) + end +end diff --git a/spec/ruby/core/math/expm1_spec.rb b/spec/ruby/core/math/expm1_spec.rb new file mode 100644 index 0000000000..5725319abb --- /dev/null +++ b/spec/ruby/core/math/expm1_spec.rb @@ -0,0 +1,37 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +ruby_version_is "4.0" do + describe "Math.expm1" do + it "calculates Math.exp(arg) - 1" do + Math.expm1(3).should == Math.exp(3) - 1 + end + + it "preserves precision that can be lost otherwise" do + Math.expm1(1.0e-16).should be_close(1.0e-16, TOLERANCE) + Math.expm1(1.0e-16).should != 0.0 + end + + it "raises a TypeError if the argument cannot be coerced with Float()" do + -> { Math.expm1("test") }.should raise_error(TypeError, "can't convert String into Float") + end + + it "returns NaN given NaN" do + Math.expm1(nan_value).nan?.should be_true + end + + it "raises a TypeError if the argument is nil" do + -> { Math.expm1(nil) }.should raise_error(TypeError, "can't convert nil into Float") + end + + it "accepts any argument that can be coerced with Float()" do + Math.expm1(MathSpecs::Float.new).should be_close(Math::E - 1, TOLERANCE) + end + end + + describe "Math#expm1" do + it "is accessible as a private instance method" do + IncludesMath.new.send(:expm1, 23.1415).should be_close(11226018483.0012, TOLERANCE) + end + end +end diff --git a/spec/ruby/core/math/fixtures/classes.rb b/spec/ruby/core/math/fixtures/classes.rb new file mode 100644 index 0000000000..6f2241e739 --- /dev/null +++ b/spec/ruby/core/math/fixtures/classes.rb @@ -0,0 +1,28 @@ +class IncludesMath + include Math +end + +module MathSpecs + class Float < Numeric + def initialize(value=1.0) + @value = value + end + + def to_f + @value + end + end + + class Integer + def to_int + 2 + end + end + + class UserClass + end + + class StringSubClass < String + end + +end diff --git a/spec/ruby/core/math/fixtures/common.rb b/spec/ruby/core/math/fixtures/common.rb new file mode 100644 index 0000000000..024732fa7a --- /dev/null +++ b/spec/ruby/core/math/fixtures/common.rb @@ -0,0 +1,3 @@ +class IncludesMath + include Math +end diff --git a/spec/ruby/core/math/frexp_spec.rb b/spec/ruby/core/math/frexp_spec.rb new file mode 100644 index 0000000000..7dfb493d20 --- /dev/null +++ b/spec/ruby/core/math/frexp_spec.rb @@ -0,0 +1,37 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Math.frexp" do + it "returns the normalized fraction and exponent" do + frac, exp = Math.frexp(102.83) + frac.should be_close(0.803359375, TOLERANCE) + exp.should == 7 + end + + it "raises a TypeError if the argument cannot be coerced with Float()" do + -> { Math.frexp("test") }.should raise_error(TypeError) + end + + it "returns NaN given NaN" do + frac, _exp = Math.frexp(nan_value) + frac.nan?.should be_true + end + + it "raises a TypeError if the argument is nil" do + -> { Math.frexp(nil) }.should raise_error(TypeError) + end + + it "accepts any argument that can be coerced with Float()" do + frac, exp = Math.frexp(MathSpecs::Float.new) + frac.should be_close(0.5, TOLERANCE) + exp.should == 1 + end +end + +describe "Math#frexp" do + it "is accessible as a private instance method" do + frac, exp = IncludesMath.new.send(:frexp, 2.1415) + frac.should be_close(0.535375, TOLERANCE) + exp.should == 2 + end +end diff --git a/spec/ruby/core/math/gamma_spec.rb b/spec/ruby/core/math/gamma_spec.rb new file mode 100644 index 0000000000..386162a087 --- /dev/null +++ b/spec/ruby/core/math/gamma_spec.rb @@ -0,0 +1,69 @@ +require_relative '../../spec_helper' + +describe "Math.gamma" do + it "returns +infinity given 0" do + Math.gamma(0).should == Float::INFINITY + end + + platform_is_not :windows do + # https://bugs.ruby-lang.org/issues/12249 + it "returns -infinity given -0.0" do + Math.gamma(-0.0).should == -Float::INFINITY + end + end + + it "returns Math.sqrt(Math::PI) given 0.5" do + Math.gamma(0.5).should be_close(Math.sqrt(Math::PI), TOLERANCE) + end + + # stop at n == 23 because 23! cannot be exactly represented by IEEE 754 double + it "returns exactly (n-1)! given n for n between 2 and 23" do + fact = 1 + 2.upto(23) do |n| + fact *= (n - 1) + Math.gamma(n).should == fact + end + end + + it "returns approximately (n-1)! given n for n between 24 and 30" do + fact2 = 1124000727777607680000 # 22! + 24.upto(30) do |n| + fact2 *= n - 1 + # compare only the first 12 places, tolerate the rest + Math.gamma(n).should be_close(fact2, fact2.to_s[12..-1].to_i) + end + end + + it "returns good numerical approximation for gamma(3.2)" do + Math.gamma(3.2).should be_close(2.423965, TOLERANCE) + end + + it "returns good numerical approximation for gamma(-2.15)" do + Math.gamma(-2.15).should be_close(-2.999619, TOLERANCE) + end + + it "returns good numerical approximation for gamma(0.00001)" do + Math.gamma(0.00001).should be_close(99999.422794, TOLERANCE) + end + + it "returns good numerical approximation for gamma(-0.00001)" do + Math.gamma(-0.00001).should be_close(-100000.577225, TOLERANCE) + end + + it "raises Math::DomainError given -1" do + -> { Math.gamma(-1) }.should raise_error(Math::DomainError) + end + + # See https://bugs.ruby-lang.org/issues/10642 + it "returns +infinity given +infinity" do + Math.gamma(infinity_value).infinite?.should == 1 + end + + it "raises Math::DomainError given negative infinity" do + -> { Math.gamma(-Float::INFINITY) }.should raise_error(Math::DomainError) + end + + it "returns NaN given NaN" do + Math.gamma(nan_value).nan?.should be_true + end +end diff --git a/spec/ruby/core/math/hypot_spec.rb b/spec/ruby/core/math/hypot_spec.rb new file mode 100644 index 0000000000..3e0ce74597 --- /dev/null +++ b/spec/ruby/core/math/hypot_spec.rb @@ -0,0 +1,41 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Math.hypot" do + it "returns a float" do + Math.hypot(3, 4).should be_kind_of(Float) + end + + it "returns the length of the hypotenuse of a right triangle with legs given by the arguments" do + Math.hypot(0, 0).should be_close(0.0, TOLERANCE) + Math.hypot(2, 10).should be_close( 10.1980390271856, TOLERANCE) + Math.hypot(5000, 5000).should be_close(7071.06781186548, TOLERANCE) + Math.hypot(0.0001, 0.0002).should be_close(0.000223606797749979, TOLERANCE) + Math.hypot(-2, -10).should be_close(10.1980390271856, TOLERANCE) + Math.hypot(2, 10).should be_close(10.1980390271856, TOLERANCE) + end + + it "raises a TypeError if the argument cannot be coerced with Float()" do + -> { Math.hypot("test", "this") }.should raise_error(TypeError) + end + + it "returns NaN given NaN" do + Math.hypot(nan_value, 0).nan?.should be_true + Math.hypot(0, nan_value).nan?.should be_true + Math.hypot(nan_value, nan_value).nan?.should be_true + end + + it "raises a TypeError if the argument is nil" do + -> { Math.hypot(nil, nil) }.should raise_error(TypeError) + end + + it "accepts any argument that can be coerced with Float()" do + Math.hypot(MathSpecs::Float.new, MathSpecs::Float.new).should be_close(1.4142135623731, TOLERANCE) + end +end + +describe "Math#hypot" do + it "is accessible as a private instance method" do + IncludesMath.new.send(:hypot, 2, 3.1415).should be_close(3.72411361937307, TOLERANCE) + end +end diff --git a/spec/ruby/core/math/ldexp_spec.rb b/spec/ruby/core/math/ldexp_spec.rb new file mode 100644 index 0000000000..6dcf94a663 --- /dev/null +++ b/spec/ruby/core/math/ldexp_spec.rb @@ -0,0 +1,60 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Math.ldexp" do + it "returns a float" do + Math.ldexp(1.0, 2).should be_kind_of(Float) + end + + it "returns the argument multiplied by 2**n" do + Math.ldexp(0.0, 0.0).should == 0.0 + Math.ldexp(0.0, 1.0).should == 0.0 + Math.ldexp(-1.25, 2).should be_close(-5.0, TOLERANCE) + Math.ldexp(2.1, -3).should be_close(0.2625, TOLERANCE) + Math.ldexp(5.7, 4).should be_close(91.2, TOLERANCE) + end + + it "raises a TypeError if the first argument cannot be coerced with Float()" do + -> { Math.ldexp("test", 2) }.should raise_error(TypeError) + end + + it "returns NaN given NaN" do + Math.ldexp(nan_value, 0).nan?.should be_true + end + + it "raises RangeError if NaN is given as the second arg" do + -> { Math.ldexp(0, nan_value) }.should raise_error(RangeError) + end + + it "raises a TypeError if the second argument cannot be coerced with Integer()" do + -> { Math.ldexp(3.2, "this") }.should raise_error(TypeError) + end + + it "raises a TypeError if the first argument is nil" do + -> { Math.ldexp(nil, 2) }.should raise_error(TypeError) + end + + it "raises a TypeError if the second argument is nil" do + -> { Math.ldexp(3.1, nil) }.should raise_error(TypeError) + end + + it "accepts any first argument that can be coerced with Float()" do + Math.ldexp(MathSpecs::Float.new, 2).should be_close(4.0, TOLERANCE) + end + + it "accepts any second argument that can be coerced with Integer()" do + Math.ldexp(3.23, MathSpecs::Integer.new).should be_close(12.92, TOLERANCE) + end + + it "returns correct value that closes to the max value of double type" do + Math.ldexp(0.5122058490966879, 1024).should == 9.207889385574391e+307 + Math.ldexp(0.9999999999999999, 1024).should == 1.7976931348623157e+308 + Math.ldexp(0.99999999999999999, 1024).should == Float::INFINITY + end +end + +describe "Math#ldexp" do + it "is accessible as a private instance method" do + IncludesMath.new.send(:ldexp, 3.1415, 2).should be_close(12.566, TOLERANCE) + end +end diff --git a/spec/ruby/core/math/lgamma_spec.rb b/spec/ruby/core/math/lgamma_spec.rb new file mode 100644 index 0000000000..2bf350993e --- /dev/null +++ b/spec/ruby/core/math/lgamma_spec.rb @@ -0,0 +1,51 @@ +require_relative '../../spec_helper' + +describe "Math.lgamma" do + it "returns [Infinity, 1] when passed 0" do + Math.lgamma(0).should == [infinity_value, 1] + end + + it "returns [Infinity, ...] when passed -1" do + Math.lgamma(-1)[0].should == infinity_value + end + + it "returns [Infinity, -1] when passed -0.0" do + Math.lgamma(-0.0).should == [infinity_value, -1] + end + + it "returns [log(sqrt(PI)), 1] when passed 0.5" do + lg1 = Math.lgamma(0.5) + lg1[0].should be_close(Math.log(Math.sqrt(Math::PI)), TOLERANCE) + lg1[1].should == 1 + end + + it "returns [log(2/3*PI, 1] when passed 6.0" do + lg2 = Math.lgamma(6.0) + lg2[0].should be_close(Math.log(120.0), TOLERANCE) + lg2[1].should == 1 + end + + it "returns an approximate value when passed -0.5" do + lg1 = Math.lgamma(-0.5) + lg1[0].should be_close(1.2655121, TOLERANCE) + lg1[1].should == -1 + end + + it "returns an approximate value when passed -1.5" do + lg2 = Math.lgamma(-1.5) + lg2[0].should be_close(0.8600470, TOLERANCE) + lg2[1].should == 1 + end + + it "raises Math::DomainError when passed -Infinity" do + -> { Math.lgamma(-infinity_value) }.should raise_error(Math::DomainError) + end + + it "returns [Infinity, 1] when passed Infinity" do + Math.lgamma(infinity_value).should == [infinity_value, 1] + end + + it "returns [NaN, ...] when passed NaN" do + Math.lgamma(nan_value)[0].should.nan? + end +end diff --git a/spec/ruby/core/math/log10_spec.rb b/spec/ruby/core/math/log10_spec.rb new file mode 100644 index 0000000000..c4daedcd5c --- /dev/null +++ b/spec/ruby/core/math/log10_spec.rb @@ -0,0 +1,43 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +# The common logarithm, having base 10 +describe "Math.log10" do + it "returns a float" do + Math.log10(1).should be_kind_of(Float) + end + + it "returns the base-10 logarithm of the argument" do + Math.log10(0.0001).should be_close(-4.0, TOLERANCE) + Math.log10(0.000000000001e-15).should be_close(-27.0, TOLERANCE) + Math.log10(1).should be_close(0.0, TOLERANCE) + Math.log10(10).should be_close(1.0, TOLERANCE) + Math.log10(10e15).should be_close(16.0, TOLERANCE) + end + + it "raises an Math::DomainError if the argument is less than 0" do + -> { Math.log10(-1e-15) }.should raise_error(Math::DomainError) + end + + it "raises a TypeError if the argument cannot be coerced with Float()" do + -> { Math.log10("test") }.should raise_error(TypeError) + end + + it "returns NaN given NaN" do + Math.log10(nan_value).nan?.should be_true + end + + it "raises a TypeError if the argument is nil" do + -> { Math.log10(nil) }.should raise_error(TypeError) + end + + it "accepts any argument that can be coerced with Float()" do + Math.log10(MathSpecs::Float.new).should be_close(0.0, TOLERANCE) + end +end + +describe "Math#log10" do + it "is accessible as a private instance method" do + IncludesMath.new.send(:log10, 4.15).should be_close(0.618048096712093, TOLERANCE) + end +end diff --git a/spec/ruby/core/math/log1p_spec.rb b/spec/ruby/core/math/log1p_spec.rb new file mode 100644 index 0000000000..216358a3c4 --- /dev/null +++ b/spec/ruby/core/math/log1p_spec.rb @@ -0,0 +1,49 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +ruby_version_is "4.0" do + describe "Math.log1p" do + it "calculates Math.log(1 + arg)" do + Math.log1p(3).should == Math.log(1 + 3) + end + + it "preserves precision that can be lost otherwise" do + Math.log1p(1e-16).should be_close(1.0e-16, TOLERANCE) + Math.log1p(1e-16).should != 0.0 + end + + it "raises an Math::DomainError if the argument is less than 1" do + -> { Math.log1p(-1-1e-15) }.should raise_error(Math::DomainError, "Numerical argument is out of domain - log1p") + end + + it "raises a TypeError if the argument cannot be coerced with Float()" do + -> { Math.log1p("test") }.should raise_error(TypeError, "can't convert String into Float") + end + + it "raises a TypeError for numerical values passed as string" do + -> { Math.log1p("10") }.should raise_error(TypeError, "can't convert String into Float") + end + + it "does not accept a second argument for the base" do + -> { Math.log1p(9, 3) }.should raise_error(ArgumentError, "wrong number of arguments (given 2, expected 1)") + end + + it "returns NaN given NaN" do + Math.log1p(nan_value).nan?.should be_true + end + + it "raises a TypeError if the argument is nil" do + -> { Math.log1p(nil) }.should raise_error(TypeError, "can't convert nil into Float") + end + + it "accepts any argument that can be coerced with Float()" do + Math.log1p(MathSpecs::Float.new).should be_close(0.6931471805599453, TOLERANCE) + end + end + + describe "Math#log1p" do + it "is accessible as a private instance method" do + IncludesMath.new.send(:log1p, 4.21).should be_close(1.65057985576528, TOLERANCE) + end + end +end diff --git a/spec/ruby/core/math/log2_spec.rb b/spec/ruby/core/math/log2_spec.rb new file mode 100644 index 0000000000..3d4d41d130 --- /dev/null +++ b/spec/ruby/core/math/log2_spec.rb @@ -0,0 +1,41 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Math.log2" do + it "returns a float" do + Math.log2(5.79).should be_close(2.53356334821451, TOLERANCE) + end + + it "returns the natural logarithm of the argument" do + Math.log2(1.1).should be_close(0.137503523749935, TOLERANCE) + Math.log2(3.14).should be_close(1.6507645591169, TOLERANCE) + Math.log2((2**101+45677544234809571)).should be_close(101.00000000000003, TOLERANCE) + + Math.log2((2**10001+45677544234809571)).should == 10001.0 + Math.log2((2**301+45677544234809571)).should == 301.0 + end + + it "raises Math::DomainError if the argument is less than 0" do + -> { Math.log2(-1e-15) }.should raise_error( Math::DomainError) + end + + it "raises a TypeError if the argument cannot be coerced with Float()" do + -> { Math.log2("test") }.should raise_error(TypeError) + end + + it "raises a TypeError if passed a numerical argument as a string" do + -> { Math.log2("1.0") }.should raise_error(TypeError) + end + + it "returns NaN given NaN" do + Math.log2(nan_value).nan?.should be_true + end + + it "raises a TypeError if the argument is nil" do + -> { Math.log2(nil) }.should raise_error(TypeError) + end + + it "accepts any argument that can be coerced with Float()" do + Math.log2(MathSpecs::Float.new).should be_close(0.0, TOLERANCE) + end +end diff --git a/spec/ruby/core/math/log_spec.rb b/spec/ruby/core/math/log_spec.rb new file mode 100644 index 0000000000..6c5036ba81 --- /dev/null +++ b/spec/ruby/core/math/log_spec.rb @@ -0,0 +1,57 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +# The natural logarithm, having base Math::E +describe "Math.log" do + it "returns a float" do + Math.log(1).should be_kind_of(Float) + end + + it "returns the natural logarithm of the argument" do + Math.log(0.0001).should be_close(-9.21034037197618, TOLERANCE) + Math.log(0.000000000001e-15).should be_close(-62.1697975108392, TOLERANCE) + Math.log(1).should be_close(0.0, TOLERANCE) + Math.log(10).should be_close( 2.30258509299405, TOLERANCE) + Math.log(10e15).should be_close(36.8413614879047, TOLERANCE) + end + + it "raises an Math::DomainError if the argument is less than 0" do + -> { Math.log(-1e-15) }.should raise_error(Math::DomainError) + end + + it "raises a TypeError if the argument cannot be coerced with Float()" do + -> { Math.log("test") }.should raise_error(TypeError) + end + + it "raises a TypeError for numerical values passed as string" do + -> { Math.log("10") }.should raise_error(TypeError) + end + + it "accepts a second argument for the base" do + Math.log(9, 3).should be_close(2, TOLERANCE) + Math.log(8, 1.4142135623730951).should be_close(6, TOLERANCE) + end + + it "raises a TypeError when the numerical base cannot be coerced to a float" do + -> { Math.log(10, "2") }.should raise_error(TypeError) + -> { Math.log(10, nil) }.should raise_error(TypeError) + end + + it "returns NaN given NaN" do + Math.log(nan_value).nan?.should be_true + end + + it "raises a TypeError if the argument is nil" do + -> { Math.log(nil) }.should raise_error(TypeError) + end + + it "accepts any argument that can be coerced with Float()" do + Math.log(MathSpecs::Float.new).should be_close(0.0, TOLERANCE) + end +end + +describe "Math#log" do + it "is accessible as a private instance method" do + IncludesMath.new.send(:log, 5.21).should be_close(1.65057985576528, TOLERANCE) + end +end diff --git a/spec/ruby/core/math/shared/atanh.rb b/spec/ruby/core/math/shared/atanh.rb new file mode 100644 index 0000000000..3fb64153a0 --- /dev/null +++ b/spec/ruby/core/math/shared/atanh.rb @@ -0,0 +1,44 @@ +describe :math_atanh_base, shared: true do + it "returns a float" do + @object.send(@method, 0.5).should be_an_instance_of(Float) + end + + it "returns the inverse hyperbolic tangent of the argument" do + @object.send(@method, 0.0).should == 0.0 + @object.send(@method, -0.0).should == -0.0 + @object.send(@method, 0.5).should be_close(0.549306144334055, TOLERANCE) + @object.send(@method, -0.2).should be_close(-0.202732554054082, TOLERANCE) + end + + it "raises a TypeError if the argument is nil" do + -> { @object.send(@method, nil) }.should raise_error(TypeError) + end + + it "raises a TypeError if the argument is not a Numeric" do + -> { @object.send(@method, "test") }.should raise_error(TypeError) + end + + it "returns Infinity if x == 1.0" do + @object.send(@method, 1.0).should == Float::INFINITY + end + + it "return -Infinity if x == -1.0" do + @object.send(@method, -1.0).should == -Float::INFINITY + end +end + +describe :math_atanh_private, shared: true do + it "is a private instance method" do + Math.should have_private_instance_method(@method) + end +end + +describe :math_atanh_no_complex, shared: true do + it "raises a Math::DomainError for arguments greater than 1.0" do + -> { @object.send(@method, 1.0 + Float::EPSILON) }.should raise_error(Math::DomainError) + end + + it "raises a Math::DomainError for arguments less than -1.0" do + -> { @object.send(@method, -1.0 - Float::EPSILON) }.should raise_error(Math::DomainError) + end +end diff --git a/spec/ruby/core/math/sin_spec.rb b/spec/ruby/core/math/sin_spec.rb new file mode 100644 index 0000000000..8e944bc95f --- /dev/null +++ b/spec/ruby/core/math/sin_spec.rb @@ -0,0 +1,39 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +# sine : (-Inf, Inf) --> (-1.0, 1.0) +describe "Math.sin" do + it "returns a float" do + Math.sin(Math::PI).should be_kind_of(Float) + end + + it "returns the sine of the argument expressed in radians" do + Math.sin(Math::PI).should be_close(0.0, TOLERANCE) + Math.sin(0).should be_close(0.0, TOLERANCE) + Math.sin(Math::PI/2).should be_close(1.0, TOLERANCE) + Math.sin(3*Math::PI/2).should be_close(-1.0, TOLERANCE) + Math.sin(2*Math::PI).should be_close(0.0, TOLERANCE) + end + + it "raises a TypeError if the argument cannot be coerced with Float()" do + -> { Math.sin("test") }.should raise_error(TypeError) + end + + it "returns NaN given NaN" do + Math.sin(nan_value).nan?.should be_true + end + + it "raises a TypeError if the argument is nil" do + -> { Math.sin(nil) }.should raise_error(TypeError) + end + + it "accepts any argument that can be coerced with Float()" do + Math.sin(MathSpecs::Float.new).should be_close(0.841470984807897, TOLERANCE) + end +end + +describe "Math#sin" do + it "is accessible as a private instance method" do + IncludesMath.new.send(:sin, 1.21).should be_close(0.935616001553386, TOLERANCE) + end +end diff --git a/spec/ruby/core/math/sinh_spec.rb b/spec/ruby/core/math/sinh_spec.rb new file mode 100644 index 0000000000..027c2395a7 --- /dev/null +++ b/spec/ruby/core/math/sinh_spec.rb @@ -0,0 +1,37 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Math.sinh" do + it "returns a float" do + Math.sinh(1.2).should be_kind_of(Float) + end + + it "returns the hyperbolic sin of the argument" do + Math.sinh(0.0).should == 0.0 + Math.sinh(-0.0).should == 0.0 + Math.sinh(1.5).should be_close(2.12927945509482, TOLERANCE) + Math.sinh(-2.8).should be_close(-8.19191835423591, TOLERANCE) + end + + it "raises a TypeError if the argument cannot be coerced with Float()" do + -> { Math.sinh("test") }.should raise_error(TypeError) + end + + it "returns NaN given NaN" do + Math.sinh(nan_value).nan?.should be_true + end + + it "raises a TypeError if the argument is nil" do + -> { Math.sinh(nil) }.should raise_error(TypeError) + end + + it "accepts any argument that can be coerced with Float()" do + Math.sinh(MathSpecs::Float.new).should be_close(1.1752011936438, TOLERANCE) + end +end + +describe "Math#sinh" do + it "is accessible as a private instance method" do + IncludesMath.new.send(:sinh, 1.99).should be_close(3.58941916843202, TOLERANCE) + end +end diff --git a/spec/ruby/core/math/sqrt_spec.rb b/spec/ruby/core/math/sqrt_spec.rb new file mode 100644 index 0000000000..918e7c3a17 --- /dev/null +++ b/spec/ruby/core/math/sqrt_spec.rb @@ -0,0 +1,40 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Math.sqrt" do + it "returns a float" do + Math.sqrt(1).should be_kind_of(Float) + end + + it "returns the square root of the argument" do + Math.sqrt(1).should == 1.0 + Math.sqrt(4.0).should == 2.0 + Math.sqrt(15241578780673814.441547445).should be_close(123456789.123457, TOLERANCE) + end + + it "raises a TypeError if the argument cannot be coerced with Float()" do + -> { Math.sqrt("test") }.should raise_error(TypeError) + end + + it "returns NaN given NaN" do + Math.sqrt(nan_value).nan?.should be_true + end + + it "raises a TypeError if the argument is nil" do + -> { Math.sqrt(nil) }.should raise_error(TypeError) + end + + it "accepts any argument that can be coerced with Float()" do + Math.sqrt(MathSpecs::Float.new).should be_close(1.0, TOLERANCE) + end + + it "raises a Math::DomainError when given a negative number" do + -> { Math.sqrt(-1) }.should raise_error(Math::DomainError) + end +end + +describe "Math#sqrt" do + it "is accessible as a private instance method" do + IncludesMath.new.send(:sqrt, 2.23).should be_close(1.49331845230681, TOLERANCE) + end +end diff --git a/spec/ruby/core/math/tan_spec.rb b/spec/ruby/core/math/tan_spec.rb new file mode 100644 index 0000000000..67307f1e6e --- /dev/null +++ b/spec/ruby/core/math/tan_spec.rb @@ -0,0 +1,42 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Math.tan" do + it "returns a float" do + Math.tan(1.35).should be_kind_of(Float) + end + + it "returns the tangent of the argument" do + Math.tan(0.0).should == 0.0 + Math.tan(-0.0).should == -0.0 + Math.tan(4.22).should be_close(1.86406937682395, TOLERANCE) + Math.tan(-9.65).should be_close(-0.229109052606441, TOLERANCE) + end + + it "returns NaN if called with +-Infinity" do + Math.tan(infinity_value).should.nan? + Math.tan(-infinity_value).should.nan? + end + + it "raises a TypeError if the argument cannot be coerced with Float()" do + -> { Math.tan("test") }.should raise_error(TypeError) + end + + it "returns NaN given NaN" do + Math.tan(nan_value).nan?.should be_true + end + + it "raises a TypeError if the argument is nil" do + -> { Math.tan(nil) }.should raise_error(TypeError) + end + + it "accepts any argument that can be coerced with Float()" do + Math.tan(MathSpecs::Float.new).should be_close(1.5574077246549, TOLERANCE) + end +end + +describe "Math#tan" do + it "is accessible as a private instance method" do + IncludesMath.new.send(:tan, 1.0).should be_close(1.5574077246549, TOLERANCE) + end +end diff --git a/spec/ruby/core/math/tanh_spec.rb b/spec/ruby/core/math/tanh_spec.rb new file mode 100644 index 0000000000..568f8dfa77 --- /dev/null +++ b/spec/ruby/core/math/tanh_spec.rb @@ -0,0 +1,39 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Math.tanh" do + it "returns a float" do + Math.tanh(0.5).should be_kind_of(Float) + end + + it "returns the hyperbolic tangent of the argument" do + Math.tanh(0.0).should == 0.0 + Math.tanh(-0.0).should == -0.0 + Math.tanh(infinity_value).should == 1.0 + Math.tanh(-infinity_value).should == -1.0 + Math.tanh(2.5).should be_close(0.98661429815143, TOLERANCE) + Math.tanh(-4.892).should be_close(-0.999887314427707, TOLERANCE) + end + + it "raises a TypeError if the argument cannot be coerced with Float()" do + -> { Math.tanh("test") }.should raise_error(TypeError) + end + + it "returns NaN given NaN" do + Math.tanh(nan_value).nan?.should be_true + end + + it "raises a TypeError if the argument is nil" do + -> { Math.tanh(nil) }.should raise_error(TypeError) + end + + it "accepts any argument that can be coerced with Float()" do + Math.tanh(MathSpecs::Float.new).should be_close(0.761594155955765, TOLERANCE) + end +end + +describe "Math#tanh" do + it "is accessible as a private instance method" do + IncludesMath.new.send(:tanh, 5.21).should be_close(0.99994034202065, TOLERANCE) + end +end diff --git a/spec/ruby/core/method/arity_spec.rb b/spec/ruby/core/method/arity_spec.rb new file mode 100644 index 0000000000..4bb821735a --- /dev/null +++ b/spec/ruby/core/method/arity_spec.rb @@ -0,0 +1,222 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Method#arity" do + SpecEvaluate.desc = "for method definition" + + context "returns zero" do + evaluate <<-ruby do + def m() end + ruby + + method(:m).arity.should == 0 + end + + evaluate <<-ruby do + def n(&b) end + ruby + + method(:n).arity.should == 0 + end + end + + context "returns positive values" do + evaluate <<-ruby do + def m(a) end + def n(a, b) end + def o(a, b, c) end + def p(a, b, c, d) end + ruby + + method(:m).arity.should == 1 + method(:n).arity.should == 2 + method(:o).arity.should == 3 + method(:p).arity.should == 4 + end + + evaluate <<-ruby do + def m(a:) end + def n(a:, b:) end + def o(a: 1, b:, c:, d: 2) end + ruby + + method(:m).arity.should == 1 + method(:n).arity.should == 1 + method(:o).arity.should == 1 + end + + evaluate <<-ruby do + def m(a, b:) end + def n(a, b:, &l) end + ruby + + method(:m).arity.should == 2 + method(:n).arity.should == 2 + end + + evaluate <<-ruby do + def m(a, b, c:, d: 1) end + def n(a, b, c:, d: 1, **k, &l) end + ruby + + method(:m).arity.should == 3 + method(:n).arity.should == 3 + end + end + + context "returns negative values" do + evaluate <<-ruby do + def m(a=1) end + def n(a=1, b=2) end + ruby + + method(:m).arity.should == -1 + method(:n).arity.should == -1 + end + + evaluate <<-ruby do + def m(a, b=1) end + def n(a, b, c=1, d=2) end + ruby + + method(:m).arity.should == -2 + method(:n).arity.should == -3 + end + + evaluate <<-ruby do + def m(a=1, *b) end + def n(a=1, b=2, *c) end + ruby + + method(:m).arity.should == -1 + method(:n).arity.should == -1 + end + + evaluate <<-ruby do + def m(*) end + def n(*a) end + ruby + + method(:m).arity.should == -1 + method(:n).arity.should == -1 + end + + evaluate <<-ruby do + def m(a, *) end + def n(a, *b) end + def o(a, b, *c) end + def p(a, b, c, *d) end + ruby + + method(:m).arity.should == -2 + method(:n).arity.should == -2 + method(:o).arity.should == -3 + method(:p).arity.should == -4 + end + + evaluate <<-ruby do + def m(*a, b) end + def n(*a, b, c) end + def o(*a, b, c, d) end + ruby + + method(:m).arity.should == -2 + method(:n).arity.should == -3 + method(:o).arity.should == -4 + end + + evaluate <<-ruby do + def m(a, *b, c) end + def n(a, b, *c, d, e) end + ruby + + method(:m).arity.should == -3 + method(:n).arity.should == -5 + end + + evaluate <<-ruby do + def m(a, b=1, c=2, *d, e, f) end + def n(a, b, c=1, *d, e, f, g) end + ruby + + method(:m).arity.should == -4 + method(:n).arity.should == -6 + end + + evaluate <<-ruby do + def m(a: 1) end + def n(a: 1, b: 2) end + ruby + + method(:m).arity.should == -1 + method(:n).arity.should == -1 + end + + evaluate <<-ruby do + def m(a=1, b: 2) end + def n(*a, b: 1) end + def o(a=1, b: 2) end + def p(a=1, *b, c: 2, &l) end + ruby + + method(:m).arity.should == -1 + method(:n).arity.should == -1 + method(:o).arity.should == -1 + method(:p).arity.should == -1 + end + + evaluate <<-ruby do + def m(**k, &l) end + def n(*a, **k) end + def o(a: 1, b: 2, **k) end + ruby + + method(:m).arity.should == -1 + method(:n).arity.should == -1 + method(:o).arity.should == -1 + end + + evaluate <<-ruby do + def m(a=1, *b, c:, d: 2, **k, &l) end + ruby + + method(:m).arity.should == -2 + end + + evaluate <<-ruby do + def m(a, b=1, *c, d, e:, f: 2, **k, &l) end + def n(a, b=1, *c, d:, e:, f: 2, **k, &l) end + def o(a=0, b=1, *c, d, e:, f: 2, **k, &l) end + def p(a=0, b=1, *c, d:, e:, f: 2, **k, &l) end + ruby + + method(:m).arity.should == -4 + method(:n).arity.should == -3 + method(:o).arity.should == -3 + method(:p).arity.should == -2 + end + end + + context "for a Method generated by respond_to_missing?" do + it "returns -1" do + obj = mock("method arity respond_to_missing") + obj.should_receive(:respond_to_missing?).and_return(true) + + obj.method(:m).arity.should == -1 + end + end + + context "for a Method generated by attr_reader" do + it "return 0" do + obj = MethodSpecs::Methods.new + obj.method(:reader).arity.should == 0 + end + end + + context "for a Method generated by attr_writer" do + it "returns 1" do + obj = MethodSpecs::Methods.new + obj.method(:writer=).arity.should == 1 + end + end +end diff --git a/spec/ruby/core/method/call_spec.rb b/spec/ruby/core/method/call_spec.rb new file mode 100644 index 0000000000..6d997325fa --- /dev/null +++ b/spec/ruby/core/method/call_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/call' + +describe "Method#call" do + it_behaves_like :method_call, :call +end diff --git a/spec/ruby/core/method/case_compare_spec.rb b/spec/ruby/core/method/case_compare_spec.rb new file mode 100644 index 0000000000..a78953e8ad --- /dev/null +++ b/spec/ruby/core/method/case_compare_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/call' + +describe "Method#===" do + it_behaves_like :method_call, :=== +end diff --git a/spec/ruby/core/method/clone_spec.rb b/spec/ruby/core/method/clone_spec.rb new file mode 100644 index 0000000000..b0eb5751a9 --- /dev/null +++ b/spec/ruby/core/method/clone_spec.rb @@ -0,0 +1,13 @@ +require_relative '../../spec_helper' +require_relative 'shared/dup' + +describe "Method#clone" do + it_behaves_like :method_dup, :clone + + it "preserves frozen status" do + method = Object.new.method(:method) + method.freeze + method.frozen?.should == true + method.clone.frozen?.should == true + end +end diff --git a/spec/ruby/core/method/compose_spec.rb b/spec/ruby/core/method/compose_spec.rb new file mode 100644 index 0000000000..7506e33ea8 --- /dev/null +++ b/spec/ruby/core/method/compose_spec.rb @@ -0,0 +1,99 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative '../proc/shared/compose' + +describe "Method#<<" do + it "returns a Proc that is the composition of self and the passed Proc" do + succ = MethodSpecs::Composition.new.method(:succ) + upcase = proc { |s| s.upcase } + + (succ << upcase).call('Ruby').should == "RUBZ" + end + + it "calls passed Proc with arguments and then calls self with result" do + pow_2_proc = proc { |x| x * x } + double_proc = proc { |x| x + x } + + pow_2_method = MethodSpecs::Composition.new.method(:pow_2) + double_method = MethodSpecs::Composition.new.method(:double) + + (pow_2_method << double_proc).call(2).should == 16 + (double_method << pow_2_proc).call(2).should == 8 + end + + it "accepts any callable object" do + inc = MethodSpecs::Composition.new.method(:inc) + + double = Object.new + def double.call(n); n * 2; end + + (inc << double).call(3).should == 7 + end + + it_behaves_like :proc_compose, :<<, -> { MethodSpecs::Composition.new.method(:upcase) } + + describe "composition" do + it "is a lambda" do + pow_2 = MethodSpecs::Composition.new.method(:pow_2) + double = proc { |x| x + x } + + (pow_2 << double).is_a?(Proc).should == true + (pow_2 << double).should_not.lambda? + end + + it "may accept multiple arguments" do + inc = MethodSpecs::Composition.new.method(:inc) + mul = proc { |n, m| n * m } + + (inc << mul).call(2, 3).should == 7 + end + end +end + +describe "Method#>>" do + it "returns a Proc that is the composition of self and the passed Proc" do + upcase = proc { |s| s.upcase } + succ = MethodSpecs::Composition.new.method(:succ) + + (succ >> upcase).call('Ruby').should == "RUBZ" + end + + it "calls passed Proc with arguments and then calls self with result" do + pow_2_proc = proc { |x| x * x } + double_proc = proc { |x| x + x } + + pow_2_method = MethodSpecs::Composition.new.method(:pow_2) + double_method = MethodSpecs::Composition.new.method(:double) + + (pow_2_method >> double_proc).call(2).should == 8 + (double_method >> pow_2_proc).call(2).should == 16 + end + + it "accepts any callable object" do + inc = MethodSpecs::Composition.new.method(:inc) + + double = Object.new + def double.call(n); n * 2; end + + (inc >> double).call(3).should == 8 + end + + it_behaves_like :proc_compose, :>>, -> { MethodSpecs::Composition.new.method(:upcase) } + + describe "composition" do + it "is a lambda" do + pow_2 = MethodSpecs::Composition.new.method(:pow_2) + double = proc { |x| x + x } + + (pow_2 >> double).is_a?(Proc).should == true + (pow_2 >> double).should.lambda? + end + + it "may accept multiple arguments" do + mul = MethodSpecs::Composition.new.method(:mul) + inc = proc { |n| n + 1 } + + (mul >> inc).call(2, 3).should == 7 + end + end +end diff --git a/spec/ruby/core/method/curry_spec.rb b/spec/ruby/core/method/curry_spec.rb new file mode 100644 index 0000000000..219de0f6b5 --- /dev/null +++ b/spec/ruby/core/method/curry_spec.rb @@ -0,0 +1,36 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Method#curry" do + it "returns a curried proc" do + x = Object.new + def x.foo(a,b,c); [a,b,c]; end + + c = x.method(:foo).curry + c.should be_kind_of(Proc) + c.call(1).call(2, 3).should == [1,2,3] + end + + describe "with optional arity argument" do + before(:each) do + @obj = MethodSpecs::Methods.new + end + + it "returns a curried proc when given correct arity" do + @obj.method(:one_req).curry(1).should be_kind_of(Proc) + @obj.method(:zero_with_splat).curry(100).should be_kind_of(Proc) + @obj.method(:two_req_with_splat).curry(2).should be_kind_of(Proc) + end + + it "raises ArgumentError when the method requires less arguments than the given arity" do + -> { @obj.method(:zero).curry(1) }.should raise_error(ArgumentError) + -> { @obj.method(:one_req_one_opt).curry(3) }.should raise_error(ArgumentError) + -> { @obj.method(:two_req_one_opt_with_block).curry(4) }.should raise_error(ArgumentError) + end + + it "raises ArgumentError when the method requires more arguments than the given arity" do + -> { @obj.method(:two_req_with_splat).curry(1) }.should raise_error(ArgumentError) + -> { @obj.method(:one_req).curry(0) }.should raise_error(ArgumentError) + end + end +end diff --git a/spec/ruby/core/method/dup_spec.rb b/spec/ruby/core/method/dup_spec.rb new file mode 100644 index 0000000000..e3e29d8a68 --- /dev/null +++ b/spec/ruby/core/method/dup_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../spec_helper' +require_relative 'shared/dup' + +describe "Method#dup" do + ruby_version_is "3.4" do + it_behaves_like :method_dup, :dup + + it "resets frozen status" do + method = Object.new.method(:method) + method.freeze + method.frozen?.should == true + method.dup.frozen?.should == false + end + end +end diff --git a/spec/ruby/core/method/element_reference_spec.rb b/spec/ruby/core/method/element_reference_spec.rb new file mode 100644 index 0000000000..aa6c54d1cb --- /dev/null +++ b/spec/ruby/core/method/element_reference_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/call' + +describe "Method#[]" do + it_behaves_like :method_call, :[] +end diff --git a/spec/ruby/core/method/eql_spec.rb b/spec/ruby/core/method/eql_spec.rb new file mode 100644 index 0000000000..b97c9e4db0 --- /dev/null +++ b/spec/ruby/core/method/eql_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/eql' + +describe "Method#eql?" do + it_behaves_like :method_equal, :eql? +end diff --git a/spec/ruby/core/method/equal_value_spec.rb b/spec/ruby/core/method/equal_value_spec.rb new file mode 100644 index 0000000000..0431d0c5f6 --- /dev/null +++ b/spec/ruby/core/method/equal_value_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/eql' + +describe "Method#==" do + it_behaves_like :method_equal, :== +end diff --git a/spec/ruby/core/method/fixtures/classes.rb b/spec/ruby/core/method/fixtures/classes.rb new file mode 100644 index 0000000000..464a519aea --- /dev/null +++ b/spec/ruby/core/method/fixtures/classes.rb @@ -0,0 +1,246 @@ +module MethodSpecs + + + class SourceLocation + def self.location # This needs to be on this line + :location # for the spec to pass + end + + def self.redefined + :first + end + + def self.redefined + :last + end + + def original + end + + alias :aka :original + end + + class Methods + def foo + true + end + + alias bar foo + alias baz bar + + def same_as_foo + true + end + + def respond_to_missing? method, bool + [:handled_via_method_missing, :also_handled].include? method + end + + def method_missing(method, *arguments) + if [:handled_via_method_missing, :also_handled].include? method + arguments + else + super + end + end + + attr_accessor :attr + + def zero; end + def one_req(a); end + def two_req(a, b); end + + def one_req_named(a:); end + + def zero_with_block(&blk); end + def one_req_with_block(a, &blk); end + def two_req_with_block(a, b, &blk); end + + def one_opt(a=nil); end + def one_req_one_opt(a, b=nil); end + def one_req_two_opt(a, b=nil, c=nil); end + def two_req_one_opt(a, b, c=nil); end + + def one_opt_named(a: nil); end + + def one_opt_with_block(a=nil, &blk); end + def one_req_one_opt_with_block(a, b=nil, &blk); end + def one_req_two_opt_with_block(a, b=nil, c=nil, &blk); end + def two_req_one_opt_with_block(a, b, c=nil, &blk); end + + def zero_with_splat(*a); end + def one_req_with_splat(a, *b); end + def two_req_with_splat(a, b, *c); end + def one_req_one_opt_with_splat(a, b=nil, *c); end + def two_req_one_opt_with_splat(a, b, c=nil, *d); end + def one_req_two_opt_with_splat(a, b=nil, c=nil, *d); end + + def zero_with_double_splat(**a); end + + def zero_with_splat_and_block(*a, &blk); end + def one_req_with_splat_and_block(a, *b, &blk); end + def two_req_with_splat_and_block(a, b, *c, &blk); end + def one_req_one_opt_with_splat_and_block(a, b=nil, *c, &blk); end + def two_req_one_opt_with_splat_and_block(a, b, c=nil, *d, &blk); end + def one_req_two_opt_with_splat_and_block(a, b=nil, c=nil, *d, &blk); end + + def my_public_method; end + def my_protected_method; end + def my_private_method; end + protected :my_protected_method + private :my_private_method + + define_method(:zero_defined_method, Proc.new {||}) + define_method(:zero_with_splat_defined_method, Proc.new {|*x|}) + define_method(:one_req_defined_method, Proc.new {|x|}) + define_method(:two_req_defined_method, Proc.new {|x, y|}) + define_method(:no_args_defined_method) {} + define_method(:two_grouped_defined_method) {|(_x1,_x2)|} + + attr_reader :reader + attr_writer :writer + end + + module MyMod + def bar; :bar; end + end + + class MySuper + include MyMod + end + + class MySub < MySuper; end + + class A + def baz(a, b) + self.class + end + def overridden; end + end + + class B < A + def overridden; end + end + + module BetweenBAndC + def overridden; end + end + + class C < B + include BetweenBAndC + def overridden; end + end + + module OverrideAgain + def overridden; end + end + + class D + def bar() 'done' end + end + + class Eql + + def same_body + 1 + 1 + end + + alias :same_body_alias :same_body + + def same_body_with_args(arg) + 1 + 1 + end + + def different_body + 1 + 2 + end + + def same_body_two + 1 + 1 + end + + private + def same_body_private + 1 + 1 + end + end + + class Eql2 + + def same_body + 1 + 1 + end + + end + + class ToProc + def method_called(a, b) + ScratchPad << [a, b] + end + + def to_proc + method(:method_called).to_proc + end + end + + class ToProcBeta + def method_called(a) + ScratchPad << a + a + end + + def to_proc + method(:method_called).to_proc + end + end + + class Composition + def upcase(s) + s.upcase + end + + def succ(s) + s.succ + end + + def pow_2(n) + n * n + end + + def double(n) + n + n + end + + def inc(n) + n + 1 + end + + def mul(n, m) + n * m + end + end + + module InheritedMethods + module A + private + def derp(message) + 'A' + end + end + + module B + private + def derp + 'B' + super('superclass') + end + end + + class C + include A + include B + + public :derp + alias_method :meow, :derp + end + end +end diff --git a/spec/ruby/core/method/hash_spec.rb b/spec/ruby/core/method/hash_spec.rb new file mode 100644 index 0000000000..d6c8440acc --- /dev/null +++ b/spec/ruby/core/method/hash_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Method#hash" do + it "returns the same value for user methods that are eql?" do + obj = MethodSpecs::Methods.new + obj.method(:foo).hash.should == obj.method(:bar).hash + end + + # See also redmine #6048 + it "returns the same value for builtin methods that are eql?" do + obj = [42] + obj.method(:to_s).hash.should == obj.method(:inspect).hash + end +end diff --git a/spec/ruby/core/method/inspect_spec.rb b/spec/ruby/core/method/inspect_spec.rb new file mode 100644 index 0000000000..e0fe1afdd0 --- /dev/null +++ b/spec/ruby/core/method/inspect_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/to_s' + +describe "Method#inspect" do + it_behaves_like :method_to_s, :inspect +end diff --git a/spec/ruby/core/method/name_spec.rb b/spec/ruby/core/method/name_spec.rb new file mode 100644 index 0000000000..de390c6f52 --- /dev/null +++ b/spec/ruby/core/method/name_spec.rb @@ -0,0 +1,22 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Method#name" do + it "returns the name of the method" do + "abc".method(:upcase).name.should == :upcase + end + + it "returns the name even when aliased" do + obj = MethodSpecs::Methods.new + obj.method(:foo).name.should == :foo + obj.method(:bar).name.should == :bar + obj.method(:bar).unbind.bind(obj).name.should == :bar + end + + describe "for a Method generated by respond_to_missing?" do + it "returns the name passed to respond_to_missing?" do + @m = MethodSpecs::Methods.new + @m.method(:handled_via_method_missing).name.should == :handled_via_method_missing + end + end +end diff --git a/spec/ruby/core/method/original_name_spec.rb b/spec/ruby/core/method/original_name_spec.rb new file mode 100644 index 0000000000..676fdaedb4 --- /dev/null +++ b/spec/ruby/core/method/original_name_spec.rb @@ -0,0 +1,22 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Method#original_name" do + it "returns the name of the method" do + "abc".method(:upcase).original_name.should == :upcase + end + + it "returns the original name when aliased" do + obj = MethodSpecs::Methods.new + obj.method(:foo).original_name.should == :foo + obj.method(:bar).original_name.should == :foo + obj.method(:bar).unbind.bind(obj).original_name.should == :foo + end + + it "returns the original name even when aliased twice" do + obj = MethodSpecs::Methods.new + obj.method(:foo).original_name.should == :foo + obj.method(:baz).original_name.should == :foo + obj.method(:baz).unbind.bind(obj).original_name.should == :foo + end +end diff --git a/spec/ruby/core/method/owner_spec.rb b/spec/ruby/core/method/owner_spec.rb new file mode 100644 index 0000000000..1cdc4edfa7 --- /dev/null +++ b/spec/ruby/core/method/owner_spec.rb @@ -0,0 +1,30 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Method#owner" do + it "returns the owner of the method" do + "abc".method(:upcase).owner.should == String + end + + it "returns the same owner when aliased in the same classes" do + obj = MethodSpecs::Methods.new + obj.method(:foo).owner.should == MethodSpecs::Methods + obj.method(:bar).owner.should == MethodSpecs::Methods + end + + it "returns the class/module it was defined in" do + MethodSpecs::C.new.method(:baz).owner.should == MethodSpecs::A + MethodSpecs::MySuper.new.method(:bar).owner.should == MethodSpecs::MyMod + end + + describe "for a Method generated by respond_to_missing?" do + it "returns the owner of the method" do + @m = MethodSpecs::Methods.new + @m.method(:handled_via_method_missing).owner.should == MethodSpecs::Methods + end + end + + it "returns the class on which public was called for a private method in ancestor" do + MethodSpecs::InheritedMethods::C.new.method(:derp).owner.should == MethodSpecs::InheritedMethods::C + end +end diff --git a/spec/ruby/core/method/parameters_spec.rb b/spec/ruby/core/method/parameters_spec.rb new file mode 100644 index 0000000000..f1c2523cf0 --- /dev/null +++ b/spec/ruby/core/method/parameters_spec.rb @@ -0,0 +1,304 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Method#parameters" do + class MethodSpecs::Methods + def one_key(a: 1); end + def one_keyrest(**a); end + + def one_keyreq(a:); end + def one_nokey(**nil); end + + def one_splat_one_req(*a,b); end + def one_splat_two_req(*a,b,c); end + def one_splat_one_req_with_block(*a,b,&blk); end + + def one_opt_with_stabby(a=-> b { true }); end + + def one_unnamed_splat(*); end + def one_unnamed_keyrest(**); end + + def one_splat_one_block(*args, &block) + local_is_not_parameter = {} + end + + def forward_parameters(...) end + + def underscore_parameters(_, _, _ = 1, *_, _:, _: 2, **_, &_); end + + define_method(:one_optional_defined_method) {|x = 1|} + end + + it "returns an empty Array when the method expects no arguments" do + MethodSpecs::Methods.instance_method(:zero).parameters.should == [] + end + + it "returns [[:req,:name]] for a method expecting one required argument called 'name'" do + MethodSpecs::Methods.instance_method(:one_req).parameters.should == [[:req,:a]] + end + + it "returns [[:req,:a],[:req,:b]] for a method expecting two required arguments called 'a' and 'b''" do + m = MethodSpecs::Methods.instance_method(:two_req) + m.parameters.should == [[:req,:a], [:req,:b]] + end + + it "returns [[:block,:blk]] for a method expecting one block argument called 'a'" do + m = MethodSpecs::Methods.instance_method(:zero_with_block) + m.parameters.should == [[:block,:blk]] + end + + it "returns [[:req,:a],[:block,:b] for a method expecting a required argument ('a') and a block argument ('b')" do + m = MethodSpecs::Methods.instance_method(:one_req_with_block) + m.parameters.should == [[:req,:a], [:block,:blk]] + end + + it "returns [[:req,:a],[:req,:b],[:block,:c] for a method expecting two required arguments ('a','b') and a block argument ('c')" do + m = MethodSpecs::Methods.instance_method(:two_req_with_block) + m.parameters.should == [[:req,:a], [:req,:b], [:block,:blk]] + end + + it "returns [[:opt,:a]] for a method expecting one optional argument ('a')" do + m = MethodSpecs::Methods.instance_method(:one_opt) + m.parameters.should == [[:opt,:a]] + end + + it "returns [[:req,:a],[:opt,:b]] for a method expecting one required argument ('a') and one optional argument ('b')" do + m = MethodSpecs::Methods.instance_method(:one_req_one_opt) + m.parameters.should == [[:req,:a],[:opt,:b]] + end + + it "returns [[:req,:a],[:opt,:b]] for a method expecting one required argument ('a') and one optional argument ('b')" do + m = MethodSpecs::Methods.instance_method(:one_req_one_opt) + m.parameters.should == [[:req,:a],[:opt,:b]] + end + + it "returns [[:req,:a],[:opt,:b],[:opt,:c]] for a method expecting one required argument ('a') and two optional arguments ('b','c')" do + m = MethodSpecs::Methods.instance_method(:one_req_two_opt) + m.parameters.should == [[:req,:a],[:opt,:b],[:opt,:c]] + end + + it "returns [[:req,:a],[:req,:b],[:opt,:c]] for a method expecting two required arguments ('a','b') and one optional arguments ('c')" do + m = MethodSpecs::Methods.instance_method(:two_req_one_opt) + m.parameters.should == [[:req,:a],[:req,:b],[:opt,:c]] + end + + it "returns [[:opt,:a],[:block,:b]] for a method expecting one required argument ('a') and one block argument ('b')" do + m = MethodSpecs::Methods.instance_method(:one_opt_with_block) + m.parameters.should == [[:opt,:a],[:block,:blk]] + end + + it "returns [[:req,:a],[:opt,:b],[:block,:c]] for a method expecting one required argument ('a'), one optional argument ('b'), and a block ('c')" do + m = MethodSpecs::Methods.instance_method(:one_req_one_opt_with_block) + m.parameters.should == [[:req,:a],[:opt,:b],[:block,:blk]] + end + + it "returns [[:req,:a],[:opt,:b],[:opt,:c],[:block,:d]] for a method expecting one required argument ('a'), two optional arguments ('b','c'), and a block ('d')" do + m = MethodSpecs::Methods.instance_method(:one_req_two_opt_with_block) + m.parameters.should == [[:req,:a],[:opt,:b],[:opt,:c],[:block,:blk]] + end + + it "returns [[:rest,:a]] for a method expecting a single splat argument ('a')" do + m = MethodSpecs::Methods.instance_method(:zero_with_splat) + m.parameters.should == [[:rest,:a]] + end + + it "returns [[:req,:a],[:rest,:b]] for a method expecting a splat argument ('a') and a required argument ('b')" do + m = MethodSpecs::Methods.instance_method(:one_req_with_splat) + m.parameters.should == [[:req,:a],[:rest,:b]] + end + + it "returns [[:req,:a],[:req,:b],[:rest,:c]] for a method expecting two required arguments ('a','b') and a splat argument ('c')" do + m = MethodSpecs::Methods.instance_method(:two_req_with_splat) + m.parameters.should == [[:req,:a],[:req,:b],[:rest,:c]] + end + + it "returns [[:req,:a],[:opt,:b],[:rest,:c]] for a method expecting a required argument ('a','b'), an optional argument ('b'), and a splat argument ('c')" do + m = MethodSpecs::Methods.instance_method(:one_req_one_opt_with_splat) + m.parameters.should == [[:req,:a],[:opt,:b],[:rest,:c]] + end + + it "returns [[:req,:a],[:req,:b],[:opt,:b],[:rest,:d]] for a method expecting two required arguments ('a','b'), an optional argument ('c'), and a splat argument ('d')" do + m = MethodSpecs::Methods.instance_method(:two_req_one_opt_with_splat) + m.parameters.should == [[:req,:a],[:req,:b],[:opt,:c],[:rest,:d]] + end + + it "returns [[:req,:a],[:opt,:b],[:opt,:c],[:rest,:d]] for a method expecting a required argument ('a'), two optional arguments ('b','c'), and a splat argument ('d')" do + m = MethodSpecs::Methods.instance_method(:one_req_two_opt_with_splat) + m.parameters.should == [[:req,:a],[:opt,:b],[:opt,:c],[:rest,:d]] + end + + it "returns [[:rest,:a],[:block,:b]] for a method expecting a splat argument ('a') and a block argument ('b')" do + m = MethodSpecs::Methods.instance_method(:zero_with_splat_and_block) + m.parameters.should == [[:rest,:a],[:block,:blk]] + end + + it "returns [[:req,:a],[:rest,:b],[:block,:c]] for a method expecting a required argument ('a'), a splat argument ('b'), and a block ('c')" do + m = MethodSpecs::Methods.instance_method(:one_req_with_splat_and_block) + m.parameters.should == [[:req,:a],[:rest,:b],[:block,:blk]] + end + + it "returns [[:req,:a],[:req,:b],[:rest,:c],[:block,:d]] for a method expecting two required arguments ('a','b'), a splat argument ('c'), and a block ('d')" do + m = MethodSpecs::Methods.instance_method(:two_req_with_splat_and_block) + m.parameters.should == [[:req,:a],[:req,:b],[:rest,:c],[:block,:blk]] + end + + it "returns [[:req,:a],[:opt,:b],[:rest,:c],[:block,:d]] for a method expecting a required argument ('a'), a splat argument ('c'), and a block ('d')" do + m = MethodSpecs::Methods.instance_method(:one_req_one_opt_with_splat_and_block) + m.parameters.should == [[:req,:a],[:opt,:b],[:rest,:c],[:block,:blk]] + end + + it "returns [[:req,:a],[:req,:b],[:opt,:c],[:block,:d]] for a method expecting two required arguments ('a','b'), an optional argument ('c'), a splat argument ('d'), and a block ('e')" do + m = MethodSpecs::Methods.instance_method(:two_req_one_opt_with_splat_and_block) + m.parameters.should == [[:req,:a],[:req,:b],[:opt,:c],[:rest,:d],[:block,:blk]] + end + + it "returns [[:rest,:a],[:req,:b]] for a method expecting a splat argument ('a') and a required argument ('b')" do + m = MethodSpecs::Methods.instance_method(:one_splat_one_req) + m.parameters.should == [[:rest,:a],[:req,:b]] + end + + it "returns [[:rest,:a],[:req,:b],[:req,:c]] for a method expecting a splat argument ('a') and two required arguments ('b','c')" do + m = MethodSpecs::Methods.instance_method(:one_splat_two_req) + m.parameters.should == [[:rest,:a],[:req,:b],[:req,:c]] + end + + it "returns [[:rest,:a],[:req,:b],[:block,:c]] for a method expecting a splat argument ('a'), a required argument ('b'), and a block ('c')" do + m = MethodSpecs::Methods.instance_method(:one_splat_one_req_with_block) + m.parameters.should == [[:rest,:a],[:req,:b],[:block,:blk]] + end + + it "returns [[:key,:a]] for a method with a single optional keyword argument" do + m = MethodSpecs::Methods.instance_method(:one_key) + m.parameters.should == [[:key,:a]] + end + + it "returns [[:keyrest,:a]] for a method with a keyword rest argument" do + m = MethodSpecs::Methods.instance_method(:one_keyrest) + m.parameters.should == [[:keyrest,:a]] + end + + it "returns [[:keyreq,:a]] for a method with a single required keyword argument" do + m = MethodSpecs::Methods.instance_method(:one_keyreq) + m.parameters.should == [[:keyreq,:a]] + end + + it "returns [[:nokey]] for a method with a single **nil parameter" do + m = MethodSpecs::Methods.instance_method(:one_nokey) + m.parameters.should == [[:nokey]] + end + + it "works with ->(){} as the value of an optional argument" do + m = MethodSpecs::Methods.instance_method(:one_opt_with_stabby) + m.parameters.should == [[:opt,:a]] + end + + # define_method variants + it "returns [] for a define_method method with explicit no-args || specification" do + m = MethodSpecs::Methods.instance_method(:zero_defined_method) + m.parameters.should == [] + end + + it "returns [[:rest, :x]] for a define_method method with rest arg 'x' only" do + m = MethodSpecs::Methods.instance_method(:zero_with_splat_defined_method) + m.parameters.should == [[:rest, :x]] + end + + it "returns [[:req, :x]] for a define_method method expecting one required argument 'x'" do + m = MethodSpecs::Methods.instance_method(:one_req_defined_method) + m.parameters.should == [[:req, :x]] + end + + it "returns [[:req, :x], [:req, :y]] for a define_method method expecting two required arguments 'x' and 'y'" do + m = MethodSpecs::Methods.instance_method(:two_req_defined_method) + m.parameters.should == [[:req, :x], [:req, :y]] + end + + it "returns [] for a define_method method with no args specification" do + m = MethodSpecs::Methods.instance_method(:no_args_defined_method) + m.parameters.should == [] + end + + it "returns [[:req]] for a define_method method with a grouping as its only argument" do + m = MethodSpecs::Methods.instance_method(:two_grouped_defined_method) + m.parameters.should == [[:req]] + end + + it "returns [[:opt, :x]] for a define_method method with an optional argument 'x'" do + m = MethodSpecs::Methods.instance_method(:one_optional_defined_method) + m.parameters.should == [[:opt, :x]] + end + + it "returns [[:rest]] for a Method generated by respond_to_missing?" do + m = MethodSpecs::Methods.new + m.method(:handled_via_method_missing).parameters.should == [[:rest]] + end + + it "adds rest arg with name * for \"star\" argument" do + m = MethodSpecs::Methods.new + m.method(:one_unnamed_splat).parameters.should == [[:rest, :*]] + end + + it "adds keyrest arg with ** as a name for \"double star\" argument" do + m = MethodSpecs::Methods.new + m.method(:one_unnamed_keyrest).parameters.should == [[:keyrest, :**]] + end + + it "adds block arg with name & for anonymous block argument" do + object = Object.new + def object.foo(&) + end + + object.method(:foo).parameters.should == [[:block, :&]] + end + + it "returns [:rest, :*], [:keyrest, :**], [:block, :&] for forward parameters operator" do + m = MethodSpecs::Methods.new + m.method(:forward_parameters).parameters.should == [[:rest, :*], [:keyrest, :**], [:block, :&]] + end + + it "returns the args and block for a splat and block argument" do + m = MethodSpecs::Methods.new + m.method(:one_splat_one_block).parameters.should == [[:rest, :args], [:block, :block]] + end + + it "returns [] for a Method generated by attr_reader" do + m = MethodSpecs::Methods.new + m.method(:reader).parameters.should == [] + end + + it "return [[:req]] for a Method generated by attr_writer" do + m = MethodSpecs::Methods.new + m.method(:writer=).parameters.should == [[:req]] + end + + it "returns all parameters defined with the name _ as _" do + m = MethodSpecs::Methods.instance_method(:underscore_parameters) + m.parameters.should == [ + [:req, :_], + [:req, :_], + [:opt, :_], + [:rest, :_], + [:keyreq, :_], + [:key, :_], + [:keyrest, :_], + [:block, :_] + ] + end + + it "returns [[:rest]] for core methods with variable-length argument lists" do + # delete! takes rest args + "foo".method(:delete!).parameters.should == [[:rest]] + end + + it "returns [[:rest]] or [[:opt]] for core methods with optional arguments" do + # pop takes 1 optional argument + [ + [[:rest]], + [[:opt]] + ].should include([].method(:pop).parameters) + end + + it "returns [[:req]] for each parameter for core methods with fixed-length argument lists" do + "foo".method(:+).parameters.should == [[:req]] + end +end diff --git a/spec/ruby/core/method/private_spec.rb b/spec/ruby/core/method/private_spec.rb new file mode 100644 index 0000000000..e708542b2e --- /dev/null +++ b/spec/ruby/core/method/private_spec.rb @@ -0,0 +1,9 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Method#private?" do + it "has been removed" do + obj = MethodSpecs::Methods.new + obj.method(:my_private_method).should_not.respond_to?(:private?) + end +end diff --git a/spec/ruby/core/method/protected_spec.rb b/spec/ruby/core/method/protected_spec.rb new file mode 100644 index 0000000000..f9e422ae3d --- /dev/null +++ b/spec/ruby/core/method/protected_spec.rb @@ -0,0 +1,9 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Method#protected?" do + it "has been removed" do + obj = MethodSpecs::Methods.new + obj.method(:my_protected_method).should_not.respond_to?(:protected?) + end +end diff --git a/spec/ruby/core/method/public_spec.rb b/spec/ruby/core/method/public_spec.rb new file mode 100644 index 0000000000..4cb23f4cf1 --- /dev/null +++ b/spec/ruby/core/method/public_spec.rb @@ -0,0 +1,9 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Method#public?" do + it "has been removed" do + obj = MethodSpecs::Methods.new + obj.method(:my_public_method).should_not.respond_to?(:public?) + end +end diff --git a/spec/ruby/core/method/receiver_spec.rb b/spec/ruby/core/method/receiver_spec.rb new file mode 100644 index 0000000000..2b2e11dd2e --- /dev/null +++ b/spec/ruby/core/method/receiver_spec.rb @@ -0,0 +1,22 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Method#receiver" do + it "returns the receiver of the method" do + s = "abc" + s.method(:upcase).receiver.should equal(s) + end + + it "returns the right receiver even when aliased" do + obj = MethodSpecs::Methods.new + obj.method(:foo).receiver.should equal(obj) + obj.method(:bar).receiver.should equal(obj) + end + + describe "for a Method generated by respond_to_missing?" do + it "returns the receiver of the method" do + m = MethodSpecs::Methods.new + m.method(:handled_via_method_missing).receiver.should equal(m) + end + end +end diff --git a/spec/ruby/core/method/shared/call.rb b/spec/ruby/core/method/shared/call.rb new file mode 100644 index 0000000000..f26e373695 --- /dev/null +++ b/spec/ruby/core/method/shared/call.rb @@ -0,0 +1,51 @@ +describe :method_call, shared: true do + it "invokes the method with the specified arguments, returning the method's return value" do + m = 12.method("+") + m.send(@method, 3).should == 15 + m.send(@method, 20).should == 32 + + m = MethodSpecs::Methods.new.method(:attr=) + m.send(@method, 42).should == 42 + end + + it "raises an ArgumentError when given incorrect number of arguments" do + -> { + MethodSpecs::Methods.new.method(:two_req).send(@method, 1, 2, 3) + }.should raise_error(ArgumentError) + -> { + MethodSpecs::Methods.new.method(:two_req).send(@method, 1) + }.should raise_error(ArgumentError) + end + + describe "for a Method generated by respond_to_missing?" do + it "invokes method_missing with the specified arguments and returns the result" do + @m = MethodSpecs::Methods.new + meth = @m.method(:handled_via_method_missing) + meth.send(@method, :argument).should == [:argument] + end + + it "invokes method_missing with the method name and the specified arguments" do + @m = MethodSpecs::Methods.new + meth = @m.method(:handled_via_method_missing) + + @m.should_receive(:method_missing).with(:handled_via_method_missing, :argument) + meth.send(@method, :argument) + end + + it "invokes method_missing dynamically" do + @m = MethodSpecs::Methods.new + meth = @m.method(:handled_via_method_missing) + + def @m.method_missing(*); :changed; end + meth.send(@method, :argument).should == :changed + end + + it "does not call the original method name even if it now exists" do + @m = MethodSpecs::Methods.new + meth = @m.method(:handled_via_method_missing) + + def @m.handled_via_method_missing(*); :not_called; end + meth.send(@method, :argument).should == [:argument] + end + end +end diff --git a/spec/ruby/core/method/shared/dup.rb b/spec/ruby/core/method/shared/dup.rb new file mode 100644 index 0000000000..c74847083f --- /dev/null +++ b/spec/ruby/core/method/shared/dup.rb @@ -0,0 +1,32 @@ +describe :method_dup, shared: true do + it "returns a copy of self" do + a = Object.new.method(:method) + b = a.send(@method) + + a.should == b + a.should_not equal(b) + end + + ruby_version_is "3.4" do + it "copies instance variables" do + method = Object.new.method(:method) + method.instance_variable_set(:@ivar, 1) + cl = method.send(@method) + cl.instance_variables.should == [:@ivar] + end + + it "copies the finalizer" do + code = <<-'RUBY' + obj = Object.new.method(:method) + + ObjectSpace.define_finalizer(obj, Proc.new { STDOUT.write "finalized\n" }) + + obj.clone + + exit 0 + RUBY + + ruby_exe(code).lines.sort.should == ["finalized\n", "finalized\n"] + end + end +end diff --git a/spec/ruby/core/method/shared/eql.rb b/spec/ruby/core/method/shared/eql.rb new file mode 100644 index 0000000000..5c720cbac1 --- /dev/null +++ b/spec/ruby/core/method/shared/eql.rb @@ -0,0 +1,94 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' + +describe :method_equal, shared: true do + before :each do + @m = MethodSpecs::Methods.new + @m_foo = @m.method(:foo) + @m2 = MethodSpecs::Methods.new + @a = MethodSpecs::A.new + end + + it "returns true if methods are the same" do + m2 = @m.method(:foo) + + @m_foo.send(@method, @m_foo).should be_true + @m_foo.send(@method, m2).should be_true + end + + it "returns true on aliased methods" do + m_bar = @m.method(:bar) + + m_bar.send(@method, @m_foo).should be_true + end + + it "returns true if the two core methods are aliases" do + s = "hello" + a = s.method(:size) + b = s.method(:length) + a.send(@method, b).should be_true + end + + it "returns false on a method which is neither aliased nor the same method" do + m2 = @m.method(:zero) + + @m_foo.send(@method, m2).should be_false + end + + it "returns false for a method which is not bound to the same object" do + m2_foo = @m2.method(:foo) + a_baz = @a.method(:baz) + + @m_foo.send(@method, m2_foo).should be_false + @m_foo.send(@method, a_baz).should be_false + end + + it "returns false if the two methods are bound to the same object but were defined independently" do + m2 = @m.method(:same_as_foo) + @m_foo.send(@method, m2).should be_false + end + + it "returns true if a method was defined using the other one" do + MethodSpecs::Methods.send :define_method, :defined_foo, MethodSpecs::Methods.instance_method(:foo) + m2 = @m.method(:defined_foo) + @m_foo.send(@method, m2).should be_true + end + + it "returns false if comparing a method defined via define_method and def" do + defn = @m.method(:zero) + defined = @m.method(:zero_defined_method) + + defn.send(@method, defined).should be_false + defined.send(@method, defn).should be_false + end + + describe 'missing methods' do + it "returns true for the same method missing" do + miss1 = @m.method(:handled_via_method_missing) + miss1bis = @m.method(:handled_via_method_missing) + miss2 = @m.method(:also_handled) + + miss1.send(@method, miss1bis).should be_true + miss1.send(@method, miss2).should be_false + end + + it 'calls respond_to_missing? with true to include private methods' do + @m.should_receive(:respond_to_missing?).with(:some_missing_method, true).and_return(true) + @m.method(:some_missing_method) + end + end + + it "returns false if the two methods are bound to different objects, have the same names, and identical bodies" do + a = MethodSpecs::Eql.instance_method(:same_body) + b = MethodSpecs::Eql2.instance_method(:same_body) + a.send(@method, b).should be_false + end + + it "returns false if the argument is not a Method object" do + String.instance_method(:size).send(@method, 7).should be_false + end + + it "returns false if the argument is an unbound version of self" do + method(:load).send(@method, method(:load).unbind).should be_false + end +end diff --git a/spec/ruby/core/method/shared/to_s.rb b/spec/ruby/core/method/shared/to_s.rb new file mode 100644 index 0000000000..b2d27d370f --- /dev/null +++ b/spec/ruby/core/method/shared/to_s.rb @@ -0,0 +1,81 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' + +describe :method_to_s, shared: true do + before :each do + @m = MethodSpecs::MySub.new.method :bar + @string = @m.send(@method) + end + + it "returns a String" do + @m.send(@method).should be_kind_of(String) + end + + it "returns a String for methods defined with attr_accessor" do + m = MethodSpecs::Methods.new.method :attr + m.send(@method).should be_kind_of(String) + end + + it "returns a String containing 'Method'" do + @string.should =~ /\bMethod\b/ + end + + it "returns a String containing the method name" do + @string.should =~ /\#bar/ + end + + it "returns a String containing method arguments" do + obj = MethodSpecs::Methods.new + obj.method(:zero).send(@method).should.include?("()") + obj.method(:one_req).send(@method).should.include?("(a)") + obj.method(:one_req_named).send(@method).should.include?("(a:)") + obj.method(:zero_with_block).send(@method).should.include?("(&blk)") + obj.method(:one_opt).send(@method).should.include?("(a=...)") + obj.method(:one_opt_named).send(@method).should.include?("(a: ...)") + obj.method(:zero_with_splat).send(@method).should.include?("(*a)") + obj.method(:zero_with_double_splat).send(@method).should.include?("(**a)") + obj.method(:one_req_one_opt_with_splat_and_block).send(@method).should.include?("(a, b=..., *c, &blk)") + end + + it "returns a String containing the Module the method is defined in" do + @string.should =~ /MethodSpecs::MyMod/ + end + + it "returns a String containing the Module the method is referenced from" do + @string.should =~ /MethodSpecs::MySub/ + end + + it "returns a String including all details" do + @string.should.start_with? "#<Method: MethodSpecs::MySub(MethodSpecs::MyMod)#bar" + end + + it "does not show the defining module if it is the same as the receiver class" do + MethodSpecs::A.new.method(:baz).send(@method).should.start_with? "#<Method: MethodSpecs::A#baz" + end + + it "returns a String containing the Module containing the method if object has a singleton class but method is not defined in the singleton class" do + obj = MethodSpecs::MySub.new + obj.singleton_class + @m = obj.method(:bar) + @string = @m.send(@method) + @string.should.start_with? "#<Method: MethodSpecs::MySub(MethodSpecs::MyMod)#bar" + + c = MethodSpecs::MySub.dup + m = Module.new{def bar; end} + c.extend(m) + @string = c.method(:bar).send(@method) + @string.should.start_with? "#<Method: #<Class:#{c.inspect}>(#{m.inspect})#bar" + end + + it "returns a String containing the singleton class if method is defined in the singleton class" do + obj = MethodSpecs::MySub.new + def obj.bar; end + @m = obj.method(:bar) + @string = @m.send(@method).sub(/0x\h+/, '0xXXXXXX') + @string.should.start_with? "#<Method: #<MethodSpecs::MySub:0xXXXXXX>.bar" + end + + it "shows the metaclass and the owner for a Module instance method retrieved from a class" do + String.method(:include).inspect.should.start_with?("#<Method: #<Class:String>(Module)#include") + end +end diff --git a/spec/ruby/core/method/source_location_spec.rb b/spec/ruby/core/method/source_location_spec.rb new file mode 100644 index 0000000000..87413a2ab6 --- /dev/null +++ b/spec/ruby/core/method/source_location_spec.rb @@ -0,0 +1,126 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Method#source_location" do + before :each do + @method = MethodSpecs::SourceLocation.method(:location) + end + + it "returns an Array" do + @method.source_location.should be_an_instance_of(Array) + end + + it "sets the first value to the path of the file in which the method was defined" do + file = @method.source_location[0] + file.should be_an_instance_of(String) + file.should == File.realpath('fixtures/classes.rb', __dir__) + end + + it "sets the last value to an Integer representing the line on which the method was defined" do + line = @method.source_location[1] + line.should be_an_instance_of(Integer) + line.should == 5 + end + + it "returns the last place the method was defined" do + MethodSpecs::SourceLocation.method(:redefined).source_location[1].should == 13 + end + + it "returns the location of the original method even if it was aliased" do + MethodSpecs::SourceLocation.new.method(:aka).source_location[1].should == 17 + end + + it "works for methods defined with a block" do + line = nil + klass = Class.new do + line = __LINE__ + 1 + define_method(:f) { } + end + + method = klass.new.method(:f) + method.source_location[0].should =~ /#{__FILE__}/ + method.source_location[1].should == line + end + + it "works for methods defined with a Method" do + line = nil + klass = Class.new do + line = __LINE__ + 1 + def f + end + define_method :g, new.method(:f) + end + + method = klass.new.method(:g) + method.source_location[0].should =~ /#{__FILE__}/ + method.source_location[1].should == line + end + + it "works for methods defined with an UnboundMethod" do + line = nil + klass = Class.new do + line = __LINE__ + 1 + def f + end + define_method :g, instance_method(:f) + end + + method = klass.new.method(:g) + method.source_location[0].should =~ /#{__FILE__}/ + method.source_location[1].should == line + end + + it "works for methods whose visibility has been overridden in a subclass" do + line = nil + superclass = Class.new do + line = __LINE__ + 1 + def f + end + end + subclass = Class.new(superclass) do + private :f + end + + method = subclass.new.method(:f) + method.source_location[0].should =~ /#{__FILE__}/ + method.source_location[1].should == line + end + + it "works for core methods where it returns nil or <internal:" do + loc = method(:__id__).source_location + if loc == nil + loc.should == nil + else + loc[0].should.start_with?('<internal:') + loc[1].should be_kind_of(Integer) + end + + loc = method(:tap).source_location + if loc == nil + loc.should == nil + else + loc[0].should.start_with?('<internal:') + loc[1].should be_kind_of(Integer) + end + end + + it "works for eval with a given line" do + c = Class.new do + eval('def self.m; end', nil, "foo", 100) + end + location = c.method(:m).source_location + ruby_version_is(""..."4.1") do + location.should == ["foo", 100] + end + ruby_version_is("4.1") do + location.should == ["foo", 100, 0, 100, 15] + end + end + + describe "for a Method generated by respond_to_missing?" do + it "returns nil" do + m = MethodSpecs::Methods.new + m.method(:handled_via_method_missing).source_location.should be_nil + end + end +end diff --git a/spec/ruby/core/method/super_method_spec.rb b/spec/ruby/core/method/super_method_spec.rb new file mode 100644 index 0000000000..c63a7aaa0f --- /dev/null +++ b/spec/ruby/core/method/super_method_spec.rb @@ -0,0 +1,64 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Method#super_method" do + it "returns the method that would be called by super in the method" do + obj = MethodSpecs::C.new + obj.extend MethodSpecs::OverrideAgain + meth = obj.method(:overridden) + + s_meth = meth.super_method + s_meth.owner.should == MethodSpecs::C + s_meth.receiver.should == obj + s_meth.name.should == :overridden + + ss_meth = meth.super_method.super_method + ss_meth.owner.should == MethodSpecs::BetweenBAndC + ss_meth.receiver.should == obj + ss_meth.name.should == :overridden + + sss_meth = meth.super_method.super_method.super_method + sss_meth.owner.should == MethodSpecs::B + sss_meth.receiver.should == obj + sss_meth.name.should == :overridden + end + + it "returns nil when there's no super method in the parent" do + method = Object.new.method(:method) + method.super_method.should == nil + end + + it "returns nil when the parent's method is removed" do + klass = Class.new do + def overridden; end + end + sub = Class.new(klass) do + def overridden; end + end + object = sub.new + method = object.method(:overridden) + + klass.class_eval { undef :overridden } + + method.super_method.should == nil + end + + # https://github.com/jruby/jruby/issues/7240 + context "after changing an inherited methods visibility" do + it "calls the proper super method" do + MethodSpecs::InheritedMethods::C.new.derp.should == 'BA' + end + + it "returns the expected super_method" do + method = MethodSpecs::InheritedMethods::C.new.method(:derp) + method.super_method.owner.should == MethodSpecs::InheritedMethods::A + end + end + + context "after aliasing an inherited method" do + it "returns the expected super_method" do + method = MethodSpecs::InheritedMethods::C.new.method(:meow) + method.super_method.owner.should == MethodSpecs::InheritedMethods::A + end + end +end diff --git a/spec/ruby/core/method/to_proc_spec.rb b/spec/ruby/core/method/to_proc_spec.rb new file mode 100644 index 0000000000..4993cce239 --- /dev/null +++ b/spec/ruby/core/method/to_proc_spec.rb @@ -0,0 +1,104 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Method#to_proc" do + before :each do + ScratchPad.record [] + + @m = MethodSpecs::Methods.new + @meth = @m.method(:foo) + end + + it "returns a Proc object corresponding to the method" do + @meth.to_proc.kind_of?(Proc).should == true + end + + it "returns a Proc which does not depends on the value of self" do + 3.instance_exec(4, &5.method(:+)).should == 9 + end + + + it "returns a Proc object with the correct arity" do + # This may seem redundant but this bug has cropped up in jruby, mri and yarv. + # http://jira.codehaus.org/browse/JRUBY-124 + [ :zero, :one_req, :two_req, + :zero_with_block, :one_req_with_block, :two_req_with_block, + :one_opt, :one_req_one_opt, :one_req_two_opt, :two_req_one_opt, + :one_opt_with_block, :one_req_one_opt_with_block, :one_req_two_opt_with_block, :two_req_one_opt_with_block, + :zero_with_splat, :one_req_with_splat, :two_req_with_splat, + :one_req_one_opt_with_splat, :one_req_two_opt_with_splat, :two_req_one_opt_with_splat, + :zero_with_splat_and_block, :one_req_with_splat_and_block, :two_req_with_splat_and_block, + :one_req_one_opt_with_splat_and_block, :one_req_two_opt_with_splat_and_block, :two_req_one_opt_with_splat_and_block + ].each do |m| + @m.method(m).to_proc.arity.should == @m.method(m).arity + end + end + + it "returns a proc that can be used by define_method" do + x = +'test' + to_s = class << x + define_method :foo, method(:to_s).to_proc + to_s + end + + x.foo.should == to_s + end + + it "returns a proc that can be yielded to" do + x = Object.new + def x.foo(*a); a; end + def x.bar; yield; end + def x.baz(*a); yield(*a); end + + m = x.method :foo + x.bar(&m).should == [] + x.baz(1,2,3,&m).should == [1,2,3] + end + + it "returns a proc whose binding has the same receiver as the method" do + @meth.receiver.should == @meth.to_proc.binding.receiver + end + + # #5926 + it "returns a proc that can receive a block" do + x = Object.new + def x.foo; yield 'bar'; end + + m = x.method :foo + result = nil + m.to_proc.call {|val| result = val} + result.should == 'bar' + end + + it "can be called directly and not unwrap arguments like a block" do + obj = MethodSpecs::ToProcBeta.new + obj.to_proc.call([1]).should == [1] + end + + it "should correct handle arguments (unwrap)" do + obj = MethodSpecs::ToProcBeta.new + + array = [[1]] + array.each(&obj) + ScratchPad.recorded.should == [[1]] + end + + it "executes method with whole array (one argument)" do + obj = MethodSpecs::ToProcBeta.new + + array = [[1, 2]] + array.each(&obj) + ScratchPad.recorded.should == [[1, 2]] + end + + it "returns a proc that properly invokes module methods with super" do + m1 = Module.new { def foo(ary); ary << :m1; end; } + m2 = Module.new { def foo(ary = []); super(ary); ary << :m2; end; } + c2 = Class.new do + include m1 + include m2 + end + + c2.new.method(:foo).to_proc.call.should == %i[m1 m2] + end +end diff --git a/spec/ruby/core/method/to_s_spec.rb b/spec/ruby/core/method/to_s_spec.rb new file mode 100644 index 0000000000..9f19011302 --- /dev/null +++ b/spec/ruby/core/method/to_s_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/to_s' + +describe "Method#to_s" do + it_behaves_like :method_to_s, :to_s +end diff --git a/spec/ruby/core/method/unbind_spec.rb b/spec/ruby/core/method/unbind_spec.rb new file mode 100644 index 0000000000..0b630e4d88 --- /dev/null +++ b/spec/ruby/core/method/unbind_spec.rb @@ -0,0 +1,46 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Method#unbind" do + before :each do + @normal = MethodSpecs::Methods.new + @normal_m = @normal.method :foo + @normal_um = @normal_m.unbind + @pop_um = MethodSpecs::MySub.new.method(:bar).unbind + @string = @pop_um.inspect.sub(/0x\w+/, '0xXXXXXX') + end + + it "returns an UnboundMethod" do + @normal_um.should be_kind_of(UnboundMethod) + end + + describe "#inspect" do + it "returns a String containing 'UnboundMethod'" do + @string.should =~ /\bUnboundMethod\b/ + end + + it "returns a String containing the method name" do + @string.should =~ /\#bar/ + end + + it "returns a String containing the Module the method is defined in" do + @string.should =~ /MethodSpecs::MyMod/ + end + + it "returns a String containing the Module the method is referenced from" do + @string.should =~ /MethodSpecs::MyMod/ + end + end + + it "keeps the origin singleton class if there is one" do + obj = Object.new + def obj.foo + end + obj.method(:foo).unbind.inspect.should.start_with?("#<UnboundMethod: #{obj.singleton_class}#foo") + end + + specify "rebinding UnboundMethod to Method's obj produces exactly equivalent Methods" do + @normal_um.bind(@normal).should == @normal_m + @normal_m.should == @normal_um.bind(@normal) + end +end diff --git a/spec/ruby/core/module/alias_method_spec.rb b/spec/ruby/core/module/alias_method_spec.rb new file mode 100644 index 0000000000..c36dedd2d8 --- /dev/null +++ b/spec/ruby/core/module/alias_method_spec.rb @@ -0,0 +1,171 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Module#alias_method" do + before :each do + @class = Class.new(ModuleSpecs::Aliasing) + @object = @class.new + end + + it "makes a copy of the method" do + @class.make_alias :uno, :public_one + @class.make_alias :double, :public_two + @object.uno.should == @object.public_one + @object.double(12).should == @object.public_two(12) + end + + it "creates methods that are == to each other" do + @class.make_alias :uno, :public_one + @object.method(:uno).should == @object.method(:public_one) + end + + it "preserves the arguments information of the original methods" do + @class.make_alias :uno, :public_one + @class.make_alias :double, :public_two + @class.instance_method(:uno).parameters.should == @class.instance_method(:public_one).parameters + @class.instance_method(:double).parameters.should == @class.instance_method(:public_two).parameters + end + + it "retains method visibility" do + @class.make_alias :private_ichi, :private_one + -> { @object.private_one }.should raise_error(NameError) + -> { @object.private_ichi }.should raise_error(NameError) + @class.make_alias :public_ichi, :public_one + @object.public_ichi.should == @object.public_one + @class.make_alias :protected_ichi, :protected_one + -> { @object.protected_ichi }.should raise_error(NameError) + end + + it "handles aliasing a stub that changes visibility" do + @class.__send__ :public, :private_one + @class.make_alias :was_private_one, :private_one + @object.was_private_one.should == 1 + end + + it "handles aliasing a method only present in a refinement" do + c = @class + Module.new do + refine c do + def uno_refined_method + end + alias_method :double_refined_method, :uno_refined_method + instance_method(:uno_refined_method).should == instance_method(:double_refined_method) + end + end + end + + it "fails if origin method not found" do + -> { @class.make_alias :ni, :san }.should raise_error(NameError) { |e| + # a NameError and not a NoMethodError + e.class.should == NameError + } + end + + it "raises FrozenError if frozen" do + @class.freeze + -> { @class.make_alias :uno, :public_one }.should raise_error(FrozenError) + end + + it "converts the names using #to_str" do + @class.make_alias "un", "public_one" + @class.make_alias :deux, "public_one" + @class.make_alias "trois", :public_one + @class.make_alias :quatre, :public_one + name = mock('cinq') + name.should_receive(:to_str).any_number_of_times.and_return("cinq") + @class.make_alias name, "public_one" + @class.make_alias "cinq", name + end + + it "raises a TypeError when the given name can't be converted using to_str" do + -> { @class.make_alias mock('x'), :public_one }.should raise_error(TypeError) + end + + it "raises a NoMethodError if the given name raises a NoMethodError during type coercion using to_str" do + obj = mock("mock-name") + obj.should_receive(:to_str).and_raise(NoMethodError) + -> { @class.make_alias obj, :public_one }.should raise_error(NoMethodError) + end + + it "is a public method" do + Module.should have_public_instance_method(:alias_method, false) + end + + describe "returned value" do + it "returns symbol of the defined method name" do + @class.send(:alias_method, :checking_return_value, :public_one).should equal(:checking_return_value) + @class.send(:alias_method, 'checking_return_value', :public_one).should equal(:checking_return_value) + end + end + + it "works in module" do + ModuleSpecs::Allonym.new.publish.should == :report + end + + it "works on private module methods in a module that has been reopened" do + ModuleSpecs::ReopeningModule.foo.should == true + -> { ModuleSpecs::ReopeningModule.foo2 }.should_not raise_error(NoMethodError) + end + + it "accesses a method defined on Object from Kernel" do + Kernel.should_not have_public_instance_method(:module_specs_public_method_on_object) + + Kernel.should have_public_instance_method(:module_specs_alias_on_kernel) + Object.should have_public_instance_method(:module_specs_alias_on_kernel) + end + + it "can call a method with super aliased twice" do + ModuleSpecs::AliasingSuper::Target.new.super_call(1).should == 1 + end + + it "preserves original super call after alias redefine" do + ModuleSpecs::AliasingSuper::RedefineAfterAlias.new.alias_super_call(1).should == 1 + end + + describe "aliasing special methods" do + before :all do + @class = ModuleSpecs::Aliasing + @subclass = ModuleSpecs::AliasingSubclass + end + + it "keeps initialize private when aliasing" do + @class.make_alias(:initialize, :public_one) + @class.private_instance_methods.include?(:initialize).should be_true + + @subclass.make_alias(:initialize, :public_one) + @subclass.private_instance_methods.include?(:initialize).should be_true + end + + it "keeps initialize_copy private when aliasing" do + @class.make_alias(:initialize_copy, :public_one) + @class.private_instance_methods.include?(:initialize_copy).should be_true + + @subclass.make_alias(:initialize_copy, :public_one) + @subclass.private_instance_methods.include?(:initialize_copy).should be_true + end + + it "keeps initialize_clone private when aliasing" do + @class.make_alias(:initialize_clone, :public_one) + @class.private_instance_methods.include?(:initialize_clone).should be_true + + @subclass.make_alias(:initialize_clone, :public_one) + @subclass.private_instance_methods.include?(:initialize_clone).should be_true + end + + it "keeps initialize_dup private when aliasing" do + @class.make_alias(:initialize_dup, :public_one) + @class.private_instance_methods.include?(:initialize_dup).should be_true + + @subclass.make_alias(:initialize_dup, :public_one) + @subclass.private_instance_methods.include?(:initialize_dup).should be_true + end + + it "keeps respond_to_missing? private when aliasing" do + @class.make_alias(:respond_to_missing?, :public_one) + @class.private_instance_methods.include?(:respond_to_missing?).should be_true + + @subclass.make_alias(:respond_to_missing?, :public_one) + @subclass.private_instance_methods.include?(:respond_to_missing?).should be_true + end + end +end diff --git a/spec/ruby/core/module/ancestors_spec.rb b/spec/ruby/core/module/ancestors_spec.rb new file mode 100644 index 0000000000..90c26941d1 --- /dev/null +++ b/spec/ruby/core/module/ancestors_spec.rb @@ -0,0 +1,88 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Module#ancestors" do + it "returns a list of modules included in self (including self)" do + BasicObject.ancestors.should == [BasicObject] + ModuleSpecs.ancestors.should == [ModuleSpecs] + ModuleSpecs::Basic.ancestors.should == [ModuleSpecs::Basic] + ModuleSpecs::Super.ancestors.should == [ModuleSpecs::Super, ModuleSpecs::Basic] + if defined?(Ruby::Box) && Ruby::Box.enabled? + ModuleSpecs.without_test_modules(ModuleSpecs::Parent.ancestors).should == + [ModuleSpecs::Parent, Object, Ruby::Box::Loader, Kernel, BasicObject] + ModuleSpecs.without_test_modules(ModuleSpecs::Child.ancestors).should == + [ModuleSpecs::Child, ModuleSpecs::Super, ModuleSpecs::Basic, ModuleSpecs::Parent, Object, Ruby::Box::Loader, Kernel, BasicObject] + else + ModuleSpecs.without_test_modules(ModuleSpecs::Parent.ancestors).should == + [ModuleSpecs::Parent, Object, Kernel, BasicObject] + ModuleSpecs.without_test_modules(ModuleSpecs::Child.ancestors).should == + [ModuleSpecs::Child, ModuleSpecs::Super, ModuleSpecs::Basic, ModuleSpecs::Parent, Object, Kernel, BasicObject] + end + end + + it "returns only modules and classes" do + class << ModuleSpecs::Child; self; end.ancestors.should include(ModuleSpecs::Internal, Class, Module, Object, Kernel) + end + + it "has 1 entry per module or class" do + ModuleSpecs::Parent.ancestors.should == ModuleSpecs::Parent.ancestors.uniq + end + + it "returns a module that is included later into a nested module as well" do + m1 = Module.new + m2 = Module.new + m3 = Module.new do + include m2 + end + m2.include m1 # should be after m3 includes m2 + + m3.ancestors.should == [m3, m2, m1] + end + + describe "when called on a singleton class" do + it "includes the singleton classes of ancestors" do + parent = Class.new + child = Class.new(parent) + schild = child.singleton_class + + schild.ancestors.should include(schild, + parent.singleton_class, + Object.singleton_class, + BasicObject.singleton_class, + Class, + Module, + Object, + Kernel, + BasicObject) + + end + + describe 'for a standalone module' do + it 'does not include Class' do + s_mod = ModuleSpecs.singleton_class + s_mod.ancestors.should_not include(Class) + end + + it 'does not include other singleton classes' do + s_standalone_mod = ModuleSpecs.singleton_class + s_module = Module.singleton_class + s_object = Object.singleton_class + s_basic_object = BasicObject.singleton_class + + s_standalone_mod.ancestors.should_not include(s_module, s_object, s_basic_object) + end + + it 'includes its own singleton class' do + s_mod = ModuleSpecs.singleton_class + + s_mod.ancestors.should include(s_mod) + end + + it 'includes standard chain' do + s_mod = ModuleSpecs.singleton_class + + s_mod.ancestors.should include(Module, Object, Kernel, BasicObject) + end + end + end +end diff --git a/spec/ruby/core/module/append_features_spec.rb b/spec/ruby/core/module/append_features_spec.rb new file mode 100644 index 0000000000..1724cde5d6 --- /dev/null +++ b/spec/ruby/core/module/append_features_spec.rb @@ -0,0 +1,61 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Module#append_features" do + it "is a private method" do + Module.should have_private_instance_method(:append_features) + end + + describe "on Class" do + it "is undefined" do + Class.should_not have_private_instance_method(:append_features, true) + end + + it "raises a TypeError if calling after rebinded to Class" do + -> { + Module.instance_method(:append_features).bind(Class.new).call Module.new + }.should raise_error(TypeError) + end + end + + it "gets called when self is included in another module/class" do + begin + m = Module.new do + def self.append_features(mod) + $appended_to = mod + end + end + + c = Class.new do + include m + end + + $appended_to.should == c + ensure + $appended_to = nil + end + end + + it "raises an ArgumentError on a cyclic include" do + -> { + ModuleSpecs::CyclicAppendA.send(:append_features, ModuleSpecs::CyclicAppendA) + }.should raise_error(ArgumentError) + + -> { + ModuleSpecs::CyclicAppendB.send(:append_features, ModuleSpecs::CyclicAppendA) + }.should raise_error(ArgumentError) + + end + + describe "when other is frozen" do + before :each do + @receiver = Module.new + @other = Module.new.freeze + end + + it "raises a FrozenError before appending self" do + -> { @receiver.send(:append_features, @other) }.should raise_error(FrozenError) + @other.ancestors.should_not include(@receiver) + end + end +end diff --git a/spec/ruby/core/module/attr_accessor_spec.rb b/spec/ruby/core/module/attr_accessor_spec.rb new file mode 100644 index 0000000000..503dccc61e --- /dev/null +++ b/spec/ruby/core/module/attr_accessor_spec.rb @@ -0,0 +1,112 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/attr_added' + +describe "Module#attr_accessor" do + it "creates a getter and setter for each given attribute name" do + c = Class.new do + attr_accessor :a, "b" + end + + o = c.new + + ['a','b'].each do |x| + o.respond_to?(x).should == true + o.respond_to?("#{x}=").should == true + end + + o.a = "a" + o.a.should == "a" + + o.b = "b" + o.b.should == "b" + o.a = o.b = nil + + o.send(:a=,"a") + o.send(:a).should == "a" + + o.send(:b=, "b") + o.send(:b).should == "b" + end + + it "not allows creating an attr_accessor on an immediate class" do + class TrueClass + attr_accessor :spec_attr_accessor + end + + -> { true.spec_attr_accessor = "a" }.should raise_error(FrozenError) + end + + it "raises FrozenError if the receiver if frozen" do + c = Class.new do + attr_accessor :foo + end + obj = c.new + obj.foo = 1 + obj.foo.should == 1 + + obj.freeze + -> { obj.foo = 42 }.should raise_error(FrozenError) + obj.foo.should == 1 + end + + it "converts non string/symbol names to strings using to_str" do + (o = mock('test')).should_receive(:to_str).any_number_of_times.and_return("test") + c = Class.new do + attr_accessor o + end + + c.new.respond_to?("test").should == true + c.new.respond_to?("test=").should == true + end + + it "raises a TypeError when the given names can't be converted to strings using to_str" do + o = mock('o') + -> { Class.new { attr_accessor o } }.should raise_error(TypeError) + (o = mock('123')).should_receive(:to_str).and_return(123) + -> { Class.new { attr_accessor o } }.should raise_error(TypeError) + end + + it "applies current visibility to methods created" do + c = Class.new do + protected + attr_accessor :foo + end + + -> { c.new.foo }.should raise_error(NoMethodError) + -> { c.new.foo=1 }.should raise_error(NoMethodError) + end + + it "is a public method" do + Module.should have_public_instance_method(:attr_accessor, false) + end + + it "returns an array of defined method names as symbols" do + Class.new do + (attr_accessor :foo, 'bar').should == [:foo, :foo=, :bar, :bar=] + end + end + + describe "on immediates" do + before :each do + class Integer + attr_accessor :foobar + end + end + + after :each do + if Integer.method_defined?(:foobar) + Integer.send(:remove_method, :foobar) + end + if Integer.method_defined?(:foobar=) + Integer.send(:remove_method, :foobar=) + end + end + + it "can read through the accessor" do + 1.foobar.should be_nil + end + end + + it_behaves_like :module_attr_added, :attr_accessor +end diff --git a/spec/ruby/core/module/attr_reader_spec.rb b/spec/ruby/core/module/attr_reader_spec.rb new file mode 100644 index 0000000000..37fd537ff5 --- /dev/null +++ b/spec/ruby/core/module/attr_reader_spec.rb @@ -0,0 +1,73 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/attr_added' + +describe "Module#attr_reader" do + it "creates a getter for each given attribute name" do + c = Class.new do + attr_reader :a, "b" + + def initialize + @a = "test" + @b = "test2" + end + end + + o = c.new + %w{a b}.each do |x| + o.respond_to?(x).should == true + o.respond_to?("#{x}=").should == false + end + + o.a.should == "test" + o.b.should == "test2" + o.send(:a).should == "test" + o.send(:b).should == "test2" + end + + it "not allows for adding an attr_reader to an immediate" do + class TrueClass + attr_reader :spec_attr_reader + end + + -> { true.instance_variable_set("@spec_attr_reader", "a") }.should raise_error(RuntimeError) + end + + it "converts non string/symbol names to strings using to_str" do + (o = mock('test')).should_receive(:to_str).any_number_of_times.and_return("test") + c = Class.new do + attr_reader o + end + + c.new.respond_to?("test").should == true + c.new.respond_to?("test=").should == false + end + + it "raises a TypeError when the given names can't be converted to strings using to_str" do + o = mock('o') + -> { Class.new { attr_reader o } }.should raise_error(TypeError) + (o = mock('123')).should_receive(:to_str).and_return(123) + -> { Class.new { attr_reader o } }.should raise_error(TypeError) + end + + it "applies current visibility to methods created" do + c = Class.new do + protected + attr_reader :foo + end + + -> { c.new.foo }.should raise_error(NoMethodError) + end + + it "is a public method" do + Module.should have_public_instance_method(:attr_reader, false) + end + + it "returns an array of defined method names as symbols" do + Class.new do + (attr_reader :foo, 'bar').should == [:foo, :bar] + end + end + + it_behaves_like :module_attr_added, :attr_reader +end diff --git a/spec/ruby/core/module/attr_spec.rb b/spec/ruby/core/module/attr_spec.rb new file mode 100644 index 0000000000..2f9f4e26dc --- /dev/null +++ b/spec/ruby/core/module/attr_spec.rb @@ -0,0 +1,159 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/attr_added' + +describe "Module#attr" do + before :each do + $VERBOSE, @verbose = false, $VERBOSE + end + + after :each do + $VERBOSE = @verbose + end + + it "creates a getter for the given attribute name" do + c = Class.new do + attr :attr + attr "attr3" + + def initialize + @attr, @attr2, @attr3 = "test", "test2", "test3" + end + end + + o = c.new + + %w{attr attr3}.each do |a| + o.respond_to?(a).should == true + o.respond_to?("#{a}=").should == false + end + + o.attr.should == "test" + o.attr3.should == "test3" + o.send(:attr).should == "test" + o.send(:attr3).should == "test3" + end + + it "creates a setter for the given attribute name if writable is true" do + c = Class.new do + attr :attr, true + attr "attr3", true + + def initialize + @attr, @attr2, @attr3 = "test", "test2", "test3" + end + end + + o = c.new + + %w{attr attr3}.each do |a| + o.respond_to?(a).should == true + o.respond_to?("#{a}=").should == true + end + + o.attr = "test updated" + o.attr3 = "test3 updated" + end + + it "creates a getter and setter for the given attribute name if called with and without writable is true" do + c = Class.new do + attr :attr, true + attr :attr + + attr "attr3", true + attr "attr3" + + def initialize + @attr, @attr2, @attr3 = "test", "test2", "test3" + end + end + + o = c.new + + %w{attr attr3}.each do |a| + o.respond_to?(a).should == true + o.respond_to?("#{a}=").should == true + end + + o.attr.should == "test" + o.attr = "test updated" + o.attr.should == "test updated" + + o.attr3.should == "test3" + o.attr3 = "test3 updated" + o.attr3.should == "test3 updated" + end + + it "applies current visibility to methods created" do + c = Class.new do + protected + attr :foo, true + end + + -> { c.new.foo }.should raise_error(NoMethodError) + -> { c.new.foo=1 }.should raise_error(NoMethodError) + end + + it "creates a getter but no setter for all given attribute names" do + c = Class.new do + attr :attr, "attr2", "attr3" + + def initialize + @attr, @attr2, @attr3 = "test", "test2", "test3" + end + end + + o = c.new + + %w{attr attr2 attr3}.each do |a| + o.respond_to?(a).should == true + o.respond_to?("#{a}=").should == false + end + + o.attr.should == "test" + o.attr2.should == "test2" + o.attr3.should == "test3" + end + + it "applies current visibility to methods created" do + c = Class.new do + protected + attr :foo, :bar + end + + -> { c.new.foo }.should raise_error(NoMethodError) + -> { c.new.bar }.should raise_error(NoMethodError) + end + + it "converts non string/symbol names to strings using to_str" do + (o = mock('test')).should_receive(:to_str).any_number_of_times.and_return("test") + Class.new { attr o }.new.respond_to?("test").should == true + end + + it "raises a TypeError when the given names can't be converted to strings using to_str" do + o = mock('o') + -> { Class.new { attr o } }.should raise_error(TypeError) + (o = mock('123')).should_receive(:to_str).and_return(123) + -> { Class.new { attr o } }.should raise_error(TypeError) + end + + it "with a boolean argument emits a warning when $VERBOSE is true" do + -> { + Class.new { attr :foo, true } + }.should complain(/boolean argument is obsoleted/, verbose: true) + end + + it "is a public method" do + Module.should have_public_instance_method(:attr, false) + end + + it "returns an array of defined method names as symbols" do + Class.new do + (attr :foo, 'bar').should == [:foo, :bar] + (attr :baz, false).should == [:baz] + (attr :qux, true).should == [:qux, :qux=] + end + end + + it_behaves_like :module_attr_added, :attr +end diff --git a/spec/ruby/core/module/attr_writer_spec.rb b/spec/ruby/core/module/attr_writer_spec.rb new file mode 100644 index 0000000000..5b863ef88c --- /dev/null +++ b/spec/ruby/core/module/attr_writer_spec.rb @@ -0,0 +1,83 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/attr_added' + +describe "Module#attr_writer" do + it "creates a setter for each given attribute name" do + c = Class.new do + attr_writer :test1, "test2" + end + o = c.new + + o.respond_to?("test1").should == false + o.respond_to?("test2").should == false + + o.respond_to?("test1=").should == true + o.test1 = "test_1" + o.instance_variable_get(:@test1).should == "test_1" + + o.respond_to?("test2=").should == true + o.test2 = "test_2" + o.instance_variable_get(:@test2).should == "test_2" + o.send(:test1=,"test_1 updated") + o.instance_variable_get(:@test1).should == "test_1 updated" + o.send(:test2=,"test_2 updated") + o.instance_variable_get(:@test2).should == "test_2 updated" + end + + it "not allows for adding an attr_writer to an immediate" do + class TrueClass + attr_writer :spec_attr_writer + end + + -> { true.spec_attr_writer = "a" }.should raise_error(FrozenError) + end + + it "raises FrozenError if the receiver if frozen" do + c = Class.new do + attr_writer :foo + end + obj = c.new + obj.freeze + + -> { obj.foo = 42 }.should raise_error(FrozenError) + end + + it "converts non string/symbol names to strings using to_str" do + (o = mock('test')).should_receive(:to_str).any_number_of_times.and_return("test") + c = Class.new do + attr_writer o + end + + c.new.respond_to?("test").should == false + c.new.respond_to?("test=").should == true + end + + it "raises a TypeError when the given names can't be converted to strings using to_str" do + o = mock('test1') + -> { Class.new { attr_writer o } }.should raise_error(TypeError) + (o = mock('123')).should_receive(:to_str).and_return(123) + -> { Class.new { attr_writer o } }.should raise_error(TypeError) + end + + it "applies current visibility to methods created" do + c = Class.new do + protected + attr_writer :foo + end + + -> { c.new.foo=1 }.should raise_error(NoMethodError) + end + + it "is a public method" do + Module.should have_public_instance_method(:attr_writer, false) + end + + it "returns an array of defined method names as symbols" do + Class.new do + (attr_writer :foo, 'bar').should == [:foo=, :bar=] + end + end + + it_behaves_like :module_attr_added, :attr_writer +end diff --git a/spec/ruby/core/module/autoload_spec.rb b/spec/ruby/core/module/autoload_spec.rb new file mode 100644 index 0000000000..625d945686 --- /dev/null +++ b/spec/ruby/core/module/autoload_spec.rb @@ -0,0 +1,1026 @@ +require_relative '../../spec_helper' +require_relative '../../fixtures/code_loading' +require_relative 'fixtures/classes' + +describe "Module#autoload?" do + it "returns the name of the file that will be autoloaded" do + ModuleSpecs::Autoload.autoload :Autoload, "autoload.rb" + ModuleSpecs::Autoload.autoload?(:Autoload).should == "autoload.rb" + end + + it "returns nil if no file has been registered for a constant" do + ModuleSpecs::Autoload.autoload?(:Manualload).should be_nil + end + + it "returns the name of the file that will be autoloaded if an ancestor defined that autoload" do + ModuleSpecs::Autoload::Parent.autoload :AnotherAutoload, "another_autoload.rb" + ModuleSpecs::Autoload::Child.autoload?(:AnotherAutoload).should == "another_autoload.rb" + end + + it "returns nil if an ancestor defined that autoload but recursion is disabled" do + ModuleSpecs::Autoload::Parent.autoload :InheritedAutoload, "inherited_autoload.rb" + ModuleSpecs::Autoload::Child.autoload?(:InheritedAutoload, false).should be_nil + end + + it "returns the name of the file that will be loaded if recursion is disabled but the autoload is defined on the class itself" do + ModuleSpecs::Autoload::Child.autoload :ChildAutoload, "child_autoload.rb" + ModuleSpecs::Autoload::Child.autoload?(:ChildAutoload, false).should == "child_autoload.rb" + end +end + +describe "Module#autoload" do + before :all do + @non_existent = fixture __FILE__, "no_autoload.rb" + CodeLoadingSpecs.preload_rubygems + end + + before :each do + @loaded_features = $".dup + + ScratchPad.clear + @remove = [] + end + + after :each do + $".replace @loaded_features + @remove.each { |const| + ModuleSpecs::Autoload.send :remove_const, const + } + end + + it "registers a file to load the first time the named constant is accessed" do + ModuleSpecs::Autoload.autoload :A, @non_existent + ModuleSpecs::Autoload.autoload?(:A).should == @non_existent + end + + it "sets the autoload constant in the constants table" do + ModuleSpecs::Autoload.autoload :B, @non_existent + ModuleSpecs::Autoload.should have_constant(:B) + end + + it "can be overridden with a second autoload on the same constant" do + ModuleSpecs::Autoload.autoload :Overridden, @non_existent + @remove << :Overridden + ModuleSpecs::Autoload.autoload?(:Overridden).should == @non_existent + + path = fixture(__FILE__, "autoload_overridden.rb") + ModuleSpecs::Autoload.autoload :Overridden, path + ModuleSpecs::Autoload.autoload?(:Overridden).should == path + + ModuleSpecs::Autoload::Overridden.should == :overridden + end + + it "loads the registered constant when it is accessed" do + ModuleSpecs::Autoload.should_not have_constant(:X) + ModuleSpecs::Autoload.autoload :X, fixture(__FILE__, "autoload_x.rb") + @remove << :X + ModuleSpecs::Autoload::X.should == :x + end + + it "loads the registered constant into a dynamically created class" do + cls = Class.new { autoload :C, fixture(__FILE__, "autoload_c.rb") } + ModuleSpecs::Autoload::DynClass = cls + @remove << :DynClass + + ScratchPad.recorded.should be_nil + ModuleSpecs::Autoload::DynClass::C.new.loaded.should == :dynclass_c + ScratchPad.recorded.should == :loaded + end + + it "loads the registered constant into a dynamically created module" do + mod = Module.new { autoload :D, fixture(__FILE__, "autoload_d.rb") } + ModuleSpecs::Autoload::DynModule = mod + @remove << :DynModule + + ScratchPad.recorded.should be_nil + ModuleSpecs::Autoload::DynModule::D.new.loaded.should == :dynmodule_d + ScratchPad.recorded.should == :loaded + end + + it "loads the registered constant when it is opened as a class" do + ModuleSpecs::Autoload.autoload :E, fixture(__FILE__, "autoload_e.rb") + class ModuleSpecs::Autoload::E + end + ModuleSpecs::Autoload::E.new.loaded.should == :autoload_e + end + + it "loads the registered constant when it is opened as a module" do + ModuleSpecs::Autoload.autoload :F, fixture(__FILE__, "autoload_f.rb") + module ModuleSpecs::Autoload::F + end + ModuleSpecs::Autoload::F.loaded.should == :autoload_f + end + + it "loads the registered constant when it is inherited from" do + ModuleSpecs::Autoload.autoload :G, fixture(__FILE__, "autoload_g.rb") + class ModuleSpecs::Autoload::Gsub < ModuleSpecs::Autoload::G + end + ModuleSpecs::Autoload::Gsub.new.loaded.should == :autoload_g + end + + it "loads the registered constant when it is included" do + ModuleSpecs::Autoload.autoload :H, fixture(__FILE__, "autoload_h.rb") + class ModuleSpecs::Autoload::HClass + include ModuleSpecs::Autoload::H + end + ModuleSpecs::Autoload::HClass.new.loaded.should == :autoload_h + end + + it "does not load the file when the constant is already set" do + ModuleSpecs::Autoload.autoload :I, fixture(__FILE__, "autoload_i.rb") + @remove << :I + ModuleSpecs::Autoload.const_set :I, 3 + ModuleSpecs::Autoload::I.should == 3 + ScratchPad.recorded.should be_nil + end + + it "loads a file with .rb extension when passed the name without the extension" do + ModuleSpecs::Autoload.autoload :J, fixture(__FILE__, "autoload_j") + ModuleSpecs::Autoload::J.should == :autoload_j + end + + it "calls main.require(path) to load the file" do + ModuleSpecs::Autoload.autoload :ModuleAutoloadCallsRequire, "module_autoload_not_exist.rb" + main = TOPLEVEL_BINDING.eval("self") + main.should_receive(:require).with("module_autoload_not_exist.rb") + # The constant won't be defined since require is mocked to do nothing + -> { ModuleSpecs::Autoload::ModuleAutoloadCallsRequire }.should raise_error(NameError) + end + + it "does not load the file if the file is manually required" do + filename = fixture(__FILE__, "autoload_k.rb") + ModuleSpecs::Autoload.autoload :KHash, filename + @remove << :KHash + + require filename + ScratchPad.recorded.should == :loaded + ScratchPad.clear + + ModuleSpecs::Autoload::KHash.should be_kind_of(Class) + ModuleSpecs::Autoload::KHash::K.should == :autoload_k + ScratchPad.recorded.should be_nil + end + + it "ignores the autoload request if the file is already loaded" do + filename = fixture(__FILE__, "autoload_s.rb") + + require filename + + ScratchPad.recorded.should == :loaded + ScratchPad.clear + + ModuleSpecs::Autoload.autoload :S, filename + @remove << :S + ModuleSpecs::Autoload.autoload?(:S).should be_nil + end + + it "retains the autoload even if the request to require fails" do + filename = fixture(__FILE__, "a_path_that_should_not_exist.rb") + + ModuleSpecs::Autoload.autoload :NotThere, filename + ModuleSpecs::Autoload.autoload?(:NotThere).should == filename + + -> { + require filename + }.should raise_error(LoadError) + + ModuleSpecs::Autoload.autoload?(:NotThere).should == filename + end + + it "allows multiple autoload constants for a single file" do + filename = fixture(__FILE__, "autoload_lm.rb") + ModuleSpecs::Autoload.autoload :L, filename + ModuleSpecs::Autoload.autoload :M, filename + ModuleSpecs::Autoload::L.should == :autoload_l + ModuleSpecs::Autoload::M.should == :autoload_m + end + + it "runs for an exception condition class and doesn't trample the exception" do + filename = fixture(__FILE__, "autoload_ex1.rb") + ModuleSpecs::Autoload.autoload :EX1, filename + ModuleSpecs::Autoload.use_ex1.should == :good + end + + it "considers an autoload constant as loaded when autoload is called for/from the current file" do + filename = fixture(__FILE__, "autoload_during_require_current_file.rb") + require filename + + ScratchPad.recorded.should be_nil + end + + describe "interacting with defined?" do + it "does not load the file when referring to the constant in defined?" do + module ModuleSpecs::Autoload::Dog + autoload :R, fixture(__FILE__, "autoload_exception.rb") + end + + defined?(ModuleSpecs::Autoload::Dog::R).should == "constant" + ScratchPad.recorded.should be_nil + + ModuleSpecs::Autoload::Dog.should have_constant(:R) + end + + it "loads an autoloaded parent when referencing a nested constant" do + module ModuleSpecs::Autoload + autoload :GoodParent, fixture(__FILE__, "autoload_nested.rb") + end + @remove << :GoodParent + + defined?(ModuleSpecs::Autoload::GoodParent::Nested).should == 'constant' + ScratchPad.recorded.should == :loaded + end + + it "returns nil when it fails to load an autoloaded parent when referencing a nested constant" do + module ModuleSpecs::Autoload + autoload :BadParent, fixture(__FILE__, "autoload_exception.rb") + end + + defined?(ModuleSpecs::Autoload::BadParent::Nested).should be_nil + ScratchPad.recorded.should == :exception + end + end + + describe "the autoload is triggered when the same file is required directly" do + before :each do + module ModuleSpecs::Autoload + autoload :RequiredDirectly, fixture(__FILE__, "autoload_required_directly.rb") + end + @remove << :RequiredDirectly + @path = fixture(__FILE__, "autoload_required_directly.rb") + @check = -> { + [ + defined?(ModuleSpecs::Autoload::RequiredDirectly), + ModuleSpecs::Autoload.autoload?(:RequiredDirectly) + ] + } + ScratchPad.record @check + end + + it "with a full path" do + @check.call.should == ["constant", @path] + require @path + ScratchPad.recorded.should == [nil, nil] + @check.call.should == ["constant", nil] + end + + it "with a relative path" do + @check.call.should == ["constant", @path] + $:.push File.dirname(@path) + begin + require "autoload_required_directly.rb" + ensure + $:.pop + end + ScratchPad.recorded.should == [nil, nil] + @check.call.should == ["constant", nil] + end + + it "in a nested require" do + nested = fixture(__FILE__, "autoload_required_directly_nested.rb") + nested_require = -> { + result = nil + ScratchPad.record -> { + result = @check.call + } + require nested + result + } + ScratchPad.record nested_require + + @check.call.should == ["constant", @path] + require @path + ScratchPad.recorded.should == [nil, nil] + @check.call.should == ["constant", nil] + end + + it "does not raise an error if the autoload constant was not defined" do + module ModuleSpecs::Autoload + autoload :RequiredDirectlyNoConstant, fixture(__FILE__, "autoload_required_directly_no_constant.rb") + end + @path = fixture(__FILE__, "autoload_required_directly_no_constant.rb") + @remove << :RequiredDirectlyNoConstant + @check = -> { + [ + defined?(ModuleSpecs::Autoload::RequiredDirectlyNoConstant), + ModuleSpecs::Autoload.constants(false).include?(:RequiredDirectlyNoConstant), + ModuleSpecs::Autoload.const_defined?(:RequiredDirectlyNoConstant), + ModuleSpecs::Autoload.autoload?(:RequiredDirectlyNoConstant) + ] + } + ScratchPad.record @check + @check.call.should == ["constant", true, true, @path] + $:.push File.dirname(@path) + begin + require "autoload_required_directly_no_constant.rb" + ensure + $:.pop + end + ScratchPad.recorded.should == [nil, true, false, nil] + @check.call.should == [nil, true, false, nil] + end + end + + describe "after the autoload is triggered by require" do + before :each do + @path = tmp("autoload.rb") + end + + after :each do + rm_r @path + end + + it "the mapping feature to autoload is removed, and a new autoload with the same path is considered" do + ModuleSpecs::Autoload.autoload :RequireMapping1, @path + touch(@path) { |f| f.puts "ModuleSpecs::Autoload::RequireMapping1 = 1" } + ModuleSpecs::Autoload::RequireMapping1.should == 1 + + $LOADED_FEATURES.delete(@path) + ModuleSpecs::Autoload.autoload :RequireMapping2, @path[0...-3] + @remove << :RequireMapping2 + touch(@path) { |f| f.puts "ModuleSpecs::Autoload::RequireMapping2 = 2" } + ModuleSpecs::Autoload::RequireMapping2.should == 2 + end + end + + def check_before_during_thread_after(const, &check) + before = check.call + to_autoload_thread, from_autoload_thread = Queue.new, Queue.new + ScratchPad.record -> { + from_autoload_thread.push check.call + to_autoload_thread.pop + } + t = Thread.new { + in_loading_thread = from_autoload_thread.pop + in_other_thread = check.call + to_autoload_thread.push :done + [in_loading_thread, in_other_thread] + } + in_loading_thread, in_other_thread = nil + begin + ModuleSpecs::Autoload.const_get(const) + ensure + in_loading_thread, in_other_thread = t.value + end + after = check.call + [before, in_loading_thread, in_other_thread, after] + end + + describe "during the autoload before the constant is assigned" do + before :each do + @path = fixture(__FILE__, "autoload_during_autoload.rb") + ModuleSpecs::Autoload.autoload :DuringAutoload, @path + @remove << :DuringAutoload + raise unless ModuleSpecs::Autoload.autoload?(:DuringAutoload) == @path + end + + it "returns nil in autoload thread and 'constant' otherwise for defined?" do + results = check_before_during_thread_after(:DuringAutoload) { + defined?(ModuleSpecs::Autoload::DuringAutoload) + } + results.should == ['constant', nil, 'constant', 'constant'] + end + + it "keeps the constant in Module#constants" do + results = check_before_during_thread_after(:DuringAutoload) { + ModuleSpecs::Autoload.constants(false).include?(:DuringAutoload) + } + results.should == [true, true, true, true] + end + + it "returns false in autoload thread and true otherwise for Module#const_defined?" do + results = check_before_during_thread_after(:DuringAutoload) { + ModuleSpecs::Autoload.const_defined?(:DuringAutoload, false) + } + results.should == [true, false, true, true] + end + + it "returns nil in autoload thread and returns the path in other threads for Module#autoload?" do + results = check_before_during_thread_after(:DuringAutoload) { + ModuleSpecs::Autoload.autoload?(:DuringAutoload) + } + results.should == [@path, nil, @path, nil] + end + end + + describe "during the autoload after the constant is assigned" do + before :each do + @path = fixture(__FILE__, "autoload_during_autoload_after_define.rb") + ModuleSpecs::Autoload.autoload :DuringAutoloadAfterDefine, @path + @autoload_location = [__FILE__, __LINE__ - 1] + @const_location = [@path, 2] + @remove << :DuringAutoloadAfterDefine + raise unless ModuleSpecs::Autoload.autoload?(:DuringAutoloadAfterDefine) == @path + end + + it "returns 'constant' in both threads" do + results = check_before_during_thread_after(:DuringAutoloadAfterDefine) { + defined?(ModuleSpecs::Autoload::DuringAutoloadAfterDefine) + } + results.should == ['constant', 'constant', 'constant', 'constant'] + end + + it "Module#constants include the autoloaded in both threads" do + results = check_before_during_thread_after(:DuringAutoloadAfterDefine) { + ModuleSpecs::Autoload.constants(false).include?(:DuringAutoloadAfterDefine) + } + results.should == [true, true, true, true] + end + + it "Module#const_defined? returns true in both threads" do + results = check_before_during_thread_after(:DuringAutoloadAfterDefine) { + ModuleSpecs::Autoload.const_defined?(:DuringAutoloadAfterDefine, false) + } + results.should == [true, true, true, true] + end + + it "returns nil in autoload thread and returns the path in other threads for Module#autoload?" do + results = check_before_during_thread_after(:DuringAutoloadAfterDefine) { + ModuleSpecs::Autoload.autoload?(:DuringAutoloadAfterDefine) + } + results.should == [@path, nil, @path, nil] + end + + ruby_bug("#20188", ""..."3.4") do + it "returns the real constant location in autoload thread and returns the autoload location in other threads for Module#const_source_location" do + results = check_before_during_thread_after(:DuringAutoloadAfterDefine) { + ModuleSpecs::Autoload.const_source_location(:DuringAutoloadAfterDefine) + } + results.should == [@autoload_location, @const_location, @autoload_location, @const_location] + end + end + end + + it "does not remove the constant from Module#constants if load fails and keeps it as an autoload" do + ModuleSpecs::Autoload.autoload :Fail, @non_existent + + ModuleSpecs::Autoload.const_defined?(:Fail).should == true + ModuleSpecs::Autoload.should have_constant(:Fail) + ModuleSpecs::Autoload.autoload?(:Fail).should == @non_existent + + -> { ModuleSpecs::Autoload::Fail }.should raise_error(LoadError) + + ModuleSpecs::Autoload.should have_constant(:Fail) + ModuleSpecs::Autoload.const_defined?(:Fail).should == true + ModuleSpecs::Autoload.autoload?(:Fail).should == @non_existent + + -> { ModuleSpecs::Autoload::Fail }.should raise_error(LoadError) + end + + it "does not remove the constant from Module#constants if load raises a RuntimeError and keeps it as an autoload" do + path = fixture(__FILE__, "autoload_raise.rb") + ScratchPad.record [] + ModuleSpecs::Autoload.autoload :Raise, path + + ModuleSpecs::Autoload.const_defined?(:Raise).should == true + ModuleSpecs::Autoload.should have_constant(:Raise) + ModuleSpecs::Autoload.autoload?(:Raise).should == path + + -> { ModuleSpecs::Autoload::Raise }.should raise_error(RuntimeError) + ScratchPad.recorded.should == [:raise] + + ModuleSpecs::Autoload.should have_constant(:Raise) + ModuleSpecs::Autoload.const_defined?(:Raise).should == true + ModuleSpecs::Autoload.autoload?(:Raise).should == path + + -> { ModuleSpecs::Autoload::Raise }.should raise_error(RuntimeError) + ScratchPad.recorded.should == [:raise, :raise] + end + + it "removes the constant from Module#constants if the loaded file does not define it" do + path = fixture(__FILE__, "autoload_o.rb") + ScratchPad.record [] + ModuleSpecs::Autoload.autoload :O, path + + ModuleSpecs::Autoload.const_defined?(:O).should == true + ModuleSpecs::Autoload.should have_constant(:O) + ModuleSpecs::Autoload.autoload?(:O).should == path + + -> { ModuleSpecs::Autoload::O }.should raise_error(NameError) + + ModuleSpecs::Autoload.const_defined?(:O).should == false + ModuleSpecs::Autoload.should_not have_constant(:O) + ModuleSpecs::Autoload.autoload?(:O).should == nil + -> { ModuleSpecs::Autoload.const_get(:O) }.should raise_error(NameError) + end + + it "does not try to load the file again if the loaded file did not define the constant" do + path = fixture(__FILE__, "autoload_o.rb") + ScratchPad.record [] + ModuleSpecs::Autoload.autoload :NotDefinedByFile, path + + -> { ModuleSpecs::Autoload::NotDefinedByFile }.should raise_error(NameError) + ScratchPad.recorded.should == [:loaded] + -> { ModuleSpecs::Autoload::NotDefinedByFile }.should raise_error(NameError) + ScratchPad.recorded.should == [:loaded] + + Thread.new { + -> { ModuleSpecs::Autoload::NotDefinedByFile }.should raise_error(NameError) + }.join + ScratchPad.recorded.should == [:loaded] + end + + it "returns 'constant' on referring the constant with defined?()" do + module ModuleSpecs::Autoload::Q + autoload :R, fixture(__FILE__, "autoload.rb") + defined?(R).should == 'constant' + end + ModuleSpecs::Autoload::Q.should have_constant(:R) + end + + it "does not load the file when removing an autoload constant" do + module ModuleSpecs::Autoload::Q + autoload :R, fixture(__FILE__, "autoload.rb") + remove_const :R + end + ModuleSpecs::Autoload::Q.should_not have_constant(:R) + end + + it "does not load the file when accessing the constants table of the module" do + ModuleSpecs::Autoload.autoload :P, @non_existent + ModuleSpecs::Autoload.const_defined?(:P).should be_true + ModuleSpecs::Autoload.const_defined?("P").should be_true + end + + it "loads the file when opening a module that is the autoloaded constant" do + module ModuleSpecs::Autoload::U + autoload :V, fixture(__FILE__, "autoload_v.rb") + + class V + X = get_value + end + end + @remove << :U + + ModuleSpecs::Autoload::U::V::X.should == :autoload_uvx + end + + it "loads the file that defines subclass XX::CS_CONST_AUTOLOAD < CS_CONST_AUTOLOAD and CS_CONST_AUTOLOAD is a top level constant" do + module ModuleSpecs::Autoload::XX + autoload :CS_CONST_AUTOLOAD, fixture(__FILE__, "autoload_subclass.rb") + end + + ModuleSpecs::Autoload::XX::CS_CONST_AUTOLOAD.superclass.should == CS_CONST_AUTOLOAD + end + + describe "after autoloading searches for the constant like the original lookup" do + it "in lexical scopes if both declared and defined in parent" do + module ModuleSpecs::Autoload + ScratchPad.record -> { + DeclaredAndDefinedInParent = :declared_and_defined_in_parent + } + autoload :DeclaredAndDefinedInParent, fixture(__FILE__, "autoload_callback.rb") + class LexicalScope + DeclaredAndDefinedInParent.should == :declared_and_defined_in_parent + + # The constant is really in Autoload, not Autoload::LexicalScope + self.should_not have_constant(:DeclaredAndDefinedInParent) + -> { const_get(:DeclaredAndDefinedInParent) }.should raise_error(NameError) + end + DeclaredAndDefinedInParent.should == :declared_and_defined_in_parent + end + end + + it "in lexical scopes if declared in parent and defined in current" do + module ModuleSpecs::Autoload + ScratchPad.record -> { + class LexicalScope + DeclaredInParentDefinedInCurrent = :declared_in_parent_defined_in_current + end + } + autoload :DeclaredInParentDefinedInCurrent, fixture(__FILE__, "autoload_callback.rb") + + class LexicalScope + DeclaredInParentDefinedInCurrent.should == :declared_in_parent_defined_in_current + LexicalScope::DeclaredInParentDefinedInCurrent.should == :declared_in_parent_defined_in_current + end + + # Basically, the parent autoload constant remains in a "undefined" state + self.autoload?(:DeclaredInParentDefinedInCurrent).should == nil + const_defined?(:DeclaredInParentDefinedInCurrent).should == false + -> { DeclaredInParentDefinedInCurrent }.should raise_error(NameError) + + ModuleSpecs::Autoload::LexicalScope.send(:remove_const, :DeclaredInParentDefinedInCurrent) + end + end + + it "warns once in verbose mode if the constant was defined in a parent scope" do + ScratchPad.record -> { + ModuleSpecs::DeclaredInCurrentDefinedInParent = :declared_in_current_defined_in_parent + } + + module ModuleSpecs + module Autoload + autoload :DeclaredInCurrentDefinedInParent, fixture(__FILE__, "autoload_callback.rb") + self.autoload?(:DeclaredInCurrentDefinedInParent).should == fixture(__FILE__, "autoload_callback.rb") + const_defined?(:DeclaredInCurrentDefinedInParent).should == true + + -> { + DeclaredInCurrentDefinedInParent + }.should complain( + /Expected .*autoload_callback.rb to define ModuleSpecs::Autoload::DeclaredInCurrentDefinedInParent but it didn't/, + verbose: true, + ) + + -> { + DeclaredInCurrentDefinedInParent + }.should_not complain(/.*/, verbose: true) + self.autoload?(:DeclaredInCurrentDefinedInParent).should == nil + const_defined?(:DeclaredInCurrentDefinedInParent).should == false + ModuleSpecs.const_defined?(:DeclaredInCurrentDefinedInParent).should == true + end + end + end + + it "looks up in parent scope after failed autoload" do + @remove << :DeclaredInCurrentDefinedInParent + module ModuleSpecs::Autoload + ScratchPad.record -> { + DeclaredInCurrentDefinedInParent = :declared_in_current_defined_in_parent + } + + class LexicalScope + autoload :DeclaredInCurrentDefinedInParent, fixture(__FILE__, "autoload_callback.rb") + -> { DeclaredInCurrentDefinedInParent }.should_not raise_error(NameError) + # Basically, the autoload constant remains in a "undefined" state + self.autoload?(:DeclaredInCurrentDefinedInParent).should == nil + const_defined?(:DeclaredInCurrentDefinedInParent).should == false + -> { const_get(:DeclaredInCurrentDefinedInParent) }.should raise_error(NameError) + end + + DeclaredInCurrentDefinedInParent.should == :declared_in_current_defined_in_parent + end + end + + it "in the included modules" do + @remove << :DefinedInIncludedModule + module ModuleSpecs::Autoload + ScratchPad.record -> { + module DefinedInIncludedModule + Incl = :defined_in_included_module + end + include DefinedInIncludedModule + } + autoload :Incl, fixture(__FILE__, "autoload_callback.rb") + Incl.should == :defined_in_included_module + end + end + + it "in the included modules of the superclass" do + @remove << :DefinedInSuperclassIncludedModule + module ModuleSpecs::Autoload + class LookupAfterAutoloadSuper + end + class LookupAfterAutoloadChild < LookupAfterAutoloadSuper + end + + ScratchPad.record -> { + module DefinedInSuperclassIncludedModule + InclS = :defined_in_superclass_included_module + end + LookupAfterAutoloadSuper.include DefinedInSuperclassIncludedModule + } + + class LookupAfterAutoloadChild + autoload :InclS, fixture(__FILE__, "autoload_callback.rb") + InclS.should == :defined_in_superclass_included_module + end + end + end + + it "in the prepended modules" do + @remove << :DefinedInPrependedModule + module ModuleSpecs::Autoload + ScratchPad.record -> { + module DefinedInPrependedModule + Prep = :defined_in_prepended_module + end + include DefinedInPrependedModule + } + autoload :Prep, fixture(__FILE__, "autoload_callback.rb") + Prep.should == :defined_in_prepended_module + end + end + + it "in a meta class scope" do + module ModuleSpecs::Autoload + ScratchPad.record -> { + class MetaScope + end + } + autoload :MetaScope, fixture(__FILE__, "autoload_callback.rb") + class << self + def r + MetaScope.new + end + end + end + ModuleSpecs::Autoload.r.should be_kind_of(ModuleSpecs::Autoload::MetaScope) + end + end + + it "should trigger the autoload when using `private_constant`" do + @remove << :DynClass + module ModuleSpecs::Autoload + autoload :DynClass, fixture(__FILE__, "autoload_c.rb") + private_constant :DynClass + + ScratchPad.recorded.should be_nil + + DynClass::C.new.loaded.should == :dynclass_c + ScratchPad.recorded.should == :loaded + end + + -> { ModuleSpecs::Autoload::DynClass }.should raise_error(NameError, /private constant/) + end + + # [ruby-core:19127] [ruby-core:29941] + it "does NOT raise a NameError when the autoload file did not define the constant and a module is opened with the same name" do + module ModuleSpecs::Autoload + class W + autoload :Y, fixture(__FILE__, "autoload_w.rb") + + class Y + end + end + end + @remove << :W + + ModuleSpecs::Autoload::W::Y.should be_kind_of(Class) + ScratchPad.recorded.should == :loaded + end + + it "does not call #require a second time and does not warn if already loading the same feature with #require" do + main = TOPLEVEL_BINDING.eval("self") + main.should_not_receive(:require) + + module ModuleSpecs::Autoload + autoload :AutoloadDuringRequire, fixture(__FILE__, "autoload_during_require.rb") + end + + -> { + Kernel.require fixture(__FILE__, "autoload_during_require.rb") + }.should_not complain(verbose: true) + ModuleSpecs::Autoload::AutoloadDuringRequire.should be_kind_of(Class) + end + + it "does not call #require a second time and does not warn if feature sets and trigger autoload on itself" do + main = TOPLEVEL_BINDING.eval("self") + main.should_not_receive(:require) + + -> { + Kernel.require fixture(__FILE__, "autoload_self_during_require.rb") + }.should_not complain(verbose: true) + ModuleSpecs::Autoload::AutoloadSelfDuringRequire.should be_kind_of(Class) + end + + it "handles multiple autoloads in the same file" do + $LOAD_PATH.unshift(File.expand_path('../fixtures/multi', __FILE__)) + begin + require 'foo/bar_baz' + ModuleSpecs::Autoload::Foo::Bar.should be_kind_of(Class) + ModuleSpecs::Autoload::Foo::Baz.should be_kind_of(Class) + ensure + $LOAD_PATH.shift + end + end + + it "calls #to_path on non-string filenames" do + p = mock('path') + p.should_receive(:to_path).and_return @non_existent + ModuleSpecs.autoload :A, p + end + + it "raises an ArgumentError when an empty filename is given" do + -> { ModuleSpecs.autoload :A, "" }.should raise_error(ArgumentError) + end + + it "raises a NameError when the constant name starts with a lower case letter" do + -> { ModuleSpecs.autoload "a", @non_existent }.should raise_error(NameError) + end + + it "raises a NameError when the constant name starts with a number" do + -> { ModuleSpecs.autoload "1two", @non_existent }.should raise_error(NameError) + end + + it "raises a NameError when the constant name has a space in it" do + -> { ModuleSpecs.autoload "a name", @non_existent }.should raise_error(NameError) + end + + it "shares the autoload request across dup'ed copies of modules" do + require fixture(__FILE__, "autoload_s.rb") + @remove << :S + filename = fixture(__FILE__, "autoload_t.rb") + mod1 = Module.new { autoload :T, filename } + -> { + ModuleSpecs::Autoload::S = mod1 + }.should complain(/already initialized constant/) + mod2 = mod1.dup + + mod1.autoload?(:T).should == filename + mod2.autoload?(:T).should == filename + + mod1::T.should == :autoload_t + -> { mod2::T }.should raise_error(NameError) + end + + it "raises a TypeError if opening a class with a different superclass than the class defined in the autoload file" do + ModuleSpecs::Autoload.autoload :Z, fixture(__FILE__, "autoload_z.rb") + class ModuleSpecs::Autoload::ZZ + end + + -> do + class ModuleSpecs::Autoload::Z < ModuleSpecs::Autoload::ZZ + end + end.should raise_error(TypeError) + end + + it "raises a TypeError if not passed a String or object responding to #to_path for the filename" do + name = mock("autoload_name.rb") + + -> { ModuleSpecs::Autoload.autoload :Str, name }.should raise_error(TypeError) + end + + it "calls #to_path on non-String filename arguments" do + name = mock("autoload_name.rb") + name.should_receive(:to_path).and_return("autoload_name.rb") + + -> { ModuleSpecs::Autoload.autoload :Str, name }.should_not raise_error + end + + describe "on a frozen module" do + it "raises a FrozenError before setting the name" do + frozen_module = Module.new.freeze + -> { frozen_module.autoload :Foo, @non_existent }.should raise_error(FrozenError) + frozen_module.should_not have_constant(:Foo) + end + end + + describe "when changing $LOAD_PATH" do + before do + $LOAD_PATH.unshift(File.expand_path('../fixtures/path1', __FILE__)) + end + + after do + $LOAD_PATH.shift + $LOAD_PATH.shift + end + + it "does not reload a file due to a different load path" do + ModuleSpecs::Autoload.autoload :LoadPath, "load_path" + ModuleSpecs::Autoload::LoadPath.loaded.should == :autoload_load_path + end + end + + describe "(concurrently)" do + it "blocks a second thread while a first is doing the autoload" do + ModuleSpecs::Autoload.autoload :Concur, fixture(__FILE__, "autoload_concur.rb") + @remove << :Concur + + start = false + + ScratchPad.record [] + + t1_val = nil + t2_val = nil + + fin = false + + t1 = Thread.new do + Thread.pass until start + t1_val = ModuleSpecs::Autoload::Concur + ScratchPad.recorded << :t1_post + fin = true + end + + t2_exc = nil + + t2 = Thread.new do + Thread.pass until t1 and t1[:in_autoload_rb] + begin + t2_val = ModuleSpecs::Autoload::Concur + rescue Exception => e + t2_exc = e + else + Thread.pass until fin + ScratchPad.recorded << :t2_post + end + end + + start = true + + t1.join + t2.join + + ScratchPad.recorded.should == [:con_pre, :con_post, :t1_post, :t2_post] + + t1_val.should == 1 + t2_val.should == t1_val + + t2_exc.should be_nil + end + + # https://bugs.ruby-lang.org/issues/10892 + it "blocks others threads while doing an autoload" do + file_path = fixture(__FILE__, "repeated_concurrent_autoload.rb") + autoload_path = file_path.sub(/\.rb\Z/, '') + mod_count = 30 + thread_count = 16 + + mod_names = [] + mod_count.times do |i| + mod_name = :"Mod#{i}" + Object.autoload mod_name, autoload_path + mod_names << mod_name + end + + barrier = ModuleSpecs::CyclicBarrier.new thread_count + ScratchPad.record ModuleSpecs::ThreadSafeCounter.new + + threads = (1..thread_count).map do + Thread.new do + mod_names.each do |mod_name| + break false unless barrier.enabled? + + was_last_one_in = barrier.await # wait for all threads to finish the iteration + # clean up so we can autoload the same file again + $LOADED_FEATURES.delete(file_path) if was_last_one_in && $LOADED_FEATURES.include?(file_path) + barrier.await # get ready for race + + begin + Object.const_get(mod_name).foo + rescue NoMethodError + barrier.disable! + break false + end + end + end + end + + # check that no thread got a NoMethodError because of partially loaded module + threads.all? {|t| t.value}.should be_true + + # check that the autoloaded file was evaled exactly once + ScratchPad.recorded.get.should == mod_count + + mod_names.each do |mod_name| + Object.send(:remove_const, mod_name) + end + end + + it "raises a NameError in each thread if the constant is not set" do + file = fixture(__FILE__, "autoload_never_set.rb") + start = false + + threads = Array.new(10) do + Thread.new do + Thread.pass until start + begin + ModuleSpecs::Autoload.autoload :NeverSetConstant, file + Thread.pass + ModuleSpecs::Autoload::NeverSetConstant + rescue NameError => e + e + ensure + Thread.pass + end + end + end + + start = true + threads.each { |t| + t.value.should be_an_instance_of(NameError) + } + end + + it "raises a LoadError in each thread if the file does not exist" do + file = fixture(__FILE__, "autoload_does_not_exist.rb") + start = false + + threads = Array.new(10) do + Thread.new do + Thread.pass until start + begin + ModuleSpecs::Autoload.autoload :FileDoesNotExist, file + Thread.pass + ModuleSpecs::Autoload::FileDoesNotExist + rescue LoadError => e + e + ensure + Thread.pass + end + end + end + + start = true + threads.each { |t| + t.value.should be_an_instance_of(LoadError) + } + end + end + + it "loads the registered constant even if the constant was already loaded by another thread" do + Thread.new { + ModuleSpecs::Autoload::FromThread::D.foo + }.value.should == :foo + end +end diff --git a/spec/ruby/core/module/case_compare_spec.rb b/spec/ruby/core/module/case_compare_spec.rb new file mode 100644 index 0000000000..49ac359f6f --- /dev/null +++ b/spec/ruby/core/module/case_compare_spec.rb @@ -0,0 +1,31 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Module#===" do + it "returns true when the given Object is an instance of self or of self's descendants" do + (ModuleSpecs::Child === ModuleSpecs::Child.new).should == true + (ModuleSpecs::Parent === ModuleSpecs::Parent.new).should == true + + (ModuleSpecs::Parent === ModuleSpecs::Child.new).should == true + (Object === ModuleSpecs::Child.new).should == true + + (ModuleSpecs::Child === String.new).should == false + (ModuleSpecs::Child === mock('x')).should == false + end + + it "returns true when the given Object's class includes self or when the given Object is extended by self" do + (ModuleSpecs::Basic === ModuleSpecs::Child.new).should == true + (ModuleSpecs::Super === ModuleSpecs::Child.new).should == true + (ModuleSpecs::Basic === mock('x').extend(ModuleSpecs::Super)).should == true + (ModuleSpecs::Super === mock('y').extend(ModuleSpecs::Super)).should == true + + (ModuleSpecs::Basic === ModuleSpecs::Parent.new).should == false + (ModuleSpecs::Super === ModuleSpecs::Parent.new).should == false + (ModuleSpecs::Basic === mock('z')).should == false + (ModuleSpecs::Super === mock('a')).should == false + end + + it "does not let a module singleton class interfere when its on the RHS" do + (Class === ModuleSpecs::CaseCompareOnSingleton).should == false + end +end diff --git a/spec/ruby/core/module/class_eval_spec.rb b/spec/ruby/core/module/class_eval_spec.rb new file mode 100644 index 0000000000..c6665d5aff --- /dev/null +++ b/spec/ruby/core/module/class_eval_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/class_eval' + +describe "Module#class_eval" do + it_behaves_like :module_class_eval, :class_eval +end diff --git a/spec/ruby/core/module/class_exec_spec.rb b/spec/ruby/core/module/class_exec_spec.rb new file mode 100644 index 0000000000..4acd0169ad --- /dev/null +++ b/spec/ruby/core/module/class_exec_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/class_exec' + +describe "Module#class_exec" do + it_behaves_like :module_class_exec, :class_exec +end diff --git a/spec/ruby/core/module/class_variable_defined_spec.rb b/spec/ruby/core/module/class_variable_defined_spec.rb new file mode 100644 index 0000000000..c0f2072a37 --- /dev/null +++ b/spec/ruby/core/module/class_variable_defined_spec.rb @@ -0,0 +1,72 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Module#class_variable_defined?" do + it "returns true if a class variable with the given name is defined in self" do + c = Class.new { class_variable_set :@@class_var, "test" } + c.class_variable_defined?(:@@class_var).should == true + c.class_variable_defined?("@@class_var").should == true + c.class_variable_defined?(:@@no_class_var).should == false + c.class_variable_defined?("@@no_class_var").should == false + ModuleSpecs::CVars.class_variable_defined?("@@cls").should == true + end + + it "returns true if a class variable with the given name is defined in the metaclass" do + ModuleSpecs::CVars.class_variable_defined?("@@meta").should == true + end + + it "returns true if the class variable is defined in a metaclass" do + obj = mock("metaclass class variable") + meta = obj.singleton_class + meta.send :class_variable_set, :@@var, 1 + meta.send(:class_variable_defined?, :@@var).should be_true + end + + it "returns false if the class variable is not defined in a metaclass" do + obj = mock("metaclass class variable") + meta = obj.singleton_class + meta.class_variable_defined?(:@@var).should be_false + end + + it "returns true if a class variables with the given name is defined in an included module" do + c = Class.new { include ModuleSpecs::MVars } + c.class_variable_defined?("@@mvar").should == true + end + + it "returns false if a class variables with the given name is defined in an extended module" do + c = Class.new + c.extend ModuleSpecs::MVars + c.class_variable_defined?("@@mvar").should == false + end + + it "raises a NameError when the given name is not allowed" do + c = Class.new + + -> { + c.class_variable_defined?(:invalid_name) + }.should raise_error(NameError) + + -> { + c.class_variable_defined?("@invalid_name") + }.should raise_error(NameError) + end + + it "converts a non string/symbol name to string using to_str" do + c = Class.new { class_variable_set :@@class_var, "test" } + (o = mock('@@class_var')).should_receive(:to_str).and_return("@@class_var") + c.class_variable_defined?(o).should == true + end + + it "raises a TypeError when the given names can't be converted to strings using to_str" do + c = Class.new { class_variable_set :@@class_var, "test" } + o = mock('123') + -> { + c.class_variable_defined?(o) + }.should raise_error(TypeError) + + o.should_receive(:to_str).and_return(123) + -> { + c.class_variable_defined?(o) + }.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/module/class_variable_get_spec.rb b/spec/ruby/core/module/class_variable_get_spec.rb new file mode 100644 index 0000000000..e5d06731ec --- /dev/null +++ b/spec/ruby/core/module/class_variable_get_spec.rb @@ -0,0 +1,76 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Module#class_variable_get" do + it "returns the value of the class variable with the given name" do + c = Class.new { class_variable_set :@@class_var, "test" } + c.send(:class_variable_get, :@@class_var).should == "test" + c.send(:class_variable_get, "@@class_var").should == "test" + end + + it "returns the value of a class variable with the given name defined in an included module" do + c = Class.new { include ModuleSpecs::MVars } + c.send(:class_variable_get, "@@mvar").should == :mvar + end + + it "raises a NameError for a class variable named '@@'" do + c = Class.new + -> { c.send(:class_variable_get, "@@") }.should raise_error(NameError) + -> { c.send(:class_variable_get, :"@@") }.should raise_error(NameError) + end + + it "raises a NameError for a class variables with the given name defined in an extended module" do + c = Class.new + c.extend ModuleSpecs::MVars + -> { + c.send(:class_variable_get, "@@mvar") + }.should raise_error(NameError) + end + + it "returns class variables defined in the class body and accessed in the metaclass" do + ModuleSpecs::CVars.cls.should == :class + end + + it "returns class variables defined in the metaclass and accessed by class methods" do + ModuleSpecs::CVars.meta.should == :metainfo + end + + it "returns class variables defined in the metaclass and accessed by instance methods" do + ModuleSpecs::CVars.new.meta.should == :metainfo + end + + it "returns a class variable defined in a metaclass" do + obj = mock("metaclass class variable") + meta = obj.singleton_class + meta.send :class_variable_set, :@@var, :cvar_value + meta.send(:class_variable_get, :@@var).should == :cvar_value + end + + it "raises a NameError when an uninitialized class variable is accessed" do + c = Class.new + [:@@no_class_var, "@@no_class_var"].each do |cvar| + -> { c.send(:class_variable_get, cvar) }.should raise_error(NameError) + end + end + + it "raises a NameError when the given name is not allowed" do + c = Class.new + + -> { c.send(:class_variable_get, :invalid_name) }.should raise_error(NameError) + -> { c.send(:class_variable_get, "@invalid_name") }.should raise_error(NameError) + end + + it "converts a non string/symbol name to string using to_str" do + c = Class.new { class_variable_set :@@class_var, "test" } + (o = mock('@@class_var')).should_receive(:to_str).and_return("@@class_var") + c.send(:class_variable_get, o).should == "test" + end + + it "raises a TypeError when the given names can't be converted to strings using to_str" do + c = Class.new { class_variable_set :@@class_var, "test" } + o = mock('123') + -> { c.send(:class_variable_get, o) }.should raise_error(TypeError) + o.should_receive(:to_str).and_return(123) + -> { c.send(:class_variable_get, o) }.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/module/class_variable_set_spec.rb b/spec/ruby/core/module/class_variable_set_spec.rb new file mode 100644 index 0000000000..63f32f5389 --- /dev/null +++ b/spec/ruby/core/module/class_variable_set_spec.rb @@ -0,0 +1,62 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Module#class_variable_set" do + it "sets the class variable with the given name to the given value" do + c = Class.new + + c.send(:class_variable_set, :@@test, "test") + c.send(:class_variable_set, "@@test3", "test3") + + c.send(:class_variable_get, :@@test).should == "test" + c.send(:class_variable_get, :@@test3).should == "test3" + end + + it "sets a class variable on a metaclass" do + obj = mock("metaclass class variable") + meta = obj.singleton_class + meta.send(:class_variable_set, :@@var, :cvar_value).should == :cvar_value + meta.send(:class_variable_get, :@@var).should == :cvar_value + end + + it "sets the value of a class variable with the given name defined in an included module" do + c = Class.new { include ModuleSpecs::MVars.dup } + c.send(:class_variable_set, "@@mvar", :new_mvar).should == :new_mvar + c.send(:class_variable_get, "@@mvar").should == :new_mvar + end + + it "raises a FrozenError when self is frozen" do + -> { + Class.new.freeze.send(:class_variable_set, :@@test, "test") + }.should raise_error(FrozenError) + -> { + Module.new.freeze.send(:class_variable_set, :@@test, "test") + }.should raise_error(FrozenError) + end + + it "raises a NameError when the given name is not allowed" do + c = Class.new + + -> { + c.send(:class_variable_set, :invalid_name, "test") + }.should raise_error(NameError) + -> { + c.send(:class_variable_set, "@invalid_name", "test") + }.should raise_error(NameError) + end + + it "converts a non string/symbol name to string using to_str" do + (o = mock('@@class_var')).should_receive(:to_str).and_return("@@class_var") + c = Class.new + c.send(:class_variable_set, o, "test") + c.send(:class_variable_get, :@@class_var).should == "test" + end + + it "raises a TypeError when the given names can't be converted to strings using to_str" do + c = Class.new { class_variable_set :@@class_var, "test" } + o = mock('123') + -> { c.send(:class_variable_set, o, "test") }.should raise_error(TypeError) + o.should_receive(:to_str).and_return(123) + -> { c.send(:class_variable_set, o, "test") }.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/module/class_variables_spec.rb b/spec/ruby/core/module/class_variables_spec.rb new file mode 100644 index 0000000000..e155f1deac --- /dev/null +++ b/spec/ruby/core/module/class_variables_spec.rb @@ -0,0 +1,34 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Module#class_variables" do + it "returns an Array with the names of class variables of self" do + ModuleSpecs::ClassVars::A.class_variables.should include(:@@a_cvar) + ModuleSpecs::ClassVars::M.class_variables.should include(:@@m_cvar) + end + + it "returns an Array of Symbols of class variable names defined in a metaclass" do + obj = mock("metaclass class variable") + meta = obj.singleton_class + meta.send :class_variable_set, :@@var, :cvar_value + meta.class_variables.should == [:@@var] + end + + it "returns an Array with names of class variables defined in metaclasses" do + ModuleSpecs::CVars.class_variables.should include(:@@cls, :@@meta) + end + + it "does not return class variables defined in extended modules" do + c = Class.new + c.extend ModuleSpecs::MVars + c.class_variables.should_not include(:@@mvar) + end + + it "returns the correct class variables when inherit is given" do + ModuleSpecs::SubCVars.class_variables(false).should == [:@@sub] + ModuleSpecs::SubCVars.new.singleton_class.class_variables(false).should == [] + + ModuleSpecs::SubCVars.class_variables(true).should == [:@@sub, :@@cls, :@@meta] + ModuleSpecs::SubCVars.new.singleton_class.class_variables(true).should == [:@@sub, :@@cls, :@@meta] + end +end diff --git a/spec/ruby/core/module/comparison_spec.rb b/spec/ruby/core/module/comparison_spec.rb new file mode 100644 index 0000000000..86ee5db22a --- /dev/null +++ b/spec/ruby/core/module/comparison_spec.rb @@ -0,0 +1,36 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Module#<=>" do + it "returns -1 if self is a subclass of or includes the given module" do + (ModuleSpecs::Child <=> ModuleSpecs::Parent).should == -1 + (ModuleSpecs::Child <=> ModuleSpecs::Basic).should == -1 + (ModuleSpecs::Child <=> ModuleSpecs::Super).should == -1 + (ModuleSpecs::Super <=> ModuleSpecs::Basic).should == -1 + end + + it "returns 0 if self is the same as the given module" do + (ModuleSpecs::Child <=> ModuleSpecs::Child).should == 0 + (ModuleSpecs::Parent <=> ModuleSpecs::Parent).should == 0 + (ModuleSpecs::Basic <=> ModuleSpecs::Basic).should == 0 + (ModuleSpecs::Super <=> ModuleSpecs::Super).should == 0 + end + + it "returns +1 if self is a superclass of or included by the given module" do + (ModuleSpecs::Parent <=> ModuleSpecs::Child).should == +1 + (ModuleSpecs::Basic <=> ModuleSpecs::Child).should == +1 + (ModuleSpecs::Super <=> ModuleSpecs::Child).should == +1 + (ModuleSpecs::Basic <=> ModuleSpecs::Super).should == +1 + end + + it "returns nil if self and the given module are not related" do + (ModuleSpecs::Parent <=> ModuleSpecs::Basic).should == nil + (ModuleSpecs::Parent <=> ModuleSpecs::Super).should == nil + (ModuleSpecs::Basic <=> ModuleSpecs::Parent).should == nil + (ModuleSpecs::Super <=> ModuleSpecs::Parent).should == nil + end + + it "returns nil if the argument is not a class/module" do + (ModuleSpecs::Parent <=> mock('x')).should == nil + end +end diff --git a/spec/ruby/core/module/const_added_spec.rb b/spec/ruby/core/module/const_added_spec.rb new file mode 100644 index 0000000000..90cd36551a --- /dev/null +++ b/spec/ruby/core/module/const_added_spec.rb @@ -0,0 +1,238 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'fixtures/const_added' + +describe "Module#const_added" do + it "is a private instance method" do + Module.should have_private_instance_method(:const_added) + end + + it "returns nil in the default implementation" do + Module.new do + const_added(:TEST).should == nil + end + end + + it "for a class defined with the `class` keyword, const_added runs before inherited" do + ScratchPad.record [] + + mod = Module.new do + def self.const_added(_) + ScratchPad << :const_added + end + end + + parent = Class.new do + def self.inherited(_) + ScratchPad << :inherited + end + end + + class mod::C < parent; end + + ScratchPad.recorded.should == [:const_added, :inherited] + end + + it "the superclass of a class assigned to a constant is set before const_added is called" do + ScratchPad.record [] + + parent = Class.new do + def self.const_added(name) + ScratchPad << name + ScratchPad << const_get(name).superclass + end + end + + class parent::C < parent; end + + ScratchPad.recorded.should == [:C, parent] + end + + it "is called when a new constant is assigned on self" do + ScratchPad.record [] + + mod = Module.new do + def self.const_added(name) + ScratchPad << name + end + end + + mod.module_eval(<<-RUBY, __FILE__, __LINE__ + 1) + TEST = 1 + RUBY + + ScratchPad.recorded.should == [:TEST] + end + + it "is called when a new constant is assigned on self through const_set" do + ScratchPad.record [] + + mod = Module.new do + def self.const_added(name) + ScratchPad << name + end + end + + mod.const_set(:TEST, 1) + + ScratchPad.recorded.should == [:TEST] + end + + it "is called when a new module is defined under self" do + ScratchPad.record [] + + mod = Module.new do + def self.const_added(name) + ScratchPad << name + end + end + + mod.module_eval(<<-RUBY, __FILE__, __LINE__ + 1) + module SubModule + end + + module SubModule + end + RUBY + + ScratchPad.recorded.should == [:SubModule] + end + + it "is called when a new module is defined under a named module (assigned to a constant)" do + ScratchPad.record [] + + ModuleSpecs::ConstAddedSpecs::NamedModule = Module.new do + def self.const_added(name) + ScratchPad << name + end + + module self::A + def self.const_added(name) + ScratchPad << name + end + + module self::B + end + end + end + + ScratchPad.recorded.should == [:A, :B] + ModuleSpecs::ConstAddedSpecs.send :remove_const, :NamedModule + end + + it "is called when a new class is defined under self" do + ScratchPad.record [] + + mod = Module.new do + def self.const_added(name) + ScratchPad << name + end + end + + mod.module_eval(<<-RUBY, __FILE__, __LINE__ + 1) + class SubClass + end + + class SubClass + end + RUBY + + ScratchPad.recorded.should == [:SubClass] + end + + it "is called when a new class is defined under a named module (assigned to a constant)" do + ScratchPad.record [] + + ModuleSpecs::ConstAddedSpecs::NamedModuleB = Module.new do + def self.const_added(name) + ScratchPad << name + end + + class self::A + def self.const_added(name) + ScratchPad << name + end + + class self::B + end + end + end + + ScratchPad.recorded.should == [:A, :B] + ModuleSpecs::ConstAddedSpecs.send :remove_const, :NamedModuleB + end + + it "is called when an autoload is defined" do + ScratchPad.record [] + + mod = Module.new do + def self.const_added(name) + ScratchPad << name + end + end + + mod.autoload :Autoload, "foo" + ScratchPad.recorded.should == [:Autoload] + end + + it "is called with a precise caller location with the line of definition" do + ScratchPad.record [] + + mod = Module.new do + def self.const_added(name) + location = caller_locations(1, 1)[0] + ScratchPad << location.lineno + end + end + + line = __LINE__ + mod.module_eval(<<-RUBY, __FILE__, __LINE__ + 1) + TEST = 1 + + module SubModule + end + + class SubClass + end + RUBY + + mod.const_set(:CONST_SET, 1) + + ScratchPad.recorded.should == [line + 2, line + 4, line + 7, line + 11] + end + + it "is called when the constant is already assigned a value" do + ScratchPad.record [] + + mod = Module.new do + def self.const_added(name) + ScratchPad.record const_get(name) + end + end + + mod.module_eval(<<-RUBY, __FILE__, __LINE__ + 1) + TEST = 123 + RUBY + + ScratchPad.recorded.should == 123 + end + + it "records re-definition of existing constants" do + ScratchPad.record [] + + mod = Module.new do + def self.const_added(name) + ScratchPad << const_get(name) + end + end + + -> { + mod.module_eval(<<-RUBY, __FILE__, __LINE__ + 1) + TEST = 123 + TEST = 456 + RUBY + }.should complain(/warning: already initialized constant .+::TEST/) + + ScratchPad.recorded.should == [123, 456] + end +end diff --git a/spec/ruby/core/module/const_defined_spec.rb b/spec/ruby/core/module/const_defined_spec.rb new file mode 100644 index 0000000000..8b137cd134 --- /dev/null +++ b/spec/ruby/core/module/const_defined_spec.rb @@ -0,0 +1,169 @@ +# encoding: utf-8 + +require_relative '../../spec_helper' +require_relative '../../fixtures/constants' +require_relative 'fixtures/constant_unicode' + +describe "Module#const_defined?" do + it "returns true if the given Symbol names a constant defined in the receiver" do + ConstantSpecs.const_defined?(:CS_CONST2).should == true + ConstantSpecs.const_defined?(:ModuleA).should == true + ConstantSpecs.const_defined?(:ClassA).should == true + ConstantSpecs::ContainerA.const_defined?(:ChildA).should == true + end + + it "returns true if the constant is defined in the receiver's superclass" do + # CS_CONST4 is defined in the superclass of ChildA + ConstantSpecs::ContainerA::ChildA.const_defined?(:CS_CONST4).should be_true + end + + it "returns true if the constant is defined in a mixed-in module of the receiver's parent" do + # CS_CONST10 is defined in a module included by ChildA + ConstantSpecs::ContainerA::ChildA.const_defined?(:CS_CONST10).should be_true + end + + it "returns true if the constant is defined in a mixed-in module (with prepends) of the receiver" do + # CS_CONST11 is defined in the module included by ContainerPrepend + ConstantSpecs::ContainerPrepend.const_defined?(:CS_CONST11).should be_true + end + + it "returns true if the constant is defined in Object and the receiver is a module" do + # CS_CONST1 is defined in Object + ConstantSpecs::ModuleA.const_defined?(:CS_CONST1).should be_true + end + + it "returns true if the constant is defined in Object and the receiver is a class that has Object among its ancestors" do + # CS_CONST1 is defined in Object + ConstantSpecs::ContainerA::ChildA.const_defined?(:CS_CONST1).should be_true + end + + it "returns false if the constant is defined in the receiver's superclass and the inherit flag is false" do + ConstantSpecs::ContainerA::ChildA.const_defined?(:CS_CONST4, false).should be_false + end + + it "returns true if the constant is defined in the receiver's superclass and the inherit flag is true" do + ConstantSpecs::ContainerA::ChildA.const_defined?(:CS_CONST4, true).should be_true + end + + it "coerces the inherit flag to a boolean" do + ConstantSpecs::ContainerA::ChildA.const_defined?(:CS_CONST4, nil).should be_false + ConstantSpecs::ContainerA::ChildA.const_defined?(:CS_CONST4, :true).should be_true + end + + it "returns true if the given String names a constant defined in the receiver" do + ConstantSpecs.const_defined?("CS_CONST2").should == true + ConstantSpecs.const_defined?("ModuleA").should == true + ConstantSpecs.const_defined?("ClassA").should == true + ConstantSpecs::ContainerA.const_defined?("ChildA").should == true + end + + it "returns true when passed a constant name with unicode characters" do + ConstantUnicodeSpecs.const_defined?("CS_CONSTλ").should be_true + end + + it "returns true when passed a constant name with EUC-JP characters" do + str = "CS_CONSTλ".encode("euc-jp") + ConstantSpecs.const_set str, 1 + ConstantSpecs.const_defined?(str).should be_true + ensure + ConstantSpecs.send(:remove_const, str) + end + + it "returns false if the constant is not defined in the receiver, its superclass, or any included modules" do + # The following constant isn't defined at all. + ConstantSpecs::ContainerA::ChildA.const_defined?(:CS_CONST4726).should be_false + # DETACHED_CONSTANT is defined in ConstantSpecs::Detached, which isn't + # included by or inherited from ParentA + ConstantSpecs::ParentA.const_defined?(:DETACHED_CONSTANT).should be_false + end + + it "does not call #const_missing if the constant is not defined in the receiver" do + ConstantSpecs::ClassA.should_not_receive(:const_missing) + ConstantSpecs::ClassA.const_defined?(:CS_CONSTX).should == false + end + + describe "converts the given name to a String using #to_str" do + it "calls #to_str to convert the given name to a String" do + name = mock("ClassA") + name.should_receive(:to_str).and_return("ClassA") + ConstantSpecs.const_defined?(name).should == true + end + + it "raises a TypeError if the given name can't be converted to a String" do + -> { ConstantSpecs.const_defined?(nil) }.should raise_error(TypeError) + -> { ConstantSpecs.const_defined?([]) }.should raise_error(TypeError) + end + + it "raises a NoMethodError if the given argument raises a NoMethodError during type coercion to a String" do + name = mock("classA") + name.should_receive(:to_str).and_raise(NoMethodError) + -> { ConstantSpecs.const_defined?(name) }.should raise_error(NoMethodError) + end + end + + it "special cases Object and checks it's included Modules" do + Object.const_defined?(:CS_CONST10).should be_true + end + + it "returns true for toplevel constant when the name begins with '::'" do + ConstantSpecs.const_defined?("::Array").should be_true + end + + it "returns true when passed a scoped constant name" do + ConstantSpecs.const_defined?("ClassC::CS_CONST1").should be_true + end + + it "returns true when passed a scoped constant name for a constant in the inheritance hierarchy and the inherited flag is default" do + ConstantSpecs::ClassD.const_defined?("ClassE::CS_CONST2").should be_true + end + + it "returns true when passed a scoped constant name for a constant in the inheritance hierarchy and the inherited flag is true" do + ConstantSpecs::ClassD.const_defined?("ClassE::CS_CONST2", true).should be_true + end + + it "returns false when passed a scoped constant name for a constant in the inheritance hierarchy and the inherited flag is false" do + ConstantSpecs::ClassD.const_defined?("ClassE::CS_CONST2", false).should be_false + end + + it "returns false when the name begins with '::' and the toplevel constant does not exist" do + ConstantSpecs.const_defined?("::Name").should be_false + end + + it "raises a NameError if the name does not start with a capital letter" do + -> { ConstantSpecs.const_defined? "name" }.should raise_error(NameError) + end + + it "raises a NameError if the name starts with '_'" do + -> { ConstantSpecs.const_defined? "__CONSTX__" }.should raise_error(NameError) + end + + it "raises a NameError if the name starts with '@'" do + -> { ConstantSpecs.const_defined? "@Name" }.should raise_error(NameError) + end + + it "raises a NameError if the name starts with '!'" do + -> { ConstantSpecs.const_defined? "!Name" }.should raise_error(NameError) + end + + it "returns true or false for the nested name" do + ConstantSpecs.const_defined?("NotExist::Name").should == false + ConstantSpecs.const_defined?("::Name").should == false + ConstantSpecs.const_defined?("::Object").should == true + ConstantSpecs.const_defined?("ClassA::CS_CONST10").should == true + ConstantSpecs.const_defined?("ClassA::CS_CONST10_").should == false + end + + it "raises a NameError if the name contains non-alphabetic characters except '_'" do + ConstantSpecs.const_defined?("CS_CONSTX").should == false + -> { ConstantSpecs.const_defined? "Name=" }.should raise_error(NameError) + -> { ConstantSpecs.const_defined? "Name?" }.should raise_error(NameError) + end + + it "raises a TypeError if conversion to a String by calling #to_str fails" do + name = mock('123') + -> { ConstantSpecs.const_defined? name }.should raise_error(TypeError) + + name.should_receive(:to_str).and_return(123) + -> { ConstantSpecs.const_defined? name }.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/module/const_get_spec.rb b/spec/ruby/core/module/const_get_spec.rb new file mode 100644 index 0000000000..4b53cbe7b3 --- /dev/null +++ b/spec/ruby/core/module/const_get_spec.rb @@ -0,0 +1,273 @@ +require_relative '../../spec_helper' +require_relative '../../fixtures/constants' +require_relative 'fixtures/constants_autoload' + +describe "Module#const_get" do + it "accepts a String or Symbol name" do + Object.const_get(:CS_CONST1).should == :const1 + Object.const_get("CS_CONST1").should == :const1 + end + + it "raises a NameError if no constant is defined in the search path" do + -> { ConstantSpecs.const_get :CS_CONSTX }.should raise_error(NameError) + end + + it "raises a NameError with the not found constant symbol" do + error_inspection = -> e { e.name.should == :CS_CONSTX } + -> { ConstantSpecs.const_get :CS_CONSTX }.should raise_error(NameError, &error_inspection) + end + + it "raises a NameError if the name does not start with a capital letter" do + -> { ConstantSpecs.const_get "name" }.should raise_error(NameError) + end + + it "raises a NameError if the name starts with a non-alphabetic character" do + -> { ConstantSpecs.const_get "__CONSTX__" }.should raise_error(NameError) + -> { ConstantSpecs.const_get "@CS_CONST1" }.should raise_error(NameError) + -> { ConstantSpecs.const_get "!CS_CONST1" }.should raise_error(NameError) + end + + it "raises a NameError if the name contains non-alphabetic characters except '_'" do + Object.const_get("CS_CONST1").should == :const1 + -> { ConstantSpecs.const_get "CS_CONST1=" }.should raise_error(NameError) + -> { ConstantSpecs.const_get "CS_CONST1?" }.should raise_error(NameError) + end + + it "calls #to_str to convert the given name to a String" do + name = mock("ClassA") + name.should_receive(:to_str).and_return("ClassA") + ConstantSpecs.const_get(name).should == ConstantSpecs::ClassA + end + + it "raises a TypeError if conversion to a String by calling #to_str fails" do + name = mock('123') + -> { ConstantSpecs.const_get(name) }.should raise_error(TypeError) + + name.should_receive(:to_str).and_return(123) + -> { ConstantSpecs.const_get(name) }.should raise_error(TypeError) + end + + it "calls #const_missing on the receiver if unable to locate the constant" do + ConstantSpecs::ContainerA.should_receive(:const_missing).with(:CS_CONSTX) + ConstantSpecs::ContainerA.const_get(:CS_CONSTX) + end + + it "does not search the singleton class of a Class or Module" do + -> do + ConstantSpecs::ContainerA::ChildA.const_get(:CS_CONST14) + end.should raise_error(NameError) + -> { ConstantSpecs.const_get(:CS_CONST14) }.should raise_error(NameError) + end + + it "does not search the containing scope" do + ConstantSpecs::ContainerA::ChildA.const_get(:CS_CONST20).should == :const20_2 + -> do + ConstantSpecs::ContainerA::ChildA.const_get(:CS_CONST5) + end.should raise_error(NameError) + end + + it "raises a NameError if the constant is defined in the receiver's superclass and the inherit flag is false" do + -> do + ConstantSpecs::ContainerA::ChildA.const_get(:CS_CONST4, false) + end.should raise_error(NameError) + end + + it "searches into the receiver superclasses if the inherit flag is true" do + ConstantSpecs::ContainerA::ChildA.const_get(:CS_CONST4, true).should == :const4 + end + + it "raises a NameError when the receiver is a Module, the constant is defined at toplevel and the inherit flag is false" do + -> do + ConstantSpecs::ModuleA.const_get(:CS_CONST1, false) + end.should raise_error(NameError) + end + + it "raises a NameError when the receiver is a Class, the constant is defined at toplevel and the inherit flag is false" do + -> do + ConstantSpecs::ContainerA::ChildA.const_get(:CS_CONST1, false) + end.should raise_error(NameError) + end + + it "coerces the inherit flag to a boolean" do + ConstantSpecs::ContainerA::ChildA.const_get(:CS_CONST4, :true).should == :const4 + + -> do + ConstantSpecs::ContainerA::ChildA.const_get(:CS_CONST1, nil) + end.should raise_error(NameError) + end + + it "accepts a toplevel scope qualifier" do + ConstantSpecs.const_get("::CS_CONST1").should == :const1 + end + + it "accepts a toplevel scope qualifier when inherit is false" do + ConstantSpecs.const_get("::CS_CONST1", false).should == :const1 + -> { ConstantSpecs.const_get("CS_CONST1", false) }.should raise_error(NameError) + end + + it "returns a constant whose module is defined the toplevel" do + ConstantSpecs.const_get("ConstantSpecsTwo::Foo").should == :cs_two_foo + ConstantSpecsThree.const_get("ConstantSpecsTwo::Foo").should == :cs_three_foo + end + + it "accepts a scoped constant name" do + ConstantSpecs.const_get("ClassA::CS_CONST10").should == :const10_10 + end + + it "raises a NameError if the name includes two successive scope separators" do + -> { ConstantSpecs.const_get("ClassA::::CS_CONST10") }.should raise_error(NameError) + end + + it "raises a NameError if only '::' is passed" do + -> { ConstantSpecs.const_get("::") }.should raise_error(NameError) + end + + it "raises a NameError if a Symbol has a toplevel scope qualifier" do + -> { ConstantSpecs.const_get(:'::CS_CONST1') }.should raise_error(NameError) + end + + it "raises a NameError if a Symbol is a scoped constant name" do + -> { ConstantSpecs.const_get(:'ClassA::CS_CONST10') }.should raise_error(NameError) + end + + it "does read private constants" do + ConstantSpecs.const_get(:CS_PRIVATE).should == :cs_private + end + + it 'does autoload a constant' do + Object.const_get('CSAutoloadA').name.should == 'CSAutoloadA' + end + + it 'does autoload a constant with a toplevel scope qualifier' do + Object.const_get('::CSAutoloadB').name.should == 'CSAutoloadB' + end + + it 'does autoload a module and resolve a constant within' do + Object.const_get('CSAutoloadC::CONST').should == 7 + end + + it 'does autoload a non-toplevel module' do + Object.const_get('CSAutoloadD::InnerModule').name.should == 'CSAutoloadD::InnerModule' + end + + it "raises a NameError when the nested constant does not exist on the module but exists in Object" do + -> { Object.const_get('ConstantSpecs::CS_CONST1') }.should raise_error(NameError) + end + + describe "with statically assigned constants" do + it "searches the immediate class or module first" do + ConstantSpecs::ClassA.const_get(:CS_CONST10).should == :const10_10 + ConstantSpecs::ModuleA.const_get(:CS_CONST10).should == :const10_1 + ConstantSpecs::ParentA.const_get(:CS_CONST10).should == :const10_5 + ConstantSpecs::ContainerA.const_get(:CS_CONST10).should == :const10_2 + ConstantSpecs::ContainerA::ChildA.const_get(:CS_CONST10).should == :const10_3 + end + + it "searches a module included in the immediate class before the superclass" do + ConstantSpecs::ContainerA::ChildA.const_get(:CS_CONST15).should == :const15_1 + end + + it "searches the superclass before a module included in the superclass" do + ConstantSpecs::ContainerA::ChildA.const_get(:CS_CONST11).should == :const11_1 + end + + it "searches a module included in the superclass" do + ConstantSpecs::ContainerA::ChildA.const_get(:CS_CONST12).should == :const12_1 + end + + it "searches the superclass chain" do + ConstantSpecs::ContainerA::ChildA.const_get(:CS_CONST13).should == :const13 + end + + it "returns a toplevel constant when the receiver is a Class" do + ConstantSpecs::ContainerA::ChildA.const_get(:CS_CONST1).should == :const1 + end + + it "returns a toplevel constant when the receiver is a Module" do + ConstantSpecs.const_get(:CS_CONST1).should == :const1 + ConstantSpecs::ModuleA.const_get(:CS_CONST1).should == :const1 + end + end + + describe "with dynamically assigned constants" do + it "searches the immediate class or module first" do + ConstantSpecs::ClassA::CS_CONST301 = :const301_1 + ConstantSpecs::ClassA.const_get(:CS_CONST301).should == :const301_1 + + ConstantSpecs::ModuleA::CS_CONST301 = :const301_2 + ConstantSpecs::ModuleA.const_get(:CS_CONST301).should == :const301_2 + + ConstantSpecs::ParentA::CS_CONST301 = :const301_3 + ConstantSpecs::ParentA.const_get(:CS_CONST301).should == :const301_3 + + ConstantSpecs::ContainerA::ChildA::CS_CONST301 = :const301_5 + ConstantSpecs::ContainerA::ChildA.const_get(:CS_CONST301).should == :const301_5 + ensure + ConstantSpecs::ClassA.send(:remove_const, :CS_CONST301) + ConstantSpecs::ModuleA.send(:remove_const, :CS_CONST301) + ConstantSpecs::ParentA.send(:remove_const, :CS_CONST301) + ConstantSpecs::ContainerA::ChildA.send(:remove_const, :CS_CONST301) + end + + it "searches a module included in the immediate class before the superclass" do + ConstantSpecs::ParentB::CS_CONST302 = :const302_1 + ConstantSpecs::ModuleF::CS_CONST302 = :const302_2 + ConstantSpecs::ContainerB::ChildB.const_get(:CS_CONST302).should == :const302_2 + ensure + ConstantSpecs::ParentB.send(:remove_const, :CS_CONST302) + ConstantSpecs::ModuleF.send(:remove_const, :CS_CONST302) + end + + it "searches the superclass before a module included in the superclass" do + ConstantSpecs::ModuleE::CS_CONST303 = :const303_1 + ConstantSpecs::ParentB::CS_CONST303 = :const303_2 + ConstantSpecs::ContainerB::ChildB.const_get(:CS_CONST303).should == :const303_2 + ensure + ConstantSpecs::ModuleE.send(:remove_const, :CS_CONST303) + ConstantSpecs::ParentB.send(:remove_const, :CS_CONST303) + end + + it "searches a module included in the superclass" do + ConstantSpecs::ModuleA::CS_CONST304 = :const304_1 + ConstantSpecs::ModuleE::CS_CONST304 = :const304_2 + ConstantSpecs::ContainerB::ChildB.const_get(:CS_CONST304).should == :const304_2 + ensure + ConstantSpecs::ModuleA.send(:remove_const, :CS_CONST304) + ConstantSpecs::ModuleE.send(:remove_const, :CS_CONST304) + end + + it "searches the superclass chain" do + ConstantSpecs::ModuleA::CS_CONST305 = :const305 + ConstantSpecs::ContainerB::ChildB.const_get(:CS_CONST305).should == :const305 + ensure + ConstantSpecs::ModuleA.send(:remove_const, :CS_CONST305) + end + + it "returns a toplevel constant when the receiver is a Class" do + Object::CS_CONST306 = :const306 + ConstantSpecs::ContainerB::ChildB.const_get(:CS_CONST306).should == :const306 + ensure + Object.send(:remove_const, :CS_CONST306) + end + + it "returns a toplevel constant when the receiver is a Module" do + Object::CS_CONST308 = :const308 + ConstantSpecs.const_get(:CS_CONST308).should == :const308 + ConstantSpecs::ModuleA.const_get(:CS_CONST308).should == :const308 + ensure + Object.send(:remove_const, :CS_CONST308) + end + + it "returns the updated value of a constant" do + ConstantSpecs::ClassB::CS_CONST309 = :const309_1 + ConstantSpecs::ClassB.const_get(:CS_CONST309).should == :const309_1 + + -> { + ConstantSpecs::ClassB::CS_CONST309 = :const309_2 + }.should complain(/already initialized constant/) + ConstantSpecs::ClassB.const_get(:CS_CONST309).should == :const309_2 + ensure + ConstantSpecs::ClassB.send(:remove_const, :CS_CONST309) + end + end +end diff --git a/spec/ruby/core/module/const_missing_spec.rb b/spec/ruby/core/module/const_missing_spec.rb new file mode 100644 index 0000000000..742218281c --- /dev/null +++ b/spec/ruby/core/module/const_missing_spec.rb @@ -0,0 +1,36 @@ +require_relative '../../spec_helper' +require_relative '../../fixtures/constants' + +describe "Module#const_missing" do + it "is called when an undefined constant is referenced via literal form" do + ConstantSpecs::ClassA::CS_CONSTX.should == :CS_CONSTX + end + + it "is called when an undefined constant is referenced via #const_get" do + ConstantSpecs::ClassA.const_get(:CS_CONSTX).should == :CS_CONSTX + end + + it "raises NameError and includes the name of the value that wasn't found" do + -> { + ConstantSpecs.const_missing("HelloMissing") + }.should raise_error(NameError, /ConstantSpecs::HelloMissing/) + end + + it "raises NameError and does not include toplevel Object" do + begin + Object.const_missing("HelloMissing") + rescue NameError => e + e.message.should_not =~ / Object::/ + end + end + + it "is called regardless of visibility" do + klass = Class.new do + def self.const_missing(name) + "Found:#{name}" + end + private_class_method :const_missing + end + klass::Hello.should == 'Found:Hello' + end +end diff --git a/spec/ruby/core/module/const_set_spec.rb b/spec/ruby/core/module/const_set_spec.rb new file mode 100644 index 0000000000..823768b882 --- /dev/null +++ b/spec/ruby/core/module/const_set_spec.rb @@ -0,0 +1,145 @@ +require_relative '../../spec_helper' +require_relative '../../fixtures/constants' + +describe "Module#const_set" do + it "sets the constant specified by a String or Symbol to the given value" do + ConstantSpecs.const_set :CS_CONST401, :const401 + ConstantSpecs::CS_CONST401.should == :const401 + + ConstantSpecs.const_set "CS_CONST402", :const402 + ConstantSpecs.const_get(:CS_CONST402).should == :const402 + ensure + ConstantSpecs.send(:remove_const, :CS_CONST401) + ConstantSpecs.send(:remove_const, :CS_CONST402) + end + + it "returns the value set" do + ConstantSpecs.const_set(:CS_CONST403, :const403).should == :const403 + ensure + ConstantSpecs.send(:remove_const, :CS_CONST403) + end + + it "sets the name of an anonymous module" do + m = Module.new + ConstantSpecs.const_set(:CS_CONST1000, m) + m.name.should == "ConstantSpecs::CS_CONST1000" + ensure + ConstantSpecs.send(:remove_const, :CS_CONST1000) + end + + it "sets the name of a module scoped by an anonymous module" do + a, b = Module.new, Module.new + a.const_set :B, b + b.name.should.end_with? '::B' + end + + it "sets the name of contained modules when assigning a toplevel anonymous module" do + a, b, c, d = Module.new, Module.new, Module.new, Module.new + a::B = b + a::B::C = c + a::B::C::E = c + a::D = d + + Object.const_set :ModuleSpecs_CS3, a + a.name.should == "ModuleSpecs_CS3" + b.name.should == "ModuleSpecs_CS3::B" + c.name.should == "ModuleSpecs_CS3::B::C" + d.name.should == "ModuleSpecs_CS3::D" + ensure + Object.send(:remove_const, :ModuleSpecs_CS3) + end + + it "raises a NameError if the name does not start with a capital letter" do + -> { ConstantSpecs.const_set "name", 1 }.should raise_error(NameError) + end + + it "raises a NameError if the name starts with a non-alphabetic character" do + -> { ConstantSpecs.const_set "__CONSTX__", 1 }.should raise_error(NameError) + -> { ConstantSpecs.const_set "@Name", 1 }.should raise_error(NameError) + -> { ConstantSpecs.const_set "!Name", 1 }.should raise_error(NameError) + -> { ConstantSpecs.const_set "::Name", 1 }.should raise_error(NameError) + end + + it "raises a NameError if the name contains non-alphabetic characters except '_'" do + ConstantSpecs.const_set("CS_CONST404", :const404).should == :const404 + -> { ConstantSpecs.const_set "Name=", 1 }.should raise_error(NameError) + -> { ConstantSpecs.const_set "Name?", 1 }.should raise_error(NameError) + ensure + ConstantSpecs.send(:remove_const, :CS_CONST404) + end + + it "calls #to_str to convert the given name to a String" do + name = mock("CS_CONST405") + name.should_receive(:to_str).and_return("CS_CONST405") + ConstantSpecs.const_set(name, :const405).should == :const405 + ConstantSpecs::CS_CONST405.should == :const405 + ensure + ConstantSpecs.send(:remove_const, :CS_CONST405) + end + + it "raises a TypeError if conversion to a String by calling #to_str fails" do + name = mock('123') + -> { ConstantSpecs.const_set name, 1 }.should raise_error(TypeError) + + name.should_receive(:to_str).and_return(123) + -> { ConstantSpecs.const_set name, 1 }.should raise_error(TypeError) + end + + describe "when overwriting an existing constant" do + it "warns if the previous value was a normal value" do + mod = Module.new + mod.const_set :Foo, 42 + -> { + mod.const_set :Foo, 1 + }.should complain(/already initialized constant/) + mod.const_get(:Foo).should == 1 + end + + it "does not warn if the previous value was an autoload" do + mod = Module.new + mod.autoload :Foo, "not-existing" + -> { + mod.const_set :Foo, 1 + }.should_not complain + mod.const_get(:Foo).should == 1 + end + + it "does not warn after a failed autoload" do + path = fixture(__FILE__, "autoload_o.rb") + ScratchPad.record [] + mod = Module.new + + mod.autoload :Foo, path + -> { mod::Foo }.should raise_error(NameError) + + mod.const_defined?(:Foo).should == false + mod.autoload?(:Foo).should == nil + + -> { + mod.const_set :Foo, 1 + }.should_not complain + mod.const_get(:Foo).should == 1 + end + + it "does not warn if the new value is an autoload" do + mod = Module.new + mod.const_set :Foo, 42 + -> { + mod.autoload :Foo, "not-existing" + }.should_not complain + mod.const_get(:Foo).should == 42 + end + end + + describe "on a frozen module" do + before :each do + @frozen = Module.new.freeze + @name = :Foo + end + + it "raises a FrozenError before setting the name" do + -> { @frozen.const_set @name, nil }.should raise_error(FrozenError) + @frozen.should_not have_constant(@name) + end + end +end diff --git a/spec/ruby/core/module/const_source_location_spec.rb b/spec/ruby/core/module/const_source_location_spec.rb new file mode 100644 index 0000000000..96649ea10b --- /dev/null +++ b/spec/ruby/core/module/const_source_location_spec.rb @@ -0,0 +1,281 @@ +require_relative '../../spec_helper' +require_relative '../../fixtures/constants' + +describe "Module#const_source_location" do + before do + @constants_fixture_path = File.expand_path('../../fixtures/constants.rb', __dir__) + end + + describe "with dynamically assigned constants" do + it "searches a path in the immediate class or module first" do + ConstantSpecs::ClassA::CSL_CONST301 = :const301_1 + ConstantSpecs::ClassA.const_source_location(:CSL_CONST301).should == [__FILE__, __LINE__ - 1] + + ConstantSpecs::ModuleA::CSL_CONST301 = :const301_2 + ConstantSpecs::ModuleA.const_source_location(:CSL_CONST301).should == [__FILE__, __LINE__ - 1] + + ConstantSpecs::ParentA::CSL_CONST301 = :const301_3 + ConstantSpecs::ParentA.const_source_location(:CSL_CONST301).should == [__FILE__, __LINE__ - 1] + + ConstantSpecs::ContainerA::ChildA::CSL_CONST301 = :const301_5 + ConstantSpecs::ContainerA::ChildA.const_source_location(:CSL_CONST301).should == [__FILE__, __LINE__ - 1] + ensure + ConstantSpecs::ClassA.send(:remove_const, :CSL_CONST301) + ConstantSpecs::ModuleA.send(:remove_const, :CSL_CONST301) + ConstantSpecs::ParentA.send(:remove_const, :CSL_CONST301) + ConstantSpecs::ContainerA::ChildA.send(:remove_const, :CSL_CONST301) + end + + it "searches a path in a module included in the immediate class before the superclass" do + ConstantSpecs::ParentB::CSL_CONST302 = :const302_1 + ConstantSpecs::ModuleF::CSL_CONST302 = :const302_2 + ConstantSpecs::ContainerB::ChildB.const_source_location(:CSL_CONST302).should == [__FILE__, __LINE__ - 1] + ensure + ConstantSpecs::ParentB.send(:remove_const, :CSL_CONST302) + ConstantSpecs::ModuleF.send(:remove_const, :CSL_CONST302) + end + + it "searches a path in the superclass before a module included in the superclass" do + ConstantSpecs::ModuleE::CSL_CONST303 = :const303_1 + ConstantSpecs::ParentB::CSL_CONST303 = :const303_2 + ConstantSpecs::ContainerB::ChildB.const_source_location(:CSL_CONST303).should == [__FILE__, __LINE__ - 1] + ensure + ConstantSpecs::ModuleE.send(:remove_const, :CSL_CONST303) + ConstantSpecs::ParentB.send(:remove_const, :CSL_CONST303) + end + + it "searches a path in a module included in the superclass" do + ConstantSpecs::ModuleA::CSL_CONST304 = :const304_1 + ConstantSpecs::ModuleE::CSL_CONST304 = :const304_2 + ConstantSpecs::ContainerB::ChildB.const_source_location(:CSL_CONST304).should == [__FILE__, __LINE__ - 1] + ensure + ConstantSpecs::ModuleA.send(:remove_const, :CSL_CONST304) + ConstantSpecs::ModuleE.send(:remove_const, :CSL_CONST304) + end + + it "searches a path in the superclass chain" do + ConstantSpecs::ModuleA::CSL_CONST305 = :const305 + ConstantSpecs::ContainerB::ChildB.const_source_location(:CSL_CONST305).should == [__FILE__, __LINE__ - 1] + ensure + ConstantSpecs::ModuleA.send(:remove_const, :CSL_CONST305) + end + + it "returns path to a toplevel constant when the receiver is a Class" do + Object::CSL_CONST306 = :const306 + ConstantSpecs::ContainerB::ChildB.const_source_location(:CSL_CONST306).should == [__FILE__, __LINE__ - 1] + ensure + Object.send(:remove_const, :CSL_CONST306) + end + + it "returns path to a toplevel constant when the receiver is a Module" do + Object::CSL_CONST308 = :const308 + ConstantSpecs.const_source_location(:CSL_CONST308).should == [__FILE__, __LINE__ - 1] + ConstantSpecs::ModuleA.const_source_location(:CSL_CONST308).should == [__FILE__, __LINE__ - 2] + ensure + Object.send(:remove_const, :CSL_CONST308) + end + + it "returns path to the updated value of a constant" do + ConstantSpecs::ClassB::CSL_CONST309 = :const309_1 + ConstantSpecs::ClassB.const_source_location(:CSL_CONST309).should == [__FILE__, __LINE__ - 1] + + -> { + ConstantSpecs::ClassB::CSL_CONST309 = :const309_2 + }.should complain(/already initialized constant/) + ConstantSpecs::ClassB.const_source_location(:CSL_CONST309).should == [__FILE__, __LINE__ - 2] + ensure + ConstantSpecs::ClassB.send(:remove_const, :CSL_CONST309) + end + end + + describe "with statically assigned constants" do + it "works for the module and class keywords" do + ConstantSpecs.const_source_location(:ModuleB).should == [@constants_fixture_path, ConstantSpecs::ModuleB::LINE] + ConstantSpecs.const_source_location(:ClassA).should == [@constants_fixture_path, ConstantSpecs::ClassA::LINE] + end + + it "searches location path the immediate class or module first" do + ConstantSpecs::ClassA.const_source_location(:CS_CONST10).should == [@constants_fixture_path, ConstantSpecs::ClassA::CS_CONST10_LINE] + ConstantSpecs::ModuleA.const_source_location(:CS_CONST10).should == [@constants_fixture_path, ConstantSpecs::ModuleA::CS_CONST10_LINE] + ConstantSpecs::ParentA.const_source_location(:CS_CONST10).should == [@constants_fixture_path, ConstantSpecs::ParentA::CS_CONST10_LINE] + ConstantSpecs::ContainerA.const_source_location(:CS_CONST10).should == [@constants_fixture_path, ConstantSpecs::ContainerA::CS_CONST10_LINE] + ConstantSpecs::ContainerA::ChildA.const_source_location(:CS_CONST10).should == [@constants_fixture_path, ConstantSpecs::ContainerA::ChildA::CS_CONST10_LINE] + end + + it "searches location path a module included in the immediate class before the superclass" do + ConstantSpecs::ContainerA::ChildA.const_source_location(:CS_CONST15).should == [@constants_fixture_path, ConstantSpecs::ModuleC::CS_CONST15_LINE] + end + + it "searches location path the superclass before a module included in the superclass" do + ConstantSpecs::ContainerA::ChildA.const_source_location(:CS_CONST11).should == [@constants_fixture_path, ConstantSpecs::ParentA::CS_CONST11_LINE] + end + + it "searches location path a module included in the superclass" do + ConstantSpecs::ContainerA::ChildA.const_source_location(:CS_CONST12).should == [@constants_fixture_path, ConstantSpecs::ModuleB::CS_CONST12_LINE] + end + + it "searches location path the superclass chain" do + ConstantSpecs::ContainerA::ChildA.const_source_location(:CS_CONST13).should == [@constants_fixture_path, ConstantSpecs::ModuleA::CS_CONST13_LINE] + end + + it "returns location path a toplevel constant when the receiver is a Class" do + ConstantSpecs::ContainerA::ChildA.const_source_location(:CS_CONST1).should == [@constants_fixture_path, CS_CONST1_LINE] + end + + it "returns location path a toplevel constant when the receiver is a Module" do + ConstantSpecs.const_source_location(:CS_CONST1).should == [@constants_fixture_path, CS_CONST1_LINE] + ConstantSpecs::ModuleA.const_source_location(:CS_CONST1).should == [@constants_fixture_path, CS_CONST1_LINE] + end + end + + it "return empty path if constant defined in C code" do + Object.const_source_location(:String).should == [] + end + + it "accepts a String or Symbol name" do + Object.const_source_location(:CS_CONST1).should == [@constants_fixture_path, CS_CONST1_LINE] + Object.const_source_location("CS_CONST1").should == [@constants_fixture_path, CS_CONST1_LINE] + end + + it "returns nil if no constant is defined in the search path" do + ConstantSpecs.const_source_location(:CS_CONSTX).should == nil + end + + it "raises a NameError if the name does not start with a capital letter" do + -> { ConstantSpecs.const_source_location "name" }.should raise_error(NameError) + end + + it "raises a NameError if the name starts with a non-alphabetic character" do + -> { ConstantSpecs.const_source_location "__CONSTX__" }.should raise_error(NameError) + -> { ConstantSpecs.const_source_location "@CS_CONST1" }.should raise_error(NameError) + -> { ConstantSpecs.const_source_location "!CS_CONST1" }.should raise_error(NameError) + end + + it "raises a NameError if the name contains non-alphabetic characters except '_'" do + Object.const_source_location("CS_CONST1").should == [@constants_fixture_path, CS_CONST1_LINE] + -> { ConstantSpecs.const_source_location "CS_CONST1=" }.should raise_error(NameError) + -> { ConstantSpecs.const_source_location "CS_CONST1?" }.should raise_error(NameError) + end + + it "calls #to_str to convert the given name to a String" do + name = mock("ClassA") + name.should_receive(:to_str).and_return("ClassA") + ConstantSpecs.const_source_location(name).should == [@constants_fixture_path, ConstantSpecs::ClassA::LINE] + end + + it "raises a TypeError if conversion to a String by calling #to_str fails" do + name = mock('123') + -> { ConstantSpecs.const_source_location(name) }.should raise_error(TypeError) + + name.should_receive(:to_str).and_return(123) + -> { ConstantSpecs.const_source_location(name) }.should raise_error(TypeError) + end + + it "does not search the singleton class of a Class or Module" do + ConstantSpecs::ContainerA::ChildA.const_source_location(:CS_CONST14).should == nil + ConstantSpecs.const_source_location(:CS_CONST14).should == nil + end + + it "does not search the containing scope" do + ConstantSpecs::ContainerA::ChildA.const_source_location(:CS_CONST20).should == [@constants_fixture_path, ConstantSpecs::ParentA::CS_CONST20_LINE] + ConstantSpecs::ContainerA::ChildA.const_source_location(:CS_CONST5) == nil + end + + it "returns nil if the constant is defined in the receiver's superclass and the inherit flag is false" do + ConstantSpecs::ContainerA::ChildA.const_source_location(:CS_CONST4, false).should == nil + end + + it "searches into the receiver superclasses if the inherit flag is true" do + ConstantSpecs::ContainerA::ChildA.const_source_location(:CS_CONST4, true).should == [@constants_fixture_path, ConstantSpecs::ParentA::CS_CONST4_LINE] + end + + it "returns nil when the receiver is a Module, the constant is defined at toplevel and the inherit flag is false" do + ConstantSpecs::ModuleA.const_source_location(:CS_CONST1, false).should == nil + end + + it "returns nil when the receiver is a Class, the constant is defined at toplevel and the inherit flag is false" do + ConstantSpecs::ContainerA::ChildA.const_source_location(:CS_CONST1, false).should == nil + end + + it "accepts a toplevel scope qualifier" do + ConstantSpecs.const_source_location("::CS_CONST1").should == [@constants_fixture_path, CS_CONST1_LINE] + end + + it "accepts a scoped constant name" do + ConstantSpecs.const_source_location("ClassA::CS_CONST10").should == [@constants_fixture_path, ConstantSpecs::ClassA::CS_CONST10_LINE] + end + + it "returns updated location from const_set" do + mod = Module.new + const_line = __LINE__ + 1 + mod.const_set :Foo, 1 + mod.const_source_location(:Foo).should == [__FILE__, const_line] + end + + it "raises a NameError if the name includes two successive scope separators" do + -> { ConstantSpecs.const_source_location("ClassA::::CS_CONST10") }.should raise_error(NameError) + end + + it "raises a NameError if only '::' is passed" do + -> { ConstantSpecs.const_source_location("::") }.should raise_error(NameError) + end + + it "raises a NameError if a Symbol has a toplevel scope qualifier" do + -> { ConstantSpecs.const_source_location(:'::CS_CONST1') }.should raise_error(NameError) + end + + it "raises a NameError if a Symbol is a scoped constant name" do + -> { ConstantSpecs.const_source_location(:'ClassA::CS_CONST10') }.should raise_error(NameError) + end + + it "does search private constants path" do + ConstantSpecs.const_source_location(:CS_PRIVATE).should == [@constants_fixture_path, ConstantSpecs::CS_PRIVATE_LINE] + end + + it "works for eval with a given line" do + c = Class.new do + eval('self::C = 1', nil, "foo", 100) + end + c.const_source_location(:C).should == ["foo", 100] + end + + context 'autoload' do + before :all do + ConstantSpecs.autoload :CSL_CONST1, "#{__dir__}/notexisting.rb" + @line = __LINE__ - 1 + end + + before :each do + @loaded_features = $".dup + end + + after :each do + $".replace @loaded_features + end + + it 'returns the autoload location while not resolved' do + ConstantSpecs.const_source_location('CSL_CONST1').should == [__FILE__, @line] + end + + it 'returns where the constant was resolved when resolved' do + file = fixture(__FILE__, 'autoload_location.rb') + ConstantSpecs.autoload :CONST_LOCATION, file + line = ConstantSpecs::CONST_LOCATION + ConstantSpecs.const_source_location('CONST_LOCATION').should == [file, line] + end + + ruby_bug("#20188", ""..."3.4") do + it 'returns the real constant location as soon as it is defined' do + file = fixture(__FILE__, 'autoload_const_source_location.rb') + ConstantSpecs.autoload :ConstSource, file + autoload_location = [__FILE__, __LINE__ - 1] + + ConstantSpecs.const_source_location(:ConstSource).should == autoload_location + ConstantSpecs::ConstSource::LOCATION.should == ConstantSpecs.const_source_location(:ConstSource) + ConstantSpecs::BEFORE_DEFINE_LOCATION.should == autoload_location + ConstantSpecs.send :remove_const, :ConstSource + ConstantSpecs.send :remove_const, :BEFORE_DEFINE_LOCATION + end + end + end +end diff --git a/spec/ruby/core/module/constants_spec.rb b/spec/ruby/core/module/constants_spec.rb new file mode 100644 index 0000000000..330da1cc88 --- /dev/null +++ b/spec/ruby/core/module/constants_spec.rb @@ -0,0 +1,97 @@ +require_relative '../../spec_helper' +require_relative '../../fixtures/constants' +require_relative 'fixtures/classes' + +describe "Module.constants" do + it "returns an array of the names of all toplevel constants" do + count = Module.constants.size + module ConstantSpecsAdded + end + Module.constants.size.should == count + 1 + Object.send(:remove_const, :ConstantSpecsAdded) + end + + it "returns an array of Symbol names" do + # This in NOT an exhaustive list + Module.constants.should include(:Array, :Class, :Comparable, :Dir, + :Enumerable, :ENV, :Exception, :FalseClass, + :File, :Float, :Hash, :Integer, :IO, + :Kernel, :Math, :Method, :Module, :NilClass, + :Numeric, :Object, :Range, :Regexp, :String, + :Symbol, :Thread, :Time, :TrueClass) + end + + it "returns Module's constants when given a parameter" do + direct = Module.constants(false) + indirect = Module.constants(true) + module ConstantSpecsIncludedModule + MODULE_CONSTANTS_SPECS_INDIRECT = :foo + end + + class Module + MODULE_CONSTANTS_SPECS_DIRECT = :bar + include ConstantSpecsIncludedModule + end + (Module.constants(false) - direct).should == [:MODULE_CONSTANTS_SPECS_DIRECT] + (Module.constants(true) - indirect).sort.should == [:MODULE_CONSTANTS_SPECS_DIRECT, :MODULE_CONSTANTS_SPECS_INDIRECT] + + Module.send(:remove_const, :MODULE_CONSTANTS_SPECS_DIRECT) + ConstantSpecsIncludedModule.send(:remove_const, :MODULE_CONSTANTS_SPECS_INDIRECT) + end +end + +describe "Module#constants" do + it "returns an array of Symbol names of all constants defined in the module and all included modules" do + ConstantSpecs::ContainerA.constants.sort.should == [ + :CS_CONST10, :CS_CONST10_LINE, :CS_CONST23, :CS_CONST24, :CS_CONST5, :ChildA + ] + end + + it "returns all constants including inherited when passed true" do + ConstantSpecs::ContainerA.constants(true).sort.should == [ + :CS_CONST10, :CS_CONST10_LINE, :CS_CONST23, :CS_CONST24, :CS_CONST5, :ChildA + ] + end + + it "returns all constants including inherited when passed some object" do + ConstantSpecs::ContainerA.constants(Object.new).sort.should == [ + :CS_CONST10, :CS_CONST10_LINE, :CS_CONST23, :CS_CONST24, :CS_CONST5, :ChildA + ] + end + + it "doesn't returns inherited constants when passed false" do + ConstantSpecs::ContainerA.constants(false).sort.should == [ + :CS_CONST10, :CS_CONST10_LINE, :CS_CONST23, :CS_CONST5, :ChildA + ] + end + + it "doesn't returns inherited constants when passed nil" do + ConstantSpecs::ContainerA.constants(nil).sort.should == [ + :CS_CONST10, :CS_CONST10_LINE, :CS_CONST23, :CS_CONST5, :ChildA + ] + end + + it "returns only public constants" do + ModuleSpecs::PrivConstModule.constants.should == [:PUBLIC_CONSTANT] + end + + it "returns only constants starting with an uppercase letter" do + # e.g. fatal, IO::generic_readable and IO::generic_writable should not be returned by Module#constants + Object.constants.each { |c| c[0].should == c[0].upcase } + IO.constants.each { |c| c[0].should == c[0].upcase } + end +end + +describe "Module#constants" do + before :each do + ConstantSpecs::ModuleM::CS_CONST251 = :const251 + end + + after :each do + ConstantSpecs::ModuleM.send(:remove_const, :CS_CONST251) + end + + it "includes names of constants defined after a module is included" do + ConstantSpecs::ContainerA.constants.should include(:CS_CONST251) + end +end diff --git a/spec/ruby/core/module/define_method_spec.rb b/spec/ruby/core/module/define_method_spec.rb new file mode 100644 index 0000000000..c5dfc53764 --- /dev/null +++ b/spec/ruby/core/module/define_method_spec.rb @@ -0,0 +1,846 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +class DefineMethodSpecClass +end + +describe "passed { |a, b = 1| } creates a method that" do + before :each do + @klass = Class.new do + define_method(:m) { |a, b = 1| return a, b } + end + end + + it "raises an ArgumentError when passed zero arguments" do + -> { @klass.new.m }.should raise_error(ArgumentError) + end + + it "has a default value for b when passed one argument" do + @klass.new.m(1).should == [1, 1] + end + + it "overrides the default argument when passed two arguments" do + @klass.new.m(1, 2).should == [1, 2] + end + + it "raises an ArgumentError when passed three arguments" do + -> { @klass.new.m(1, 2, 3) }.should raise_error(ArgumentError) + end +end + +describe "Module#define_method when given an UnboundMethod" do + it "passes the given arguments to the new method" do + klass = Class.new do + def test_method(arg1, arg2) + [arg1, arg2] + end + define_method(:another_test_method, instance_method(:test_method)) + end + + klass.new.another_test_method(1, 2).should == [1, 2] + end + + it "adds the new method to the methods list" do + klass = Class.new do + def test_method(arg1, arg2) + [arg1, arg2] + end + define_method(:another_test_method, instance_method(:test_method)) + end + klass.new.should have_method(:another_test_method) + end + + describe "defining a method on a singleton class" do + before do + klass = Class.new + class << klass + def test_method + :foo + end + end + child = Class.new(klass) + sc = class << child; self; end + sc.send :define_method, :another_test_method, klass.method(:test_method).unbind + + @class = child + end + + it "doesn't raise TypeError when calling the method" do + @class.another_test_method.should == :foo + end + end + + it "sets the new method's visibility to the current frame's visibility" do + foo = Class.new do + def ziggy + 'piggy' + end + private :ziggy + + # make sure frame visibility is public + public + + define_method :piggy, instance_method(:ziggy) + end + + -> { foo.new.ziggy }.should raise_error(NoMethodError) + foo.new.piggy.should == 'piggy' + end +end + +describe "Module#define_method" do + describe "when the default definee is not the same as the module" do + it "sets the visibility of the method to public" do + klass = Class.new + class << klass + private + define_method(:meta) do + define_method(:foo) { :foo } + end + end + + klass.send :meta + klass.new.foo.should == :foo + end + end +end + +describe "Module#define_method when name is not a special private name" do + describe "given an UnboundMethod" do + describe "and called from the target module" do + it "sets the visibility of the method to the current visibility" do + klass = Class.new do + define_method(:bar, ModuleSpecs::EmptyFooMethod) + private + define_method(:baz, ModuleSpecs::EmptyFooMethod) + end + + klass.should have_public_instance_method(:bar) + klass.should have_private_instance_method(:baz) + end + end + + describe "and called from another module" do + it "sets the visibility of the method to public" do + klass = Class.new + Class.new do + klass.send(:define_method, :bar, ModuleSpecs::EmptyFooMethod) + private + klass.send(:define_method, :baz, ModuleSpecs::EmptyFooMethod) + end + + klass.should have_public_instance_method(:bar) + klass.should have_public_instance_method(:baz) + end + end + + it "sets the method owner for a dynamically added method with a different original owner" do + mixin_module = Module.new do + def bar; end + end + + foo = Object.new + foo.singleton_class.define_method(:bar, mixin_module.instance_method(:bar)) + + foo.method(:bar).owner.should == foo.singleton_class + end + end + + describe "passed a block" do + describe "and called from the target module" do + it "sets the visibility of the method to the current visibility" do + klass = Class.new do + define_method(:bar) {} + private + define_method(:baz) {} + end + + klass.should have_public_instance_method(:bar) + klass.should have_private_instance_method(:baz) + end + end + + describe "and called from another module" do + it "sets the visibility of the method to public" do + klass = Class.new + Class.new do + klass.send(:define_method, :bar) {} + private + klass.send(:define_method, :baz) {} + end + + klass.should have_public_instance_method(:bar) + klass.should have_public_instance_method(:baz) + end + end + end +end + +describe "Module#define_method when name is :initialize" do + describe "passed a block" do + it "sets visibility to private when method name is :initialize" do + klass = Class.new do + define_method(:initialize) { } + end + klass.should have_private_instance_method(:initialize) + end + end + + describe "given an UnboundMethod" do + it "sets the visibility to private when method is named :initialize" do + klass = Class.new do + def test_method + end + define_method(:initialize, instance_method(:test_method)) + end + klass.should have_private_instance_method(:initialize) + end + end +end + +describe "Module#define_method" do + it "defines the given method as an instance method with the given name in self" do + class DefineMethodSpecClass + def test1 + "test" + end + define_method(:another_test, instance_method(:test1)) + end + + o = DefineMethodSpecClass.new + o.test1.should == o.another_test + end + + it "calls #method_added after the method is added to the Module" do + DefineMethodSpecClass.should_receive(:method_added).with(:test_ma) + + class DefineMethodSpecClass + define_method(:test_ma) { true } + end + end + + it "defines a new method with the given name and the given block as body in self" do + class DefineMethodSpecClass + define_method(:block_test1) { self } + define_method(:block_test2, &-> { self }) + end + + o = DefineMethodSpecClass.new + o.block_test1.should == o + o.block_test2.should == o + end + + it "raises TypeError if name cannot converted to String" do + -> { + Class.new { define_method(1001, -> {}) } + }.should raise_error(TypeError, /is not a symbol nor a string/) + + -> { + Class.new { define_method([], -> {}) } + }.should raise_error(TypeError, /is not a symbol nor a string/) + end + + it "converts non-String name to String with #to_str" do + obj = Object.new + def obj.to_str() "foo" end + + new_class = Class.new { define_method(obj, -> { :called }) } + new_class.new.foo.should == :called + end + + it "raises TypeError when #to_str called on non-String name returns non-String value" do + obj = Object.new + def obj.to_str() [] end + + -> { + Class.new { define_method(obj, -> {}) } + }.should raise_error(TypeError, /can't convert Object to String/) + end + + it "raises a TypeError when the given method is no Method/Proc" do + -> { + Class.new { define_method(:test, "self") } + }.should raise_error(TypeError, "wrong argument type String (expected Proc/Method/UnboundMethod)") + + -> { + Class.new { define_method(:test, 1234) } + }.should raise_error(TypeError, "wrong argument type Integer (expected Proc/Method/UnboundMethod)") + + -> { + Class.new { define_method(:test, nil) } + }.should raise_error(TypeError, "wrong argument type NilClass (expected Proc/Method/UnboundMethod)") + end + + it "uses provided Method/Proc even if block is specified" do + new_class = Class.new do + define_method(:test, -> { :method_is_called }) do + :block_is_called + end + end + + new_class.new.test.should == :method_is_called + end + + it "raises an ArgumentError when no block is given" do + -> { + Class.new { define_method(:test) } + }.should raise_error(ArgumentError) + end + + it "does not use the caller block when no block is given" do + o = Object.new + def o.define(name) + self.class.class_eval do + define_method(name) + end + end + + -> { + o.define(:foo) { raise "not used" } + }.should raise_error(ArgumentError) + end + + it "does not change the arity check style of the original proc" do + class DefineMethodSpecClass + prc = Proc.new { || true } + define_method("proc_style_test", &prc) + end + + obj = DefineMethodSpecClass.new + -> { obj.proc_style_test :arg }.should raise_error(ArgumentError) + end + + it "raises a FrozenError if frozen" do + -> { + Class.new { freeze; define_method(:foo) {} } + }.should raise_error(FrozenError) + end + + it "accepts a Method (still bound)" do + class DefineMethodSpecClass + attr_accessor :data + def inspect_data + "data is #{@data}" + end + end + o = DefineMethodSpecClass.new + o.data = :foo + m = o.method(:inspect_data) + m.should be_an_instance_of(Method) + klass = Class.new(DefineMethodSpecClass) + klass.send(:define_method,:other_inspect, m) + c = klass.new + c.data = :bar + c.other_inspect.should == "data is bar" + ->{o.other_inspect}.should raise_error(NoMethodError) + end + + it "raises a TypeError when a Method from a singleton class is defined on another class" do + c = Class.new do + class << self + def foo + end + end + end + m = c.method(:foo) + + -> { + Class.new { define_method :bar, m } + }.should raise_error(TypeError, /can't bind singleton method to a different class/) + end + + it "raises a TypeError when a Method from one class is defined on an unrelated class" do + c = Class.new do + def foo + end + end + m = c.new.method(:foo) + + -> { + Class.new { define_method :bar, m } + }.should raise_error(TypeError) + end + + it "accepts an UnboundMethod from an attr_accessor method" do + class DefineMethodSpecClass + attr_accessor :accessor_method + end + + m = DefineMethodSpecClass.instance_method(:accessor_method) + o = DefineMethodSpecClass.new + + DefineMethodSpecClass.send(:undef_method, :accessor_method) + -> { o.accessor_method }.should raise_error(NoMethodError) + + DefineMethodSpecClass.send(:define_method, :accessor_method, m) + + o.accessor_method = :abc + o.accessor_method.should == :abc + end + + it "accepts a proc from a method" do + class ProcFromMethod + attr_accessor :data + def cool_method + "data is #{@data}" + end + end + + object1 = ProcFromMethod.new + object1.data = :foo + + method_proc = object1.method(:cool_method).to_proc + klass = Class.new(ProcFromMethod) + klass.send(:define_method, :other_cool_method, &method_proc) + + object2 = klass.new + object2.data = :bar + object2.other_cool_method.should == "data is foo" + end + + it "accepts a proc from a Symbol" do + symbol_proc = :+.to_proc + klass = Class.new do + define_method :foo, &symbol_proc + end + klass.new.foo(1, 2).should == 3 + end + + it "maintains the Proc's scope" do + class DefineMethodByProcClass + in_scope = true + method_proc = proc { in_scope } + + define_method(:proc_test, &method_proc) + end + + o = DefineMethodByProcClass.new + o.proc_test.should be_true + end + + it "accepts a String method name" do + klass = Class.new do + define_method("string_test") do + "string_test result" + end + end + + klass.new.string_test.should == "string_test result" + end + + it "is a public method" do + Module.should have_public_instance_method(:define_method) + end + + it "returns its symbol" do + class DefineMethodSpecClass + method = define_method("return_test") { true } + method.should == :return_test + end + end + + it "allows an UnboundMethod from a module to be defined on a class" do + klass = Class.new { + define_method :bar, ModuleSpecs::UnboundMethodTest.instance_method(:foo) + } + klass.new.should respond_to(:bar) + end + + it "allows an UnboundMethod from a parent class to be defined on a child class" do + parent = Class.new { define_method(:foo) { :bar } } + child = Class.new(parent) { + define_method :baz, parent.instance_method(:foo) + } + child.new.should respond_to(:baz) + end + + it "allows an UnboundMethod from a module to be defined on another unrelated module" do + mod = Module.new { + define_method :bar, ModuleSpecs::UnboundMethodTest.instance_method(:foo) + } + klass = Class.new { include mod } + klass.new.should respond_to(:bar) + end + + + it "allows an UnboundMethod of a Kernel method retrieved from Object to defined on a BasicObject subclass" do + klass = Class.new(BasicObject) do + define_method :instance_of?, ::Object.instance_method(:instance_of?) + end + klass.new.instance_of?(klass).should == true + end + + it "raises a TypeError when an UnboundMethod from a child class is defined on a parent class" do + -> { + ParentClass = Class.new { define_method(:foo) { :bar } } + ChildClass = Class.new(ParentClass) { define_method(:foo) { :baz } } + ParentClass.send :define_method, :foo, ChildClass.instance_method(:foo) + }.should raise_error(TypeError, /bind argument must be a subclass of ChildClass/) + ensure + Object.send(:remove_const, :ParentClass) + Object.send(:remove_const, :ChildClass) + end + + it "raises a TypeError when an UnboundMethod from one class is defined on an unrelated class" do + -> { + DestinationClass = Class.new { + define_method :bar, ModuleSpecs::InstanceMeth.instance_method(:foo) + } + }.should raise_error(TypeError, /bind argument must be a subclass of ModuleSpecs::InstanceMeth/) + end + + it "raises a TypeError when an UnboundMethod from a singleton class is defined on another class" do + c = Class.new do + class << self + def foo + end + end + end + m = c.method(:foo).unbind + + -> { + Class.new { define_method :bar, m } + }.should raise_error(TypeError, /can't bind singleton method to a different class/) + end + + it "defines a new method with public visibility when a Method passed and the class/module of the context isn't equal to the receiver of #define_method" do + c = Class.new do + private def foo + "public" + end + end + + object = c.new + object.singleton_class.define_method(:bar, object.method(:foo)) + + object.bar.should == "public" + end + + it "defines the new method according to the scope visibility when a Method passed and the class/module of the context is equal to the receiver of #define_method" do + c = Class.new do + def foo; end + end + + object = c.new + object.singleton_class.class_eval do + private + define_method(:bar, c.new.method(:foo)) + end + + -> { object.bar }.should raise_error(NoMethodError) + end +end + +describe "Module#define_method" do + describe "passed { } creates a method that" do + before :each do + @klass = Class.new do + define_method(:m) { :called } + end + end + + it "returns the value computed by the block when passed zero arguments" do + @klass.new.m().should == :called + end + + it "raises an ArgumentError when passed one argument" do + -> { @klass.new.m 1 }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when passed two arguments" do + -> { @klass.new.m 1, 2 }.should raise_error(ArgumentError) + end + end + + describe "passed { || } creates a method that" do + before :each do + @klass = Class.new do + define_method(:m) { || :called } + end + end + + it "returns the value computed by the block when passed zero arguments" do + @klass.new.m().should == :called + end + + it "raises an ArgumentError when passed one argument" do + -> { @klass.new.m 1 }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when passed two arguments" do + -> { @klass.new.m 1, 2 }.should raise_error(ArgumentError) + end + end + + describe "passed { |a| } creates a method that" do + before :each do + @klass = Class.new do + define_method(:m) { |a| a } + end + end + + it "raises an ArgumentError when passed zero arguments" do + -> { @klass.new.m }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when passed zero arguments and a block" do + -> { @klass.new.m { :computed } }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when passed two arguments" do + -> { @klass.new.m 1, 2 }.should raise_error(ArgumentError) + end + + it "receives the value passed as the argument when passed one argument" do + @klass.new.m(1).should == 1 + end + end + + describe "passed { |a,| } creates a method that" do + before :each do + @klass = Class.new do + define_method(:m) { |a,| a } + end + end + + it "raises an ArgumentError when passed zero arguments" do + -> { @klass.new.m }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when passed zero arguments and a block" do + -> { @klass.new.m { :computed } }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when passed two arguments" do + -> { @klass.new.m 1, 2 }.should raise_error(ArgumentError) + end + + it "receives the value passed as the argument when passed one argument" do + @klass.new.m(1).should == 1 + end + + it "does not destructure the passed argument" do + @klass.new.m([1, 2]).should == [1, 2] + # for comparison: + proc { |a,| a }.call([1, 2]).should == 1 + end + end + + describe "passed { |*a| } creates a method that" do + before :each do + @klass = Class.new do + define_method(:m) { |*a| a } + end + end + + it "receives an empty array as the argument when passed zero arguments" do + @klass.new.m().should == [] + end + + it "receives the value in an array when passed one argument" do + @klass.new.m(1).should == [1] + end + + it "receives the values in an array when passed two arguments" do + @klass.new.m(1, 2).should == [1, 2] + end + end + + describe "passed { |a, *b| } creates a method that" do + before :each do + @klass = Class.new do + define_method(:m) { |a, *b| return a, b } + end + end + + it "raises an ArgumentError when passed zero arguments" do + -> { @klass.new.m }.should raise_error(ArgumentError) + end + + it "returns the value computed by the block when passed one argument" do + @klass.new.m(1).should == [1, []] + end + + it "returns the value computed by the block when passed two arguments" do + @klass.new.m(1, 2).should == [1, [2]] + end + + it "returns the value computed by the block when passed three arguments" do + @klass.new.m(1, 2, 3).should == [1, [2, 3]] + end + end + + describe "passed { |a, b| } creates a method that" do + before :each do + @klass = Class.new do + define_method(:m) { |a, b| return a, b } + end + end + + it "returns the value computed by the block when passed two arguments" do + @klass.new.m(1, 2).should == [1, 2] + end + + it "raises an ArgumentError when passed zero arguments" do + -> { @klass.new.m }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when passed one argument" do + -> { @klass.new.m 1 }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when passed one argument and a block" do + -> { @klass.new.m(1) { } }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when passed three arguments" do + -> { @klass.new.m 1, 2, 3 }.should raise_error(ArgumentError) + end + end + + describe "passed { |a, b, *c| } creates a method that" do + before :each do + @klass = Class.new do + define_method(:m) { |a, b, *c| return a, b, c } + end + end + + it "raises an ArgumentError when passed zero arguments" do + -> { @klass.new.m }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when passed one argument" do + -> { @klass.new.m 1 }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when passed one argument and a block" do + -> { @klass.new.m(1) { } }.should raise_error(ArgumentError) + end + + it "receives an empty array as the third argument when passed two arguments" do + @klass.new.m(1, 2).should == [1, 2, []] + end + + it "receives the third argument in an array when passed three arguments" do + @klass.new.m(1, 2, 3).should == [1, 2, [3]] + end + end +end + +describe "Module#define_method when passed a Method object" do + before :each do + @klass = Class.new do + def m(a, b, *c) + :m + end + end + + @obj = @klass.new + m = @obj.method :m + + @klass.class_exec do + define_method :n, m + end + end + + it "defines a method with the same #arity as the original" do + @obj.method(:n).arity.should == @obj.method(:m).arity + end + + it "defines a method with the same #parameters as the original" do + @obj.method(:n).parameters.should == @obj.method(:m).parameters + end +end + +describe "Module#define_method when passed an UnboundMethod object" do + before :each do + @klass = Class.new do + def m(a, b, *c) + :m + end + end + + @obj = @klass.new + m = @klass.instance_method :m + + @klass.class_exec do + define_method :n, m + end + end + + it "defines a method with the same #arity as the original" do + @obj.method(:n).arity.should == @obj.method(:m).arity + end + + it "defines a method with the same #parameters as the original" do + @obj.method(:n).parameters.should == @obj.method(:m).parameters + end +end + +describe "Module#define_method when passed a Proc object" do + describe "and a method is defined inside" do + it "defines the nested method in the default definee where the Proc was created" do + prc = nil + t = Class.new do + prc = -> { + def nested_method_in_proc_for_define_method + 42 + end + } + end + + c = Class.new do + define_method(:test, prc) + end + + o = c.new + o.test + o.should_not have_method :nested_method_in_proc_for_define_method + + t.new.nested_method_in_proc_for_define_method.should == 42 + end + end +end + +describe "Module#define_method when passed a block" do + describe "behaves exactly like a lambda" do + it "for return" do + Class.new do + define_method(:foo) do + return 42 + end + end.new.foo.should == 42 + end + + it "for break" do + Class.new do + define_method(:foo) do + break 42 + end + end.new.foo.should == 42 + end + + it "for next" do + Class.new do + define_method(:foo) do + next 42 + end + end.new.foo.should == 42 + end + + it "for redo" do + Class.new do + result = [] + define_method(:foo) do + if result.empty? + result << :first + redo + else + result << :second + result + end + end + end.new.foo.should == [:first, :second] + end + end +end diff --git a/spec/ruby/core/module/define_singleton_method_spec.rb b/spec/ruby/core/module/define_singleton_method_spec.rb new file mode 100644 index 0000000000..eb5cb89ed1 --- /dev/null +++ b/spec/ruby/core/module/define_singleton_method_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../spec_helper' + +describe "Module#define_singleton_method" do + it "defines the given method as an class method with the given name in self" do + klass = Module.new do + define_singleton_method :a do + 42 + end + define_singleton_method(:b, -> x { 2*x }) + end + + klass.a.should == 42 + klass.b(10).should == 20 + end +end diff --git a/spec/ruby/core/module/deprecate_constant_spec.rb b/spec/ruby/core/module/deprecate_constant_spec.rb new file mode 100644 index 0000000000..ec0de6782f --- /dev/null +++ b/spec/ruby/core/module/deprecate_constant_spec.rb @@ -0,0 +1,70 @@ +require_relative '../../spec_helper' + +describe "Module#deprecate_constant" do + before :each do + @module = Module.new + @value = :value + @module::PUBLIC1 = @value + @module::PUBLIC2 = @value + @module::PRIVATE = @value + @module.private_constant :PRIVATE + @module.deprecate_constant :PRIVATE + end + + describe "when accessing the deprecated module" do + it "passes the accessing" do + @module.deprecate_constant :PUBLIC1 + + value = nil + -> { + value = @module::PUBLIC1 + }.should complain(/warning: constant .+::PUBLIC1 is deprecated/) + value.should equal(@value) + + -> { @module::PRIVATE }.should raise_error(NameError) + end + + it "warns with a message" do + @module.deprecate_constant :PUBLIC1 + + -> { @module::PUBLIC1 }.should complain(/warning: constant .+::PUBLIC1 is deprecated/) + -> { @module.const_get :PRIVATE }.should complain(/warning: constant .+::PRIVATE is deprecated/) + end + + it "does not warn if Warning[:deprecated] is false" do + @module.deprecate_constant :PUBLIC1 + + deprecated = Warning[:deprecated] + begin + Warning[:deprecated] = false + -> { @module::PUBLIC1 }.should_not complain + ensure + Warning[:deprecated] = deprecated + end + end + end + + ruby_bug '#20900', ''...'3.4' do + describe "when removing the deprecated module" do + it "warns with a message" do + @module.deprecate_constant :PUBLIC1 + -> { @module.module_eval {remove_const :PUBLIC1} }.should complain(/warning: constant .+::PUBLIC1 is deprecated/) + end + end + end + + it "accepts multiple symbols and strings as constant names" do + @module.deprecate_constant "PUBLIC1", :PUBLIC2 + + -> { @module::PUBLIC1 }.should complain(/warning: constant .+::PUBLIC1 is deprecated/) + -> { @module::PUBLIC2 }.should complain(/warning: constant .+::PUBLIC2 is deprecated/) + end + + it "returns self" do + @module.deprecate_constant(:PUBLIC1).should equal(@module) + end + + it "raises a NameError when given an undefined name" do + -> { @module.deprecate_constant :UNDEFINED }.should raise_error(NameError) + end +end diff --git a/spec/ruby/core/module/eql_spec.rb b/spec/ruby/core/module/eql_spec.rb new file mode 100644 index 0000000000..76bb271d8d --- /dev/null +++ b/spec/ruby/core/module/eql_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/equal_value' + +describe "Module#eql?" do + it_behaves_like :module_equal, :eql? +end diff --git a/spec/ruby/core/module/equal_spec.rb b/spec/ruby/core/module/equal_spec.rb new file mode 100644 index 0000000000..01ab06152d --- /dev/null +++ b/spec/ruby/core/module/equal_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/equal_value' + +describe "Module#equal?" do + it_behaves_like :module_equal, :equal? +end diff --git a/spec/ruby/core/module/equal_value_spec.rb b/spec/ruby/core/module/equal_value_spec.rb new file mode 100644 index 0000000000..a7191cd755 --- /dev/null +++ b/spec/ruby/core/module/equal_value_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/equal_value' + +describe "Module#==" do + it_behaves_like :module_equal, :== +end diff --git a/spec/ruby/core/module/extend_object_spec.rb b/spec/ruby/core/module/extend_object_spec.rb new file mode 100644 index 0000000000..1fd1abc0b5 --- /dev/null +++ b/spec/ruby/core/module/extend_object_spec.rb @@ -0,0 +1,56 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Module#extend_object" do + before :each do + ScratchPad.clear + end + + it "is a private method" do + Module.should have_private_instance_method(:extend_object) + end + + describe "on Class" do + it "is undefined" do + Class.should_not have_private_instance_method(:extend_object, true) + end + + it "raises a TypeError if calling after rebinded to Class" do + -> { + Module.instance_method(:extend_object).bind(Class.new).call Object.new + }.should raise_error(TypeError) + end + end + + it "is called when #extend is called on an object" do + ModuleSpecs::ExtendObject.should_receive(:extend_object) + obj = mock("extended object") + obj.extend ModuleSpecs::ExtendObject + end + + it "extends the given object with its constants and methods by default" do + obj = mock("extended direct") + ModuleSpecs::ExtendObject.send :extend_object, obj + + obj.test_method.should == "hello test" + obj.singleton_class.const_get(:C).should == :test + end + + it "is called even when private" do + obj = mock("extended private") + obj.extend ModuleSpecs::ExtendObjectPrivate + ScratchPad.recorded.should == :extended + end + + describe "when given a frozen object" do + before :each do + @receiver = Module.new + @object = Object.new.freeze + end + + it "raises a RuntimeError before extending the object" do + -> { @receiver.send(:extend_object, @object) }.should raise_error(RuntimeError) + @object.should_not be_kind_of(@receiver) + end + end +end diff --git a/spec/ruby/core/module/extended_spec.rb b/spec/ruby/core/module/extended_spec.rb new file mode 100644 index 0000000000..c6300ffa0b --- /dev/null +++ b/spec/ruby/core/module/extended_spec.rb @@ -0,0 +1,44 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Module#extended" do + it "is called when an object gets extended with self" do + begin + m = Module.new do + def self.extended(o) + $extended_object = o + end + end + + (o = mock('x')).extend(m) + + $extended_object.should == o + ensure + $extended_object = nil + end + end + + it "is called after Module#extend_object" do + begin + m = Module.new do + def self.extend_object(o) + $extended_object = nil + end + + def self.extended(o) + $extended_object = o + end + end + + (o = mock('x')).extend(m) + + $extended_object.should == o + ensure + $extended_object = nil + end + end + + it "is private in its default implementation" do + Module.new.private_methods.should include(:extended) + end +end diff --git a/spec/ruby/core/module/fixtures/autoload.rb b/spec/ruby/core/module/fixtures/autoload.rb new file mode 100644 index 0000000000..5a77a2d9d4 --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload.rb @@ -0,0 +1 @@ +$m.const_set(:AAA, "test") unless $m.nil? diff --git a/spec/ruby/core/module/fixtures/autoload_abc.rb b/spec/ruby/core/module/fixtures/autoload_abc.rb new file mode 100644 index 0000000000..ffaec91cfe --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_abc.rb @@ -0,0 +1,11 @@ +module ModuleSpecs::Autoload::FromThread + module A + class B + class C + def self.foo + :foo + end + end + end + end +end diff --git a/spec/ruby/core/module/fixtures/autoload_c.rb b/spec/ruby/core/module/fixtures/autoload_c.rb new file mode 100644 index 0000000000..ff2bcc548c --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_c.rb @@ -0,0 +1,11 @@ +module ModuleSpecs::Autoload + class DynClass + class C + def loaded + :dynclass_c + end + end + end +end + +ScratchPad.record :loaded diff --git a/spec/ruby/core/module/fixtures/autoload_callback.rb b/spec/ruby/core/module/fixtures/autoload_callback.rb new file mode 100644 index 0000000000..51d53eb580 --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_callback.rb @@ -0,0 +1,2 @@ +block = ScratchPad.recorded +block.call diff --git a/spec/ruby/core/module/fixtures/autoload_concur.rb b/spec/ruby/core/module/fixtures/autoload_concur.rb new file mode 100644 index 0000000000..0585e36880 --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_concur.rb @@ -0,0 +1,9 @@ +ScratchPad.recorded << :con_pre +Thread.current[:in_autoload_rb] = true +sleep 0.1 + +module ModuleSpecs::Autoload + Concur = 1 +end + +ScratchPad.recorded << :con_post diff --git a/spec/ruby/core/module/fixtures/autoload_const_source_location.rb b/spec/ruby/core/module/fixtures/autoload_const_source_location.rb new file mode 100644 index 0000000000..ee0e5a689f --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_const_source_location.rb @@ -0,0 +1,6 @@ +module ConstantSpecs + BEFORE_DEFINE_LOCATION = const_source_location(:ConstSource) + module ConstSource + LOCATION = Object.const_source_location(name) + end +end diff --git a/spec/ruby/core/module/fixtures/autoload_d.rb b/spec/ruby/core/module/fixtures/autoload_d.rb new file mode 100644 index 0000000000..6f5eee741c --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_d.rb @@ -0,0 +1,11 @@ +module ModuleSpecs::Autoload + module DynModule + class D + def loaded + :dynmodule_d + end + end + end +end + +ScratchPad.record :loaded diff --git a/spec/ruby/core/module/fixtures/autoload_during_autoload.rb b/spec/ruby/core/module/fixtures/autoload_during_autoload.rb new file mode 100644 index 0000000000..5202bd8b23 --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_during_autoload.rb @@ -0,0 +1,7 @@ +block = ScratchPad.recorded +ScratchPad.record(block.call) + +module ModuleSpecs::Autoload + class DuringAutoload + end +end diff --git a/spec/ruby/core/module/fixtures/autoload_during_autoload_after_define.rb b/spec/ruby/core/module/fixtures/autoload_during_autoload_after_define.rb new file mode 100644 index 0000000000..a9d886dfd6 --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_during_autoload_after_define.rb @@ -0,0 +1,6 @@ +module ModuleSpecs::Autoload + class DuringAutoloadAfterDefine + block = ScratchPad.recorded + ScratchPad.record(block.call) + end +end diff --git a/spec/ruby/core/module/fixtures/autoload_during_require.rb b/spec/ruby/core/module/fixtures/autoload_during_require.rb new file mode 100644 index 0000000000..6fd81592e3 --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_during_require.rb @@ -0,0 +1,4 @@ +module ModuleSpecs::Autoload + class AutoloadDuringRequire + end +end diff --git a/spec/ruby/core/module/fixtures/autoload_during_require_current_file.rb b/spec/ruby/core/module/fixtures/autoload_during_require_current_file.rb new file mode 100644 index 0000000000..5aa8595065 --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_during_require_current_file.rb @@ -0,0 +1,5 @@ +module ModuleSpecs::Autoload + autoload(:AutoloadCurrentFile, __FILE__) + + ScratchPad.record autoload?(:AutoloadCurrentFile) +end diff --git a/spec/ruby/core/module/fixtures/autoload_e.rb b/spec/ruby/core/module/fixtures/autoload_e.rb new file mode 100644 index 0000000000..fb78c6cbd4 --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_e.rb @@ -0,0 +1,7 @@ +module ModuleSpecs::Autoload + class E + def loaded + :autoload_e + end + end +end diff --git a/spec/ruby/core/module/fixtures/autoload_empty.rb b/spec/ruby/core/module/fixtures/autoload_empty.rb new file mode 100644 index 0000000000..d7116f3049 --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_empty.rb @@ -0,0 +1 @@ +# This file is left empty on purpose diff --git a/spec/ruby/core/module/fixtures/autoload_ex1.rb b/spec/ruby/core/module/fixtures/autoload_ex1.rb new file mode 100644 index 0000000000..a90092389c --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_ex1.rb @@ -0,0 +1,16 @@ + +class ModuleSpecs::Autoload::EX1 < Exception + def self.trample1 + 1.times { return } + end + + def self.trample2 + begin + raise "hello" + rescue + end + end + + trample1 + trample2 +end diff --git a/spec/ruby/core/module/fixtures/autoload_exception.rb b/spec/ruby/core/module/fixtures/autoload_exception.rb new file mode 100644 index 0000000000..09acf9f537 --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_exception.rb @@ -0,0 +1,3 @@ +ScratchPad.record(:exception) + +raise 'intentional error to test failure conditions during autoloading' diff --git a/spec/ruby/core/module/fixtures/autoload_f.rb b/spec/ruby/core/module/fixtures/autoload_f.rb new file mode 100644 index 0000000000..54c2b05b7b --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_f.rb @@ -0,0 +1,7 @@ +module ModuleSpecs::Autoload + module F + def self.loaded + :autoload_f + end + end +end diff --git a/spec/ruby/core/module/fixtures/autoload_g.rb b/spec/ruby/core/module/fixtures/autoload_g.rb new file mode 100644 index 0000000000..a1c4e429d9 --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_g.rb @@ -0,0 +1,7 @@ +module ModuleSpecs::Autoload + class G + def loaded + :autoload_g + end + end +end diff --git a/spec/ruby/core/module/fixtures/autoload_h.rb b/spec/ruby/core/module/fixtures/autoload_h.rb new file mode 100644 index 0000000000..53988c5382 --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_h.rb @@ -0,0 +1,7 @@ +module ModuleSpecs::Autoload + module H + def loaded + :autoload_h + end + end +end diff --git a/spec/ruby/core/module/fixtures/autoload_i.rb b/spec/ruby/core/module/fixtures/autoload_i.rb new file mode 100644 index 0000000000..f7f720516e --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_i.rb @@ -0,0 +1,5 @@ +module ModuleSpecs::Autoload + I = :autoloaded +end + +ScratchPad.record :loaded diff --git a/spec/ruby/core/module/fixtures/autoload_j.rb b/spec/ruby/core/module/fixtures/autoload_j.rb new file mode 100644 index 0000000000..da6d35d43d --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_j.rb @@ -0,0 +1,3 @@ +module ModuleSpecs::Autoload + J = :autoload_j +end diff --git a/spec/ruby/core/module/fixtures/autoload_k.rb b/spec/ruby/core/module/fixtures/autoload_k.rb new file mode 100644 index 0000000000..431602bf80 --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_k.rb @@ -0,0 +1,7 @@ +module ModuleSpecs::Autoload + class KHash < Hash + K = :autoload_k + end +end + +ScratchPad.record :loaded diff --git a/spec/ruby/core/module/fixtures/autoload_lm.rb b/spec/ruby/core/module/fixtures/autoload_lm.rb new file mode 100644 index 0000000000..d93d783cd0 --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_lm.rb @@ -0,0 +1,4 @@ +module ModuleSpecs::Autoload + L = :autoload_l + M = :autoload_m +end diff --git a/spec/ruby/core/module/fixtures/autoload_location.rb b/spec/ruby/core/module/fixtures/autoload_location.rb new file mode 100644 index 0000000000..318851b2df --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_location.rb @@ -0,0 +1,3 @@ +module ConstantSpecs + CONST_LOCATION = __LINE__ +end diff --git a/spec/ruby/core/module/fixtures/autoload_nested.rb b/spec/ruby/core/module/fixtures/autoload_nested.rb new file mode 100644 index 0000000000..073cec0dce --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_nested.rb @@ -0,0 +1,8 @@ +module ModuleSpecs::Autoload + module GoodParent + class Nested + end + end +end + +ScratchPad.record(:loaded) diff --git a/spec/ruby/core/module/fixtures/autoload_never_set.rb b/spec/ruby/core/module/fixtures/autoload_never_set.rb new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_never_set.rb @@ -0,0 +1 @@ + diff --git a/spec/ruby/core/module/fixtures/autoload_o.rb b/spec/ruby/core/module/fixtures/autoload_o.rb new file mode 100644 index 0000000000..7d88f969b2 --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_o.rb @@ -0,0 +1,2 @@ +# does not define ModuleSpecs::Autoload::O +ScratchPad << :loaded diff --git a/spec/ruby/core/module/fixtures/autoload_overridden.rb b/spec/ruby/core/module/fixtures/autoload_overridden.rb new file mode 100644 index 0000000000..7062bcfabc --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_overridden.rb @@ -0,0 +1,3 @@ +module ModuleSpecs::Autoload + Overridden = :overridden +end diff --git a/spec/ruby/core/module/fixtures/autoload_r.rb b/spec/ruby/core/module/fixtures/autoload_r.rb new file mode 100644 index 0000000000..34209d20c3 --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_r.rb @@ -0,0 +1,4 @@ +module ModuleSpecs::Autoload + class R + end +end diff --git a/spec/ruby/core/module/fixtures/autoload_raise.rb b/spec/ruby/core/module/fixtures/autoload_raise.rb new file mode 100644 index 0000000000..f6051e3ba2 --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_raise.rb @@ -0,0 +1,2 @@ +ScratchPad << :raise +raise "exception during autoload" diff --git a/spec/ruby/core/module/fixtures/autoload_required_directly.rb b/spec/ruby/core/module/fixtures/autoload_required_directly.rb new file mode 100644 index 0000000000..bed60a71ec --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_required_directly.rb @@ -0,0 +1,7 @@ +block = ScratchPad.recorded +ScratchPad.record(block.call) + +module ModuleSpecs::Autoload + class RequiredDirectly + end +end diff --git a/spec/ruby/core/module/fixtures/autoload_required_directly_nested.rb b/spec/ruby/core/module/fixtures/autoload_required_directly_nested.rb new file mode 100644 index 0000000000..a9f11c2188 --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_required_directly_nested.rb @@ -0,0 +1 @@ +ScratchPad.recorded.call diff --git a/spec/ruby/core/module/fixtures/autoload_required_directly_no_constant.rb b/spec/ruby/core/module/fixtures/autoload_required_directly_no_constant.rb new file mode 100644 index 0000000000..25e08c1129 --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_required_directly_no_constant.rb @@ -0,0 +1,2 @@ +block = ScratchPad.recorded +ScratchPad.record(block.call) diff --git a/spec/ruby/core/module/fixtures/autoload_s.rb b/spec/ruby/core/module/fixtures/autoload_s.rb new file mode 100644 index 0000000000..f5d412ff18 --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_s.rb @@ -0,0 +1,5 @@ +module ModuleSpecs::Autoload + S = :autoload_s +end + +ScratchPad.record :loaded diff --git a/spec/ruby/core/module/fixtures/autoload_self_during_require.rb b/spec/ruby/core/module/fixtures/autoload_self_during_require.rb new file mode 100644 index 0000000000..f4a514a807 --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_self_during_require.rb @@ -0,0 +1,5 @@ +module ModuleSpecs::Autoload + autoload :AutoloadSelfDuringRequire, __FILE__ + class AutoloadSelfDuringRequire + end +end diff --git a/spec/ruby/core/module/fixtures/autoload_subclass.rb b/spec/ruby/core/module/fixtures/autoload_subclass.rb new file mode 100644 index 0000000000..8027fa3fcd --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_subclass.rb @@ -0,0 +1,11 @@ +class CS_CONST_AUTOLOAD +end + +module ModuleSpecs + module Autoload + module XX + class CS_CONST_AUTOLOAD < CS_CONST_AUTOLOAD + end + end + end +end diff --git a/spec/ruby/core/module/fixtures/autoload_t.rb b/spec/ruby/core/module/fixtures/autoload_t.rb new file mode 100644 index 0000000000..4962c97f4c --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_t.rb @@ -0,0 +1,3 @@ +module ModuleSpecs::Autoload::S + T = :autoload_t +end diff --git a/spec/ruby/core/module/fixtures/autoload_v.rb b/spec/ruby/core/module/fixtures/autoload_v.rb new file mode 100644 index 0000000000..2aa8c44169 --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_v.rb @@ -0,0 +1,7 @@ +module ModuleSpecs::Autoload::U + class V + def self.get_value + :autoload_uvx + end + end +end diff --git a/spec/ruby/core/module/fixtures/autoload_w.rb b/spec/ruby/core/module/fixtures/autoload_w.rb new file mode 100644 index 0000000000..997273812e --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_w.rb @@ -0,0 +1,2 @@ +# Fails to define ModuleSpecs::Autoload::W::Y +ScratchPad.record :loaded diff --git a/spec/ruby/core/module/fixtures/autoload_w2.rb b/spec/ruby/core/module/fixtures/autoload_w2.rb new file mode 100644 index 0000000000..a8dbebf322 --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_w2.rb @@ -0,0 +1 @@ +ScratchPad.record :loaded diff --git a/spec/ruby/core/module/fixtures/autoload_x.rb b/spec/ruby/core/module/fixtures/autoload_x.rb new file mode 100644 index 0000000000..a6c5842609 --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_x.rb @@ -0,0 +1,3 @@ +module ModuleSpecs::Autoload + X = :x +end diff --git a/spec/ruby/core/module/fixtures/autoload_z.rb b/spec/ruby/core/module/fixtures/autoload_z.rb new file mode 100644 index 0000000000..cce1c13f37 --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_z.rb @@ -0,0 +1,5 @@ +class ModuleSpecs::Autoload::YY +end + +class ModuleSpecs::Autoload::Z < ModuleSpecs::Autoload::YY +end diff --git a/spec/ruby/core/module/fixtures/classes.rb b/spec/ruby/core/module/fixtures/classes.rb new file mode 100644 index 0000000000..964f64c593 --- /dev/null +++ b/spec/ruby/core/module/fixtures/classes.rb @@ -0,0 +1,653 @@ +module ModuleSpecs + def self.without_test_modules(modules) + ignore = %w[MSpecRSpecAdapter PP::ObjectMixin MainSpecs::Module ConstantSpecs::ModuleA] + modules.reject { |k| ignore.include?(k.name) } + end + + CONST = :plain_constant + + class NamedClass + end + + module PrivConstModule + PRIVATE_CONSTANT = 1 + private_constant :PRIVATE_CONSTANT + PUBLIC_CONSTANT = 2 + end + + class Subclass < Module + end + + class SubclassSpec + end + + class RemoveClassVariable + end + + module LookupModInMod + INCS = :ethereal + end + + module LookupMod + include LookupModInMod + + MODS = :rockers + end + + class Lookup + include LookupMod + LOOKIE = :lookie + end + + class LookupChild < Lookup + end + + module ModuleWithPrepend + prepend LookupMod + end + + class WithPrependedModule + include ModuleWithPrepend + end + + class Parent + # For private_class_method spec + def self.private_method; end + private_class_method :private_method + + def undefed_method() end + undef_method :undefed_method + + def parent_method; end + def another_parent_method; end + + # For public_class_method spec + private + def self.public_method; end + public_class_method :public_method + + public + def public_parent() end + + protected + def protected_parent() end + + private + def private_parent() end + end + + module Basic + def public_module() end + + protected + def protected_module() end + + private + def private_module() end + end + + module Super + include Basic + + def public_super_module() end + + protected + def protected_super_module() end + + private + def private_super_module() end + + def super_included_method; end + + class SuperChild + end + end + + module Internal + end + + class Child < Parent + include Super + + class << self + include Internal + end + attr_accessor :accessor_method + + def public_child() end + + undef_method :parent_method + undef_method :another_parent_method + + protected + def protected_child() end + + private + def private_child() end + end + + class Grandchild < Child + undef_method :super_included_method + end + + class Child2 < Parent + attr_reader :foo + end + + # Be careful touching the Counts* classes as there used for testing + # private_instance_methods, public_instance_methods, etc. So adding, removing + # a method will break those tests. + module CountsMixin + def public_3; end + public :public_3 + + def private_3; end + private :private_3 + + def protected_3; end + protected :protected_3 + end + + class CountsParent + include CountsMixin + + def public_2; end + + private + def private_2; end + + protected + def protected_2; end + end + + class CountsChild < CountsParent + def public_1; end + + private + def private_1; end + + protected + def protected_1; end + end + + module AddConstant + end + + module A + CONSTANT_A = :a + OVERRIDE = :a + def ma(); :a; end + def self.cma(); :a; end + end + + module B + CONSTANT_B = :b + OVERRIDE = :b + include A + def mb(); :b; end + def self.cmb(); :b; end + end + + class C + OVERRIDE = :c + include B + end + + module Z + MODULE_SPEC_TOPLEVEL_CONSTANT = 1 + end + + module Alias + def report() :report end + alias publish report + end + + class Allonym + include ModuleSpecs::Alias + end + + class Aliasing + def self.make_alias(*a) + alias_method(*a) + end + + def public_one; 1; end + + def public_two(n); n * 2; end + + private + def private_one; 1; end + + protected + def protected_one; 1; end + end + + class AliasingSubclass < Aliasing + end + + module AliasingSuper + + module Parent + def super_call(arg) + arg + end + end + + module Child + include Parent + def super_call(arg) + super(arg) + end + end + + class Target + include Child + alias_method :alias_super_call, :super_call + alias_method :super_call, :alias_super_call + end + + class RedefineAfterAlias + include Parent + + def super_call(arg) + super(arg) + end + + alias_method :alias_super_call, :super_call + + def super_call(arg) + :wrong + end + end + end + + + module ReopeningModule + def foo; true; end + module_function :foo + private :foo + end + + # Yes, we want to re-open the module + module ReopeningModule + alias :foo2 :foo + module_function :foo2 + end + + module Nesting + @tests = {} + def self.[](name); @tests[name]; end + def self.[]=(name, val); @tests[name] = val; end + def self.meta; class << self; self; end; end + + Nesting[:basic] = Module.nesting + + module ::ModuleSpecs + Nesting[:open_first_level] = Module.nesting + end + + class << self + Nesting[:open_meta] = Module.nesting + end + + def self.called_from_module_method + Module.nesting + end + + class NestedClass + Nesting[:nest_class] = Module.nesting + + def self.called_from_class_method + Module.nesting + end + + def called_from_inst_method + Module.nesting + end + end + + end + + Nesting[:first_level] = Module.nesting + + module InstanceMethMod + def bar(); :bar; end + end + + class InstanceMeth + include InstanceMethMod + def foo(); :foo; end + end + + class InstanceMethChild < InstanceMeth + end + + module ClassVars + class A + @@a_cvar = :a_cvar + end + + module M + @@m_cvar = :m_cvar + end + + class B < A + include M + + @@b_cvar = :b_cvar + end + end + + class CVars + @@cls = :class + + # Singleton class lexical scopes are ignored for class variables + class << self + def cls + # This looks in the parent lexical scope, class CVars + @@cls + end + # This actually adds it to the parent lexical scope, class CVars + @@meta = :metainfo + end + + def self.meta + @@meta + end + + def meta + @@meta + end + end + + class SubCVars < CVars + @@sub = :sub + end + + module MVars + @@mvar = :mvar + end + + class SubModule < Module + attr_reader :special + def initialize + @special = 10 + end + end + + module MA; end + module MB + include MA + end + module MC; end + + class MultipleIncludes + include MB + end + + # empty modules + module M1; end + module M2; end + module M3; end + + module Autoload + def self.use_ex1 + begin + begin + raise "test exception" + rescue ModuleSpecs::Autoload::EX1 + end + rescue RuntimeError + return :good + end + end + + class Parent + end + + class Child < Parent + end + + module FromThread + module A + autoload :B, fixture(__FILE__, "autoload_empty.rb") + + class B + autoload :C, fixture(__FILE__, "autoload_abc.rb") + + def self.foo + C.foo + end + end + end + + class D < A::B; end + end + end + + # This class isn't inherited from or included in anywhere. + # It exists to test the constant scoping rules. + class Detached + DETACHED_CONSTANT = :d + end + + class ParentPrivateMethodRedef + private + def private_method_redefined + :before_redefinition + end + end + + class ChildPrivateMethodMadePublic < ParentPrivateMethodRedef + public :private_method_redefined + end + + class ParentPrivateMethodRedef + def private_method_redefined + :after_redefinition + end + end + + module CyclicAppendA + end + + module CyclicAppendB + include CyclicAppendA + end + + module CyclicPrepend + end + + module ExtendObject + C = :test + def test_method + "hello test" + end + end + + module ExtendObjectPrivate + class << self + def extend_object(obj) + ScratchPad.record :extended + end + private :extend_object + end + end + + class CyclicBarrier + def initialize(count = 1) + @count = count + @state = 0 + @mutex = Mutex.new + @cond = ConditionVariable.new + end + + def await + @mutex.synchronize do + @state += 1 + if @state >= @count + @state = 0 + @cond.broadcast + true + else + @cond.wait @mutex + false + end + end + end + + def enabled? + @mutex.synchronize { @count != -1 } + end + + def disable! + @mutex.synchronize do + @count = -1 + @cond.broadcast + end + end + end + + class ThreadSafeCounter + def initialize(value = 0) + @value = 0 + @mutex = Mutex.new + end + + def get + @mutex.synchronize { @value } + end + + def increment_and_get + @mutex.synchronize do + prev_value = @value + @value += 1 + prev_value + end + end + end + + module ShadowingOuter + module M + SHADOW = 123 + end + + module N + SHADOW = 456 + end + end + + module UnboundMethodTest + def foo + 'bar' + end + end + + module ClassEvalTest + def self.get_constant_from_scope + module_eval("Lookup") + end + + def self.get_constant_from_scope_with_send(method) + send(method, "Lookup") + end + end + + class RecordIncludedModules + def self.inherited(base) + ScratchPad.record base + end + end + + module SingletonOnModuleCase + module Foo + class << Foo + def included(base) + base.included_called + super + end + end + end + + class Bar + @included_called = false + + class << self + def included_called + @included_called = true + end + + def included_called? + @included_called + end + end + end + end + + module CaseCompareOnSingleton + def self.===(*) + raise 'method contents are irrelevant to test' + end + end + + m = Module.new do + def foo + end + private :foo + end + EmptyFooMethod = m.instance_method(:foo) + + # for undefined_instance_methods spec + module UndefinedInstanceMethods + module Super + def super_included_method; end + end + + class Parent + def undefed_method; end + undef_method :undefed_method + + def parent_method; end + def another_parent_method; end + end + + class Child < Parent + include Super + + undef_method :parent_method + undef_method :another_parent_method + end + + class Grandchild < Child + undef_method :super_included_method + end + end +end + +class Object + def module_specs_public_method_on_object; end + + def module_specs_private_method_on_object; end + private :module_specs_private_method_on_object + + def module_specs_protected_method_on_object; end + protected :module_specs_private_method_on_object + + def module_specs_private_method_on_object_for_kernel_public; end + private :module_specs_private_method_on_object_for_kernel_public + + def module_specs_public_method_on_object_for_kernel_protected; end + def module_specs_public_method_on_object_for_kernel_private; end +end + +module Kernel + def module_specs_public_method_on_kernel; end + + alias_method :module_specs_alias_on_kernel, :module_specs_public_method_on_object + + public :module_specs_private_method_on_object_for_kernel_public + protected :module_specs_public_method_on_object_for_kernel_protected + private :module_specs_public_method_on_object_for_kernel_private +end + +ModuleSpecs::Nesting[:root_level] = Module.nesting diff --git a/spec/ruby/core/module/fixtures/const_added.rb b/spec/ruby/core/module/fixtures/const_added.rb new file mode 100644 index 0000000000..0f5baad65d --- /dev/null +++ b/spec/ruby/core/module/fixtures/const_added.rb @@ -0,0 +1,4 @@ +module ModuleSpecs + module ConstAddedSpecs + end +end diff --git a/spec/ruby/core/module/fixtures/constant_unicode.rb b/spec/ruby/core/module/fixtures/constant_unicode.rb new file mode 100644 index 0000000000..415911576d --- /dev/null +++ b/spec/ruby/core/module/fixtures/constant_unicode.rb @@ -0,0 +1,5 @@ +# encoding: utf-8 + +module ConstantUnicodeSpecs + CS_CONSTλ = :const_unicode +end diff --git a/spec/ruby/core/module/fixtures/constants_autoload.rb b/spec/ruby/core/module/fixtures/constants_autoload.rb new file mode 100644 index 0000000000..8e9aa8de0c --- /dev/null +++ b/spec/ruby/core/module/fixtures/constants_autoload.rb @@ -0,0 +1,6 @@ +autoload :CSAutoloadA, fixture(__FILE__, 'constants_autoload_a.rb') +autoload :CSAutoloadB, fixture(__FILE__, 'constants_autoload_b.rb') +autoload :CSAutoloadC, fixture(__FILE__, 'constants_autoload_c.rb') +module CSAutoloadD + autoload :InnerModule, fixture(__FILE__, 'constants_autoload_d.rb') +end diff --git a/spec/ruby/core/module/fixtures/constants_autoload_a.rb b/spec/ruby/core/module/fixtures/constants_autoload_a.rb new file mode 100644 index 0000000000..48d3b63681 --- /dev/null +++ b/spec/ruby/core/module/fixtures/constants_autoload_a.rb @@ -0,0 +1,2 @@ +module CSAutoloadA +end diff --git a/spec/ruby/core/module/fixtures/constants_autoload_b.rb b/spec/ruby/core/module/fixtures/constants_autoload_b.rb new file mode 100644 index 0000000000..29cd742d03 --- /dev/null +++ b/spec/ruby/core/module/fixtures/constants_autoload_b.rb @@ -0,0 +1,2 @@ +module CSAutoloadB +end diff --git a/spec/ruby/core/module/fixtures/constants_autoload_c.rb b/spec/ruby/core/module/fixtures/constants_autoload_c.rb new file mode 100644 index 0000000000..9d6a6bf4d7 --- /dev/null +++ b/spec/ruby/core/module/fixtures/constants_autoload_c.rb @@ -0,0 +1,3 @@ +module CSAutoloadC + CONST = 7 +end diff --git a/spec/ruby/core/module/fixtures/constants_autoload_d.rb b/spec/ruby/core/module/fixtures/constants_autoload_d.rb new file mode 100644 index 0000000000..52d550bab0 --- /dev/null +++ b/spec/ruby/core/module/fixtures/constants_autoload_d.rb @@ -0,0 +1,4 @@ +module CSAutoloadD + module InnerModule + end +end diff --git a/spec/ruby/core/module/fixtures/module.rb b/spec/ruby/core/module/fixtures/module.rb new file mode 100644 index 0000000000..34543ca2b4 --- /dev/null +++ b/spec/ruby/core/module/fixtures/module.rb @@ -0,0 +1,8 @@ +module ModuleSpecs + module Anonymous + module Child + end + + SameChild = Child + end +end diff --git a/spec/ruby/core/module/fixtures/multi/foo.rb b/spec/ruby/core/module/fixtures/multi/foo.rb new file mode 100644 index 0000000000..549996f08f --- /dev/null +++ b/spec/ruby/core/module/fixtures/multi/foo.rb @@ -0,0 +1,6 @@ +module ModuleSpecs::Autoload + module Foo + autoload :Bar, 'foo/bar_baz' + autoload :Baz, 'foo/bar_baz' + end +end diff --git a/spec/ruby/core/module/fixtures/multi/foo/bar_baz.rb b/spec/ruby/core/module/fixtures/multi/foo/bar_baz.rb new file mode 100644 index 0000000000..53d3849e1f --- /dev/null +++ b/spec/ruby/core/module/fixtures/multi/foo/bar_baz.rb @@ -0,0 +1,11 @@ +require 'foo' + +module ModuleSpecs::Autoload + module Foo + class Bar + end + + class Baz + end + end +end diff --git a/spec/ruby/core/module/fixtures/name.rb b/spec/ruby/core/module/fixtures/name.rb new file mode 100644 index 0000000000..25c74d3944 --- /dev/null +++ b/spec/ruby/core/module/fixtures/name.rb @@ -0,0 +1,13 @@ +# -*- encoding: utf-8 -*- +module ModuleSpecs + class NameEncoding + class Cß + end + def name + Cß.name + end + end + + module NameSpecs + end +end diff --git a/spec/ruby/core/module/fixtures/path1/load_path.rb b/spec/ruby/core/module/fixtures/path1/load_path.rb new file mode 100644 index 0000000000..d4c45463dc --- /dev/null +++ b/spec/ruby/core/module/fixtures/path1/load_path.rb @@ -0,0 +1,9 @@ +$LOAD_PATH.unshift(File.expand_path('../../path2', __FILE__)) + +module ModuleSpecs::Autoload + module LoadPath + def self.loaded + :autoload_load_path + end + end +end diff --git a/spec/ruby/core/module/fixtures/path2/load_path.rb b/spec/ruby/core/module/fixtures/path2/load_path.rb new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/spec/ruby/core/module/fixtures/path2/load_path.rb @@ -0,0 +1 @@ + diff --git a/spec/ruby/core/module/fixtures/refine.rb b/spec/ruby/core/module/fixtures/refine.rb new file mode 100644 index 0000000000..e8215aa640 --- /dev/null +++ b/spec/ruby/core/module/fixtures/refine.rb @@ -0,0 +1,25 @@ +module ModuleSpecs + class ClassWithFoo + def foo; "foo" end + end + + class ClassWithSuperFoo + def foo; [:C] end + end + + module PrependedModule + def foo; "foo from prepended module"; end + end + + module IncludedModule + def foo; "foo from included module"; end + end + + def self.build_refined_class(for_super: false) + if for_super + Class.new(ClassWithSuperFoo) + else + Class.new(ClassWithFoo) + end + end +end diff --git a/spec/ruby/core/module/fixtures/repeated_concurrent_autoload.rb b/spec/ruby/core/module/fixtures/repeated_concurrent_autoload.rb new file mode 100644 index 0000000000..32b770e6cf --- /dev/null +++ b/spec/ruby/core/module/fixtures/repeated_concurrent_autoload.rb @@ -0,0 +1,8 @@ +prev_value = ScratchPad.recorded.increment_and_get +eval <<-RUBY_EVAL + module Mod#{prev_value} + sleep(0.05) + def self.foo + end + end +RUBY_EVAL diff --git a/spec/ruby/core/module/fixtures/set_temporary_name.rb b/spec/ruby/core/module/fixtures/set_temporary_name.rb new file mode 100644 index 0000000000..901b3b94d1 --- /dev/null +++ b/spec/ruby/core/module/fixtures/set_temporary_name.rb @@ -0,0 +1,4 @@ +module ModuleSpecs + module SetTemporaryNameSpec + end +end diff --git a/spec/ruby/core/module/freeze_spec.rb b/spec/ruby/core/module/freeze_spec.rb new file mode 100644 index 0000000000..fd76141431 --- /dev/null +++ b/spec/ruby/core/module/freeze_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Module#freeze" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/module/gt_spec.rb b/spec/ruby/core/module/gt_spec.rb new file mode 100644 index 0000000000..b8a73c9ff9 --- /dev/null +++ b/spec/ruby/core/module/gt_spec.rb @@ -0,0 +1,36 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Module#>" do + it "returns false if self is a subclass of or includes the given module" do + (ModuleSpecs::Child > ModuleSpecs::Parent).should be_false + (ModuleSpecs::Child > ModuleSpecs::Basic).should be_false + (ModuleSpecs::Child > ModuleSpecs::Super).should be_false + (ModuleSpecs::Super > ModuleSpecs::Basic).should be_false + end + + it "returns true if self is a superclass of or included by the given module" do + (ModuleSpecs::Parent > ModuleSpecs::Child).should == true + (ModuleSpecs::Basic > ModuleSpecs::Child).should == true + (ModuleSpecs::Super > ModuleSpecs::Child).should == true + (ModuleSpecs::Basic > ModuleSpecs::Super).should == true + end + + it "returns false if self is the same as the given module" do + (ModuleSpecs::Child > ModuleSpecs::Child).should == false + (ModuleSpecs::Parent > ModuleSpecs::Parent).should == false + (ModuleSpecs::Basic > ModuleSpecs::Basic).should == false + (ModuleSpecs::Super > ModuleSpecs::Super).should == false + end + + it "returns nil if self is not related to the given module" do + (ModuleSpecs::Parent > ModuleSpecs::Basic).should == nil + (ModuleSpecs::Parent > ModuleSpecs::Super).should == nil + (ModuleSpecs::Basic > ModuleSpecs::Parent).should == nil + (ModuleSpecs::Super > ModuleSpecs::Parent).should == nil + end + + it "raises a TypeError if the argument is not a class/module" do + -> { ModuleSpecs::Parent > mock('x') }.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/module/gte_spec.rb b/spec/ruby/core/module/gte_spec.rb new file mode 100644 index 0000000000..18c60ba586 --- /dev/null +++ b/spec/ruby/core/module/gte_spec.rb @@ -0,0 +1,33 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Module#>=" do + it "returns true if self is a superclass of, the same as or included by given module" do + (ModuleSpecs::Parent >= ModuleSpecs::Child).should == true + (ModuleSpecs::Basic >= ModuleSpecs::Child).should == true + (ModuleSpecs::Super >= ModuleSpecs::Child).should == true + (ModuleSpecs::Basic >= ModuleSpecs::Super).should == true + (ModuleSpecs::Child >= ModuleSpecs::Child).should == true + (ModuleSpecs::Parent >= ModuleSpecs::Parent).should == true + (ModuleSpecs::Basic >= ModuleSpecs::Basic).should == true + (ModuleSpecs::Super >= ModuleSpecs::Super).should == true + end + + it "returns nil if self is not related to the given module" do + (ModuleSpecs::Parent >= ModuleSpecs::Basic).should == nil + (ModuleSpecs::Parent >= ModuleSpecs::Super).should == nil + (ModuleSpecs::Basic >= ModuleSpecs::Parent).should == nil + (ModuleSpecs::Super >= ModuleSpecs::Parent).should == nil + end + + it "returns false if self is a subclass of or includes the given module" do + (ModuleSpecs::Child >= ModuleSpecs::Parent).should == false + (ModuleSpecs::Child >= ModuleSpecs::Basic).should == false + (ModuleSpecs::Child >= ModuleSpecs::Super).should == false + (ModuleSpecs::Super >= ModuleSpecs::Basic).should == false + end + + it "raises a TypeError if the argument is not a class/module" do + -> { ModuleSpecs::Parent >= mock('x') }.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/module/include_spec.rb b/spec/ruby/core/module/include_spec.rb new file mode 100644 index 0000000000..210918b2e7 --- /dev/null +++ b/spec/ruby/core/module/include_spec.rb @@ -0,0 +1,628 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Module#include" do + it "is a public method" do + Module.should have_public_instance_method(:include, false) + end + + it "calls #append_features(self) in reversed order on each module" do + $appended_modules = [] + + m = Module.new do + def self.append_features(mod) + $appended_modules << [ self, mod ] + end + end + + m2 = Module.new do + def self.append_features(mod) + $appended_modules << [ self, mod ] + end + end + + m3 = Module.new do + def self.append_features(mod) + $appended_modules << [ self, mod ] + end + end + + c = Class.new { include(m, m2, m3) } + + $appended_modules.should == [ [ m3, c], [ m2, c ], [ m, c ] ] + end + + it "adds all ancestor modules when a previously included module is included again" do + ModuleSpecs::MultipleIncludes.ancestors.should include(ModuleSpecs::MA, ModuleSpecs::MB) + ModuleSpecs::MB.include(ModuleSpecs::MC) + ModuleSpecs::MultipleIncludes.include(ModuleSpecs::MB) + ModuleSpecs::MultipleIncludes.ancestors.should include(ModuleSpecs::MA, ModuleSpecs::MB, ModuleSpecs::MC) + end + + it "raises a TypeError when the argument is not a Module" do + -> { ModuleSpecs::Basic.include(Class.new) }.should raise_error(TypeError) + end + + it "does not raise a TypeError when the argument is an instance of a subclass of Module" do + class ModuleSpecs::SubclassSpec::AClass + end + -> { ModuleSpecs::SubclassSpec::AClass.include(ModuleSpecs::Subclass.new) }.should_not raise_error(TypeError) + ensure + ModuleSpecs::SubclassSpec.send(:remove_const, :AClass) + end + + it "raises a TypeError when the argument is a refinement" do + refinement = nil + + Module.new do + refine String do + refinement = self + end + end + + -> { ModuleSpecs::Basic.include(refinement) }.should raise_error(TypeError, "Cannot include refinement") + end + + it "imports constants to modules and classes" do + ModuleSpecs::A.constants.should include(:CONSTANT_A) + ModuleSpecs::B.constants.should include(:CONSTANT_A, :CONSTANT_B) + ModuleSpecs::C.constants.should include(:CONSTANT_A, :CONSTANT_B) + end + + it "shadows constants from ancestors" do + klass = Class.new + klass.include ModuleSpecs::ShadowingOuter::M + klass::SHADOW.should == 123 + klass.include ModuleSpecs::ShadowingOuter::N + klass::SHADOW.should == 456 + end + + it "does not override existing constants in modules and classes" do + ModuleSpecs::A::OVERRIDE.should == :a + ModuleSpecs::B::OVERRIDE.should == :b + ModuleSpecs::C::OVERRIDE.should == :c + end + + it "imports instance methods to modules and classes" do + ModuleSpecs::A.instance_methods.should include(:ma) + ModuleSpecs::B.instance_methods.should include(:ma,:mb) + ModuleSpecs::C.instance_methods.should include(:ma,:mb) + end + + it "does not import methods to modules and classes" do + ModuleSpecs::A.methods.include?(:cma).should == true + ModuleSpecs::B.methods.include?(:cma).should == false + ModuleSpecs::B.methods.include?(:cmb).should == true + ModuleSpecs::C.methods.include?(:cma).should == false + ModuleSpecs::C.methods.include?(:cmb).should == false + end + + it "attaches the module as the caller's immediate ancestor" do + module IncludeSpecsTop + def value; 5; end + end + + module IncludeSpecsMiddle + include IncludeSpecsTop + def value; 6; end + end + + class IncludeSpecsClass + include IncludeSpecsMiddle + end + + IncludeSpecsClass.new.value.should == 6 + end + + it "doesn't include module if it is included in a super class" do + module ModuleSpecs::M1 + module M; end + class A; include M; end + class B < A; include M; end + + all = [A, B, M] + + (B.ancestors.filter { |a| all.include?(a) }).should == [B, A, M] + end + end + + it "recursively includes new mixins" do + module ModuleSpecs::M1 + module U; end + module V; end + module W; end + module X; end + module Y; end + class A; include X; end; + class B < A; include U, V, W; end; + + # update V + module V; include X, U, Y; end + + # This code used to use Array#& and then compare 2 arrays, but + # the ordering from Array#& is undefined, as it uses Hash internally. + # + # Loop is more verbose, but more explicit in what we're testing. + + anc = B.ancestors + [B, U, V, W, A, X].each do |i| + anc.include?(i).should be_true + end + + class B; include V; end + + # the only new module is Y, it is added after U since it follows U in V mixin list: + anc = B.ancestors + [B, U, Y, V, W, A, X].each do |i| + anc.include?(i).should be_true + end + end + end + + it "preserves ancestor order" do + module ModuleSpecs::M2 + module M1; end + module M2; end + module M3; include M2; end + + module M2; include M1; end + module M3; include M2; end + + M3.ancestors.should == [M3, M2, M1] + + end + end + + it "detects cyclic includes" do + -> { + module ModuleSpecs::M + include ModuleSpecs::M + end + }.should raise_error(ArgumentError) + end + + it "doesn't accept no-arguments" do + -> { + Module.new do + include + end + }.should raise_error(ArgumentError) + end + + it "returns the class it's included into" do + m = Module.new + r = nil + c = Class.new { r = include m } + r.should == c + end + + it "ignores modules it has already included via module mutual inclusion" do + module ModuleSpecs::AlreadyInc + module M0 + end + + module M + include M0 + end + + class K + include M + include M + end + + K.ancestors[0].should == K + K.ancestors[1].should == M + K.ancestors[2].should == M0 + end + end + + it "clears any caches" do + module ModuleSpecs::M3 + module M1 + def foo + :m1 + end + end + + module M2 + def foo + :m2 + end + end + + class C + include M1 + + def get + foo + end + end + + c = C.new + c.get.should == :m1 + + class C + include M2 + end + + c.get.should == :m2 + + remove_const :C + end + end + + it "updates the method when an included module is updated" do + a_class = Class.new do + def foo + 'a' + end + end + + m_module = Module.new + + b_class = Class.new(a_class) do + include m_module + end + + b = b_class.new + + foo = -> { b.foo } + + foo.call.should == 'a' + + m_module.module_eval do + def foo + 'm' + end + end + + foo.call.should == 'm' + end + + + it "updates the method when a module included after a call is later updated" do + m_module = Module.new + a_class = Class.new do + def foo + 'a' + end + end + b_class = Class.new(a_class) + b = b_class.new + foo = -> { b.foo } + foo.call.should == 'a' + + b_class.include m_module + foo.call.should == 'a' + + m_module.module_eval do + def foo + "m" + end + end + foo.call.should == 'm' + end + + it "updates the method when a nested included module is updated" do + a_class = Class.new do + def foo + 'a' + end + end + + n_module = Module.new + + m_module = Module.new do + include n_module + end + + b_class = Class.new(a_class) do + include m_module + end + + b = b_class.new + + foo = -> { b.foo } + + foo.call.should == 'a' + + n_module.module_eval do + def foo + 'n' + end + end + + foo.call.should == 'n' + end + + it "updates the method when a new module is included" do + a_class = Class.new do + def foo + 'a' + end + end + + m_module = Module.new do + def foo + 'm' + end + end + + b_class = Class.new(a_class) + b = b_class.new + + foo = -> { b.foo } + + foo.call.should == 'a' + + b_class.class_eval do + include m_module + end + + foo.call.should == 'm' + end + + it "updates the method when a new module with nested module is included" do + a_class = Class.new do + def foo + 'a' + end + end + + n_module = Module.new do + def foo + 'n' + end + end + + m_module = Module.new do + include n_module + end + + b_class = Class.new(a_class) + b = b_class.new + + foo = -> { b.foo } + + foo.call.should == 'a' + + b_class.class_eval do + include m_module + end + + foo.call.should == 'n' + end + + it "updates the constant when an included module is updated" do + module ModuleSpecs::ConstUpdated + module A + FOO = 'a' + end + + module M + end + + module B + include A + include M + def self.foo + FOO + end + end + + B.foo.should == 'a' + + M.const_set(:FOO, 'm') + B.foo.should == 'm' + end + ensure + ModuleSpecs.send(:remove_const, :ConstUpdated) + end + + it "updates the constant when a module included after a call is later updated" do + module ModuleSpecs::ConstLaterUpdated + module A + FOO = 'a' + end + + module B + include A + def self.foo + FOO + end + end + + B.foo.should == 'a' + + module M + end + B.include M + + B.foo.should == 'a' + + M.const_set(:FOO, 'm') + B.foo.should == 'm' + end + ensure + ModuleSpecs.send(:remove_const, :ConstLaterUpdated) + end + + it "updates the constant when a module included in another module after a call is later updated" do + module ModuleSpecs::ConstModuleLaterUpdated + module A + FOO = 'a' + end + + module B + include A + def self.foo + FOO + end + end + + B.foo.should == 'a' + + module M + end + B.include M + + B.foo.should == 'a' + + M.const_set(:FOO, 'm') + B.foo.should == 'm' + end + ensure + ModuleSpecs.send(:remove_const, :ConstModuleLaterUpdated) + end + + it "updates the constant when a nested included module is updated" do + module ModuleSpecs::ConstUpdatedNestedIncludeUpdated + module A + FOO = 'a' + end + + module N + end + + module M + include N + end + + module B + include A + include M + def self.foo + FOO + end + end + + B.foo.should == 'a' + + N.const_set(:FOO, 'n') + B.foo.should == 'n' + end + ensure + ModuleSpecs.send(:remove_const, :ConstUpdatedNestedIncludeUpdated) + end + + it "updates the constant when a new module is included" do + module ModuleSpecs::ConstUpdatedNewInclude + module A + FOO = 'a' + end + + module M + FOO = 'm' + end + + module B + include A + def self.foo + FOO + end + end + + B.foo.should == 'a' + + B.include(M) + B.foo.should == 'm' + end + ensure + ModuleSpecs.send(:remove_const, :ConstUpdatedNewInclude) + end + + it "updates the constant when a new module with nested module is included" do + module ModuleSpecs::ConstUpdatedNestedIncluded + module A + FOO = 'a' + end + + module N + FOO = 'n' + end + + module M + include N + end + + module B + include A + def self.foo + FOO + end + end + + B.foo.should == 'a' + + B.include M + B.foo.should == 'n' + end + ensure + ModuleSpecs.send(:remove_const, :ConstUpdatedNestedIncluded) + end + + it "overrides a previous super method call" do + c1 = Class.new do + def foo + [:c1] + end + end + c2 = Class.new(c1) do + def foo + [:c2] + super + end + end + c2.new.foo.should == [:c2, :c1] + m = Module.new do + def foo + [:m1] + end + end + c2.include(m) + c2.new.foo.should == [:c2, :m1] + end + + it "update a module when a nested module is updated and includes a module on its own" do + m1 = Module.new + m2 = Module.new do + def m2; [:m2]; end + end + m3 = Module.new do + def m3; [:m3]; end + end + m4 = Module.new do + def m4; [:m4]; end + end + c = Class.new + + c.include(m1) + m1.include(m2) + m2.include(m3) + m3.include(m4) + + c.new.m2.should == [:m2] + c.new.m3.should == [:m3] + c.new.m4.should == [:m4] + end +end + +describe "Module#include?" do + it "returns true if the given module is included by self or one of it's ancestors" do + ModuleSpecs::Super.include?(ModuleSpecs::Basic).should == true + ModuleSpecs::Child.include?(ModuleSpecs::Basic).should == true + ModuleSpecs::Child.include?(ModuleSpecs::Super).should == true + ModuleSpecs::Child.include?(Kernel).should == true + + ModuleSpecs::Parent.include?(ModuleSpecs::Basic).should == false + ModuleSpecs::Basic.include?(ModuleSpecs::Super).should == false + end + + it "returns false if given module is equal to self" do + ModuleSpecs.include?(ModuleSpecs).should == false + end + + it "raises a TypeError when no module was given" do + -> { ModuleSpecs::Child.include?("Test") }.should raise_error(TypeError) + -> { ModuleSpecs::Child.include?(ModuleSpecs::Parent) }.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/module/included_modules_spec.rb b/spec/ruby/core/module/included_modules_spec.rb new file mode 100644 index 0000000000..ce94ed1285 --- /dev/null +++ b/spec/ruby/core/module/included_modules_spec.rb @@ -0,0 +1,14 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Module#included_modules" do + it "returns a list of modules included in self" do + ModuleSpecs.included_modules.should == [] + + ModuleSpecs::Child.included_modules.should include(ModuleSpecs::Super, ModuleSpecs::Basic, Kernel) + ModuleSpecs::Parent.included_modules.should include(Kernel) + ModuleSpecs::Basic.included_modules.should == [] + ModuleSpecs::Super.included_modules.should include(ModuleSpecs::Basic) + ModuleSpecs::WithPrependedModule.included_modules.should include(ModuleSpecs::ModuleWithPrepend) + end +end diff --git a/spec/ruby/core/module/included_spec.rb b/spec/ruby/core/module/included_spec.rb new file mode 100644 index 0000000000..2fdd4325f2 --- /dev/null +++ b/spec/ruby/core/module/included_spec.rb @@ -0,0 +1,44 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Module#included" do + it "is invoked when self is included in another module or class" do + begin + m = Module.new do + def self.included(o) + $included_by = o + end + end + + c = Class.new { include m } + + $included_by.should == c + ensure + $included_by = nil + end + end + + it "allows extending self with the object into which it is being included" do + m = Module.new do + def self.included(o) + o.extend(self) + end + + def test + :passed + end + end + + c = Class.new{ include(m) } + c.test.should == :passed + end + + it "is private in its default implementation" do + Module.should have_private_instance_method(:included) + end + + it "works with super using a singleton class" do + ModuleSpecs::SingletonOnModuleCase::Bar.include ModuleSpecs::SingletonOnModuleCase::Foo + ModuleSpecs::SingletonOnModuleCase::Bar.should.included_called? + end +end diff --git a/spec/ruby/core/module/initialize_copy_spec.rb b/spec/ruby/core/module/initialize_copy_spec.rb new file mode 100644 index 0000000000..7ae48f85a9 --- /dev/null +++ b/spec/ruby/core/module/initialize_copy_spec.rb @@ -0,0 +1,18 @@ +require_relative '../../spec_helper' + +describe "Module#initialize_copy" do + it "should retain singleton methods when duped" do + mod = Module.new + def mod.hello + end + mod.dup.methods(false).should == [:hello] + end + + # jruby/jruby#5245, https://bugs.ruby-lang.org/issues/3461 + it "should produce a duped module with inspectable class methods" do + mod = Module.new + def mod.hello + end + mod.dup.method(:hello).inspect.should =~ /Module.*hello/ + end +end diff --git a/spec/ruby/core/module/initialize_spec.rb b/spec/ruby/core/module/initialize_spec.rb new file mode 100644 index 0000000000..99e41e4619 --- /dev/null +++ b/spec/ruby/core/module/initialize_spec.rb @@ -0,0 +1,18 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Module#initialize" do + it "accepts a block" do + m = Module.new do + const_set :A, "A" + end + m.const_get("A").should == "A" + end + + it "is called on subclasses" do + m = ModuleSpecs::SubModule.new + m.special.should == 10 + m.methods.should_not == nil + m.constants.should_not == nil + end +end diff --git a/spec/ruby/core/module/instance_method_spec.rb b/spec/ruby/core/module/instance_method_spec.rb new file mode 100644 index 0000000000..182cdf5c54 --- /dev/null +++ b/spec/ruby/core/module/instance_method_spec.rb @@ -0,0 +1,106 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Module#instance_method" do + before :all do + @parent_um = ModuleSpecs::InstanceMeth.instance_method(:foo) + @child_um = ModuleSpecs::InstanceMethChild.instance_method(:foo) + @mod_um = ModuleSpecs::InstanceMethChild.instance_method(:bar) + end + + it "is a public method" do + Module.should have_public_instance_method(:instance_method, false) + end + + it "requires an argument" do + Module.new.method(:instance_method).arity.should == 1 + end + + it "returns an UnboundMethod corresponding to the given name" do + @parent_um.should be_kind_of(UnboundMethod) + @parent_um.bind(ModuleSpecs::InstanceMeth.new).call.should == :foo + end + + it "returns an UnboundMethod corresponding to the given name from a superclass" do + @child_um.should be_kind_of(UnboundMethod) + @child_um.bind(ModuleSpecs::InstanceMethChild.new).call.should == :foo + end + + it "returns an UnboundMethod corresponding to the given name from an included Module" do + @mod_um.should be_kind_of(UnboundMethod) + @mod_um.bind(ModuleSpecs::InstanceMethChild.new).call.should == :bar + end + + it "returns an UnboundMethod when given a protected method name" do + ModuleSpecs::Basic.instance_method(:protected_module).should be_an_instance_of(UnboundMethod) + end + + it "returns an UnboundMethod when given a private method name" do + ModuleSpecs::Basic.instance_method(:private_module).should be_an_instance_of(UnboundMethod) + end + + it "gives UnboundMethod method name, Module defined in and Module extracted from" do + @parent_um.inspect.should =~ /\bfoo\b/ + @parent_um.inspect.should =~ /\bModuleSpecs::InstanceMeth\b/ + @parent_um.inspect.should =~ /\bModuleSpecs::InstanceMeth\b/ + @child_um.inspect.should =~ /\bfoo\b/ + @child_um.inspect.should =~ /\bModuleSpecs::InstanceMeth\b/ + + @mod_um.inspect.should =~ /\bbar\b/ + @mod_um.inspect.should =~ /\bModuleSpecs::InstanceMethMod\b/ + end + + it "raises a TypeError if the given name is not a String/Symbol" do + -> { Object.instance_method([]) }.should raise_error(TypeError, /is not a symbol nor a string/) + -> { Object.instance_method(0) }.should raise_error(TypeError, /is not a symbol nor a string/) + -> { Object.instance_method(nil) }.should raise_error(TypeError, /is not a symbol nor a string/) + -> { Object.instance_method(mock('x')) }.should raise_error(TypeError, /is not a symbol nor a string/) + end + + it "accepts String name argument" do + method = ModuleSpecs::InstanceMeth.instance_method(:foo) + method.should be_kind_of(UnboundMethod) + end + + it "accepts Symbol name argument" do + method = ModuleSpecs::InstanceMeth.instance_method("foo") + method.should be_kind_of(UnboundMethod) + end + + it "converts non-String name by calling #to_str method" do + obj = Object.new + def obj.to_str() "foo" end + + method = ModuleSpecs::InstanceMeth.instance_method(obj) + method.should be_kind_of(UnboundMethod) + end + + it "raises TypeError when passed non-String name and #to_str returns non-String value" do + obj = Object.new + def obj.to_str() [] end + + -> { ModuleSpecs::InstanceMeth.instance_method(obj) }.should raise_error(TypeError, /can't convert Object to String/) + end + + it "raises a NameError if the method has been undefined" do + child = Class.new(ModuleSpecs::InstanceMeth) + child.send :undef_method, :foo + um = ModuleSpecs::InstanceMeth.instance_method(:foo) + um.should == @parent_um + -> do + child.instance_method(:foo) + end.should raise_error(NameError) + end + + it "raises a NameError if the method does not exist" do + -> { Object.instance_method(:missing) }.should raise_error(NameError) + end + + it "sets the NameError#name attribute to the name of the missing method" do + begin + Object.instance_method(:missing) + rescue NameError => e + e.name.should == :missing + end + end +end diff --git a/spec/ruby/core/module/instance_methods_spec.rb b/spec/ruby/core/module/instance_methods_spec.rb new file mode 100644 index 0000000000..d2d38cdda2 --- /dev/null +++ b/spec/ruby/core/module/instance_methods_spec.rb @@ -0,0 +1,61 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Module#instance_methods" do + it "does not return methods undefined in a superclass" do + methods = ModuleSpecs::Parent.instance_methods(false) + methods.should_not include(:undefed_method) + end + + it "only includes module methods on an included module" do + methods = ModuleSpecs::Basic.instance_methods(false) + methods.should include(:public_module) + # Child is an including class + methods = ModuleSpecs::Child.instance_methods(false) + methods.should include(:public_child) + methods.should_not include(:public_module) + end + + it "does not return methods undefined in a subclass" do + methods = ModuleSpecs::Grandchild.instance_methods + methods.should_not include(:parent_method, :another_parent_method) + end + + it "does not return methods undefined in the current class" do + class ModuleSpecs::Child + def undefed_child + end + end + ModuleSpecs::Child.send(:undef_method, :undefed_child) + methods = ModuleSpecs::Child.instance_methods + methods.should_not include(:undefed_method, :undefed_child) + end + + it "does not return methods from an included module that are undefined in the class" do + ModuleSpecs::Grandchild.instance_methods.should_not include(:super_included_method) + end + + it "returns the public and protected methods of self if include_super is false" do + methods = ModuleSpecs::Parent.instance_methods(false) + methods.should include(:protected_parent, :public_parent) + + methods = ModuleSpecs::Child.instance_methods(false) + methods.should include(:protected_child, :public_child) + end + + it "returns the public and protected methods of self and it's ancestors" do + methods = ModuleSpecs::Basic.instance_methods + methods.should include(:protected_module, :public_module) + + methods = ModuleSpecs::Super.instance_methods + methods.should include(:protected_module, :protected_super_module, + :public_module, :public_super_module) + end + + it "makes a private Object instance method public in Kernel" do + methods = Kernel.instance_methods + methods.should include(:module_specs_private_method_on_object_for_kernel_public) + methods = Object.instance_methods + methods.should_not include(:module_specs_private_method_on_object_for_kernel_public) + end +end diff --git a/spec/ruby/core/module/lt_spec.rb b/spec/ruby/core/module/lt_spec.rb new file mode 100644 index 0000000000..d7771e07a8 --- /dev/null +++ b/spec/ruby/core/module/lt_spec.rb @@ -0,0 +1,36 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Module#<" do + it "returns true if self is a subclass of or includes the given module" do + (ModuleSpecs::Child < ModuleSpecs::Parent).should == true + (ModuleSpecs::Child < ModuleSpecs::Basic).should == true + (ModuleSpecs::Child < ModuleSpecs::Super).should == true + (ModuleSpecs::Super < ModuleSpecs::Basic).should == true + end + + it "returns false if self is a superclass of or included by the given module" do + (ModuleSpecs::Parent < ModuleSpecs::Child).should be_false + (ModuleSpecs::Basic < ModuleSpecs::Child).should be_false + (ModuleSpecs::Super < ModuleSpecs::Child).should be_false + (ModuleSpecs::Basic < ModuleSpecs::Super).should be_false + end + + it "returns false if self is the same as the given module" do + (ModuleSpecs::Child < ModuleSpecs::Child).should == false + (ModuleSpecs::Parent < ModuleSpecs::Parent).should == false + (ModuleSpecs::Basic < ModuleSpecs::Basic).should == false + (ModuleSpecs::Super < ModuleSpecs::Super).should == false + end + + it "returns nil if self is not related to the given module" do + (ModuleSpecs::Parent < ModuleSpecs::Basic).should == nil + (ModuleSpecs::Parent < ModuleSpecs::Super).should == nil + (ModuleSpecs::Basic < ModuleSpecs::Parent).should == nil + (ModuleSpecs::Super < ModuleSpecs::Parent).should == nil + end + + it "raises a TypeError if the argument is not a class/module" do + -> { ModuleSpecs::Parent < mock('x') }.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/module/lte_spec.rb b/spec/ruby/core/module/lte_spec.rb new file mode 100644 index 0000000000..7a0e8496b8 --- /dev/null +++ b/spec/ruby/core/module/lte_spec.rb @@ -0,0 +1,33 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Module#<=" do + it "returns true if self is a subclass of, the same as or includes the given module" do + (ModuleSpecs::Child <= ModuleSpecs::Parent).should == true + (ModuleSpecs::Child <= ModuleSpecs::Basic).should == true + (ModuleSpecs::Child <= ModuleSpecs::Super).should == true + (ModuleSpecs::Super <= ModuleSpecs::Basic).should == true + (ModuleSpecs::Child <= ModuleSpecs::Child).should == true + (ModuleSpecs::Parent <= ModuleSpecs::Parent).should == true + (ModuleSpecs::Basic <= ModuleSpecs::Basic).should == true + (ModuleSpecs::Super <= ModuleSpecs::Super).should == true + end + + it "returns nil if self is not related to the given module" do + (ModuleSpecs::Parent <= ModuleSpecs::Basic).should == nil + (ModuleSpecs::Parent <= ModuleSpecs::Super).should == nil + (ModuleSpecs::Basic <= ModuleSpecs::Parent).should == nil + (ModuleSpecs::Super <= ModuleSpecs::Parent).should == nil + end + + it "returns false if self is a superclass of or is included by the given module" do + (ModuleSpecs::Parent <= ModuleSpecs::Child).should == false + (ModuleSpecs::Basic <= ModuleSpecs::Child).should == false + (ModuleSpecs::Super <= ModuleSpecs::Child).should == false + (ModuleSpecs::Basic <= ModuleSpecs::Super).should == false + end + + it "raises a TypeError if the argument is not a class/module" do + -> { ModuleSpecs::Parent <= mock('x') }.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/module/method_added_spec.rb b/spec/ruby/core/module/method_added_spec.rb new file mode 100644 index 0000000000..ec92cddc1e --- /dev/null +++ b/spec/ruby/core/module/method_added_spec.rb @@ -0,0 +1,146 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Module#method_added" do + before :each do + ScratchPad.record [] + end + + it "is a private instance method" do + Module.should have_private_instance_method(:method_added) + end + + it "returns nil in the default implementation" do + Module.new do + method_added(:test).should == nil + end + end + + it "is called when a new instance method is defined in self" do + Module.new do + def self.method_added(name) + ScratchPad << name + end + + def test() end + def test2() end + def test() end + alias_method :aliased_test, :test + alias aliased_test2 test + end + + ScratchPad.recorded.should == [:test, :test2, :test, :aliased_test, :aliased_test2] + end + + it "is not called when a singleton method is added" do + # obj.singleton_method_added is called instead + klass = Class.new + def klass.method_added(name) + ScratchPad << name + end + + obj = klass.new + def obj.new_singleton_method + end + + ScratchPad.recorded.should == [] + end + + it "is not called when a method is undefined in self" do + m = Module.new do + def method_to_undef + end + + def self.method_added(name) + fail("method_added called by undef_method") + end + + undef_method :method_to_undef + end + m.should_not have_method(:method_to_undef) + end + + it "is not called when a method changes visibility" do + Module.new do + def public_method + end + + def private_method + end + + def self.method_added(name) + ScratchPad << name + end + + public :public_method + private :public_method + + private :private_method + public :private_method + end + + ScratchPad.recorded.should == [] + end + + it "is called when using #private in a subclass" do + parent = Class.new do + def foo + end + end + + Class.new(parent) do + def self.method_added(name) + ScratchPad << name + end + + # Create an instance as that might initialize some method lookup caches, which is interesting to test + self.new.foo + + private :foo + public :foo + end + + ScratchPad.recorded.should == [:foo] + end + + it "is not called when a method is copied via module_function, rather #singleton_method_added is called" do + Module.new do + def mod_function + end + + def self.method_added(name) + ScratchPad << [:method_added, name] + end + + def self.singleton_method_added(name) + ScratchPad << [:singleton_method_added, name] + end + + ScratchPad.record [] + + module_function :mod_function + end + + ScratchPad.recorded.should == [[:singleton_method_added, :mod_function]] + end + + it "is called with a precise caller location with the line of the 'def'" do + line = nil + + Module.new do + def self.method_added(name) + location = caller_locations(1, 1)[0] + ScratchPad << location.lineno + end + + line = __LINE__ + def first + end + + def second + end + end + + ScratchPad.recorded.should == [line + 1, line + 4] + end +end diff --git a/spec/ruby/core/module/method_defined_spec.rb b/spec/ruby/core/module/method_defined_spec.rb new file mode 100644 index 0000000000..bc5b420e11 --- /dev/null +++ b/spec/ruby/core/module/method_defined_spec.rb @@ -0,0 +1,98 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Module#method_defined?" do + it "returns true if a public or private method with the given name is defined in self, self's ancestors or one of self's included modules" do + # Defined in Child + ModuleSpecs::Child.method_defined?(:public_child).should == true + ModuleSpecs::Child.method_defined?("private_child").should == false + ModuleSpecs::Child.method_defined?(:accessor_method).should == true + + # Defined in Parent + ModuleSpecs::Child.method_defined?("public_parent").should == true + ModuleSpecs::Child.method_defined?(:private_parent).should == false + + # Defined in Module + ModuleSpecs::Child.method_defined?(:public_module).should == true + ModuleSpecs::Child.method_defined?(:protected_module).should == true + ModuleSpecs::Child.method_defined?(:private_module).should == false + + # Defined in SuperModule + ModuleSpecs::Child.method_defined?(:public_super_module).should == true + ModuleSpecs::Child.method_defined?(:protected_super_module).should == true + ModuleSpecs::Child.method_defined?(:private_super_module).should == false + end + + # unlike alias_method, module_function, public, and friends, + it "does not search Object or Kernel when called on a module" do + m = Module.new + + m.method_defined?(:module_specs_public_method_on_kernel).should be_false + end + + it "raises a TypeError when the given object is not a string/symbol" do + c = Class.new + o = mock('123') + + -> { c.method_defined?(o) }.should raise_error(TypeError) + + o.should_receive(:to_str).and_return(123) + -> { c.method_defined?(o) }.should raise_error(TypeError) + end + + it "converts the given name to a string using to_str" do + c = Class.new { def test(); end } + (o = mock('test')).should_receive(:to_str).and_return("test") + + c.method_defined?(o).should == true + end + + # works as method_defined?(method_name) + describe "when passed true as a second optional argument" do + it "performs a lookup in ancestors" do + ModuleSpecs::Child.method_defined?(:public_child, true).should == true + ModuleSpecs::Child.method_defined?(:protected_child, true).should == true + ModuleSpecs::Child.method_defined?(:accessor_method, true).should == true + ModuleSpecs::Child.method_defined?(:private_child, true).should == false + + # Defined in Parent + ModuleSpecs::Child.method_defined?(:public_parent, true).should == true + ModuleSpecs::Child.method_defined?(:protected_parent, true).should == true + ModuleSpecs::Child.method_defined?(:private_parent, true).should == false + + # Defined in Module + ModuleSpecs::Child.method_defined?(:public_module, true).should == true + ModuleSpecs::Child.method_defined?(:protected_module, true).should == true + ModuleSpecs::Child.method_defined?(:private_module, true).should == false + + # Defined in SuperModule + ModuleSpecs::Child.method_defined?(:public_super_module, true).should == true + ModuleSpecs::Child.method_defined?(:protected_super_module, true).should == true + ModuleSpecs::Child.method_defined?(:private_super_module, true).should == false + end + end + + describe "when passed false as a second optional argument" do + it "checks only the class itself" do + ModuleSpecs::Child.method_defined?(:public_child, false).should == true + ModuleSpecs::Child.method_defined?(:protected_child, false).should == true + ModuleSpecs::Child.method_defined?(:accessor_method, false).should == true + ModuleSpecs::Child.method_defined?(:private_child, false).should == false + + # Defined in Parent + ModuleSpecs::Child.method_defined?(:public_parent, false).should == false + ModuleSpecs::Child.method_defined?(:protected_parent, false).should == false + ModuleSpecs::Child.method_defined?(:private_parent, false).should == false + + # Defined in Module + ModuleSpecs::Child.method_defined?(:public_module, false).should == false + ModuleSpecs::Child.method_defined?(:protected_module, false).should == false + ModuleSpecs::Child.method_defined?(:private_module, false).should == false + + # Defined in SuperModule + ModuleSpecs::Child.method_defined?(:public_super_module, false).should == false + ModuleSpecs::Child.method_defined?(:protected_super_module, false).should == false + ModuleSpecs::Child.method_defined?(:private_super_module, false).should == false + end + end +end diff --git a/spec/ruby/core/module/method_removed_spec.rb b/spec/ruby/core/module/method_removed_spec.rb new file mode 100644 index 0000000000..9b39eb77a6 --- /dev/null +++ b/spec/ruby/core/module/method_removed_spec.rb @@ -0,0 +1,33 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Module#method_removed" do + it "is a private instance method" do + Module.should have_private_instance_method(:method_removed) + end + + it "returns nil in the default implementation" do + Module.new do + method_removed(:test).should == nil + end + end + + it "is called when a method is removed from self" do + begin + Module.new do + def self.method_removed(name) + $method_removed = name + end + + def test + "test" + end + remove_method :test + end + + $method_removed.should == :test + ensure + $method_removed = nil + end + end +end diff --git a/spec/ruby/core/module/method_undefined_spec.rb b/spec/ruby/core/module/method_undefined_spec.rb new file mode 100644 index 0000000000..a94388fe0a --- /dev/null +++ b/spec/ruby/core/module/method_undefined_spec.rb @@ -0,0 +1,33 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Module#method_undefined" do + it "is a private instance method" do + Module.should have_private_instance_method(:method_undefined) + end + + it "returns nil in the default implementation" do + Module.new do + method_undefined(:test).should == nil + end + end + + it "is called when a method is undefined from self" do + begin + Module.new do + def self.method_undefined(name) + $method_undefined = name + end + + def test + "test" + end + undef_method :test + end + + $method_undefined.should == :test + ensure + $method_undefined = nil + end + end +end diff --git a/spec/ruby/core/module/module_eval_spec.rb b/spec/ruby/core/module/module_eval_spec.rb new file mode 100644 index 0000000000..e9e9fda28d --- /dev/null +++ b/spec/ruby/core/module/module_eval_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/class_eval' + +describe "Module#module_eval" do + it_behaves_like :module_class_eval, :module_eval +end diff --git a/spec/ruby/core/module/module_exec_spec.rb b/spec/ruby/core/module/module_exec_spec.rb new file mode 100644 index 0000000000..47cdf7ef52 --- /dev/null +++ b/spec/ruby/core/module/module_exec_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/class_exec' + +describe "Module#module_exec" do + it_behaves_like :module_class_exec, :module_exec +end diff --git a/spec/ruby/core/module/module_function_spec.rb b/spec/ruby/core/module/module_function_spec.rb new file mode 100644 index 0000000000..51f647142e --- /dev/null +++ b/spec/ruby/core/module/module_function_spec.rb @@ -0,0 +1,358 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Module#module_function" do + it "is a private method" do + Module.should have_private_instance_method(:module_function) + end + + describe "on Class" do + it "is undefined" do + Class.should_not have_private_instance_method(:module_function, true) + end + + it "raises a TypeError if calling after rebinded to Class" do + -> { + Module.instance_method(:module_function).bind(Class.new).call + }.should raise_error(TypeError) + + -> { + Module.instance_method(:module_function).bind(Class.new).call :foo + }.should raise_error(TypeError) + end + end +end + +describe "Module#module_function with specific method names" do + it "creates duplicates of the given instance methods on the Module object" do + m = Module.new do + def test() end + def test2() end + def test3() end + + module_function :test, :test2 + end + + m.respond_to?(:test).should == true + m.respond_to?(:test2).should == true + m.respond_to?(:test3).should == false + end + + it "returns argument or arguments if given" do + Module.new do + def foo; end + module_function(:foo).should equal(:foo) + module_function(:foo, :foo).should == [:foo, :foo] + end + end + + it "creates an independent copy of the method, not a redirect" do + module Mixin + def test + "hello" + end + module_function :test + end + + class BaseClass + include Mixin + def call_test + test + end + end + + Mixin.test.should == "hello" + c = BaseClass.new + c.call_test.should == "hello" + + module Mixin + def test + "goodbye" + end + end + + Mixin.test.should == "hello" + c.call_test.should == "goodbye" + end + + it "makes the instance methods private" do + m = Module.new do + def test() "hello" end + module_function :test + end + + (o = mock('x')).extend(m) + o.respond_to?(:test).should == false + m.should have_private_instance_method(:test) + o.send(:test).should == "hello" + -> { o.test }.should raise_error(NoMethodError) + end + + it "makes the new Module methods public" do + m = Module.new do + def test() "hello" end + module_function :test + end + + m.public_methods.map {|me| me.to_s }.include?('test').should == true + end + + it "tries to convert the given names to strings using to_str" do + (o = mock('test')).should_receive(:to_str).any_number_of_times.and_return("test") + (o2 = mock('test2')).should_receive(:to_str).any_number_of_times.and_return("test2") + + m = Module.new do + def test() end + def test2() end + module_function o, o2 + end + + m.respond_to?(:test).should == true + m.respond_to?(:test2).should == true + end + + it "raises a TypeError when the given names can't be converted to string using to_str" do + o = mock('123') + + -> { Module.new { module_function(o) } }.should raise_error(TypeError) + + o.should_receive(:to_str).and_return(123) + -> { Module.new { module_function(o) } }.should raise_error(TypeError) + end + + it "can make accessible private methods" do # JRUBY-4214 + m = Module.new do + module_function :require + end + m.respond_to?(:require).should be_true + end + + it "creates Module methods that super up the singleton class of the module" do + super_m = Module.new do + def foo + "super_m" + end + end + + m = Module.new do + extend super_m + module_function + def foo + ["m", super] + end + end + + m.foo.should == ["m", "super_m"] + end + + context "methods created with define_method" do + context "passed a block" do + it "creates duplicates of the given instance methods" do + m = Module.new do + define_method :test1 do; end + module_function :test1 + end + + m.respond_to?(:test1).should == true + end + end + + context "passed a method" do + it "creates duplicates of the given instance methods" do + module_with_method = Module.new do + def test1; end + end + + c = Class.new do + extend module_with_method + end + + m = Module.new do + define_method :test2, c.method(:test1) + module_function :test2 + end + + m.respond_to?(:test2).should == true + end + end + + context "passed an unbound method" do + it "creates duplicates of the given instance methods" do + module_with_method = Module.new do + def test1; end + end + + m = Module.new do + define_method :test2, module_with_method.instance_method(:test1) + module_function :test2 + end + + m.respond_to?(:test2).should == true + end + end + end +end + +describe "Module#module_function as a toggle (no arguments) in a Module body" do + it "makes any subsequently defined methods module functions with the normal semantics" do + m = Module.new do + module_function + def test1() end + def test2() end + end + + m.respond_to?(:test1).should == true + m.respond_to?(:test2).should == true + end + + it "returns nil" do + Module.new do + module_function.should equal(nil) + end + end + + it "stops creating module functions if the body encounters another toggle " \ + "like public/protected/private without arguments" do + m = Module.new do + module_function + def test1() end + def test2() end + public + def test3() end + end + + m.respond_to?(:test1).should == true + m.respond_to?(:test2).should == true + m.respond_to?(:test3).should == false + end + + it "does not stop creating module functions if the body encounters " \ + "public/protected/private WITH arguments" do + m = Module.new do + def foo() end + module_function + def test1() end + def test2() end + public :foo + def test3() end + end + + m.respond_to?(:test1).should == true + m.respond_to?(:test2).should == true + m.respond_to?(:test3).should == true + end + + it "does not affect module_evaled method definitions also if outside the eval itself" do + m = Module.new do + module_function + module_eval { def test1() end } + module_eval " def test2() end " + end + + m.respond_to?(:test1).should == false + m.respond_to?(:test2).should == false + end + + it "has no effect if inside a module_eval if the definitions are outside of it" do + m = Module.new do + module_eval { module_function } + def test1() end + def test2() end + end + + m.respond_to?(:test1).should == false + m.respond_to?(:test2).should == false + end + + it "functions normally if both toggle and definitions inside a module_eval" do + m = Module.new do + module_eval do + module_function + def test1() end + def test2() end + end + end + + m.respond_to?(:test1).should == true + m.respond_to?(:test2).should == true + end + + it "affects eval'ed method definitions also even when outside the eval itself" do + m = Module.new do + module_function + eval "def test1() end" + end + + m.respond_to?(:test1).should == true + end + + it "doesn't affect definitions when inside an eval even if the definitions are outside of it" do + m = Module.new do + eval "module_function" + def test1() end + end + + m.respond_to?(:test1).should == false + end + + it "functions normally if both toggle and definitions inside a eval" do + m = Module.new do + eval <<-CODE + module_function + + def test1() end + def test2() end + CODE + end + + m.respond_to?(:test1).should == true + m.respond_to?(:test2).should == true + end + + context "methods are defined with define_method" do + context "passed a block" do + it "makes any subsequently defined methods module functions with the normal semantics" do + m = Module.new do + module_function + define_method :test1 do; end + end + + m.respond_to?(:test1).should == true + end + end + + context "passed a method" do + it "makes any subsequently defined methods module functions with the normal semantics" do + module_with_method = Module.new do + def test1; end + end + + c = Class.new do + extend module_with_method + end + + m = Module.new do + module_function + define_method :test2, c.method(:test1) + end + + m.respond_to?(:test2).should == true + end + end + + context "passed an unbound method" do + it "makes any subsequently defined methods module functions with the normal semantics" do + module_with_method = Module.new do + def test1; end + end + + m = Module.new do + module_function + define_method :test2, module_with_method.instance_method(:test1) + end + + m.respond_to?(:test2).should == true + end + end + end +end diff --git a/spec/ruby/core/module/name_spec.rb b/spec/ruby/core/module/name_spec.rb new file mode 100644 index 0000000000..d3318e1645 --- /dev/null +++ b/spec/ruby/core/module/name_spec.rb @@ -0,0 +1,205 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/module' + +describe "Module#name" do + it "is nil for an anonymous module" do + Module.new.name.should be_nil + end + + it "is not nil when assigned to a constant in an anonymous module" do + m = Module.new + m::N = Module.new + m::N.name.should.end_with? '::N' + end + + it "is not nil for a nested module created with the module keyword" do + m = Module.new + module m::N; end + m::N.name.should =~ /\A#<Module:0x[0-9a-f]+>::N\z/ + end + + it "returns nil for a singleton class" do + Module.new.singleton_class.name.should be_nil + String.singleton_class.name.should be_nil + Object.new.singleton_class.name.should be_nil + end + + it "changes when the module is reachable through a constant path" do + m = Module.new + module m::N; end + m::N.name.should =~ /\A#<Module:0x\h+>::N\z/ + ModuleSpecs::Anonymous::WasAnnon = m::N + m::N.name.should == "ModuleSpecs::Anonymous::WasAnnon" + ensure + ModuleSpecs::Anonymous.send(:remove_const, :WasAnnon) + end + + it "may be the repeated in different module objects" do + m = Module.new + n = Module.new + + suppress_warning do + ModuleSpecs::Anonymous::SameName = m + ModuleSpecs::Anonymous::SameName = n + end + + m.name.should == "ModuleSpecs::Anonymous::SameName" + n.name.should == "ModuleSpecs::Anonymous::SameName" + end + + it "is set after it is removed from a constant" do + module ModuleSpecs + module ModuleToRemove + end + + mod = ModuleToRemove + remove_const(:ModuleToRemove) + mod.name.should == "ModuleSpecs::ModuleToRemove" + end + end + + it "is set after it is removed from a constant under an anonymous module" do + m = Module.new + module m::Child; end + child = m::Child + m.send(:remove_const, :Child) + child.name.should =~ /\A#<Module:0x\h+>::Child\z/ + end + + it "is set when opened with the module keyword" do + ModuleSpecs.name.should == "ModuleSpecs" + end + + it "is set when a nested module is opened with the module keyword" do + ModuleSpecs::Anonymous.name.should == "ModuleSpecs::Anonymous" + end + + it "is set when assigning to a constant (constant path matches outer module name)" do + m = Module.new + ModuleSpecs::Anonymous::A = m + m.name.should == "ModuleSpecs::Anonymous::A" + ensure + ModuleSpecs::Anonymous.send(:remove_const, :A) + end + + it "is set when assigning to a constant (constant path does not match outer module name)" do + m = Module.new + ModuleSpecs::Anonymous::SameChild::A = m + m.name.should == "ModuleSpecs::Anonymous::Child::A" + ensure + ModuleSpecs::Anonymous::SameChild.send(:remove_const, :A) + end + + it "is not modified when assigning to a new constant after it has been accessed" do + m = Module.new + ModuleSpecs::Anonymous::B = m + m.name.should == "ModuleSpecs::Anonymous::B" + ModuleSpecs::Anonymous::C = m + m.name.should == "ModuleSpecs::Anonymous::B" + ensure + ModuleSpecs::Anonymous.send(:remove_const, :B) + ModuleSpecs::Anonymous.send(:remove_const, :C) + end + + it "is not modified when assigned to a different anonymous module" do + m = Module.new + module m::M; end + first_name = m::M.name.dup + module m::N; end + m::N::F = m::M + m::M.name.should == first_name + end + + # http://bugs.ruby-lang.org/issues/6067 + it "is set with a conditional assignment to a nested constant" do + eval("ModuleSpecs::Anonymous::F ||= Module.new") + ModuleSpecs::Anonymous::F.name.should == "ModuleSpecs::Anonymous::F" + end + + it "is set with a conditional assignment to a constant" do + module ModuleSpecs::Anonymous + D ||= Module.new + end + ModuleSpecs::Anonymous::D.name.should == "ModuleSpecs::Anonymous::D" + end + + # http://redmine.ruby-lang.org/issues/show/1833 + it "preserves the encoding in which the class was defined" do + require fixture(__FILE__, "name") + ModuleSpecs::NameEncoding.new.name.encoding.should == Encoding::UTF_8 + end + + it "is set when the anonymous outer module name is set (module in one single constant)" do + m = Module.new + m::N = Module.new + ModuleSpecs::Anonymous::E = m + m::N.name.should == "ModuleSpecs::Anonymous::E::N" + ensure + ModuleSpecs::Anonymous.send(:remove_const, :E) + end + + # https://bugs.ruby-lang.org/issues/19681 + it "is set when the anonymous outer module name is set (module in several constants)" do + m = Module.new + m::N = Module.new + m::O = m::N + ModuleSpecs::Anonymous::StoredInMultiplePlaces = m + valid_names = [ + "ModuleSpecs::Anonymous::StoredInMultiplePlaces::N", + "ModuleSpecs::Anonymous::StoredInMultiplePlaces::O" + ] + valid_names.should include(m::N.name) # You get one of the two, but you don't know which one. + ensure + ModuleSpecs::Anonymous.send(:remove_const, :StoredInMultiplePlaces) + end + + it "is set in #const_added callback when a module defined in the top-level scope" do + ruby_exe(<<~RUBY, args: "2>&1").chomp.should == "TEST1\nTEST2" + class Module + def const_added(name) + puts const_get(name).name + end + end + + # module with name + module TEST1 + end + + # anonymous module + TEST2 = Module.new + RUBY + end + + it "is set in #const_added callback for a nested module when an outer module defined in the top-level scope" do + ScratchPad.record [] + + ModuleSpecs::NameSpecs::NamedModule = Module.new do + def self.const_added(name) + ScratchPad << const_get(name).name + end + + module self::A + def self.const_added(name) + ScratchPad << const_get(name).name + end + + module self::B + end + end + end + + ScratchPad.recorded.should.one?(/#<Module.+>::A$/) + ScratchPad.recorded.should.one?(/#<Module.+>::A::B$/) + ModuleSpecs::NameSpecs.send :remove_const, :NamedModule + end + + it "returns a frozen String" do + ModuleSpecs.name.should.frozen? + end + + it "always returns the same String for a given Module" do + s1 = ModuleSpecs.name + s2 = ModuleSpecs.name + s1.should equal(s2) + end +end diff --git a/spec/ruby/core/module/nesting_spec.rb b/spec/ruby/core/module/nesting_spec.rb new file mode 100644 index 0000000000..d0611b3efe --- /dev/null +++ b/spec/ruby/core/module/nesting_spec.rb @@ -0,0 +1,31 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Module::Nesting" do + + it "returns the list of Modules nested at the point of call" do + ModuleSpecs::Nesting[:root_level].should == [] + ModuleSpecs::Nesting[:first_level].should == [ModuleSpecs] + ModuleSpecs::Nesting[:basic].should == [ModuleSpecs::Nesting, ModuleSpecs] + ModuleSpecs::Nesting[:open_first_level].should == + [ModuleSpecs, ModuleSpecs::Nesting, ModuleSpecs] + ModuleSpecs::Nesting[:open_meta].should == + [ModuleSpecs::Nesting.meta, ModuleSpecs::Nesting, ModuleSpecs] + ModuleSpecs::Nesting[:nest_class].should == + [ModuleSpecs::Nesting::NestedClass, ModuleSpecs::Nesting, ModuleSpecs] + end + + it "returns the nesting for module/class declaring the called method" do + ModuleSpecs::Nesting.called_from_module_method.should == + [ModuleSpecs::Nesting, ModuleSpecs] + ModuleSpecs::Nesting::NestedClass.called_from_class_method.should == + [ModuleSpecs::Nesting::NestedClass, ModuleSpecs::Nesting, ModuleSpecs] + ModuleSpecs::Nesting::NestedClass.new.called_from_inst_method.should == + [ModuleSpecs::Nesting::NestedClass, ModuleSpecs::Nesting, ModuleSpecs] + end + +end + +describe "Module.nesting" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/module/new_spec.rb b/spec/ruby/core/module/new_spec.rb new file mode 100644 index 0000000000..ec521360bd --- /dev/null +++ b/spec/ruby/core/module/new_spec.rb @@ -0,0 +1,35 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Module.new" do + it "creates a new anonymous Module" do + Module.new.is_a?(Module).should == true + end + + it "creates a module without a name" do + Module.new.name.should be_nil + end + + it "creates a new Module and passes it to the provided block" do + test_mod = nil + m = Module.new do |mod| + mod.should_not == nil + self.should == mod + test_mod = mod + mod.is_a?(Module).should == true + Object.new # trying to return something + end + test_mod.should == m + end + + it "evaluates a passed block in the context of the module" do + fred = Module.new do + def hello() "hello" end + def bye() "bye" end + end + + (o = mock('x')).extend(fred) + o.hello.should == "hello" + o.bye.should == "bye" + end +end diff --git a/spec/ruby/core/module/prepend_features_spec.rb b/spec/ruby/core/module/prepend_features_spec.rb new file mode 100644 index 0000000000..09c15c5c15 --- /dev/null +++ b/spec/ruby/core/module/prepend_features_spec.rb @@ -0,0 +1,64 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Module#prepend_features" do + it "is a private method" do + Module.should have_private_instance_method(:prepend_features, true) + end + + it "gets called when self is included in another module/class" do + ScratchPad.record [] + + m = Module.new do + def self.prepend_features(mod) + ScratchPad << mod + end + end + + c = Class.new do + prepend m + end + + ScratchPad.recorded.should == [c] + end + + it "raises an ArgumentError on a cyclic prepend" do + -> { + ModuleSpecs::CyclicPrepend.send(:prepend_features, ModuleSpecs::CyclicPrepend) + }.should raise_error(ArgumentError) + end + + it "clears caches of the given module" do + parent = Class.new do + def bar; :bar; end + end + + child = Class.new(parent) do + def foo; :foo; end + def bar; super; end + end + + mod = Module.new do + def foo; :fooo; end + end + + child.new.foo + child.new.bar + + child.prepend(mod) + + child.new.bar.should == :bar + end + + describe "on Class" do + it "is undefined" do + Class.should_not have_private_instance_method(:prepend_features, true) + end + + it "raises a TypeError if calling after rebinded to Class" do + -> { + Module.instance_method(:prepend_features).bind(Class.new).call Module.new + }.should raise_error(TypeError) + end + end +end diff --git a/spec/ruby/core/module/prepend_spec.rb b/spec/ruby/core/module/prepend_spec.rb new file mode 100644 index 0000000000..71e82c513e --- /dev/null +++ b/spec/ruby/core/module/prepend_spec.rb @@ -0,0 +1,823 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Module#prepend" do + it "is a public method" do + Module.should have_public_instance_method(:prepend, false) + end + + it "does not affect the superclass" do + Class.new { prepend Module.new }.superclass.should == Object + end + + it "calls #prepend_features(self) in reversed order on each module" do + ScratchPad.record [] + + m = Module.new do + def self.prepend_features(mod) + ScratchPad << [ self, mod ] + end + end + + m2 = Module.new do + def self.prepend_features(mod) + ScratchPad << [ self, mod ] + end + end + + m3 = Module.new do + def self.prepend_features(mod) + ScratchPad << [ self, mod ] + end + end + + c = Class.new { prepend(m, m2, m3) } + + ScratchPad.recorded.should == [ [ m3, c], [ m2, c ], [ m, c ] ] + end + + it "updates the method when a module is prepended" do + m_module = Module.new do + def foo + "m" + end + end + a_class = Class.new do + def foo + 'a' + end + end + a = a_class.new + foo = -> { a.foo } + foo.call.should == 'a' + a_class.class_eval do + prepend m_module + end + foo.call.should == 'm' + end + + it "updates the method when a prepended module is updated" do + m_module = Module.new + a_class = Class.new do + prepend m_module + def foo + 'a' + end + end + a = a_class.new + foo = -> { a.foo } + foo.call.should == 'a' + m_module.module_eval do + def foo + "m" + end + end + foo.call.should == 'm' + end + + it "updates the optimized method when a prepended module is updated" do + out = ruby_exe(<<~RUBY) + module M; end + class Integer + prepend M + end + l = -> { 1 + 2 } + p l.call + M.module_eval do + def +(o) + $called = true + super(o) + end + end + p l.call + p $called + RUBY + out.should == "3\n3\ntrue\n" + end + + it "updates the method when there is a base included method and the prepended module overrides it" do + base_module = Module.new do + def foo + 'a' + end + end + a_class = Class.new do + include base_module + end + a = a_class.new + foo = -> { a.foo } + foo.call.should == 'a' + + m_module = Module.new do + def foo + "m" + end + end + a_class.prepend m_module + foo.call.should == 'm' + end + + it "updates the method when there is a base included method and the prepended module is later updated" do + base_module = Module.new do + def foo + 'a' + end + end + a_class = Class.new do + include base_module + end + a = a_class.new + foo = -> { a.foo } + foo.call.should == 'a' + + m_module = Module.new + a_class.prepend m_module + foo.call.should == 'a' + + m_module.module_eval do + def foo + "m" + end + end + foo.call.should == 'm' + end + + it "updates the method when a module prepended after a call is later updated" do + m_module = Module.new + a_class = Class.new do + def foo + 'a' + end + end + a = a_class.new + foo = -> { a.foo } + foo.call.should == 'a' + + a_class.prepend m_module + foo.call.should == 'a' + + m_module.module_eval do + def foo + "m" + end + end + foo.call.should == 'm' + end + + it "updates the method when a module is prepended after another and the method is defined later on that module" do + m_module = Module.new do + def foo + 'a' + end + end + a_class = Class.new + a_class.prepend m_module + a = a_class.new + foo = -> { a.foo } + foo.call.should == 'a' + + n_module = Module.new + a_class.prepend n_module + foo.call.should == 'a' + + n_module.module_eval do + def foo + "n" + end + end + foo.call.should == 'n' + end + + it "updates the method when a module is included in a prepended module and the method is defined later" do + a_class = Class.new + base_module = Module.new do + def foo + 'a' + end + end + a_class.prepend base_module + a = a_class.new + foo = -> { a.foo } + foo.call.should == 'a' + + m_module = Module.new + n_module = Module.new + m_module.include n_module + a_class.prepend m_module + + n_module.module_eval do + def foo + "n" + end + end + foo.call.should == 'n' + end + + it "updates the method when a new module with an included module is prepended" do + a_class = Class.new do + def foo + 'a' + end + end + + n_module = Module.new do + def foo + 'n' + end + end + + m_module = Module.new do + include n_module + end + + a = a_class.new + foo = -> { a.foo } + + foo.call.should == 'a' + + a_class.class_eval do + prepend m_module + end + + foo.call.should == 'n' + end + + it "updates the constant when a module is prepended" do + module ModuleSpecs::ConstUpdatePrepended + module M + FOO = 'm' + end + module A + FOO = 'a' + end + module B + include A + def self.foo + FOO + end + end + + B.foo.should == 'a' + B.prepend M + B.foo.should == 'm' + end + ensure + ModuleSpecs.send(:remove_const, :ConstUpdatePrepended) + end + + it "updates the constant when a prepended module is updated" do + module ModuleSpecs::ConstPrependedUpdated + module M + end + module A + FOO = 'a' + end + module B + include A + prepend M + def self.foo + FOO + end + end + B.foo.should == 'a' + M.const_set(:FOO, 'm') + B.foo.should == 'm' + end + ensure + ModuleSpecs.send(:remove_const, :ConstPrependedUpdated) + end + + it "updates the constant when there is a base included constant and the prepended module overrides it" do + module ModuleSpecs::ConstIncludedPrependedOverride + module Base + FOO = 'a' + end + module A + include Base + def self.foo + FOO + end + end + A.foo.should == 'a' + + module M + FOO = 'm' + end + A.prepend M + A.foo.should == 'm' + end + ensure + ModuleSpecs.send(:remove_const, :ConstIncludedPrependedOverride) + end + + it "updates the constant when there is a base included constant and the prepended module is later updated" do + module ModuleSpecs::ConstIncludedPrependedLaterUpdated + module Base + FOO = 'a' + end + module A + include Base + def self.foo + FOO + end + end + A.foo.should == 'a' + + module M + end + A.prepend M + A.foo.should == 'a' + + M.const_set(:FOO, 'm') + A.foo.should == 'm' + end + ensure + ModuleSpecs.send(:remove_const, :ConstIncludedPrependedLaterUpdated) + end + + it "updates the constant when a module prepended after a constant is later updated" do + module ModuleSpecs::ConstUpdatedPrependedAfterLaterUpdated + module M + end + module A + FOO = 'a' + end + module B + include A + def self.foo + FOO + end + end + B.foo.should == 'a' + + B.prepend M + B.foo.should == 'a' + + M.const_set(:FOO, 'm') + B.foo.should == 'm' + end + ensure + ModuleSpecs.send(:remove_const, :ConstUpdatedPrependedAfterLaterUpdated) + end + + it "updates the constant when a module is prepended after another and the constant is defined later on that module" do + module ModuleSpecs::ConstUpdatedPrependedAfterConstDefined + module M + FOO = 'm' + end + module A + prepend M + def self.foo + FOO + end + end + + A.foo.should == 'm' + + module N + end + A.prepend N + A.foo.should == 'm' + + N.const_set(:FOO, 'n') + A.foo.should == 'n' + end + ensure + ModuleSpecs.send(:remove_const, :ConstUpdatedPrependedAfterConstDefined) + end + + it "updates the constant when a module is included in a prepended module and the constant is defined later" do + module ModuleSpecs::ConstUpdatedIncludedInPrependedConstDefinedLater + module A + def self.foo + FOO + end + end + module Base + FOO = 'a' + end + + A.prepend Base + A.foo.should == 'a' + + module N + end + module M + include N + end + + A.prepend M + + N.const_set(:FOO, 'n') + A.foo.should == 'n' + end + ensure + ModuleSpecs.send(:remove_const, :ConstUpdatedIncludedInPrependedConstDefinedLater) + end + + it "updates the constant when a new module with an included module is prepended" do + module ModuleSpecs::ConstUpdatedNewModuleIncludedPrepended + module A + FOO = 'a' + end + module B + include A + def self.foo + FOO + end + end + module N + FOO = 'n' + end + + module M + include N + end + + B.foo.should == 'a' + + B.prepend M + B.foo.should == 'n' + end + ensure + ModuleSpecs.send(:remove_const, :ConstUpdatedNewModuleIncludedPrepended) + end + + it "raises a TypeError when the argument is not a Module" do + -> { ModuleSpecs::Basic.prepend(Class.new) }.should raise_error(TypeError) + end + + it "does not raise a TypeError when the argument is an instance of a subclass of Module" do + class ModuleSpecs::SubclassSpec::AClass + end + -> { ModuleSpecs::SubclassSpec::AClass.prepend(ModuleSpecs::Subclass.new) }.should_not raise_error(TypeError) + ensure + ModuleSpecs::SubclassSpec.send(:remove_const, :AClass) + end + + it "raises a TypeError when the argument is a refinement" do + refinement = nil + + Module.new do + refine String do + refinement = self + end + end + + -> { ModuleSpecs::Basic.prepend(refinement) }.should raise_error(TypeError, "Cannot prepend refinement") + end + + it "imports constants" do + m1 = Module.new + m1::MY_CONSTANT = 1 + m2 = Module.new { prepend(m1) } + m2.constants.should include(:MY_CONSTANT) + end + + it "imports instance methods" do + Module.new { prepend ModuleSpecs::A }.instance_methods.should include(:ma) + end + + it "does not import methods to modules and classes" do + Module.new { prepend ModuleSpecs::A }.methods.should_not include(:ma) + end + + it "allows wrapping methods" do + m = Module.new { def calc(x) super + 3 end } + c = Class.new { def calc(x) x*2 end } + c.prepend(m) + c.new.calc(1).should == 5 + end + + it "also prepends included modules" do + a = Module.new { def calc(x) x end } + b = Module.new { include a } + c = Class.new { prepend b } + c.new.calc(1).should == 1 + end + + it "prepends multiple modules in the right order" do + m1 = Module.new { def chain; super << :m1; end } + m2 = Module.new { def chain; super << :m2; end; prepend(m1) } + c = Class.new { def chain; [:c]; end; prepend(m2) } + c.new.chain.should == [:c, :m2, :m1] + end + + it "includes prepended modules in ancestors" do + m = Module.new + Class.new { prepend(m) }.ancestors.should include(m) + end + + it "reports the prepended module as the method owner" do + m = Module.new { def meth; end } + c = Class.new { def meth; end; prepend(m) } + c.new.method(:meth).owner.should == m + end + + it "reports the prepended module as the unbound method owner" do + m = Module.new { def meth; end } + c = Class.new { def meth; end; prepend(m) } + c.instance_method(:meth).owner.should == m + c.public_instance_method(:meth).owner.should == m + end + + it "causes the prepended module's method to be aliased by alias_method" do + m = Module.new { def meth; :m end } + c = Class.new { def meth; :c end; prepend(m); alias_method :alias, :meth } + c.new.alias.should == :m + end + + it "reports the class for the owner of an aliased method on the class" do + m = Module.new + c = Class.new { prepend(m); def meth; :c end; alias_method :alias, :meth } + c.instance_method(:alias).owner.should == c + end + + it "reports the class for the owner of a method aliased from the prepended module" do + m = Module.new { def meth; :m end } + c = Class.new { prepend(m); alias_method :alias, :meth } + c.instance_method(:alias).owner.should == c + end + + it "sees an instance of a prepended class as kind of the prepended module" do + m = Module.new + c = Class.new { prepend(m) } + c.new.should be_kind_of(m) + end + + it "keeps the module in the chain when dupping the class" do + m = Module.new + c = Class.new { prepend(m) } + c.dup.new.should be_kind_of(m) + end + + it "uses only new module when dupping the module" do + m1 = Module.new { def calc(x) x end } + m2 = Module.new { prepend(m1) } + c1 = Class.new { prepend(m2) } + m2dup = m2.dup + m2dup.ancestors.should == [m1,m2dup] + c2 = Class.new { prepend(m2dup) } + c1.ancestors[0,3].should == [m1,m2,c1] + c1.new.should be_kind_of(m1) + c2.ancestors[0,3].should == [m1,m2dup,c2] + c2.new.should be_kind_of(m1) + end + + it "depends on prepend_features to add the module" do + m = Module.new { def self.prepend_features(mod) end } + Class.new { prepend(m) }.ancestors.should_not include(m) + end + + it "adds the module in the subclass chains" do + parent = Class.new { def chain; [:parent]; end } + child = Class.new(parent) { def chain; super << :child; end } + mod = Module.new { def chain; super << :mod; end } + parent.prepend(mod) + parent.ancestors[0,2].should == [mod, parent] + child.ancestors[0,3].should == [child, mod, parent] + + parent.new.chain.should == [:parent, :mod] + child.new.chain.should == [:parent, :mod, :child] + end + + it "inserts a later prepended module into the chain" do + m1 = Module.new { def chain; super << :m1; end } + m2 = Module.new { def chain; super << :m2; end } + c1 = Class.new { def chain; [:c1]; end; prepend m1 } + c2 = Class.new(c1) { def chain; super << :c2; end } + c2.new.chain.should == [:c1, :m1, :c2] + c1.prepend(m2) + c2.new.chain.should == [:c1, :m1, :m2, :c2] + end + + it "works with subclasses" do + m = Module.new do + def chain + super << :module + end + end + + c = Class.new do + prepend m + def chain + [:class] + end + end + + s = Class.new(c) do + def chain + super << :subclass + end + end + + s.new.chain.should == [:class, :module, :subclass] + end + + it "throws a NoMethodError when there is no more superclass" do + m = Module.new do + def chain + super << :module + end + end + + c = Class.new do + prepend m + def chain + super << :class + end + end + -> { c.new.chain }.should raise_error(NoMethodError) + end + + it "calls prepended after prepend_features" do + ScratchPad.record [] + + m = Module.new do + def self.prepend_features(klass) + ScratchPad << [:prepend_features, klass] + end + def self.prepended(klass) + ScratchPad << [:prepended, klass] + end + end + + c = Class.new { prepend(m) } + ScratchPad.recorded.should == [[:prepend_features, c], [:prepended, c]] + end + + it "prepends a module if it is included in a super class" do + module ModuleSpecs::M3 + module M; end + class A; include M; end + class B < A; prepend M; end + + all = [A, B, M] + + (B.ancestors.filter { |a| all.include?(a) }).should == [M, B, A, M] + end + end + + it "detects cyclic prepends" do + -> { + module ModuleSpecs::P + prepend ModuleSpecs::P + end + }.should raise_error(ArgumentError) + end + + it "doesn't accept no-arguments" do + -> { + Module.new do + prepend + end + }.should raise_error(ArgumentError) + end + + it "returns the class it's included into" do + m = Module.new + r = nil + c = Class.new { r = prepend m } + r.should == c + end + + it "clears any caches" do + module ModuleSpecs::M3 + module PM1 + def foo + :m1 + end + end + + module PM2 + def foo + :m2 + end + end + + klass = Class.new do + prepend PM1 + + def get + foo + end + end + + o = klass.new + o.get.should == :m1 + + klass.class_eval do + prepend PM2 + end + + o.get.should == :m2 + end + end + + it "supports super when the module is prepended into a singleton class" do + ScratchPad.record [] + + mod = Module.new do + def self.inherited(base) + super + end + end + + module_with_singleton_class_prepend = Module.new do + singleton_class.prepend(mod) + end + + klass = Class.new(ModuleSpecs::RecordIncludedModules) do + include module_with_singleton_class_prepend + end + + ScratchPad.recorded.should == klass + end + + it "supports super when the module is prepended into a singleton class with a class super" do + ScratchPad.record [] + + base_class = Class.new(ModuleSpecs::RecordIncludedModules) do + def self.inherited(base) + super + end + end + + prepended_module = Module.new + base_class.singleton_class.prepend(prepended_module) + + child_class = Class.new(base_class) + ScratchPad.recorded.should == child_class + end + + it "does not interfere with a define_method super in the original class" do + base_class = Class.new do + def foo(ary) + ary << 1 + end + end + + child_class = Class.new(base_class) do + define_method :foo do |ary| + ary << 2 + super(ary) + end + end + + prep_mod = Module.new do + def foo(ary) + ary << 3 + super(ary) + end + end + + child_class.prepend(prep_mod) + + ary = [] + child_class.new.foo(ary) + ary.should == [3, 2, 1] + end + + it "does not prepend a second copy if the module already indirectly exists in the hierarchy" do + mod = Module.new do; end + submod = Module.new do; end + klass = Class.new do; end + klass.include(mod) + mod.prepend(submod) + klass.include(mod) + + klass.ancestors.take(4).should == [klass, submod, mod, Object] + end + + # https://bugs.ruby-lang.org/issues/17423 + describe "when module already exists in ancestor chain" do + it "modifies the ancestor chain" do + m = Module.new do; end + a = Module.new do; end + b = Class.new do; end + + b.include(a) + a.prepend(m) + b.ancestors.take(4).should == [b, m, a, Object] + + b.prepend(m) + b.ancestors.take(5).should == [m, b, m, a, Object] + end + end + + describe "called on a module" do + describe "included into a class" + it "does not obscure the module's methods from reflective access" do + mod = Module.new do + def foo; end + end + cls = Class.new do + include mod + end + pre = Module.new + mod.prepend pre + + cls.instance_methods.should include(:foo) + end + end +end diff --git a/spec/ruby/core/module/prepended_spec.rb b/spec/ruby/core/module/prepended_spec.rb new file mode 100644 index 0000000000..bd95d8fd05 --- /dev/null +++ b/spec/ruby/core/module/prepended_spec.rb @@ -0,0 +1,25 @@ +# -*- encoding: us-ascii -*- + +require_relative '../../spec_helper' + +describe "Module#prepended" do + before :each do + ScratchPad.clear + end + + it "is a private method" do + Module.should have_private_instance_method(:prepended, true) + end + + it "is invoked when self is prepended to another module or class" do + m = Module.new do + def self.prepended(o) + ScratchPad.record o + end + end + + c = Class.new { prepend m } + + ScratchPad.recorded.should == c + end +end diff --git a/spec/ruby/core/module/private_class_method_spec.rb b/spec/ruby/core/module/private_class_method_spec.rb new file mode 100644 index 0000000000..f899c71a57 --- /dev/null +++ b/spec/ruby/core/module/private_class_method_spec.rb @@ -0,0 +1,91 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Module#private_class_method" do + before :each do + # This is not in classes.rb because after marking a class method private it + # will stay private. + class << ModuleSpecs::Parent + public + def private_method_1; end + def private_method_2; end + end + end + + after :each do + class << ModuleSpecs::Parent + remove_method :private_method_1 + remove_method :private_method_2 + end + end + + it "makes an existing class method private" do + ModuleSpecs::Parent.private_method_1.should == nil + ModuleSpecs::Parent.private_class_method :private_method_1 + -> { ModuleSpecs::Parent.private_method_1 }.should raise_error(NoMethodError) + + # Technically above we're testing the Singleton classes, class method(right?). + # Try a "real" class method set private. + -> { ModuleSpecs::Parent.private_method }.should raise_error(NoMethodError) + end + + it "makes an existing class method private up the inheritance tree" do + ModuleSpecs::Child.public_class_method :private_method_1 + ModuleSpecs::Child.private_method_1.should == nil + ModuleSpecs::Child.private_class_method :private_method_1 + + -> { ModuleSpecs::Child.private_method_1 }.should raise_error(NoMethodError) + -> { ModuleSpecs::Child.private_method }.should raise_error(NoMethodError) + end + + it "accepts more than one method at a time" do + ModuleSpecs::Parent.private_method_1.should == nil + ModuleSpecs::Parent.private_method_2.should == nil + + ModuleSpecs::Child.private_class_method :private_method_1, :private_method_2 + + -> { ModuleSpecs::Child.private_method_1 }.should raise_error(NoMethodError) + -> { ModuleSpecs::Child.private_method_2 }.should raise_error(NoMethodError) + end + + it "raises a NameError if class method doesn't exist" do + -> do + ModuleSpecs.private_class_method :no_method_here + end.should raise_error(NameError) + end + + it "makes a class method private" do + c = Class.new do + def self.foo() "foo" end + private_class_method :foo + end + -> { c.foo }.should raise_error(NoMethodError) + end + + it "raises a NameError when the given name is not a method" do + -> do + Class.new do + private_class_method :foo + end + end.should raise_error(NameError) + end + + it "raises a NameError when the given name is an instance method" do + -> do + Class.new do + def foo() "foo" end + private_class_method :foo + end + end.should raise_error(NameError) + end + + context "when single argument is passed and is an array" do + it "sets the visibility of the given methods to private" do + c = Class.new do + def self.foo() "foo" end + private_class_method [:foo] + end + -> { c.foo }.should raise_error(NoMethodError) + end + end +end diff --git a/spec/ruby/core/module/private_constant_spec.rb b/spec/ruby/core/module/private_constant_spec.rb new file mode 100644 index 0000000000..3a91b3c3cd --- /dev/null +++ b/spec/ruby/core/module/private_constant_spec.rb @@ -0,0 +1,32 @@ +require_relative '../../spec_helper' + +describe "Module#private_constant" do + it "can only be passed constant names defined in the target (self) module" do + cls1 = Class.new + cls1.const_set :Foo, true + cls2 = Class.new(cls1) + + -> do + cls2.send :private_constant, :Foo + end.should raise_error(NameError) + end + + it "accepts strings as constant names" do + cls = Class.new + cls.const_set :Foo, true + cls.send :private_constant, "Foo" + + -> { cls::Foo }.should raise_error(NameError) + end + + it "accepts multiple names" do + mod = Module.new + mod.const_set :Foo, true + mod.const_set :Bar, true + + mod.send :private_constant, :Foo, :Bar + + -> {mod::Foo}.should raise_error(NameError) + -> {mod::Bar}.should raise_error(NameError) + end +end diff --git a/spec/ruby/core/module/private_instance_methods_spec.rb b/spec/ruby/core/module/private_instance_methods_spec.rb new file mode 100644 index 0000000000..cce0f001bf --- /dev/null +++ b/spec/ruby/core/module/private_instance_methods_spec.rb @@ -0,0 +1,54 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative '../../fixtures/reflection' + +# TODO: rewrite +describe "Module#private_instance_methods" do + it "returns a list of private methods in module and its ancestors" do + ModuleSpecs::CountsMixin.should have_private_instance_method(:private_3) + + ModuleSpecs::CountsParent.should have_private_instance_method(:private_2) + ModuleSpecs::CountsParent.should have_private_instance_method(:private_3) + + ModuleSpecs::CountsChild.should have_private_instance_method(:private_1) + ModuleSpecs::CountsChild.should have_private_instance_method(:private_2) + ModuleSpecs::CountsChild.should have_private_instance_method(:private_3) + end + + it "when passed false as a parameter, should return only methods defined in that module" do + ModuleSpecs::CountsMixin.should have_private_instance_method(:private_3, false) + ModuleSpecs::CountsParent.should have_private_instance_method(:private_2, false) + ModuleSpecs::CountsChild.should have_private_instance_method(:private_1, false) + end + + it "default list should be the same as passing true as an argument" do + ModuleSpecs::CountsMixin.private_instance_methods(true).should == + ModuleSpecs::CountsMixin.private_instance_methods + ModuleSpecs::CountsParent.private_instance_methods(true).should == + ModuleSpecs::CountsParent.private_instance_methods + ModuleSpecs::CountsChild.private_instance_methods(true).should == + ModuleSpecs::CountsChild.private_instance_methods + end +end + +describe :module_private_instance_methods_supers, shared: true do + it "returns a unique list for a class including a module" do + m = ReflectSpecs::D.private_instance_methods(*@object) + m.select { |x| x == :pri }.sort.should == [:pri] + end + + it "returns a unique list for a subclass" do + m = ReflectSpecs::E.private_instance_methods(*@object) + m.select { |x| x == :pri }.sort.should == [:pri] + end +end + +describe "Module#private_instance_methods" do + describe "when not passed an argument" do + it_behaves_like :module_private_instance_methods_supers, nil, [] + end + + describe "when passed true" do + it_behaves_like :module_private_instance_methods_supers, nil, true + end +end diff --git a/spec/ruby/core/module/private_method_defined_spec.rb b/spec/ruby/core/module/private_method_defined_spec.rb new file mode 100644 index 0000000000..01fc8f8fb9 --- /dev/null +++ b/spec/ruby/core/module/private_method_defined_spec.rb @@ -0,0 +1,120 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Module#private_method_defined?" do + it "returns true if the named private method is defined by module or its ancestors" do + ModuleSpecs::CountsMixin.private_method_defined?("private_3").should == true + + ModuleSpecs::CountsParent.private_method_defined?("private_3").should == true + ModuleSpecs::CountsParent.private_method_defined?("private_2").should == true + + ModuleSpecs::CountsChild.private_method_defined?("private_3").should == true + ModuleSpecs::CountsChild.private_method_defined?("private_2").should == true + ModuleSpecs::CountsChild.private_method_defined?("private_1").should == true + end + + it "returns false if method is not a private method" do + ModuleSpecs::CountsChild.private_method_defined?("public_3").should == false + ModuleSpecs::CountsChild.private_method_defined?("public_2").should == false + ModuleSpecs::CountsChild.private_method_defined?("public_1").should == false + + ModuleSpecs::CountsChild.private_method_defined?("protected_3").should == false + ModuleSpecs::CountsChild.private_method_defined?("protected_2").should == false + ModuleSpecs::CountsChild.private_method_defined?("protected_1").should == false + end + + it "returns false if the named method is not defined by the module or its ancestors" do + ModuleSpecs::CountsMixin.private_method_defined?(:private_10).should == false + end + + it "accepts symbols for the method name" do + ModuleSpecs::CountsMixin.private_method_defined?(:private_3).should == true + end + + it "raises a TypeError if passed an Integer" do + -> do + ModuleSpecs::CountsMixin.private_method_defined?(1) + end.should raise_error(TypeError) + end + + it "raises a TypeError if passed nil" do + -> do + ModuleSpecs::CountsMixin.private_method_defined?(nil) + end.should raise_error(TypeError) + end + + it "raises a TypeError if passed false" do + -> do + ModuleSpecs::CountsMixin.private_method_defined?(false) + end.should raise_error(TypeError) + end + + it "raises a TypeError if passed an object that does not defined #to_str" do + -> do + ModuleSpecs::CountsMixin.private_method_defined?(mock('x')) + end.should raise_error(TypeError) + end + + it "raises a TypeError if passed an object that defines #to_sym" do + sym = mock('symbol') + def sym.to_sym() :private_3 end + + -> do + ModuleSpecs::CountsMixin.private_method_defined?(sym) + end.should raise_error(TypeError) + end + + it "calls #to_str to convert an Object" do + str = mock('string') + def str.to_str() 'private_3' end + ModuleSpecs::CountsMixin.private_method_defined?(str).should == true + end + + describe "when passed true as a second optional argument" do + it "performs a lookup in ancestors" do + ModuleSpecs::Child.private_method_defined?(:public_child, true).should == false + ModuleSpecs::Child.private_method_defined?(:protected_child, true).should == false + ModuleSpecs::Child.private_method_defined?(:accessor_method, true).should == false + ModuleSpecs::Child.private_method_defined?(:private_child, true).should == true + + # Defined in Parent + ModuleSpecs::Child.private_method_defined?(:public_parent, true).should == false + ModuleSpecs::Child.private_method_defined?(:protected_parent, true).should == false + ModuleSpecs::Child.private_method_defined?(:private_parent, true).should == true + + # Defined in Module + ModuleSpecs::Child.private_method_defined?(:public_module, true).should == false + ModuleSpecs::Child.private_method_defined?(:protected_module, true).should == false + ModuleSpecs::Child.private_method_defined?(:private_module, true).should == true + + # Defined in SuperModule + ModuleSpecs::Child.private_method_defined?(:public_super_module, true).should == false + ModuleSpecs::Child.private_method_defined?(:protected_super_module, true).should == false + ModuleSpecs::Child.private_method_defined?(:private_super_module, true).should == true + end + end + + describe "when passed false as a second optional argument" do + it "checks only the class itself" do + ModuleSpecs::Child.private_method_defined?(:public_child, false).should == false + ModuleSpecs::Child.private_method_defined?(:protected_child, false).should == false + ModuleSpecs::Child.private_method_defined?(:accessor_method, false).should == false + ModuleSpecs::Child.private_method_defined?(:private_child, false).should == true + + # Defined in Parent + ModuleSpecs::Child.private_method_defined?(:public_parent, false).should == false + ModuleSpecs::Child.private_method_defined?(:protected_parent, false).should == false + ModuleSpecs::Child.private_method_defined?(:private_parent, false).should == false + + # Defined in Module + ModuleSpecs::Child.private_method_defined?(:public_module, false).should == false + ModuleSpecs::Child.private_method_defined?(:protected_module, false).should == false + ModuleSpecs::Child.private_method_defined?(:private_module, false).should == false + + # Defined in SuperModule + ModuleSpecs::Child.private_method_defined?(:public_super_module, false).should == false + ModuleSpecs::Child.private_method_defined?(:protected_super_module, false).should == false + ModuleSpecs::Child.private_method_defined?(:private_super_module, false).should == false + end + end +end diff --git a/spec/ruby/core/module/private_spec.rb b/spec/ruby/core/module/private_spec.rb new file mode 100644 index 0000000000..9e1a297eea --- /dev/null +++ b/spec/ruby/core/module/private_spec.rb @@ -0,0 +1,95 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/set_visibility' + +describe "Module#private" do + it_behaves_like :set_visibility, :private + + it "makes the target method uncallable from other types" do + obj = Object.new + class << obj + def foo; true; end + end + + obj.foo.should == true + + class << obj + private :foo + end + + -> { obj.foo }.should raise_error(NoMethodError) + end + + it "makes a public Object instance method private in a new module" do + m = Module.new do + private :module_specs_public_method_on_object + end + + m.should have_private_instance_method(:module_specs_public_method_on_object) + + # Ensure we did not change Object's method + Object.should_not have_private_instance_method(:module_specs_public_method_on_object) + end + + it "makes a public Object instance method private in Kernel" do + Kernel.should have_private_instance_method( + :module_specs_public_method_on_object_for_kernel_private) + Object.should_not have_private_instance_method( + :module_specs_public_method_on_object_for_kernel_private) + end + + it "returns argument or arguments if given" do + (class << Object.new; self; end).class_eval do + def foo; end + private(:foo).should equal(:foo) + private([:foo, :foo]).should == [:foo, :foo] + private(:foo, :foo).should == [:foo, :foo] + private.should equal(nil) + end + end + + it "raises a NameError when given an undefined name" do + -> do + Module.new.send(:private, :undefined) + end.should raise_error(NameError) + end + + it "only makes the method private in the class it is called on" do + base = Class.new do + def wrapped + 1 + end + end + + klass = Class.new(base) do + def wrapped + super + 1 + end + private :wrapped + end + + base.new.wrapped.should == 1 + -> do + klass.new.wrapped + end.should raise_error(NameError) + end + + it "continues to allow a prepended module method to call super" do + wrapper = Module.new do + def wrapped + super + 1 + end + end + + klass = Class.new do + prepend wrapper + + def wrapped + 1 + end + private :wrapped + end + + klass.new.wrapped.should == 2 + end +end diff --git a/spec/ruby/core/module/protected_instance_methods_spec.rb b/spec/ruby/core/module/protected_instance_methods_spec.rb new file mode 100644 index 0000000000..78ce7e788f --- /dev/null +++ b/spec/ruby/core/module/protected_instance_methods_spec.rb @@ -0,0 +1,57 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative '../../fixtures/reflection' + +# TODO: rewrite +describe "Module#protected_instance_methods" do + it "returns a list of protected methods in module and its ancestors" do + methods = ModuleSpecs::CountsMixin.protected_instance_methods + methods.should include(:protected_3) + + methods = ModuleSpecs::CountsParent.protected_instance_methods + methods.should include(:protected_3) + methods.should include(:protected_2) + + methods = ModuleSpecs::CountsChild.protected_instance_methods + methods.should include(:protected_3) + methods.should include(:protected_2) + methods.should include(:protected_1) + end + + it "when passed false as a parameter, should return only methods defined in that module" do + ModuleSpecs::CountsMixin.protected_instance_methods(false).should == [:protected_3] + ModuleSpecs::CountsParent.protected_instance_methods(false).should == [:protected_2] + ModuleSpecs::CountsChild.protected_instance_methods(false).should == [:protected_1] + end + + it "default list should be the same as passing true as an argument" do + ModuleSpecs::CountsMixin.protected_instance_methods(true).should == + ModuleSpecs::CountsMixin.protected_instance_methods + ModuleSpecs::CountsParent.protected_instance_methods(true).should == + ModuleSpecs::CountsParent.protected_instance_methods + ModuleSpecs::CountsChild.protected_instance_methods(true).should == + ModuleSpecs::CountsChild.protected_instance_methods + end +end + +describe :module_protected_instance_methods_supers, shared: true do + it "returns a unique list for a class including a module" do + m = ReflectSpecs::D.protected_instance_methods(*@object) + m.select { |x| x == :pro }.sort.should == [:pro] + end + + it "returns a unique list for a subclass" do + m = ReflectSpecs::E.protected_instance_methods(*@object) + m.select { |x| x == :pro }.sort.should == [:pro] + end +end + +describe "Module#protected_instance_methods" do + describe "when not passed an argument" do + it_behaves_like :module_protected_instance_methods_supers, nil, [] + end + + describe "when passed true" do + it_behaves_like :module_protected_instance_methods_supers, nil, true + end +end diff --git a/spec/ruby/core/module/protected_method_defined_spec.rb b/spec/ruby/core/module/protected_method_defined_spec.rb new file mode 100644 index 0000000000..31e24a16c1 --- /dev/null +++ b/spec/ruby/core/module/protected_method_defined_spec.rb @@ -0,0 +1,120 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Module#protected_method_defined?" do + it "returns true if the named protected method is defined by module or its ancestors" do + ModuleSpecs::CountsMixin.protected_method_defined?("protected_3").should == true + + ModuleSpecs::CountsParent.protected_method_defined?("protected_3").should == true + ModuleSpecs::CountsParent.protected_method_defined?("protected_2").should == true + + ModuleSpecs::CountsChild.protected_method_defined?("protected_3").should == true + ModuleSpecs::CountsChild.protected_method_defined?("protected_2").should == true + ModuleSpecs::CountsChild.protected_method_defined?("protected_1").should == true + end + + it "returns false if method is not a protected method" do + ModuleSpecs::CountsChild.protected_method_defined?("public_3").should == false + ModuleSpecs::CountsChild.protected_method_defined?("public_2").should == false + ModuleSpecs::CountsChild.protected_method_defined?("public_1").should == false + + ModuleSpecs::CountsChild.protected_method_defined?("private_3").should == false + ModuleSpecs::CountsChild.protected_method_defined?("private_2").should == false + ModuleSpecs::CountsChild.protected_method_defined?("private_1").should == false + end + + it "returns false if the named method is not defined by the module or its ancestors" do + ModuleSpecs::CountsMixin.protected_method_defined?(:protected_10).should == false + end + + it "accepts symbols for the method name" do + ModuleSpecs::CountsMixin.protected_method_defined?(:protected_3).should == true + end + + it "raises a TypeError if passed an Integer" do + -> do + ModuleSpecs::CountsMixin.protected_method_defined?(1) + end.should raise_error(TypeError) + end + + it "raises a TypeError if passed nil" do + -> do + ModuleSpecs::CountsMixin.protected_method_defined?(nil) + end.should raise_error(TypeError) + end + + it "raises a TypeError if passed false" do + -> do + ModuleSpecs::CountsMixin.protected_method_defined?(false) + end.should raise_error(TypeError) + end + + it "raises a TypeError if passed an object that does not defined #to_str" do + -> do + ModuleSpecs::CountsMixin.protected_method_defined?(mock('x')) + end.should raise_error(TypeError) + end + + it "raises a TypeError if passed an object that defines #to_sym" do + sym = mock('symbol') + def sym.to_sym() :protected_3 end + + -> do + ModuleSpecs::CountsMixin.protected_method_defined?(sym) + end.should raise_error(TypeError) + end + + it "calls #to_str to convert an Object" do + str = mock('protected_3') + str.should_receive(:to_str).and_return("protected_3") + ModuleSpecs::CountsMixin.protected_method_defined?(str).should == true + end + + describe "when passed true as a second optional argument" do + it "performs a lookup in ancestors" do + ModuleSpecs::Child.protected_method_defined?(:public_child, true).should == false + ModuleSpecs::Child.protected_method_defined?(:protected_child, true).should == true + ModuleSpecs::Child.protected_method_defined?(:accessor_method, true).should == false + ModuleSpecs::Child.protected_method_defined?(:private_child, true).should == false + + # Defined in Parent + ModuleSpecs::Child.protected_method_defined?(:public_parent, true).should == false + ModuleSpecs::Child.protected_method_defined?(:protected_parent, true).should == true + ModuleSpecs::Child.protected_method_defined?(:private_parent, true).should == false + + # Defined in Module + ModuleSpecs::Child.protected_method_defined?(:public_module, true).should == false + ModuleSpecs::Child.protected_method_defined?(:protected_module, true).should == true + ModuleSpecs::Child.protected_method_defined?(:private_module, true).should == false + + # Defined in SuperModule + ModuleSpecs::Child.protected_method_defined?(:public_super_module, true).should == false + ModuleSpecs::Child.protected_method_defined?(:protected_super_module, true).should == true + ModuleSpecs::Child.protected_method_defined?(:private_super_module, true).should == false + end + end + + describe "when passed false as a second optional argument" do + it "checks only the class itself" do + ModuleSpecs::Child.protected_method_defined?(:public_child, false).should == false + ModuleSpecs::Child.protected_method_defined?(:protected_child, false).should == true + ModuleSpecs::Child.protected_method_defined?(:accessor_method, false).should == false + ModuleSpecs::Child.protected_method_defined?(:private_child, false).should == false + + # Defined in Parent + ModuleSpecs::Child.protected_method_defined?(:public_parent, false).should == false + ModuleSpecs::Child.protected_method_defined?(:protected_parent, false).should == false + ModuleSpecs::Child.protected_method_defined?(:private_parent, false).should == false + + # Defined in Module + ModuleSpecs::Child.protected_method_defined?(:public_module, false).should == false + ModuleSpecs::Child.protected_method_defined?(:protected_module, false).should == false + ModuleSpecs::Child.protected_method_defined?(:private_module, false).should == false + + # Defined in SuperModule + ModuleSpecs::Child.protected_method_defined?(:public_super_module, false).should == false + ModuleSpecs::Child.protected_method_defined?(:protected_super_module, false).should == false + ModuleSpecs::Child.protected_method_defined?(:private_super_module, false).should == false + end + end +end diff --git a/spec/ruby/core/module/protected_spec.rb b/spec/ruby/core/module/protected_spec.rb new file mode 100644 index 0000000000..9e37223e18 --- /dev/null +++ b/spec/ruby/core/module/protected_spec.rb @@ -0,0 +1,57 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/set_visibility' + +describe "Module#protected" do + before :each do + class << ModuleSpecs::Parent + def protected_method_1; 5; end + end + end + + it_behaves_like :set_visibility, :protected + + it "makes an existing class method protected" do + ModuleSpecs::Parent.protected_method_1.should == 5 + + class << ModuleSpecs::Parent + protected :protected_method_1 + end + + -> { ModuleSpecs::Parent.protected_method_1 }.should raise_error(NoMethodError) + end + + it "makes a public Object instance method protected in a new module" do + m = Module.new do + protected :module_specs_public_method_on_object + end + + m.should have_protected_instance_method(:module_specs_public_method_on_object) + + # Ensure we did not change Object's method + Object.should_not have_protected_instance_method(:module_specs_public_method_on_object) + end + + it "makes a public Object instance method protected in Kernel" do + Kernel.should have_protected_instance_method( + :module_specs_public_method_on_object_for_kernel_protected) + Object.should_not have_protected_instance_method( + :module_specs_public_method_on_object_for_kernel_protected) + end + + it "returns argument or arguments if given" do + (class << Object.new; self; end).class_eval do + def foo; end + protected(:foo).should equal(:foo) + protected([:foo, :foo]).should == [:foo, :foo] + protected(:foo, :foo).should == [:foo, :foo] + protected.should equal(nil) + end + end + + it "raises a NameError when given an undefined name" do + -> do + Module.new.send(:protected, :undefined) + end.should raise_error(NameError) + end +end diff --git a/spec/ruby/core/module/public_class_method_spec.rb b/spec/ruby/core/module/public_class_method_spec.rb new file mode 100644 index 0000000000..71b20acda5 --- /dev/null +++ b/spec/ruby/core/module/public_class_method_spec.rb @@ -0,0 +1,94 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Module#public_class_method" do + before :each do + class << ModuleSpecs::Parent + private + def public_method_1; end + def public_method_2; end + end + end + + after :each do + class << ModuleSpecs::Parent + remove_method :public_method_1 + remove_method :public_method_2 + end + end + + it "makes an existing class method public" do + -> { ModuleSpecs::Parent.public_method_1 }.should raise_error(NoMethodError) + ModuleSpecs::Parent.public_class_method :public_method_1 + ModuleSpecs::Parent.public_method_1.should == nil + + # Technically above we're testing the Singleton classes, class method(right?). + # Try a "real" class method set public. + ModuleSpecs::Parent.public_method.should == nil + end + + it "makes an existing class method public up the inheritance tree" do + ModuleSpecs::Child.private_class_method :public_method_1 + -> { ModuleSpecs::Child.public_method_1 }.should raise_error(NoMethodError) + ModuleSpecs::Child.public_class_method :public_method_1 + + ModuleSpecs::Child.public_method_1.should == nil + ModuleSpecs::Child.public_method.should == nil + end + + it "accepts more than one method at a time" do + -> { ModuleSpecs::Parent.public_method_1 }.should raise_error(NameError) + -> { ModuleSpecs::Parent.public_method_2 }.should raise_error(NameError) + + ModuleSpecs::Child.public_class_method :public_method_1, :public_method_2 + + ModuleSpecs::Child.public_method_1.should == nil + ModuleSpecs::Child.public_method_2.should == nil + end + + it "raises a NameError if class method doesn't exist" do + -> do + ModuleSpecs.public_class_method :no_method_here + end.should raise_error(NameError) + end + + it "makes a class method public" do + c = Class.new do + def self.foo() "foo" end + public_class_method :foo + end + + c.foo.should == "foo" + end + + it "raises a NameError when the given name is not a method" do + -> do + Class.new do + public_class_method :foo + end + end.should raise_error(NameError) + end + + it "raises a NameError when the given name is an instance method" do + -> do + Class.new do + def foo() "foo" end + public_class_method :foo + end + end.should raise_error(NameError) + end + + context "when single argument is passed and is an array" do + it "makes a class method public" do + c = Class.new do + class << self + private + def foo() "foo" end + end + public_class_method [:foo] + end + + c.foo.should == "foo" + end + end +end diff --git a/spec/ruby/core/module/public_constant_spec.rb b/spec/ruby/core/module/public_constant_spec.rb new file mode 100644 index 0000000000..e624d45fd2 --- /dev/null +++ b/spec/ruby/core/module/public_constant_spec.rb @@ -0,0 +1,38 @@ +require_relative '../../spec_helper' + +describe "Module#public_constant" do + it "can only be passed constant names defined in the target (self) module" do + cls1 = Class.new + cls1.const_set :Foo, true + cls2 = Class.new(cls1) + + -> do + cls2.send :public_constant, :Foo + end.should raise_error(NameError) + end + + it "accepts strings as constant names" do + cls = Class.new + cls.const_set :Foo, true + + cls.send :private_constant, :Foo + cls.send :public_constant, "Foo" + + cls::Foo.should == true + end + + # [ruby-list:48558] + it "accepts multiple names" do + mod = Module.new + mod.const_set :Foo, true + mod.const_set :Bar, true + + mod.send :private_constant, :Foo + mod.send :private_constant, :Bar + + mod.send :public_constant, :Foo, :Bar + + mod::Foo.should == true + mod::Bar.should == true + end +end diff --git a/spec/ruby/core/module/public_instance_method_spec.rb b/spec/ruby/core/module/public_instance_method_spec.rb new file mode 100644 index 0000000000..ba19ad0404 --- /dev/null +++ b/spec/ruby/core/module/public_instance_method_spec.rb @@ -0,0 +1,65 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Module#public_instance_method" do + it "is a public method" do + Module.should have_public_instance_method(:public_instance_method, false) + end + + it "requires an argument" do + Module.new.method(:public_instance_method).arity.should == 1 + end + + describe "when given a public method name" do + it "returns an UnboundMethod corresponding to the defined Module" do + ret = ModuleSpecs::Super.public_instance_method(:public_module) + ret.should be_an_instance_of(UnboundMethod) + ret.owner.should equal(ModuleSpecs::Basic) + + ret = ModuleSpecs::Super.public_instance_method(:public_super_module) + ret.should be_an_instance_of(UnboundMethod) + ret.owner.should equal(ModuleSpecs::Super) + end + + it "accepts if the name is a Symbol or String" do + ret = ModuleSpecs::Basic.public_instance_method(:public_module) + ModuleSpecs::Basic.public_instance_method("public_module").should == ret + end + end + + it "raises a TypeError when given a name is not Symbol or String" do + -> { Module.new.public_instance_method(nil) }.should raise_error(TypeError) + end + + it "raises a NameError when given a protected method name" do + -> do + ModuleSpecs::Basic.public_instance_method(:protected_module) + end.should raise_error(NameError) + end + + it "raises a NameError if the method is private" do + -> do + ModuleSpecs::Basic.public_instance_method(:private_module) + end.should raise_error(NameError) + end + + it "raises a NameError if the method has been undefined" do + -> do + ModuleSpecs::Parent.public_instance_method(:undefed_method) + end.should raise_error(NameError) + end + + it "raises a NameError if the method does not exist" do + -> do + Module.new.public_instance_method(:missing) + end.should raise_error(NameError) + end + + it "sets the NameError#name attribute to the name of the missing method" do + begin + Module.new.public_instance_method(:missing) + rescue NameError => e + e.name.should == :missing + end + end +end diff --git a/spec/ruby/core/module/public_instance_methods_spec.rb b/spec/ruby/core/module/public_instance_methods_spec.rb new file mode 100644 index 0000000000..ae7d9b5ffb --- /dev/null +++ b/spec/ruby/core/module/public_instance_methods_spec.rb @@ -0,0 +1,61 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative '../../fixtures/reflection' + +# TODO: rewrite + +describe "Module#public_instance_methods" do + it "returns a list of public methods in module and its ancestors" do + methods = ModuleSpecs::CountsMixin.public_instance_methods + methods.should include(:public_3) + + methods = ModuleSpecs::CountsParent.public_instance_methods + methods.should include(:public_3) + methods.should include(:public_2) + + methods = ModuleSpecs::CountsChild.public_instance_methods + methods.should include(:public_3) + methods.should include(:public_2) + methods.should include(:public_1) + + methods = ModuleSpecs::Child2.public_instance_methods + methods.should include(:foo) + end + + it "when passed false as a parameter, should return only methods defined in that module" do + ModuleSpecs::CountsMixin.public_instance_methods(false).should == [:public_3] + ModuleSpecs::CountsParent.public_instance_methods(false).should == [:public_2] + ModuleSpecs::CountsChild.public_instance_methods(false).should == [:public_1] + end + + it "default list should be the same as passing true as an argument" do + ModuleSpecs::CountsMixin.public_instance_methods(true).should == + ModuleSpecs::CountsMixin.public_instance_methods + ModuleSpecs::CountsParent.public_instance_methods(true).should == + ModuleSpecs::CountsParent.public_instance_methods + ModuleSpecs::CountsChild.public_instance_methods(true).should == + ModuleSpecs::CountsChild.public_instance_methods + end +end + +describe :module_public_instance_methods_supers, shared: true do + it "returns a unique list for a class including a module" do + m = ReflectSpecs::D.public_instance_methods(*@object) + m.select { |x| x == :pub }.sort.should == [:pub] + end + + it "returns a unique list for a subclass" do + m = ReflectSpecs::E.public_instance_methods(*@object) + m.select { |x| x == :pub }.sort.should == [:pub] + end +end + +describe "Module#public_instance_methods" do + describe "when not passed an argument" do + it_behaves_like :module_public_instance_methods_supers, nil, [] + end + + describe "when passed true" do + it_behaves_like :module_public_instance_methods_supers, nil, true + end +end diff --git a/spec/ruby/core/module/public_method_defined_spec.rb b/spec/ruby/core/module/public_method_defined_spec.rb new file mode 100644 index 0000000000..5c9bdf1ccc --- /dev/null +++ b/spec/ruby/core/module/public_method_defined_spec.rb @@ -0,0 +1,72 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Module#public_method_defined?" do + it "returns true if the named public method is defined by module or its ancestors" do + ModuleSpecs::CountsMixin.public_method_defined?("public_3").should == true + + ModuleSpecs::CountsParent.public_method_defined?("public_3").should == true + ModuleSpecs::CountsParent.public_method_defined?("public_2").should == true + + ModuleSpecs::CountsChild.public_method_defined?("public_3").should == true + ModuleSpecs::CountsChild.public_method_defined?("public_2").should == true + ModuleSpecs::CountsChild.public_method_defined?("public_1").should == true + end + + it "returns false if method is not a public method" do + ModuleSpecs::CountsChild.public_method_defined?("private_3").should == false + ModuleSpecs::CountsChild.public_method_defined?("private_2").should == false + ModuleSpecs::CountsChild.public_method_defined?("private_1").should == false + + ModuleSpecs::CountsChild.public_method_defined?("protected_3").should == false + ModuleSpecs::CountsChild.public_method_defined?("protected_2").should == false + ModuleSpecs::CountsChild.public_method_defined?("protected_1").should == false + end + + it "returns false if the named method is not defined by the module or its ancestors" do + ModuleSpecs::CountsMixin.public_method_defined?(:public_10).should == false + end + + it "accepts symbols for the method name" do + ModuleSpecs::CountsMixin.public_method_defined?(:public_3).should == true + end + + it "raises a TypeError if passed an Integer" do + -> do + ModuleSpecs::CountsMixin.public_method_defined?(1) + end.should raise_error(TypeError) + end + + it "raises a TypeError if passed nil" do + -> do + ModuleSpecs::CountsMixin.public_method_defined?(nil) + end.should raise_error(TypeError) + end + + it "raises a TypeError if passed false" do + -> do + ModuleSpecs::CountsMixin.public_method_defined?(false) + end.should raise_error(TypeError) + end + + it "raises a TypeError if passed an object that does not defined #to_str" do + -> do + ModuleSpecs::CountsMixin.public_method_defined?(mock('x')) + end.should raise_error(TypeError) + end + + it "raises a TypeError if passed an object that defines #to_sym" do + sym = mock('symbol') + def sym.to_sym() :public_3 end + + -> do + ModuleSpecs::CountsMixin.public_method_defined?(sym) + end.should raise_error(TypeError) + end + + it "calls #to_str to convert an Object" do + str = mock('public_3') + def str.to_str() 'public_3' end + ModuleSpecs::CountsMixin.public_method_defined?(str).should == true + end +end diff --git a/spec/ruby/core/module/public_spec.rb b/spec/ruby/core/module/public_spec.rb new file mode 100644 index 0000000000..ce31eb5d0e --- /dev/null +++ b/spec/ruby/core/module/public_spec.rb @@ -0,0 +1,45 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/set_visibility' + +describe "Module#public" do + it_behaves_like :set_visibility, :public + + it "on a superclass method calls the redefined method" do + ModuleSpecs::ChildPrivateMethodMadePublic.new.private_method_redefined.should == :after_redefinition + end + + it "makes a private Object instance method public in a new module" do + m = Module.new do + public :module_specs_private_method_on_object + end + + m.should have_public_instance_method(:module_specs_private_method_on_object) + + # Ensure we did not change Object's method + Object.should_not have_public_instance_method(:module_specs_private_method_on_object) + end + + it "makes a private Object instance method public in Kernel" do + Kernel.should have_public_instance_method( + :module_specs_private_method_on_object_for_kernel_public) + Object.should_not have_public_instance_method( + :module_specs_private_method_on_object_for_kernel_public) + end + + it "returns argument or arguments if given" do + (class << Object.new; self; end).class_eval do + def foo; end + public(:foo).should equal(:foo) + public([:foo, :foo]).should == [:foo, :foo] + public(:foo, :foo).should == [:foo, :foo] + public.should equal(nil) + end + end + + it "raises a NameError when given an undefined name" do + -> do + Module.new.send(:public, :undefined) + end.should raise_error(NameError) + end +end diff --git a/spec/ruby/core/module/refine_spec.rb b/spec/ruby/core/module/refine_spec.rb new file mode 100644 index 0000000000..d219b98825 --- /dev/null +++ b/spec/ruby/core/module/refine_spec.rb @@ -0,0 +1,714 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/refine' + +describe "Module#refine" do + it "runs its block in an anonymous module" do + inner_self = nil + mod = Module.new do + refine String do + inner_self = self + end + end + + mod.should_not == inner_self + inner_self.should be_kind_of(Module) + inner_self.name.should == nil + end + + it "uses the same anonymous module for future refines of the same class" do + selves = [] + mod = Module.new do + refine String do + selves << self + end + end + + mod.module_eval do + refine String do + selves << self + end + end + + selves[0].should == selves[1] + end + + it "adds methods defined in its block to the anonymous module's public instance methods" do + inner_self = nil + mod = Module.new do + refine String do + def blah + "blah" + end + inner_self = self + end + end + + inner_self.public_instance_methods.should include(:blah) + end + + it "returns created anonymous module" do + inner_self = nil + result = nil + mod = Module.new do + result = refine String do + inner_self = self + end + end + + result.should == inner_self + end + + it "raises ArgumentError if not passed an argument" do + -> do + Module.new do + refine {} + end + end.should raise_error(ArgumentError) + end + + it "raises TypeError if not passed a class" do + -> do + Module.new do + refine("foo") {} + end + end.should raise_error(TypeError, "wrong argument type String (expected Class or Module)") + end + + it "accepts a module as argument" do + inner_self = nil + Module.new do + refine(Enumerable) do + def blah + end + inner_self = self + end + end + + inner_self.public_instance_methods.should include(:blah) + end + + it "applies refinements to the module" do + refinement = Module.new do + refine(Enumerable) do + def foo? + self.any? ? "yes" : "no" + end + end + end + + foo = Class.new do + using refinement + + def initialize(items) + @items = items + end + + def result + @items.foo? + end + end + + foo.new([]).result.should == "no" + foo.new([1]).result.should == "yes" + end + + it "raises ArgumentError if not given a block" do + -> do + Module.new do + refine String + end + end.should raise_error(ArgumentError) + end + + it "applies refinements to calls in the refine block" do + result = nil + Module.new do + refine(String) do + def foo; "foo"; end + result = "hello".foo + end + end + result.should == "foo" + end + + it "doesn't apply refinements outside the refine block" do + Module.new do + refine(String) {def foo; "foo"; end} + -> { + "hello".foo + }.should raise_error(NoMethodError) + end + end + + it "does not apply refinements to external scopes not using the module" do + Module.new do + refine(String) {def foo; 'foo'; end} + end + + -> {"hello".foo}.should raise_error(NoMethodError) + end + + # When defining multiple refinements in the same module, + # inside a refine block all refinements from the same + # module are active when a refined method is called + it "makes available all refinements from the same module" do + refinement = Module.new do + refine Integer do + def to_json_format + to_s + end + end + + refine Array do + def to_json_format + "[" + map { |i| i.to_json_format }.join(", ") + "]" + end + end + + refine Hash do + def to_json_format + "{" + map { |k, v| k.to_s.dump + ": " + v.to_json_format }.join(", ") + "}" + end + end + end + + result = nil + + Module.new do + using refinement + + result = [{1 => 2}, {3 => 4}].to_json_format + end + + result.should == '[{"1": 2}, {"3": 4}]' + end + + it "does not make available methods from another refinement module" do + refinery_integer = Module.new do + refine Integer do + def to_json_format + to_s + end + end + end + + refinery_array = Module.new do + refine Array do + def to_json_format + "[" + map { |i| i.to_json_format }.join(",") + "]" + end + end + end + + result = nil + + -> { + Module.new do + using refinery_integer + using refinery_array + + [1, 2].to_json_format + end + }.should raise_error(NoMethodError) + end + + # method lookup: + # * The prepended modules from the refinement for C + # * The refinement for C + # * The included modules from the refinement for C + # * The prepended modules of C + # * C + # * The included modules of C + describe "method lookup" do + it "looks in the object singleton class first" do + refined_class = ModuleSpecs.build_refined_class + + refinement = Module.new do + refine refined_class do + def foo; "foo from refinement"; end + end + end + + result = nil + Module.new do + using refinement + + obj = refined_class.new + class << obj + def foo; "foo from singleton class"; end + end + result = obj.foo + end + + result.should == "foo from singleton class" + end + + it "looks in later included modules of the refined module first" do + a = Module.new do + def foo + "foo from A" + end + end + + include_me_later = Module.new do + def foo + "foo from IncludeMeLater" + end + end + + c = Class.new do + include a + end + + refinement = Module.new do + refine c do; end + end + + result = nil + Module.new do + using refinement + c.include include_me_later + result = c.new.foo + end + + result.should == "foo from IncludeMeLater" + end + + it "looks in the class then" do + refined_class = ModuleSpecs.build_refined_class + + refinement = Module.new do + refine(refined_class) { } + end + + result = nil + Module.new do + using refinement + result = refined_class.new.foo + end + + result.should == "foo" + end + end + + + # methods in a subclass have priority over refinements in a superclass + it "does not override methods in subclasses" do + refined_class = ModuleSpecs.build_refined_class + + subclass = Class.new(refined_class) do + def foo; "foo from subclass"; end + end + + refinement = Module.new do + refine refined_class do + def foo; "foo from refinement"; end + end + end + + result = nil + Module.new do + using refinement + result = subclass.new.foo + end + + result.should == "foo from subclass" + end + + context "for methods accessed indirectly" do + it "is honored by Kernel#send" do + refined_class = ModuleSpecs.build_refined_class + + refinement = Module.new do + refine refined_class do + def foo; "foo from refinement"; end + end + end + + result = nil + Module.new do + using refinement + result = refined_class.new.send :foo + end + + result.should == "foo from refinement" + end + + it "is honored by BasicObject#__send__" do + refined_class = ModuleSpecs.build_refined_class + + refinement = Module.new do + refine refined_class do + def foo; "foo from refinement"; end + end + end + + result = nil + Module.new do + using refinement + result = refined_class.new.__send__ :foo + end + + result.should == "foo from refinement" + end + + it "is honored by Symbol#to_proc" do + refinement = Module.new do + refine Integer do + def to_s + "(#{super})" + end + end + end + + result = nil + Module.new do + using refinement + result = [1, 2, 3].map(&:to_s) + end + + result.should == ["(1)", "(2)", "(3)"] + end + + it "is honored by Kernel#public_send" do + refined_class = ModuleSpecs.build_refined_class + + refinement = Module.new do + refine refined_class do + def foo; "foo from refinement"; end + end + end + + result = nil + Module.new do + using refinement + result = refined_class.new.public_send :foo + end + + result.should == "foo from refinement" + end + + it "is honored by string interpolation" do + refinement = Module.new do + refine Integer do + def to_s + "foo" + end + end + end + + result = nil + Module.new do + using refinement + result = "#{1}" + end + + result.should == "foo" + end + + it "is honored by Kernel#binding" do + refinement = Module.new do + refine String do + def to_s + "hello from refinement" + end + end + end + + klass = Class.new do + using refinement + + def foo + "foo".to_s + end + + def get_binding + binding + end + end + + result = Kernel.eval("self.foo()", klass.new.get_binding) + result.should == "hello from refinement" + end + + it "is honored by Kernel#method" do + klass = Class.new + refinement = Module.new do + refine klass do + def foo; end + end + end + + result = nil + Module.new do + using refinement + result = klass.new.method(:foo).class + end + + result.should == Method + end + + it "is honored by Kernel#public_method" do + klass = Class.new + refinement = Module.new do + refine klass do + def foo; end + end + end + + result = nil + Module.new do + using refinement + result = klass.new.public_method(:foo).class + end + + result.should == Method + end + + it "is honored by Kernel#instance_method" do + klass = Class.new + refinement = Module.new do + refine klass do + def foo; end + end + end + + result = nil + Module.new do + using refinement + result = klass.instance_method(:foo).class + end + + result.should == UnboundMethod + end + + it "is honored by Kernel#respond_to?" do + klass = Class.new + refinement = Module.new do + refine klass do + def foo; end + end + end + + result = nil + Module.new do + using refinement + result = klass.new.respond_to?(:foo) + end + + result.should == true + end + + it "is honored by &" do + refinement = Module.new do + refine String do + def to_proc(*args) + -> * { 'foo' } + end + end + end + + result = nil + Module.new do + using refinement + result = ["hola"].map(&"upcase") + end + + result.should == ['foo'] + end + end + + context "when super is called in a refinement" do + it "looks in the refined class" do + refined_class = ModuleSpecs.build_refined_class + + refinement = Module.new do + refine refined_class do + def foo + super + end + end + end + + result = nil + Module.new do + using refinement + result = refined_class.new.foo + end + + result.should == "foo" + end + + # super in a method of a refinement invokes the method in the refined + # class even if there is another refinement which has been activated + # in the same context. + it "looks in the refined class first if called from refined method" do + refined_class = ModuleSpecs.build_refined_class(for_super: true) + + refinement = Module.new do + refine refined_class do + def foo + [:R1] + end + end + end + + refinement_with_super = Module.new do + refine refined_class do + def foo + [:R2] + super + end + end + end + + result = nil + Module.new do + using refinement + using refinement_with_super + result = refined_class.new.foo + end + + result.should == [:R2, :C] + end + + it "looks only in the refined class even if there is another active refinement" do + refined_class = ModuleSpecs.build_refined_class(for_super: true) + + refinement = Module.new do + refine refined_class do + def bar + "you cannot see me from super because I belong to another active R" + end + end + end + + refinement_with_super = Module.new do + refine refined_class do + def bar + super + end + end + end + + + Module.new do + using refinement + using refinement_with_super + -> { + refined_class.new.bar + }.should raise_error(NoMethodError) + end + end + end + + it 'and alias aliases a method within a refinement module, but not outside it' do + Module.new do + using Module.new { + refine Array do + alias :orig_count :count + end + } + [1,2].orig_count.should == 2 + end + -> { [1,2].orig_count }.should raise_error(NoMethodError) + end + + it 'and alias_method aliases a method within a refinement module, but not outside it' do + Module.new do + using Module.new { + refine Array do + alias_method :orig_count, :count + end + } + [1,2].orig_count.should == 2 + end + -> { [1,2].orig_count }.should raise_error(NoMethodError) + end + + it "and instance_methods returns a list of methods including those of the refined module" do + methods = Array.instance_methods + methods_2 = [] + Module.new do + refine Array do + methods_2 = instance_methods + end + end + methods.should == methods_2 + end + + # Refinements are inherited by module inclusion. + # That is, using activates all refinements in the ancestors of the specified module. + # Refinements in a descendant have priority over refinements in an ancestor. + context "module inclusion" do + it "activates all refinements from all ancestors" do + refinement_included = Module.new do + refine Integer do + def to_json_format + to_s + end + end + end + + refinement = Module.new do + include refinement_included + + refine Array do + def to_json_format + "[" + map { |i| i.to_s }.join(", ") + "]" + end + end + end + + result = nil + Module.new do + using refinement + result = [5.to_json_format, [1, 2, 3].to_json_format] + end + + result.should == ["5", "[1, 2, 3]"] + end + + it "overrides methods of ancestors by methods in descendants" do + refinement_included = Module.new do + refine Integer do + def to_json_format + to_s + end + end + end + + refinement = Module.new do + include refinement_included + + refine Integer do + def to_json_format + "hello from refinement" + end + end + end + + result = nil + Module.new do + using refinement + result = 5.to_json_format + end + + result.should == "hello from refinement" + end + end + + it 'does not list methods defined only in refinement' do + refine_object = Module.new do + refine Object do + def refinement_only_method + end + end + end + spec = self + klass = Class.new { instance_methods.should_not spec.send(:include, :refinement_only_method) } + instance = klass.new + instance.methods.should_not include :refinement_only_method + instance.respond_to?(:refinement_only_method).should == false + -> { instance.method :refinement_only_method }.should raise_error(NameError) + end +end diff --git a/spec/ruby/core/module/refinements_spec.rb b/spec/ruby/core/module/refinements_spec.rb new file mode 100644 index 0000000000..05658a8b0e --- /dev/null +++ b/spec/ruby/core/module/refinements_spec.rb @@ -0,0 +1,43 @@ +require_relative '../../spec_helper' + +describe "Module#refinements" do + it "returns refinements defined in a module" do + ScratchPad.record [] + + m = Module.new do + refine String do + ScratchPad << self + end + + refine Array do + ScratchPad << self + end + end + + m.refinements.sort_by(&:object_id).should == ScratchPad.recorded.sort_by(&:object_id) + end + + it "does not return refinements defined in the included module" do + ScratchPad.record [] + + m1 = Module.new do + refine Integer do + nil + end + end + + m2 = Module.new do + include m1 + + refine String do + ScratchPad << self + end + end + + m2.refinements.should == ScratchPad.recorded + end + + it "returns an empty array if no refinements defined in a module" do + Module.new.refinements.should == [] + end +end diff --git a/spec/ruby/core/module/remove_class_variable_spec.rb b/spec/ruby/core/module/remove_class_variable_spec.rb new file mode 100644 index 0000000000..ab9514adf6 --- /dev/null +++ b/spec/ruby/core/module/remove_class_variable_spec.rb @@ -0,0 +1,44 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Module#remove_class_variable" do + it "removes class variable" do + m = ModuleSpecs::MVars.dup + m.send(:remove_class_variable, :@@mvar) + m.class_variable_defined?(:@@mvar).should == false + end + + it "returns the value of removing class variable" do + m = ModuleSpecs::MVars.dup + m.send(:remove_class_variable, :@@mvar).should == :mvar + end + + it "removes a class variable defined in a metaclass" do + obj = mock("metaclass class variable") + meta = obj.singleton_class + meta.send :class_variable_set, :@@var, 1 + meta.send(:remove_class_variable, :@@var).should == 1 + meta.class_variable_defined?(:@@var).should be_false + end + + it "raises a NameError when removing class variable declared in included module" do + c = ModuleSpecs::RemoveClassVariable.new { include ModuleSpecs::MVars.dup } + -> { c.send(:remove_class_variable, :@@mvar) }.should raise_error(NameError) + end + + it "raises a NameError when passed a symbol with one leading @" do + -> { ModuleSpecs::MVars.send(:remove_class_variable, :@mvar) }.should raise_error(NameError) + end + + it "raises a NameError when passed a symbol with no leading @" do + -> { ModuleSpecs::MVars.send(:remove_class_variable, :mvar) }.should raise_error(NameError) + end + + it "raises a NameError when an uninitialized class variable is given" do + -> { ModuleSpecs::MVars.send(:remove_class_variable, :@@nonexisting_class_variable) }.should raise_error(NameError) + end + + it "is public" do + Module.should_not have_private_instance_method(:remove_class_variable) + end +end diff --git a/spec/ruby/core/module/remove_const_spec.rb b/spec/ruby/core/module/remove_const_spec.rb new file mode 100644 index 0000000000..35a9d65105 --- /dev/null +++ b/spec/ruby/core/module/remove_const_spec.rb @@ -0,0 +1,107 @@ +require_relative '../../spec_helper' +require_relative '../../fixtures/constants' + +describe "Module#remove_const" do + it "removes the constant specified by a String or Symbol from the receiver's constant table" do + ConstantSpecs::ModuleM::CS_CONST252 = :const252 + ConstantSpecs::ModuleM::CS_CONST252.should == :const252 + + ConstantSpecs::ModuleM.send :remove_const, :CS_CONST252 + -> { ConstantSpecs::ModuleM::CS_CONST252 }.should raise_error(NameError) + + ConstantSpecs::ModuleM::CS_CONST253 = :const253 + ConstantSpecs::ModuleM::CS_CONST253.should == :const253 + + ConstantSpecs::ModuleM.send :remove_const, "CS_CONST253" + -> { ConstantSpecs::ModuleM::CS_CONST253 }.should raise_error(NameError) + end + + it "returns the value of the removed constant" do + ConstantSpecs::ModuleM::CS_CONST254 = :const254 + ConstantSpecs::ModuleM.send(:remove_const, :CS_CONST254).should == :const254 + end + + it "raises a NameError and does not call #const_missing if the constant is not defined" do + ConstantSpecs.should_not_receive(:const_missing) + -> { ConstantSpecs.send(:remove_const, :Nonexistent) }.should raise_error(NameError) + end + + it "raises a NameError and does not call #const_missing if the constant is not defined directly in the module" do + begin + ConstantSpecs::ModuleM::CS_CONST255 = :const255 + ConstantSpecs::ContainerA::CS_CONST255.should == :const255 + ConstantSpecs::ContainerA.should_not_receive(:const_missing) + + -> do + ConstantSpecs::ContainerA.send :remove_const, :CS_CONST255 + end.should raise_error(NameError) + ensure + ConstantSpecs::ModuleM.send :remove_const, "CS_CONST255" + end + end + + it "raises a NameError if the name does not start with a capital letter" do + -> { ConstantSpecs.send :remove_const, "name" }.should raise_error(NameError) + end + + it "raises a NameError if the name starts with a non-alphabetic character" do + -> { ConstantSpecs.send :remove_const, "__CONSTX__" }.should raise_error(NameError) + -> { ConstantSpecs.send :remove_const, "@Name" }.should raise_error(NameError) + -> { ConstantSpecs.send :remove_const, "!Name" }.should raise_error(NameError) + -> { ConstantSpecs.send :remove_const, "::Name" }.should raise_error(NameError) + end + + it "raises a NameError if the name contains non-alphabetic characters except '_'" do + ConstantSpecs::ModuleM::CS_CONST256 = :const256 + ConstantSpecs::ModuleM.send :remove_const, "CS_CONST256" + -> { ConstantSpecs.send :remove_const, "Name=" }.should raise_error(NameError) + -> { ConstantSpecs.send :remove_const, "Name?" }.should raise_error(NameError) + end + + it "calls #to_str to convert the given name to a String" do + ConstantSpecs::CS_CONST257 = :const257 + name = mock("CS_CONST257") + name.should_receive(:to_str).and_return("CS_CONST257") + ConstantSpecs.send(:remove_const, name).should == :const257 + end + + it "raises a TypeError if conversion to a String by calling #to_str fails" do + name = mock('123') + -> { ConstantSpecs.send :remove_const, name }.should raise_error(TypeError) + + name.should_receive(:to_str).and_return(123) + -> { ConstantSpecs.send :remove_const, name }.should raise_error(TypeError) + end + + it "is a private method" do + Module.private_methods.should include(:remove_const) + end + + it "returns nil when removing autoloaded constant" do + ConstantSpecs.autoload :AutoloadedConstant, 'a_file' + ConstantSpecs.send(:remove_const, :AutoloadedConstant).should be_nil + end + + it "updates the constant value" do + module ConstantSpecs::RemovedConstantUpdate + module M + FOO = 'm' + end + + module A + include M + FOO = 'a' + def self.foo + FOO + end + end + + A.foo.should == 'a' + + A.send(:remove_const,:FOO) + A.foo.should == 'm' + end + ensure + ConstantSpecs.send(:remove_const, :RemovedConstantUpdate) + end +end diff --git a/spec/ruby/core/module/remove_method_spec.rb b/spec/ruby/core/module/remove_method_spec.rb new file mode 100644 index 0000000000..94b255df62 --- /dev/null +++ b/spec/ruby/core/module/remove_method_spec.rb @@ -0,0 +1,131 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +module ModuleSpecs + class Parent + def method_to_remove; 1; end + end + + class First + def method_to_remove; 1; end + end + + class Second < First + def method_to_remove; 2; end + end +end + +describe "Module#remove_method" do + before :each do + @module = Module.new { def method_to_remove; end } + end + + it "is a public method" do + Module.should have_public_instance_method(:remove_method, false) + end + + it "removes the method from a class" do + klass = Class.new do + def method_to_remove; 1; end + end + x = klass.new + klass.send(:remove_method, :method_to_remove) + x.respond_to?(:method_to_remove).should == false + end + + it "removes method from subclass, but not parent" do + child = Class.new(ModuleSpecs::Parent) do + def method_to_remove; 2; end + remove_method :method_to_remove + end + x = child.new + x.respond_to?(:method_to_remove).should == true + x.method_to_remove.should == 1 + end + + it "updates the method implementation" do + m_module = Module.new do + def foo + 'm' + end + end + + a_class = Class.new do + include m_module + + def foo + 'a' + end + end + + a = a_class.new + foo = -> { a.foo } + foo.call.should == 'a' + a_class.remove_method(:foo) + foo.call.should == 'm' + end + + it "removes multiple methods with 1 call" do + klass = Class.new do + def method_to_remove_1; 1; end + def method_to_remove_2; 2; end + remove_method :method_to_remove_1, :method_to_remove_2 + end + x = klass.new + x.respond_to?(:method_to_remove_1).should == false + x.respond_to?(:method_to_remove_2).should == false + end + + it "accepts multiple arguments" do + Module.instance_method(:remove_method).arity.should < 0 + end + + it "does not remove any instance methods when argument not given" do + before = @module.instance_methods(true) + @module.private_instance_methods(true) + @module.send :remove_method + after = @module.instance_methods(true) + @module.private_instance_methods(true) + before.sort.should == after.sort + end + + it "returns self" do + @module.send(:remove_method, :method_to_remove).should equal(@module) + end + + it "raises a NameError when attempting to remove method further up the inheritance tree" do + Class.new(ModuleSpecs::Second) do + -> { + remove_method :method_to_remove + }.should raise_error(NameError) + end + end + + it "raises a NameError when attempting to remove a missing method" do + Class.new(ModuleSpecs::Second) do + -> { + remove_method :blah + }.should raise_error(NameError) + end + end + + describe "on frozen instance" do + before :each do + @frozen = @module.dup.freeze + end + + it "raises a FrozenError when passed a name" do + -> { @frozen.send :remove_method, :method_to_remove }.should raise_error(FrozenError) + end + + it "raises a FrozenError when passed a missing name" do + -> { @frozen.send :remove_method, :not_exist }.should raise_error(FrozenError) + end + + it "raises a TypeError when passed a not name" do + -> { @frozen.send :remove_method, Object.new }.should raise_error(TypeError) + end + + it "does not raise exceptions when no arguments given" do + @frozen.send(:remove_method).should equal(@frozen) + end + end +end diff --git a/spec/ruby/core/module/ruby2_keywords_spec.rb b/spec/ruby/core/module/ruby2_keywords_spec.rb new file mode 100644 index 0000000000..652f9f7083 --- /dev/null +++ b/spec/ruby/core/module/ruby2_keywords_spec.rb @@ -0,0 +1,248 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Module#ruby2_keywords" do + class << self + ruby2_keywords def mark(*args) + args + end + end + + it "marks the final hash argument as keyword hash" do + last = mark(1, 2, a: "a").last + Hash.ruby2_keywords_hash?(last).should == true + end + + it "makes a copy of the hash and only marks the copy as keyword hash" do + obj = Object.new + obj.singleton_class.class_exec do + def regular(*args) + args.last + end + end + + h = {a: 1} + + last = mark(**h).last + Hash.ruby2_keywords_hash?(last).should == true + Hash.ruby2_keywords_hash?(h).should == false + + last2 = mark(**last).last # last is already marked + Hash.ruby2_keywords_hash?(last2).should == true + Hash.ruby2_keywords_hash?(last).should == true + last2.should_not.equal?(last) + Hash.ruby2_keywords_hash?(h).should == false + end + + it "makes a copy and unmark the Hash when calling a method taking (arg)" do + obj = Object.new + obj.singleton_class.class_exec do + def single(arg) + arg + end + end + + h = { a: 1 } + args = mark(**h) + marked = args.last + Hash.ruby2_keywords_hash?(marked).should == true + + after_usage = obj.single(*args) + after_usage.should == h + after_usage.should_not.equal?(h) + after_usage.should_not.equal?(marked) + Hash.ruby2_keywords_hash?(after_usage).should == false + Hash.ruby2_keywords_hash?(marked).should == true + end + + it "makes a copy and unmark the Hash when calling a method taking (**kw)" do + obj = Object.new + obj.singleton_class.class_exec do + def kwargs(**kw) + kw + end + end + + h = { a: 1 } + args = mark(**h) + marked = args.last + Hash.ruby2_keywords_hash?(marked).should == true + + after_usage = obj.kwargs(*args) + after_usage.should == h + after_usage.should_not.equal?(h) + after_usage.should_not.equal?(marked) + Hash.ruby2_keywords_hash?(after_usage).should == false + Hash.ruby2_keywords_hash?(marked).should == true + end + + it "makes a copy and unmark the Hash when calling a method taking (*args)" do + obj = Object.new + obj.singleton_class.class_exec do + def splat(*args) + args.last + end + + def splat1(arg, *args) + args.last + end + + def proc_call(*args) + -> *a { a.last }.call(*args) + end + end + + h = { a: 1 } + args = mark(**h) + marked = args.last + Hash.ruby2_keywords_hash?(marked).should == true + + after_usage = obj.splat(*args) + after_usage.should == h + after_usage.should_not.equal?(h) + after_usage.should_not.equal?(marked) + Hash.ruby2_keywords_hash?(after_usage).should == false + Hash.ruby2_keywords_hash?(marked).should == true + + args = mark(1, **h) + marked = args.last + after_usage = obj.splat1(*args) + after_usage.should == h + after_usage.should_not.equal?(h) + after_usage.should_not.equal?(marked) + Hash.ruby2_keywords_hash?(after_usage).should == false + Hash.ruby2_keywords_hash?(marked).should == true + + args = mark(**h) + marked = args.last + after_usage = obj.proc_call(*args) + after_usage.should == h + after_usage.should_not.equal?(h) + after_usage.should_not.equal?(marked) + Hash.ruby2_keywords_hash?(after_usage).should == false + Hash.ruby2_keywords_hash?(marked).should == true + + args = mark(**h) + marked = args.last + after_usage = obj.send(:splat, *args) + after_usage.should == h + after_usage.should_not.equal?(h) + after_usage.should_not.equal?(marked) + Hash.ruby2_keywords_hash?(after_usage).should == false + Hash.ruby2_keywords_hash?(marked).should == true + end + + it "applies to the underlying method and applies across aliasing" do + obj = Object.new + + obj.singleton_class.class_exec do + def foo(*a) a.last end + alias_method :bar, :foo + ruby2_keywords :foo + + def baz(*a) a.last end + ruby2_keywords :baz + alias_method :bob, :baz + end + + last = obj.foo(1, 2, a: "a") + Hash.ruby2_keywords_hash?(last).should == true + + last = obj.bar(1, 2, a: "a") + Hash.ruby2_keywords_hash?(last).should == true + + last = obj.baz(1, 2, a: "a") + Hash.ruby2_keywords_hash?(last).should == true + + last = obj.bob(1, 2, a: "a") + Hash.ruby2_keywords_hash?(last).should == true + end + + it "returns nil" do + obj = Object.new + + obj.singleton_class.class_exec do + def foo(*a) end + + ruby2_keywords(:foo).should == nil + end + end + + it "raises NameError when passed not existing method name" do + obj = Object.new + + -> { + obj.singleton_class.class_exec do + ruby2_keywords :not_existing + end + }.should raise_error(NameError, /undefined method [`']not_existing'/) + end + + it "accepts String as well" do + obj = Object.new + + obj.singleton_class.class_exec do + def foo(*a) a.last end + ruby2_keywords "foo" + end + + last = obj.foo(1, 2, a: "a") + Hash.ruby2_keywords_hash?(last).should == true + end + + it "raises TypeError when passed not Symbol or String" do + obj = Object.new + + -> { + obj.singleton_class.class_exec do + ruby2_keywords Object.new + end + }.should raise_error(TypeError, /is not a symbol nor a string/) + end + + it "prints warning when a method does not accept argument splat" do + obj = Object.new + def obj.foo(a, b, c) end + + -> { + obj.singleton_class.class_exec do + ruby2_keywords :foo + end + }.should complain(/Skipping set of ruby2_keywords flag for/) + end + + it "prints warning when a method accepts keywords" do + obj = Object.new + def obj.foo(*a, b:) end + + -> { + obj.singleton_class.class_exec do + ruby2_keywords :foo + end + }.should complain(/Skipping set of ruby2_keywords flag for/) + end + + it "prints warning when a method accepts keyword splat" do + obj = Object.new + def obj.foo(*a, **b) end + + -> { + obj.singleton_class.class_exec do + ruby2_keywords :foo + end + }.should complain(/Skipping set of ruby2_keywords flag for/) + end + + ruby_version_is "4.0" do + it "prints warning when a method accepts post arguments" do + obj = Object.new + def obj.foo(*a, b) end + + -> { + obj.singleton_class.class_exec do + ruby2_keywords :foo + end + }.should complain(/Skipping set of ruby2_keywords flag for/) + end + end +end diff --git a/spec/ruby/core/module/set_temporary_name_spec.rb b/spec/ruby/core/module/set_temporary_name_spec.rb new file mode 100644 index 0000000000..46605ed675 --- /dev/null +++ b/spec/ruby/core/module/set_temporary_name_spec.rb @@ -0,0 +1,147 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/set_temporary_name' + +ruby_version_is "3.3" do + describe "Module#set_temporary_name" do + it "can assign a temporary name" do + m = Module.new + m.name.should be_nil + + m.set_temporary_name("fake_name") + m.name.should == "fake_name" + + m.set_temporary_name(nil) + m.name.should be_nil + end + + it "returns self" do + m = Module.new + m.set_temporary_name("fake_name").should.equal? m + end + + it "can assign a temporary name which is not a valid constant path" do + m = Module.new + + m.set_temporary_name("name") + m.name.should == "name" + + m.set_temporary_name("Template['foo.rb']") + m.name.should == "Template['foo.rb']" + + m.set_temporary_name("a::B") + m.name.should == "a::B" + + m.set_temporary_name("A::b") + m.name.should == "A::b" + + m.set_temporary_name("A::B::") + m.name.should == "A::B::" + + m.set_temporary_name("A::::B") + m.name.should == "A::::B" + + m.set_temporary_name("A=") + m.name.should == "A=" + end + + it "can't assign empty string as name" do + m = Module.new + -> { m.set_temporary_name("") }.should raise_error(ArgumentError, "empty class/module name") + end + + it "can't assign a constant name as a temporary name" do + m = Module.new + -> { m.set_temporary_name("Object") }.should raise_error(ArgumentError, "the temporary name must not be a constant path to avoid confusion") + end + + it "can't assign a constant path as a temporary name" do + m = Module.new + -> { m.set_temporary_name("A::B") }.should raise_error(ArgumentError, "the temporary name must not be a constant path to avoid confusion") + -> { m.set_temporary_name("::A") }.should raise_error(ArgumentError, "the temporary name must not be a constant path to avoid confusion") + -> { m.set_temporary_name("::A::B") }.should raise_error(ArgumentError, "the temporary name must not be a constant path to avoid confusion") + end + + it "can't assign name to permanent module" do + -> { Object.set_temporary_name("fake_name") }.should raise_error(RuntimeError, "can't change permanent name") + end + + it "can assign a temporary name to a module nested into an anonymous module" do + m = Module.new + module m::N; end + m::N.name.should =~ /\A#<Module:0x\h+>::N\z/ + + m::N.set_temporary_name("fake_name") + m::N.name.should == "fake_name" + + m::N.set_temporary_name(nil) + m::N.name.should be_nil + end + + it "discards a temporary name when an outer anonymous module gets a permanent name" do + m = Module.new + module m::N; end + + m::N.set_temporary_name("fake_name") + m::N.name.should == "fake_name" + + ModuleSpecs::SetTemporaryNameSpec::M = m + m::N.name.should == "ModuleSpecs::SetTemporaryNameSpec::M::N" + ModuleSpecs::SetTemporaryNameSpec.send :remove_const, :M + end + + it "can update the name when assigned to a constant" do + m = Module.new + m::N = Module.new + m::N.name.should =~ /\A#<Module:0x\h+>::N\z/ + m::N.set_temporary_name(nil) + + m::M = m::N + m::M.name.should =~ /\A#<Module:0x\h+>::M\z/m + end + + it "can reassign a temporary name repeatedly" do + m = Module.new + + m.set_temporary_name("fake_name") + m.name.should == "fake_name" + + m.set_temporary_name("fake_name_2") + m.name.should == "fake_name_2" + end + + ruby_bug "#21094", ""..."4.0" do + it "also updates a name of a nested module" do + m = Module.new + m::N = Module.new + m::N.name.should =~ /\A#<Module:0x\h+>::N\z/ + + m.set_temporary_name "m" + m::N.name.should == "m::N" + + m.set_temporary_name nil + m::N.name.should == nil + end + end + + it "keeps temporary name when assigned in an anonymous module" do + outer = Module.new + m = Module.new + m.set_temporary_name "m" + m.name.should == "m" + outer::M = m + m.name.should == "m" + m.inspect.should == "m" + end + + it "keeps temporary name when assigned in an anonymous module and nested before" do + outer = Module.new + m = Module.new + outer::A = m + m.set_temporary_name "m" + m.name.should == "m" + outer::M = m + m.name.should == "m" + m.inspect.should == "m" + end + end +end diff --git a/spec/ruby/core/module/shared/attr_added.rb b/spec/ruby/core/module/shared/attr_added.rb new file mode 100644 index 0000000000..ce832cdcff --- /dev/null +++ b/spec/ruby/core/module/shared/attr_added.rb @@ -0,0 +1,34 @@ +describe :module_attr_added, shared: true do + it "calls method_added for normal classes" do + ScratchPad.record [] + + cls = Class.new do + class << self + def method_added(name) + ScratchPad.recorded << name + end + end + end + + cls.send(@method, :foo) + + ScratchPad.recorded.each {|name| name.to_s.should =~ /foo[=]?/} + end + + it "calls singleton_method_added for singleton classes" do + ScratchPad.record [] + cls = Class.new do + class << self + def singleton_method_added(name) + # called for this def so ignore it + return if name == :singleton_method_added + ScratchPad.recorded << name + end + end + end + + cls.singleton_class.send(@method, :foo) + + ScratchPad.recorded.each {|name| name.to_s.should =~ /foo[=]?/} + end +end diff --git a/spec/ruby/core/module/shared/class_eval.rb b/spec/ruby/core/module/shared/class_eval.rb new file mode 100644 index 0000000000..b1d5cb3814 --- /dev/null +++ b/spec/ruby/core/module/shared/class_eval.rb @@ -0,0 +1,174 @@ +describe :module_class_eval, shared: true do + # TODO: This should probably be replaced with a "should behave like" that uses + # the many scoping/binding specs from kernel/eval_spec, since most of those + # behaviors are the same for instance_eval. See also module_eval/class_eval. + + it "evaluates a given string in the context of self" do + ModuleSpecs.send(@method, "self").should == ModuleSpecs + ModuleSpecs.send(@method, "1 + 1").should == 2 + end + + it "does not add defined methods to other classes" do + FalseClass.send(@method) do + def foo + 'foo' + end + end + -> {42.foo}.should raise_error(NoMethodError) + end + + it "resolves constants in the caller scope" do + ModuleSpecs::ClassEvalTest.get_constant_from_scope.should == ModuleSpecs::Lookup + end + + it "resolves constants in the caller scope ignoring send" do + ModuleSpecs::ClassEvalTest.get_constant_from_scope_with_send(@method).should == ModuleSpecs::Lookup + end + + it "resolves constants in the receiver's scope" do + ModuleSpecs.send(@method, "Lookup").should == ModuleSpecs::Lookup + ModuleSpecs.send(@method, "Lookup::LOOKIE").should == ModuleSpecs::Lookup::LOOKIE + end + + it "defines constants in the receiver's scope" do + ModuleSpecs.send(@method, "module NewEvaluatedModule;end") + ModuleSpecs.const_defined?(:NewEvaluatedModule, false).should == true + end + + it "evaluates a given block in the context of self" do + ModuleSpecs.send(@method) { self }.should == ModuleSpecs + ModuleSpecs.send(@method) { 1 + 1 }.should == 2 + end + + it "passes the module as the first argument of the block" do + given = nil + ModuleSpecs.send(@method) do |block_parameter| + given = block_parameter + end + given.should equal ModuleSpecs + end + + it "uses the optional filename and lineno parameters for error messages" do + ModuleSpecs.send(@method, "[__FILE__, __LINE__]", "test", 102).should == ["test", 102] + end + + ruby_version_is "3.3" do + it "uses the caller location as default filename" do + ModuleSpecs.send(@method, "[__FILE__, __LINE__]").should == ["(eval at #{__FILE__}:#{__LINE__})", 1] + end + end + + it "converts a non-string filename to a string using to_str" do + (file = mock(__FILE__)).should_receive(:to_str).and_return(__FILE__) + ModuleSpecs.send(@method, "1+1", file) + + (file = mock(__FILE__)).should_receive(:to_str).and_return(__FILE__) + ModuleSpecs.send(@method, "1+1", file, 15) + end + + it "raises a TypeError when the given filename can't be converted to string using to_str" do + (file = mock('123')).should_receive(:to_str).and_return(123) + -> { ModuleSpecs.send(@method, "1+1", file) }.should raise_error(TypeError, /can't convert MockObject to String/) + end + + it "converts non string eval-string to string using to_str" do + (o = mock('1 + 1')).should_receive(:to_str).and_return("1 + 1") + ModuleSpecs.send(@method, o).should == 2 + + (o = mock('1 + 1')).should_receive(:to_str).and_return("1 + 1") + ModuleSpecs.send(@method, o, "file.rb").should == 2 + + (o = mock('1 + 1')).should_receive(:to_str).and_return("1 + 1") + ModuleSpecs.send(@method, o, "file.rb", 15).should == 2 + end + + it "raises a TypeError when the given eval-string can't be converted to string using to_str" do + o = mock('x') + -> { ModuleSpecs.send(@method, o) }.should raise_error(TypeError, "no implicit conversion of MockObject into String") + + (o = mock('123')).should_receive(:to_str).and_return(123) + -> { ModuleSpecs.send(@method, o) }.should raise_error(TypeError, /can't convert MockObject to String/) + end + + it "raises an ArgumentError when no arguments and no block are given" do + -> { ModuleSpecs.send(@method) }.should raise_error(ArgumentError, "wrong number of arguments (given 0, expected 1..3)") + end + + it "raises an ArgumentError when more than 3 arguments are given" do + -> { + ModuleSpecs.send(@method, "1 + 1", "some file", 0, "bogus") + }.should raise_error(ArgumentError, "wrong number of arguments (given 4, expected 1..3)") + end + + it "raises an ArgumentError when a block and normal arguments are given" do + -> { + ModuleSpecs.send(@method, "1 + 1") { 1 + 1 } + }.should raise_error(ArgumentError, "wrong number of arguments (given 1, expected 0)") + end + + # This case was found because Rubinius was caching the compiled + # version of the string and not duping the methods within the + # eval, causing the method addition to change the static scope + # of the shared CompiledCode. + it "adds methods respecting the lexical constant scope" do + code = "def self.attribute; C; end" + + a = Class.new do + self::C = "A" + end + + b = Class.new do + self::C = "B" + end + + a.send @method, code + b.send @method, code + + a.attribute.should == "A" + b.attribute.should == "B" + end + + it "activates refinements from the eval scope" do + refinery = Module.new do + refine ModuleSpecs::NamedClass do + def foo + "bar" + end + end + end + + mid = @method + result = nil + + Class.new do + using refinery + + result = send(mid, "ModuleSpecs::NamedClass.new.foo") + end + + result.should == "bar" + end + + it "activates refinements from the eval scope with block" do + refinery = Module.new do + refine ModuleSpecs::NamedClass do + def foo + "bar" + end + end + end + + mid = @method + result = nil + + Class.new do + using refinery + + result = send(mid) do + ModuleSpecs::NamedClass.new.foo + end + end + + result.should == "bar" + end +end diff --git a/spec/ruby/core/module/shared/class_exec.rb b/spec/ruby/core/module/shared/class_exec.rb new file mode 100644 index 0000000000..c7a9e5297f --- /dev/null +++ b/spec/ruby/core/module/shared/class_exec.rb @@ -0,0 +1,29 @@ +describe :module_class_exec, shared: true do + it "does not add defined methods to other classes" do + FalseClass.send(@method) do + def foo + 'foo' + end + end + -> {42.foo}.should raise_error(NoMethodError) + end + + it "defines method in the receiver's scope" do + ModuleSpecs::Subclass.send(@method) { def foo; end } + ModuleSpecs::Subclass.new.respond_to?(:foo).should == true + end + + it "evaluates a given block in the context of self" do + ModuleSpecs::Subclass.send(@method) { self }.should == ModuleSpecs::Subclass + ModuleSpecs::Subclass.new.send(@method) { 1 + 1 }.should == 2 + end + + it "raises a LocalJumpError when no block is given" do + -> { ModuleSpecs::Subclass.send(@method) }.should raise_error(LocalJumpError) + end + + it "passes arguments to the block" do + a = ModuleSpecs::Subclass + a.send(@method, 1) { |b| b }.should equal(1) + end +end diff --git a/spec/ruby/core/module/shared/equal_value.rb b/spec/ruby/core/module/shared/equal_value.rb new file mode 100644 index 0000000000..f1227d873c --- /dev/null +++ b/spec/ruby/core/module/shared/equal_value.rb @@ -0,0 +1,14 @@ +describe :module_equal, shared: true do + it "returns true if self and the given module are the same" do + ModuleSpecs.send(@method, ModuleSpecs).should == true + ModuleSpecs::Child.send(@method, ModuleSpecs::Child).should == true + ModuleSpecs::Parent.send(@method, ModuleSpecs::Parent).should == true + ModuleSpecs::Basic.send(@method, ModuleSpecs::Basic).should == true + ModuleSpecs::Super.send(@method, ModuleSpecs::Super).should == true + + ModuleSpecs::Child.send(@method, ModuleSpecs).should == false + ModuleSpecs::Child.send(@method, ModuleSpecs::Parent).should == false + ModuleSpecs::Child.send(@method, ModuleSpecs::Basic).should == false + ModuleSpecs::Child.send(@method, ModuleSpecs::Super).should == false + end +end diff --git a/spec/ruby/core/module/shared/set_visibility.rb b/spec/ruby/core/module/shared/set_visibility.rb new file mode 100644 index 0000000000..a1586dd2bd --- /dev/null +++ b/spec/ruby/core/module/shared/set_visibility.rb @@ -0,0 +1,184 @@ +# -*- encoding: us-ascii -*- + +describe :set_visibility, shared: true do + it "is a private method" do + Module.should have_private_instance_method(@method, false) + end + + describe "with argument" do + describe "one or more arguments" do + it "sets visibility of given method names" do + visibility = @method + old_visibility = [:protected, :private].find {|vis| vis != visibility } + + mod = Module.new { + send old_visibility + def test1() end + def test2() end + send visibility, :test1, :test2 + } + mod.should send(:"have_#{visibility}_instance_method", :test1, false) + mod.should send(:"have_#{visibility}_instance_method", :test2, false) + end + end + + describe "array as a single argument" do + it "sets visibility of given method names" do + visibility = @method + old_visibility = [:protected, :private].find {|vis| vis != visibility } + + mod = Module.new { + send old_visibility + def test1() end + def test2() end + send visibility, [:test1, :test2] + } + mod.should send(:"have_#{visibility}_instance_method", :test1, false) + mod.should send(:"have_#{visibility}_instance_method", :test2, false) + end + end + + it "does not clone method from the ancestor when setting to the same visibility in a child" do + visibility = @method + parent = Module.new { + def test_method; end + send(visibility, :test_method) + } + + child = Module.new { + include parent + send(visibility, :test_method) + } + + child.should_not send(:"have_#{visibility}_instance_method", :test_method, false) + end + end + + describe "without arguments" do + it "sets visibility to following method definitions" do + visibility = @method + mod = Module.new { + send visibility + + def test1() end + def test2() end + } + + mod.should send(:"have_#{@method}_instance_method", :test1, false) + mod.should send(:"have_#{@method}_instance_method", :test2, false) + end + + it "stops setting visibility if the body encounters other visibility setters without arguments" do + visibility = @method + new_visibility = nil + mod = Module.new { + send visibility + new_visibility = [:protected, :private].find {|vis| vis != visibility } + send new_visibility + def test1() end + } + + mod.should send(:"have_#{new_visibility}_instance_method", :test1, false) + end + + it "continues setting visibility if the body encounters other visibility setters with arguments" do + visibility = @method + mod = Module.new { + send visibility + def test1() end + send([:protected, :private].find {|vis| vis != visibility }, :test1) + def test2() end + } + + mod.should send(:"have_#{@method}_instance_method", :test2, false) + end + + it "does not affect module_evaled method definitions when itself is outside the eval" do + visibility = @method + mod = Module.new { + send visibility + + module_eval { def test1() end } + module_eval " def test2() end " + } + + mod.should have_public_instance_method(:test1, false) + mod.should have_public_instance_method(:test2, false) + end + + it "does not affect outside method definitions when itself is inside a module_eval" do + visibility = @method + mod = Module.new { + module_eval { send visibility } + + def test1() end + } + + mod.should have_public_instance_method(:test1, false) + end + + it "affects normally if itself and method definitions are inside a module_eval" do + visibility = @method + mod = Module.new { + module_eval { + send visibility + + def test1() end + } + } + + mod.should send(:"have_#{@method}_instance_method", :test1, false) + end + + it "does not affect method definitions when itself is inside an eval and method definitions are outside" do + visibility = @method + initialized_visibility = [:public, :protected, :private].find {|sym| sym != visibility } + mod = Module.new { + send initialized_visibility + eval visibility.to_s + + def test1() end + } + + mod.should send(:"have_#{initialized_visibility}_instance_method", :test1, false) + end + + it "affects evaled method definitions when itself is outside the eval" do + visibility = @method + mod = Module.new { + send visibility + + eval "def test1() end" + } + + mod.should send(:"have_#{@method}_instance_method", :test1, false) + end + + it "affects normally if itself and following method definitions are inside a eval" do + visibility = @method + mod = Module.new { + eval <<-CODE + #{visibility} + + def test1() end + CODE + } + + mod.should send(:"have_#{@method}_instance_method", :test1, false) + end + + describe "within a closure" do + it "sets the visibility outside the closure" do + visibility = @method + mod = Module.new { + 1.times { + send visibility + } + def test1() end + } + + mod.should send(:"have_#{@method}_instance_method", :test1, false) + end + end + end +end diff --git a/spec/ruby/core/module/singleton_class_spec.rb b/spec/ruby/core/module/singleton_class_spec.rb new file mode 100644 index 0000000000..052755b73b --- /dev/null +++ b/spec/ruby/core/module/singleton_class_spec.rb @@ -0,0 +1,27 @@ +require_relative '../../spec_helper' + +describe "Module#singleton_class?" do + it "returns true for singleton classes" do + xs = self.singleton_class + xs.should.singleton_class? + end + + it "returns false for other classes" do + c = Class.new + c.should_not.singleton_class? + end + + describe "with singleton values" do + it "returns false for nil's singleton class" do + NilClass.should_not.singleton_class? + end + + it "returns false for true's singleton class" do + TrueClass.should_not.singleton_class? + end + + it "returns false for false's singleton class" do + FalseClass.should_not.singleton_class? + end + end +end diff --git a/spec/ruby/core/module/to_s_spec.rb b/spec/ruby/core/module/to_s_spec.rb new file mode 100644 index 0000000000..83c0ae0825 --- /dev/null +++ b/spec/ruby/core/module/to_s_spec.rb @@ -0,0 +1,70 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Module#to_s" do + it 'returns the name of the module if it has a name' do + Enumerable.to_s.should == 'Enumerable' + String.to_s.should == 'String' + end + + it "returns the full constant path leading to the module" do + ModuleSpecs::LookupMod.to_s.should == "ModuleSpecs::LookupMod" + end + + it "works with an anonymous module" do + m = Module.new + m.to_s.should =~ /\A#<Module:0x\h+>\z/ + end + + it "works with an anonymous class" do + c = Class.new + c.to_s.should =~ /\A#<Class:0x\h+>\z/ + end + + it 'for the singleton class of an object of an anonymous class' do + klass = Class.new + obj = klass.new + sclass = obj.singleton_class + sclass.to_s.should == "#<Class:#{obj}>" + sclass.to_s.should =~ /\A#<Class:#<#{klass}:0x\h+>>\z/ + sclass.to_s.should =~ /\A#<Class:#<#<Class:0x\h+>:0x\h+>>\z/ + end + + it 'for a singleton class of a module includes the module name' do + ModuleSpecs.singleton_class.to_s.should == '#<Class:ModuleSpecs>' + end + + it 'for a metaclass includes the class name' do + ModuleSpecs::NamedClass.singleton_class.to_s.should == '#<Class:ModuleSpecs::NamedClass>' + end + + it 'for objects includes class name and object ID' do + obj = ModuleSpecs::NamedClass.new + obj.singleton_class.to_s.should =~ /\A#<Class:#<ModuleSpecs::NamedClass:0x\h+>>\z/ + end + + it "always show the refinement name, even if the module is named" do + module ModuleSpecs::RefinementInspect + R = refine String do + end + end + + ModuleSpecs::RefinementInspect::R.name.should == 'ModuleSpecs::RefinementInspect::R' + ModuleSpecs::RefinementInspect::R.to_s.should == '#<refinement:String@ModuleSpecs::RefinementInspect>' + ensure + ModuleSpecs.send(:remove_const, :RefinementInspect) + end + + it 'does not call #inspect or #to_s for singleton classes' do + klass = Class.new + obj = klass.new + def obj.to_s + "to_s" + end + def obj.inspect + "inspect" + end + sclass = obj.singleton_class + sclass.to_s.should =~ /\A#<Class:#<#{Regexp.escape klass.to_s}:0x\h+>>\z/ + end +end diff --git a/spec/ruby/core/module/undef_method_spec.rb b/spec/ruby/core/module/undef_method_spec.rb new file mode 100644 index 0000000000..d4efcd51cb --- /dev/null +++ b/spec/ruby/core/module/undef_method_spec.rb @@ -0,0 +1,181 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +module ModuleSpecs + class Parent + def method_to_undef() 1 end + def another_method_to_undef() 1 end + end + + class Ancestor + def method_to_undef() 1 end + def another_method_to_undef() 1 end + end +end + +describe "Module#undef_method" do + before :each do + @module = Module.new { def method_to_undef; end } + end + + it "is a public method" do + Module.should have_public_instance_method(:undef_method, false) + end + + it "requires multiple arguments" do + Module.instance_method(:undef_method).arity.should < 0 + end + + it "allows multiple methods to be removed at once" do + klass = Class.new do + def method_to_undef() 1 end + def another_method_to_undef() 1 end + end + x = klass.new + klass.send(:undef_method, :method_to_undef, :another_method_to_undef) + + -> { x.method_to_undef }.should raise_error(NoMethodError) + -> { x.another_method_to_undef }.should raise_error(NoMethodError) + end + + it "does not undef any instance methods when argument not given" do + before = @module.instance_methods(true) + @module.private_instance_methods(true) + @module.send :undef_method + after = @module.instance_methods(true) + @module.private_instance_methods(true) + before.sort.should == after.sort + end + + it "returns self" do + @module.send(:undef_method, :method_to_undef).should equal(@module) + end + + it "raises a NameError when passed a missing name for a module" do + -> { @module.send :undef_method, :not_exist }.should raise_error(NameError, /undefined method [`']not_exist' for module [`']#{@module}'/) { |e| + # a NameError and not a NoMethodError + e.class.should == NameError + } + end + + it "raises a NameError when passed a missing name for a class" do + klass = Class.new + -> { klass.send :undef_method, :not_exist }.should raise_error(NameError, /undefined method [`']not_exist' for class [`']#{klass}'/) { |e| + # a NameError and not a NoMethodError + e.class.should == NameError + } + end + + it "raises a NameError when passed a missing name for a singleton class" do + klass = Class.new + obj = klass.new + sclass = obj.singleton_class + + -> { sclass.send :undef_method, :not_exist }.should raise_error(NameError, /undefined method [`']not_exist' for class [`']#{sclass}'/) { |e| + e.message.should =~ /[`']#<Class:#<#<Class:/ + + # a NameError and not a NoMethodError + e.class.should == NameError + } + end + + it "raises a NameError when passed a missing name for a metaclass" do + klass = String.singleton_class + -> { klass.send :undef_method, :not_exist }.should raise_error(NameError, /undefined method [`']not_exist' for class [`']String'/) { |e| + # a NameError and not a NoMethodError + e.class.should == NameError + } + end + + describe "on frozen instance" do + before :each do + @frozen = @module.dup.freeze + end + + it "raises a FrozenError when passed a name" do + -> { @frozen.send :undef_method, :method_to_undef }.should raise_error(FrozenError) + end + + it "raises a FrozenError when passed a missing name" do + -> { @frozen.send :undef_method, :not_exist }.should raise_error(FrozenError) + end + + it "raises a TypeError when passed a not name" do + -> { @frozen.send :undef_method, Object.new }.should raise_error(TypeError) + end + + it "does not raise exceptions when no arguments given" do + @frozen.send(:undef_method).should equal(@frozen) + end + end +end + +describe "Module#undef_method with symbol" do + it "removes a method defined in a class" do + klass = Class.new do + def method_to_undef() 1 end + def another_method_to_undef() 1 end + end + x = klass.new + + x.method_to_undef.should == 1 + + klass.send :undef_method, :method_to_undef + + -> { x.method_to_undef }.should raise_error(NoMethodError) + end + + it "removes a method defined in a super class" do + child_class = Class.new(ModuleSpecs::Parent) + child = child_class.new + child.method_to_undef.should == 1 + + child_class.send :undef_method, :method_to_undef + + -> { child.method_to_undef }.should raise_error(NoMethodError) + end + + it "does not remove a method defined in a super class when removed from a subclass" do + descendant = Class.new(ModuleSpecs::Ancestor) + ancestor = ModuleSpecs::Ancestor.new + ancestor.method_to_undef.should == 1 + + descendant.send :undef_method, :method_to_undef + + ancestor.method_to_undef.should == 1 + end +end + +describe "Module#undef_method with string" do + it "removes a method defined in a class" do + klass = Class.new do + def method_to_undef() 1 end + def another_method_to_undef() 1 end + end + x = klass.new + + x.another_method_to_undef.should == 1 + + klass.send :undef_method, 'another_method_to_undef' + + -> { x.another_method_to_undef }.should raise_error(NoMethodError) + end + + it "removes a method defined in a super class" do + child_class = Class.new(ModuleSpecs::Parent) + child = child_class.new + child.another_method_to_undef.should == 1 + + child_class.send :undef_method, 'another_method_to_undef' + + -> { child.another_method_to_undef }.should raise_error(NoMethodError) + end + + it "does not remove a method defined in a super class when removed from a subclass" do + descendant = Class.new(ModuleSpecs::Ancestor) + ancestor = ModuleSpecs::Ancestor.new + ancestor.another_method_to_undef.should == 1 + + descendant.send :undef_method, 'another_method_to_undef' + + ancestor.another_method_to_undef.should == 1 + end +end diff --git a/spec/ruby/core/module/undefined_instance_methods_spec.rb b/spec/ruby/core/module/undefined_instance_methods_spec.rb new file mode 100644 index 0000000000..d33ee93fc1 --- /dev/null +++ b/spec/ruby/core/module/undefined_instance_methods_spec.rb @@ -0,0 +1,24 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Module#undefined_instance_methods" do + it "returns methods undefined in the class" do + methods = ModuleSpecs::UndefinedInstanceMethods::Parent.undefined_instance_methods + methods.should == [:undefed_method] + end + + it "returns inherited methods undefined in the class" do + methods = ModuleSpecs::UndefinedInstanceMethods::Child.undefined_instance_methods + methods.should include(:parent_method, :another_parent_method) + end + + it "returns methods from an included module that are undefined in the class" do + methods = ModuleSpecs::UndefinedInstanceMethods::Grandchild.undefined_instance_methods + methods.should include(:super_included_method) + end + + it "does not returns ancestors undefined methods" do + methods = ModuleSpecs::UndefinedInstanceMethods::Grandchild.undefined_instance_methods + methods.should_not include(:parent_method, :another_parent_method) + end +end diff --git a/spec/ruby/core/module/used_refinements_spec.rb b/spec/ruby/core/module/used_refinements_spec.rb new file mode 100644 index 0000000000..40dd4a444e --- /dev/null +++ b/spec/ruby/core/module/used_refinements_spec.rb @@ -0,0 +1,85 @@ +require_relative '../../spec_helper' + +describe "Module.used_refinements" do + it "returns list of all refinements imported in the current scope" do + refinement_int = nil + refinement_str = nil + ScratchPad.record [] + + m1 = Module.new do + refine Integer do + refinement_int = self + end + end + + m2 = Module.new do + refine String do + refinement_str = self + end + end + + Module.new do + using m1 + using m2 + + Module.used_refinements.each { |r| ScratchPad << r } + end + + ScratchPad.recorded.sort_by(&:object_id).should == [refinement_int, refinement_str].sort_by(&:object_id) + end + + it "returns empty array if does not have any refinements imported" do + used_refinements = nil + + Module.new do + used_refinements = Module.used_refinements + end + + used_refinements.should == [] + end + + it "ignores refinements imported in a module that is included into the current one" do + used_refinements = nil + + m1 = Module.new do + refine Integer do + nil + end + end + + m2 = Module.new do + using m1 + end + + Module.new do + include m2 + + used_refinements = Module.used_refinements + end + + used_refinements.should == [] + end + + it "returns refinements even not defined directly in a module refinements are imported from" do + used_refinements = nil + ScratchPad.record [] + + m1 = Module.new do + refine Integer do + ScratchPad << self + end + end + + m2 = Module.new do + include m1 + end + + Module.new do + using m2 + + used_refinements = Module.used_refinements + end + + used_refinements.should == ScratchPad.recorded + end +end diff --git a/spec/ruby/core/module/using_spec.rb b/spec/ruby/core/module/using_spec.rb new file mode 100644 index 0000000000..a908363c96 --- /dev/null +++ b/spec/ruby/core/module/using_spec.rb @@ -0,0 +1,377 @@ +require_relative '../../spec_helper' + +describe "Module#using" do + it "imports class refinements from module into the current class/module" do + refinement = Module.new do + refine Integer do + def foo; "foo"; end + end + end + + result = nil + Module.new do + using refinement + result = 1.foo + end + + result.should == "foo" + end + + it "accepts module as argument" do + refinement = Module.new do + refine Integer do + def foo; "foo"; end + end + end + + -> { + Module.new do + using refinement + end + }.should_not raise_error + end + + it "accepts module without refinements" do + mod = Module.new + + -> { + Module.new do + using mod + end + }.should_not raise_error + end + + it "does not accept class" do + klass = Class.new + + -> { + Module.new do + using klass + end + }.should raise_error(TypeError) + end + + it "raises TypeError if passed something other than module" do + -> { + Module.new do + using "foo" + end + }.should raise_error(TypeError) + end + + it "returns self" do + refinement = Module.new + + result = nil + mod = Module.new do + result = using refinement + end + + result.should equal(mod) + end + + it "works in classes too" do + refinement = Module.new do + refine Integer do + def foo; "foo"; end + end + end + + result = nil + Class.new do + using refinement + result = 1.foo + end + + result.should == "foo" + end + + it "raises error in method scope" do + mod = Module.new do + def self.foo + using Module.new {} + end + end + + -> { + mod.foo + }.should raise_error(RuntimeError, /Module#using is not permitted in methods/) + end + + it "activates refinement even for existed objects" do + result = nil + + Module.new do + klass = Class.new do + def foo; "foo"; end + end + + refinement = Module.new do + refine klass do + def foo; "foo from refinement"; end + end + end + + obj = klass.new + using refinement + result = obj.foo + end + + result.should == "foo from refinement" + end + + it "activates updates when refinement reopens later" do + result = nil + + Module.new do + klass = Class.new do + def foo; "foo"; end + end + + refinement = Module.new do + refine klass do + def foo; "foo from refinement"; end + end + end + + using refinement + + refinement.class_eval do + refine klass do + def foo; "foo from reopened refinement"; end + end + end + + obj = klass.new + result = obj.foo + end + + result.should == "foo from reopened refinement" + end + + describe "scope of refinement" do + it "is active until the end of current class/module" do + ScratchPad.record [] + + Module.new do + Class.new do + using Module.new { + refine String do + def to_s; "hello from refinement"; end + end + } + ScratchPad << "1".to_s + end + + ScratchPad << "1".to_s + end + + ScratchPad.recorded.should == ["hello from refinement", "1"] + end + + # Refinements are lexical in scope. + # Refinements are only active within a scope after the call to using. + # Any code before the using statement will not have the refinement activated. + it "is not active before the `using` call" do + ScratchPad.record [] + + Module.new do + Class.new do + ScratchPad << "1".to_s + using Module.new { + refine String do + def to_s; "hello from refinement"; end + end + } + ScratchPad << "1".to_s + end + end + + ScratchPad.recorded.should == ["1", "hello from refinement"] + end + + # If you call a method that is defined outside the current scope + # the refinement will be deactivated + it "is not active for code defined outside the current scope" do + result = nil + + Module.new do + klass = Class.new do + def foo; "foo"; end + end + + refinement = Module.new do + refine klass do + def foo; "foo from refinement"; end + end + end + + def self.call_foo(c) + c.foo + end + + using refinement + + result = call_foo(klass.new) + end + + result.should == "foo" + end + + # If a method is defined in a scope where a refinement is active + # the refinement will be active when the method is called. + it "is active for method defined in a scope wherever it's called" do + klass = Class.new do + def foo; "foo"; end + end + + mod = Module.new do + refinement = Module.new do + refine klass do + def foo; "foo from refinement"; end + end + end + + using refinement + + def self.call_foo(c) + c.foo + end + end + + c = klass.new + mod.call_foo(c).should == "foo from refinement" + end + + it "is active for module defined via Module.new {}" do + refinement = Module.new do + refine Integer do + def foo; "foo from refinement"; end + end + end + + result = nil + + Module.new do + using refinement + + Module.new do + result = 1.foo + end + end + + result.should == "foo from refinement" + end + + it "is active for class defined via Class.new {}" do + refinement = Module.new do + refine Integer do + def foo; "foo from refinement"; end + end + end + + result = nil + + Module.new do + using refinement + + Class.new do + result = 1.foo + end + end + + result.should == "foo from refinement" + end + + it "is active for block called via instance_exec" do + refinement = Module.new do + refine Integer do + def foo; "foo from refinement"; end + end + end + + c = Class.new do + using refinement + + def abc + block = -> { + 1.foo + } + + self.instance_exec(&block) + end + end + + c.new.abc.should == "foo from refinement" + end + + it "is active for block called via instance_eval" do + refinement = Module.new do + refine String do + def foo; "foo from refinement"; end + end + end + + c = Class.new do + using refinement + + def initialize + @a = +"1703" + + @a.instance_eval do + def abc + "#{self}: #{self.foo}" + end + end + end + + def abc + @a.abc + end + end + + c.new.abc.should == "1703: foo from refinement" + end + + it "is not active if `using` call is not evaluated" do + result = nil + + Module.new do + if false + using Module.new { + refine String do + def to_s; "hello from refinement"; end + end + } + end + result = "1".to_s + end + + result.should == "1" + end + + # The refinements in module are not activated automatically + # if the class is reopened later + it "is not active when class/module reopens" do + refinement = Module.new do + refine String do + def to_s + "hello from refinement" + end + end + end + + result = [] + klass = Class.new do + using refinement + result << "1".to_s + end + + klass.class_eval do + result << "1".to_s + end + + result.should == ["hello from refinement", "1"] + end + end +end diff --git a/spec/ruby/core/mutex/lock_spec.rb b/spec/ruby/core/mutex/lock_spec.rb new file mode 100644 index 0000000000..e9d33f5fd9 --- /dev/null +++ b/spec/ruby/core/mutex/lock_spec.rb @@ -0,0 +1,30 @@ +require_relative '../../spec_helper' + +describe "Mutex#lock" do + it "returns self" do + m = Mutex.new + m.lock.should == m + m.unlock + end + + it "blocks the caller if already locked" do + m = Mutex.new + m.lock + -> { m.lock }.should block_caller + end + + it "does not block the caller if not locked" do + m = Mutex.new + -> { m.lock }.should_not block_caller + end + + # Unable to find a specific ticket but behavior change may be + # related to this ML thread. + it "raises a ThreadError when used recursively" do + m = Mutex.new + m.lock + -> { + m.lock + }.should raise_error(ThreadError) + end +end diff --git a/spec/ruby/core/mutex/locked_spec.rb b/spec/ruby/core/mutex/locked_spec.rb new file mode 100644 index 0000000000..1bf3ed6394 --- /dev/null +++ b/spec/ruby/core/mutex/locked_spec.rb @@ -0,0 +1,36 @@ +require_relative '../../spec_helper' + +describe "Mutex#locked?" do + it "returns true if locked" do + m = Mutex.new + m.lock + m.locked?.should be_true + end + + it "returns false if unlocked" do + m = Mutex.new + m.locked?.should be_false + end + + it "returns the status of the lock" do + m1 = Mutex.new + m2 = Mutex.new + + m2.lock # hold th with only m1 locked + m1_locked = false + + th = Thread.new do + m1.lock + m1_locked = true + m2.lock + end + + Thread.pass until m1_locked + + m1.locked?.should be_true + m2.unlock # release th + th.join + # A Thread releases its locks upon termination + m1.locked?.should be_false + end +end diff --git a/spec/ruby/core/mutex/owned_spec.rb b/spec/ruby/core/mutex/owned_spec.rb new file mode 100644 index 0000000000..7bfc7d8f83 --- /dev/null +++ b/spec/ruby/core/mutex/owned_spec.rb @@ -0,0 +1,53 @@ +require_relative '../../spec_helper' + +describe "Mutex#owned?" do + describe "when unlocked" do + it "returns false" do + m = Mutex.new + m.owned?.should be_false + end + end + + describe "when locked by the current thread" do + it "returns true" do + m = Mutex.new + m.lock + m.owned?.should be_true + end + end + + describe "when locked by another thread" do + before :each do + @checked = false + end + + after :each do + @checked = true + @th.join + end + + it "returns false" do + m = Mutex.new + locked = false + + @th = Thread.new do + m.lock + locked = true + Thread.pass until @checked + end + + Thread.pass until locked + m.owned?.should be_false + end + end + + it "is held per Fiber" do + m = Mutex.new + m.lock + + Fiber.new do + m.locked?.should == true + m.owned?.should == false + end.resume + end +end diff --git a/spec/ruby/core/mutex/sleep_spec.rb b/spec/ruby/core/mutex/sleep_spec.rb new file mode 100644 index 0000000000..9832e3125e --- /dev/null +++ b/spec/ruby/core/mutex/sleep_spec.rb @@ -0,0 +1,103 @@ +require_relative '../../spec_helper' + +describe "Mutex#sleep" do + describe "when not locked by the current thread" do + it "raises a ThreadError" do + m = Mutex.new + -> { m.sleep }.should raise_error(ThreadError) + end + + it "raises an ArgumentError if passed a negative duration" do + m = Mutex.new + -> { m.sleep(-0.1) }.should raise_error(ArgumentError) + -> { m.sleep(-1) }.should raise_error(ArgumentError) + end + end + + it "raises an ArgumentError if passed a negative duration" do + m = Mutex.new + m.lock + -> { m.sleep(-0.1) }.should raise_error(ArgumentError) + -> { m.sleep(-1) }.should raise_error(ArgumentError) + end + + it "pauses execution for approximately the duration requested" do + m = Mutex.new + m.lock + duration = 0.001 + start = Process.clock_gettime(Process::CLOCK_MONOTONIC) + m.sleep duration + now = Process.clock_gettime(Process::CLOCK_MONOTONIC) + (now - start).should >= 0 + (now - start).should < (duration + TIME_TOLERANCE) + end + + it "unlocks the mutex while sleeping" do + m = Mutex.new + locked = false + th = Thread.new { m.lock; locked = true; m.sleep } + Thread.pass until locked + Thread.pass until th.stop? + m.locked?.should be_false + th.run + th.join + end + + it "relocks the mutex when woken" do + m = Mutex.new + m.lock + m.sleep(0.001) + m.locked?.should be_true + end + + it "relocks the mutex when woken by an exception being raised" do + m = Mutex.new + locked = false + th = Thread.new do + m.lock + locked = true + begin + m.sleep + rescue Exception + m.locked? + end + end + Thread.pass until locked + Thread.pass until th.stop? + th.raise(Exception) + th.value.should be_true + end + + it "returns the rounded number of seconds asleep" do + m = Mutex.new + locked = false + th = Thread.start do + m.lock + locked = true + m.sleep + end + Thread.pass until locked + Thread.pass until th.stop? + th.wakeup + th.value.should be_kind_of(Integer) + end + + it "wakes up when requesting sleep times near or equal to zero" do + times = [] + val = 1 + + # power of two divisor so we eventually get near zero + loop do + val = val / 16.0 + times << val + break if val == 0.0 + end + + m = Mutex.new + m.lock + times.each do |time| + # just testing that sleep completes + -> {m.sleep(time)}.should_not raise_error + end + end +end diff --git a/spec/ruby/core/mutex/synchronize_spec.rb b/spec/ruby/core/mutex/synchronize_spec.rb new file mode 100644 index 0000000000..7942885197 --- /dev/null +++ b/spec/ruby/core/mutex/synchronize_spec.rb @@ -0,0 +1,66 @@ +require_relative '../../spec_helper' + +describe "Mutex#synchronize" do + it "wraps the lock/unlock pair in an ensure" do + m1 = Mutex.new + m2 = Mutex.new + m2.lock + synchronized = false + + th = Thread.new do + -> do + m1.synchronize do + synchronized = true + m2.lock + raise Exception + end + end.should raise_error(Exception) + end + + Thread.pass until synchronized + + m1.locked?.should be_true + m2.unlock + th.join + m1.locked?.should be_false + end + + it "blocks the caller if already locked" do + m = Mutex.new + m.lock + -> { m.synchronize { } }.should block_caller + end + + it "does not block the caller if not locked" do + m = Mutex.new + -> { m.synchronize { } }.should_not block_caller + end + + it "blocks the caller if another thread is also in the synchronize block" do + m = Mutex.new + q1 = Queue.new + q2 = Queue.new + + t = Thread.new { + m.synchronize { + q1.push :ready + q2.pop + } + } + + q1.pop.should == :ready + + -> { m.synchronize { } }.should block_caller + + q2.push :done + t.join + end + + it "is not recursive" do + m = Mutex.new + + m.synchronize do + -> { m.synchronize { } }.should raise_error(ThreadError) + end + end +end diff --git a/spec/ruby/core/mutex/try_lock_spec.rb b/spec/ruby/core/mutex/try_lock_spec.rb new file mode 100644 index 0000000000..8d521f4c6b --- /dev/null +++ b/spec/ruby/core/mutex/try_lock_spec.rb @@ -0,0 +1,32 @@ +require_relative '../../spec_helper' + +describe "Mutex#try_lock" do + describe "when unlocked" do + it "returns true" do + m = Mutex.new + m.try_lock.should be_true + end + + it "locks the mutex" do + m = Mutex.new + m.try_lock + m.locked?.should be_true + end + end + + describe "when locked by the current thread" do + it "returns false" do + m = Mutex.new + m.lock + m.try_lock.should be_false + end + end + + describe "when locked by another thread" do + it "returns false" do + m = Mutex.new + m.lock + Thread.new { m.try_lock }.value.should be_false + end + end +end diff --git a/spec/ruby/core/mutex/unlock_spec.rb b/spec/ruby/core/mutex/unlock_spec.rb new file mode 100644 index 0000000000..d999e66842 --- /dev/null +++ b/spec/ruby/core/mutex/unlock_spec.rb @@ -0,0 +1,38 @@ +require_relative '../../spec_helper' + +describe "Mutex#unlock" do + it "raises ThreadError unless Mutex is locked" do + mutex = Mutex.new + -> { mutex.unlock }.should raise_error(ThreadError) + end + + it "raises ThreadError unless thread owns Mutex" do + mutex = Mutex.new + wait = Mutex.new + wait.lock + th = Thread.new do + mutex.lock + wait.lock + end + + # avoid race on mutex.lock + Thread.pass until mutex.locked? + Thread.pass until th.stop? + + -> { mutex.unlock }.should raise_error(ThreadError) + + wait.unlock + th.join + end + + it "raises ThreadError if previously locking thread is gone" do + mutex = Mutex.new + th = Thread.new do + mutex.lock + end + + th.join + + -> { mutex.unlock }.should raise_error(ThreadError) + end +end diff --git a/spec/ruby/core/nil/and_spec.rb b/spec/ruby/core/nil/and_spec.rb new file mode 100644 index 0000000000..cd25aff1de --- /dev/null +++ b/spec/ruby/core/nil/and_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' + +describe "NilClass#&" do + it "returns false" do + (nil & nil).should == false + (nil & true).should == false + (nil & false).should == false + (nil & "").should == false + (nil & mock('x')).should == false + end +end diff --git a/spec/ruby/core/nil/case_compare_spec.rb b/spec/ruby/core/nil/case_compare_spec.rb new file mode 100644 index 0000000000..142560c6f5 --- /dev/null +++ b/spec/ruby/core/nil/case_compare_spec.rb @@ -0,0 +1,13 @@ +require_relative '../../spec_helper' + +describe "NilClass#===" do + it "returns true for nil" do + (nil === nil).should == true + end + + it "returns false for non-nil object" do + (nil === 1).should == false + (nil === "").should == false + (nil === Object).should == false + end +end diff --git a/spec/ruby/core/nil/dup_spec.rb b/spec/ruby/core/nil/dup_spec.rb new file mode 100644 index 0000000000..0324b3f1f4 --- /dev/null +++ b/spec/ruby/core/nil/dup_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' + +describe "NilClass#dup" do + it "returns self" do + nil.dup.should equal(nil) + end +end diff --git a/spec/ruby/core/nil/inspect_spec.rb b/spec/ruby/core/nil/inspect_spec.rb new file mode 100644 index 0000000000..1babc3d062 --- /dev/null +++ b/spec/ruby/core/nil/inspect_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' + +describe "NilClass#inspect" do + it "returns the string 'nil'" do + nil.inspect.should == "nil" + end +end diff --git a/spec/ruby/core/nil/match_spec.rb b/spec/ruby/core/nil/match_spec.rb new file mode 100644 index 0000000000..bc1c591793 --- /dev/null +++ b/spec/ruby/core/nil/match_spec.rb @@ -0,0 +1,21 @@ +require_relative '../../spec_helper' + +describe "NilClass#=~" do + it "returns nil matching any object" do + o = nil + + suppress_warning do + (o =~ /Object/).should be_nil + (o =~ 'Object').should be_nil + (o =~ Object).should be_nil + (o =~ Object.new).should be_nil + (o =~ nil).should be_nil + (o =~ false).should be_nil + (o =~ true).should be_nil + end + end + + it "should not warn" do + -> { nil =~ /a/ }.should_not complain(verbose: true) + end +end diff --git a/spec/ruby/core/nil/nil_spec.rb b/spec/ruby/core/nil/nil_spec.rb new file mode 100644 index 0000000000..2cf97621c6 --- /dev/null +++ b/spec/ruby/core/nil/nil_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' + +describe "NilClass#nil?" do + it "returns true" do + nil.should.nil? + end +end diff --git a/spec/ruby/core/nil/nilclass_spec.rb b/spec/ruby/core/nil/nilclass_spec.rb new file mode 100644 index 0000000000..7f6d8af25d --- /dev/null +++ b/spec/ruby/core/nil/nilclass_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../spec_helper' + +describe "NilClass" do + it ".allocate raises a TypeError" do + -> do + NilClass.allocate + end.should raise_error(TypeError) + end + + it ".new is undefined" do + -> do + NilClass.new + end.should raise_error(NoMethodError) + end +end diff --git a/spec/ruby/core/nil/or_spec.rb b/spec/ruby/core/nil/or_spec.rb new file mode 100644 index 0000000000..473a833d71 --- /dev/null +++ b/spec/ruby/core/nil/or_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' + +describe "NilClass#|" do + it "returns false if other is nil or false, otherwise true" do + (nil | nil).should == false + (nil | true).should == true + (nil | false).should == false + (nil | "").should == true + (nil | mock('x')).should == true + end +end diff --git a/spec/ruby/core/nil/rationalize_spec.rb b/spec/ruby/core/nil/rationalize_spec.rb new file mode 100644 index 0000000000..84d6e6f056 --- /dev/null +++ b/spec/ruby/core/nil/rationalize_spec.rb @@ -0,0 +1,16 @@ +require_relative '../../spec_helper' + +describe "NilClass#rationalize" do + it "returns 0/1" do + nil.rationalize.should == Rational(0, 1) + end + + it "ignores a single argument" do + nil.rationalize(0.1).should == Rational(0, 1) + end + + it "raises ArgumentError when passed more than one argument" do + -> { nil.rationalize(0.1, 0.1) }.should raise_error(ArgumentError) + -> { nil.rationalize(0.1, 0.1, 2) }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/nil/singleton_method_spec.rb b/spec/ruby/core/nil/singleton_method_spec.rb new file mode 100644 index 0000000000..8d898b1cc9 --- /dev/null +++ b/spec/ruby/core/nil/singleton_method_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../spec_helper' + +describe "NilClass#singleton_method" do + ruby_version_is '3.3' do + it "raises regardless of whether NilClass defines the method" do + -> { nil.singleton_method(:foo) }.should raise_error(NameError) + begin + def (nil).foo; end + -> { nil.singleton_method(:foo) }.should raise_error(NameError) + ensure + NilClass.send(:remove_method, :foo) + end + end + end +end diff --git a/spec/ruby/core/nil/to_a_spec.rb b/spec/ruby/core/nil/to_a_spec.rb new file mode 100644 index 0000000000..b8b339e330 --- /dev/null +++ b/spec/ruby/core/nil/to_a_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' + +describe "NilClass#to_a" do + it "returns an empty array" do + nil.to_a.should == [] + end +end diff --git a/spec/ruby/core/nil/to_c_spec.rb b/spec/ruby/core/nil/to_c_spec.rb new file mode 100644 index 0000000000..e0052be5bd --- /dev/null +++ b/spec/ruby/core/nil/to_c_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' + +describe "NilClass#to_c" do + it "returns Complex(0, 0)" do + nil.to_c.should eql(Complex(0, 0)) + end +end diff --git a/spec/ruby/core/nil/to_f_spec.rb b/spec/ruby/core/nil/to_f_spec.rb new file mode 100644 index 0000000000..a5f24ba3bf --- /dev/null +++ b/spec/ruby/core/nil/to_f_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' + +describe "NilClass#to_f" do + it "returns 0.0" do + nil.to_f.should == 0.0 + end + + it "does not cause NilClass to be coerced to Float" do + (0.0 == nil).should == false + end +end diff --git a/spec/ruby/core/nil/to_h_spec.rb b/spec/ruby/core/nil/to_h_spec.rb new file mode 100644 index 0000000000..5c8d5dc25a --- /dev/null +++ b/spec/ruby/core/nil/to_h_spec.rb @@ -0,0 +1,8 @@ +require_relative '../../spec_helper' + +describe "NilClass#to_h" do + it "returns an empty hash" do + nil.to_h.should == {} + nil.to_h.default.should == nil + end +end diff --git a/spec/ruby/core/nil/to_i_spec.rb b/spec/ruby/core/nil/to_i_spec.rb new file mode 100644 index 0000000000..d3d088e999 --- /dev/null +++ b/spec/ruby/core/nil/to_i_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' + +describe "NilClass#to_i" do + it "returns 0" do + nil.to_i.should == 0 + end + + it "does not cause NilClass to be coerced to Integer" do + (0 == nil).should == false + end +end diff --git a/spec/ruby/core/nil/to_r_spec.rb b/spec/ruby/core/nil/to_r_spec.rb new file mode 100644 index 0000000000..8be43baf00 --- /dev/null +++ b/spec/ruby/core/nil/to_r_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' + +describe "NilClass#to_r" do + it "returns 0/1" do + nil.to_r.should == Rational(0, 1) + end +end diff --git a/spec/ruby/core/nil/to_s_spec.rb b/spec/ruby/core/nil/to_s_spec.rb new file mode 100644 index 0000000000..fa0b929677 --- /dev/null +++ b/spec/ruby/core/nil/to_s_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../spec_helper' + +describe "NilClass#to_s" do + it "returns the string ''" do + nil.to_s.should == "" + end + + it "returns a frozen string" do + nil.to_s.should.frozen? + end + + it "always returns the same string" do + nil.to_s.should equal(nil.to_s) + end +end diff --git a/spec/ruby/core/nil/xor_spec.rb b/spec/ruby/core/nil/xor_spec.rb new file mode 100644 index 0000000000..b45da9d443 --- /dev/null +++ b/spec/ruby/core/nil/xor_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' + +describe "NilClass#^" do + it "returns false if other is nil or false, otherwise true" do + (nil ^ nil).should == false + (nil ^ true).should == true + (nil ^ false).should == false + (nil ^ "").should == true + (nil ^ mock('x')).should == true + end +end diff --git a/spec/ruby/core/numeric/abs2_spec.rb b/spec/ruby/core/numeric/abs2_spec.rb new file mode 100644 index 0000000000..0e60cd0ae7 --- /dev/null +++ b/spec/ruby/core/numeric/abs2_spec.rb @@ -0,0 +1,34 @@ +require_relative '../../spec_helper' + +describe "Numeric#abs2" do + before :each do + @numbers = [ + 0, + 0.0, + 1, + 20, + bignum_value, + 278202.292871, + 72829, + 3.333333333333, + 0.1, + infinity_value + ].map { |n| [-n, n] }.flatten + end + + it "returns the square of the absolute value of self" do + @numbers.each do |number| + number.abs2.should eql(number.abs ** 2) + end + end + + it "calls #* on self" do + number = mock_numeric('numeric') + number.should_receive(:*).and_return(:result) + number.abs2.should == :result + end + + it "returns NaN when self is NaN" do + nan_value.abs2.nan?.should be_true + end +end diff --git a/spec/ruby/core/numeric/abs_spec.rb b/spec/ruby/core/numeric/abs_spec.rb new file mode 100644 index 0000000000..8bec50e337 --- /dev/null +++ b/spec/ruby/core/numeric/abs_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/abs' + +describe "Numeric#abs" do + it_behaves_like :numeric_abs, :abs +end diff --git a/spec/ruby/core/numeric/angle_spec.rb b/spec/ruby/core/numeric/angle_spec.rb new file mode 100644 index 0000000000..bb38165777 --- /dev/null +++ b/spec/ruby/core/numeric/angle_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/arg' + +describe "Numeric#angle" do + it_behaves_like :numeric_arg, :angle +end diff --git a/spec/ruby/core/numeric/arg_spec.rb b/spec/ruby/core/numeric/arg_spec.rb new file mode 100644 index 0000000000..ba3b57c687 --- /dev/null +++ b/spec/ruby/core/numeric/arg_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/arg' + +describe "Numeric#arg" do + it_behaves_like :numeric_arg, :arg +end diff --git a/spec/ruby/core/numeric/ceil_spec.rb b/spec/ruby/core/numeric/ceil_spec.rb new file mode 100644 index 0000000000..00c856e79b --- /dev/null +++ b/spec/ruby/core/numeric/ceil_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Numeric#ceil" do + it "converts self to a Float (using #to_f) and returns the #ceil'ed result" do + o = mock_numeric("ceil") + o.should_receive(:to_f).and_return(1 + TOLERANCE) + o.ceil.should == 2 + + o2 = mock_numeric("ceil") + v = -1 - TOLERANCE + o2.should_receive(:to_f).and_return(v) + o2.ceil.should == -1 + end +end diff --git a/spec/ruby/core/numeric/clone_spec.rb b/spec/ruby/core/numeric/clone_spec.rb new file mode 100644 index 0000000000..423cec85dd --- /dev/null +++ b/spec/ruby/core/numeric/clone_spec.rb @@ -0,0 +1,30 @@ +require_relative '../../spec_helper' + +describe "Numeric#clone" do + it "returns self" do + value = 1 + value.clone.should equal(value) + + subclass = Class.new(Numeric) + value = subclass.new + value.clone.should equal(value) + end + + it "does not change frozen status" do + 1.clone.frozen?.should == true + end + + it "accepts optional keyword argument :freeze" do + value = 1 + value.clone(freeze: true).should equal(value) + end + + it "raises ArgumentError if passed freeze: false" do + -> { 1.clone(freeze: false) }.should raise_error(ArgumentError, /can't unfreeze/) + end + + it "does not change frozen status if passed freeze: nil" do + value = 1 + value.clone(freeze: nil).should equal(value) + end +end diff --git a/spec/ruby/core/numeric/coerce_spec.rb b/spec/ruby/core/numeric/coerce_spec.rb new file mode 100644 index 0000000000..4c4416d30b --- /dev/null +++ b/spec/ruby/core/numeric/coerce_spec.rb @@ -0,0 +1,59 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Numeric#coerce" do + before :each do + @obj = NumericSpecs::Subclass.new + @obj.should_receive(:to_f).any_number_of_times.and_return(10.5) + end + + it "returns [other, self] if self and other are instances of the same class" do + a = NumericSpecs::Subclass.new + b = NumericSpecs::Subclass.new + + a.coerce(b).should == [b, a] + end + + # I (emp) think that this behavior is actually a bug in MRI. It's here as documentation + # of the behavior until we find out if it's a bug. + quarantine! do + it "considers the presence of a metaclass when checking the class of the objects" do + a = NumericSpecs::Subclass.new + b = NumericSpecs::Subclass.new + + # inject a metaclass on a + class << a; true; end + + # watch it explode + -> { a.coerce(b) }.should raise_error(TypeError) + end + end + + it "returns [other.to_f, self.to_f] if self and other are instances of different classes" do + @obj.coerce(2.5).should == [2.5, 10.5] + @obj.coerce(3).should == [3.0, 10.5] + @obj.coerce("4.4").should == [4.4, 10.5] + @obj.coerce(bignum_value).should == [bignum_value.to_f, 10.5] + end + + it "raise TypeError if they are instances of different classes and other does not respond to #to_f" do + other = mock("numeric") + -> { @obj.coerce(other) }.should raise_error(TypeError) + end + + it "raises a TypeError when passed nil" do + -> { @obj.coerce(nil) }.should raise_error(TypeError) + end + + it "raises a TypeError when passed a boolean" do + -> { @obj.coerce(false) }.should raise_error(TypeError) + end + + it "raises a TypeError when passed a Symbol" do + -> { @obj.coerce(:symbol) }.should raise_error(TypeError) + end + + it "raises an ArgumentError when passed a non-numeric String" do + -> { @obj.coerce("test") }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/numeric/comparison_spec.rb b/spec/ruby/core/numeric/comparison_spec.rb new file mode 100644 index 0000000000..4b4d52501a --- /dev/null +++ b/spec/ruby/core/numeric/comparison_spec.rb @@ -0,0 +1,48 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Numeric#<=>" do + before :each do + @obj = NumericSpecs::Subclass.new + end + + it "returns 0 if self equals other" do + (@obj <=> @obj).should == 0 + end + + it "returns nil if self does not equal other" do + (@obj <=> NumericSpecs::Subclass.new).should == nil + (@obj <=> 10).should == nil + (@obj <=> -3.5).should == nil + (@obj <=> bignum_value).should == nil + end + + describe "with subclasses of Numeric" do + before :each do + @a = NumericSpecs::Comparison.new + @b = NumericSpecs::Comparison.new + + ScratchPad.clear + end + + it "is called when instances are compared with #<" do + (@a < @b).should be_false + ScratchPad.recorded.should == :numeric_comparison + end + + it "is called when instances are compared with #<=" do + (@a <= @b).should be_false + ScratchPad.recorded.should == :numeric_comparison + end + + it "is called when instances are compared with #>" do + (@a > @b).should be_true + ScratchPad.recorded.should == :numeric_comparison + end + + it "is called when instances are compared with #>=" do + (@a >= @b).should be_true + ScratchPad.recorded.should == :numeric_comparison + end + end +end diff --git a/spec/ruby/core/numeric/conj_spec.rb b/spec/ruby/core/numeric/conj_spec.rb new file mode 100644 index 0000000000..7d4777ca60 --- /dev/null +++ b/spec/ruby/core/numeric/conj_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/conj' + +describe "Numeric#conj" do + it_behaves_like :numeric_conj, :conj +end diff --git a/spec/ruby/core/numeric/conjugate_spec.rb b/spec/ruby/core/numeric/conjugate_spec.rb new file mode 100644 index 0000000000..99854766e7 --- /dev/null +++ b/spec/ruby/core/numeric/conjugate_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/conj' + +describe "Numeric#conjugate" do + it_behaves_like :numeric_conj, :conjugate +end diff --git a/spec/ruby/core/numeric/denominator_spec.rb b/spec/ruby/core/numeric/denominator_spec.rb new file mode 100644 index 0000000000..34729446a2 --- /dev/null +++ b/spec/ruby/core/numeric/denominator_spec.rb @@ -0,0 +1,24 @@ +require_relative '../../spec_helper' + +describe "Numeric#denominator" do + # The Numeric child classes override this method, so their behaviour is + # specified in the appropriate place + before :each do + @numbers = [ + 20, # Integer + 99999999**99, # Bignum + ] + end + + it "returns 1" do + @numbers.each {|number| number.denominator.should == 1} + end + + it "works with Numeric subclasses" do + rational = mock_numeric('rational') + rational.should_receive(:denominator).and_return(:denominator) + numeric = mock_numeric('numeric') + numeric.should_receive(:to_r).and_return(rational) + numeric.denominator.should == :denominator + end +end diff --git a/spec/ruby/core/numeric/div_spec.rb b/spec/ruby/core/numeric/div_spec.rb new file mode 100644 index 0000000000..53917b84c9 --- /dev/null +++ b/spec/ruby/core/numeric/div_spec.rb @@ -0,0 +1,22 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Numeric#div" do + before :each do + @obj = NumericSpecs::Subclass.new + end + + it "calls self#/ with other, then returns the #floor'ed result" do + result = mock("Numeric#div result") + result.should_receive(:floor).and_return(12) + @obj.should_receive(:/).with(10).and_return(result) + + @obj.div(10).should == 12 + end + + it "raises ZeroDivisionError for 0" do + -> { @obj.div(0) }.should raise_error(ZeroDivisionError) + -> { @obj.div(0.0) }.should raise_error(ZeroDivisionError) + -> { @obj.div(Complex(0,0)) }.should raise_error(ZeroDivisionError) + end +end diff --git a/spec/ruby/core/numeric/divmod_spec.rb b/spec/ruby/core/numeric/divmod_spec.rb new file mode 100644 index 0000000000..8d5259bbcd --- /dev/null +++ b/spec/ruby/core/numeric/divmod_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Numeric#divmod" do + before :each do + @obj = NumericSpecs::Subclass.new + end + + it "returns [quotient, modulus], with quotient being obtained as in Numeric#div then #floor and modulus being obtained by calling self#- with quotient * other" do + @obj.should_receive(:/).twice.with(10).and_return(13 - TOLERANCE, 13 - TOLERANCE) + @obj.should_receive(:-).with(120).and_return(3) + + @obj.divmod(10).should == [12, 3] + end +end diff --git a/spec/ruby/core/numeric/dup_spec.rb b/spec/ruby/core/numeric/dup_spec.rb new file mode 100644 index 0000000000..189a7ef44d --- /dev/null +++ b/spec/ruby/core/numeric/dup_spec.rb @@ -0,0 +1,16 @@ +require_relative '../../spec_helper' + +describe "Numeric#dup" do + it "returns self" do + value = 1 + value.dup.should equal(value) + + subclass = Class.new(Numeric) + value = subclass.new + value.dup.should equal(value) + end + + it "does not change frozen status" do + 1.dup.frozen?.should == true + end +end diff --git a/spec/ruby/core/numeric/eql_spec.rb b/spec/ruby/core/numeric/eql_spec.rb new file mode 100644 index 0000000000..b33e00e51f --- /dev/null +++ b/spec/ruby/core/numeric/eql_spec.rb @@ -0,0 +1,22 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Numeric#eql?" do + before :each do + @obj = NumericSpecs::Subclass.new + end + + it "returns false if self's and other's types don't match" do + @obj.should_not eql(1) + @obj.should_not eql(-1.5) + @obj.should_not eql(bignum_value) + @obj.should_not eql(:sym) + end + + it "returns the result of calling self#== with other when self's and other's types match" do + other = NumericSpecs::Subclass.new + @obj.should_receive(:==).with(other).and_return("result", nil) + @obj.should eql(other) + @obj.should_not eql(other) + end +end diff --git a/spec/ruby/core/numeric/fdiv_spec.rb b/spec/ruby/core/numeric/fdiv_spec.rb new file mode 100644 index 0000000000..e97fa77f79 --- /dev/null +++ b/spec/ruby/core/numeric/fdiv_spec.rb @@ -0,0 +1,31 @@ +require_relative '../../spec_helper' + +describe "Numeric#fdiv" do + it "coerces self with #to_f" do + numeric = mock_numeric('numeric') + numeric.should_receive(:to_f).and_return(3.0) + numeric.fdiv(0.5).should == 6.0 + end + + it "coerces other with #to_f" do + numeric = mock_numeric('numeric') + numeric.should_receive(:to_f).and_return(3.0) + 6.fdiv(numeric).should == 2.0 + end + + it "performs floating-point division" do + 3.fdiv(2).should == 1.5 + end + + it "returns a Float" do + bignum_value.fdiv(Float::MAX).should be_an_instance_of(Float) + end + + it "returns Infinity if other is 0" do + 8121.92821.fdiv(0).infinite?.should == 1 + end + + it "returns NaN if other is NaN" do + 3334.fdiv(nan_value).nan?.should be_true + end +end diff --git a/spec/ruby/core/numeric/finite_spec.rb b/spec/ruby/core/numeric/finite_spec.rb new file mode 100644 index 0000000000..05b5eebbd6 --- /dev/null +++ b/spec/ruby/core/numeric/finite_spec.rb @@ -0,0 +1,8 @@ +require_relative '../../spec_helper' + +describe "Numeric#finite?" do + it "returns true by default" do + o = mock_numeric("finite") + o.finite?.should be_true + end +end diff --git a/spec/ruby/core/numeric/fixtures/classes.rb b/spec/ruby/core/numeric/fixtures/classes.rb new file mode 100644 index 0000000000..1505584889 --- /dev/null +++ b/spec/ruby/core/numeric/fixtures/classes.rb @@ -0,0 +1,17 @@ +module NumericSpecs + class Comparison < Numeric + # This method is used because we cannot define + # singleton methods on subclasses of Numeric, + # which is needed for a.should_receive to work. + def <=>(other) + ScratchPad.record :numeric_comparison + 1 + end + end + + class Subclass < Numeric + # Allow methods to be mocked + def singleton_method_added(val) + end + end +end diff --git a/spec/ruby/core/numeric/floor_spec.rb b/spec/ruby/core/numeric/floor_spec.rb new file mode 100644 index 0000000000..80a4868e4d --- /dev/null +++ b/spec/ruby/core/numeric/floor_spec.rb @@ -0,0 +1,14 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Numeric#floor" do + before :each do + @obj = NumericSpecs::Subclass.new + end + + it "converts self to a Float (using #to_f) and returns the #floor'ed result" do + @obj.should_receive(:to_f).and_return(2 - TOLERANCE, TOLERANCE - 2) + @obj.floor.should == 1 + @obj.floor.should == -2 + end +end diff --git a/spec/ruby/core/numeric/i_spec.rb b/spec/ruby/core/numeric/i_spec.rb new file mode 100644 index 0000000000..621ecc09ec --- /dev/null +++ b/spec/ruby/core/numeric/i_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../spec_helper' + +describe "Numeric#i" do + it "returns a Complex object" do + 34.i.should be_an_instance_of(Complex) + end + + it "sets the real part to 0" do + 7342.i.real.should == 0 + end + + it "sets the imaginary part to self" do + 62.81.i.imag.should == 62.81 + end +end diff --git a/spec/ruby/core/numeric/imag_spec.rb b/spec/ruby/core/numeric/imag_spec.rb new file mode 100644 index 0000000000..b9e343cee9 --- /dev/null +++ b/spec/ruby/core/numeric/imag_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/imag' + +describe "Numeric#imag" do + it_behaves_like :numeric_imag, :imag +end diff --git a/spec/ruby/core/numeric/imaginary_spec.rb b/spec/ruby/core/numeric/imaginary_spec.rb new file mode 100644 index 0000000000..ec708cb505 --- /dev/null +++ b/spec/ruby/core/numeric/imaginary_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/imag' + +describe "Numeric#imaginary" do + it_behaves_like :numeric_imag, :imaginary +end diff --git a/spec/ruby/core/numeric/infinite_spec.rb b/spec/ruby/core/numeric/infinite_spec.rb new file mode 100644 index 0000000000..3ea7825c8c --- /dev/null +++ b/spec/ruby/core/numeric/infinite_spec.rb @@ -0,0 +1,8 @@ +require_relative '../../spec_helper' + +describe "Numeric#infinite?" do + it "returns nil by default" do + o = mock_numeric("infinite") + o.infinite?.should == nil + end +end diff --git a/spec/ruby/core/numeric/integer_spec.rb b/spec/ruby/core/numeric/integer_spec.rb new file mode 100644 index 0000000000..adbac4d7aa --- /dev/null +++ b/spec/ruby/core/numeric/integer_spec.rb @@ -0,0 +1,8 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Numeric#integer?" do + it "returns false" do + NumericSpecs::Subclass.new.should_not.integer? + end +end diff --git a/spec/ruby/core/numeric/magnitude_spec.rb b/spec/ruby/core/numeric/magnitude_spec.rb new file mode 100644 index 0000000000..1371dff21f --- /dev/null +++ b/spec/ruby/core/numeric/magnitude_spec.rb @@ -0,0 +1,6 @@ +require_relative "../../spec_helper" +require_relative 'shared/abs' + +describe "Numeric#magnitude" do + it_behaves_like :numeric_abs, :magnitude +end diff --git a/spec/ruby/core/numeric/modulo_spec.rb b/spec/ruby/core/numeric/modulo_spec.rb new file mode 100644 index 0000000000..e3dc7e56f3 --- /dev/null +++ b/spec/ruby/core/numeric/modulo_spec.rb @@ -0,0 +1,24 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe :numeric_modulo_19, shared: true do + it "returns self - other * self.div(other)" do + s = mock_numeric('self') + o = mock_numeric('other') + n3 = mock_numeric('n3') + n4 = mock_numeric('n4') + n5 = mock_numeric('n5') + s.should_receive(:div).with(o).and_return(n3) + o.should_receive(:*).with(n3).and_return(n4) + s.should_receive(:-).with(n4).and_return(n5) + s.send(@method, o).should == n5 + end +end + +describe "Numeric#modulo" do + it_behaves_like :numeric_modulo_19, :modulo +end + +describe "Numeric#%" do + it_behaves_like :numeric_modulo_19, :% +end diff --git a/spec/ruby/core/numeric/negative_spec.rb b/spec/ruby/core/numeric/negative_spec.rb new file mode 100644 index 0000000000..9c6f95fd87 --- /dev/null +++ b/spec/ruby/core/numeric/negative_spec.rb @@ -0,0 +1,41 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Numeric#negative?" do + describe "on positive numbers" do + it "returns false" do + 1.negative?.should be_false + 0.1.negative?.should be_false + end + end + + describe "on zero" do + it "returns false" do + 0.negative?.should be_false + 0.0.negative?.should be_false + end + end + + describe "on negative numbers" do + it "returns true" do + -1.negative?.should be_true + -0.1.negative?.should be_true + end + end +end + +describe "Numeric#negative?" do + before(:each) do + @obj = NumericSpecs::Subclass.new + end + + it "returns true if self is less than 0" do + @obj.should_receive(:<).with(0).and_return(true) + @obj.should.negative? + end + + it "returns false if self is greater than 0" do + @obj.should_receive(:<).with(0).and_return(false) + @obj.should_not.negative? + end +end diff --git a/spec/ruby/core/numeric/nonzero_spec.rb b/spec/ruby/core/numeric/nonzero_spec.rb new file mode 100644 index 0000000000..464ed4f4f8 --- /dev/null +++ b/spec/ruby/core/numeric/nonzero_spec.rb @@ -0,0 +1,18 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Numeric#nonzero?" do + before :each do + @obj = NumericSpecs::Subclass.new + end + + it "returns self if self#zero? is false" do + @obj.should_receive(:zero?).and_return(false) + @obj.nonzero?.should == @obj + end + + it "returns nil if self#zero? is true" do + @obj.should_receive(:zero?).and_return(true) + @obj.nonzero?.should == nil + end +end diff --git a/spec/ruby/core/numeric/numerator_spec.rb b/spec/ruby/core/numeric/numerator_spec.rb new file mode 100644 index 0000000000..668df8b797 --- /dev/null +++ b/spec/ruby/core/numeric/numerator_spec.rb @@ -0,0 +1,33 @@ +require_relative '../../spec_helper' + +describe "Numeric#numerator" do + before :all do + @numbers = [ + 0, + 29871, + 99999999999999**99, + -72628191273, + 29282.2827, + -2927.00091, + 0.0, + 12.0, + Float::MAX, + ] + end + + # This isn't entirely true, as NaN.numerator works, whereas + # Rational(NaN) raises an exception, but we test this in Float#numerator + it "converts self to a Rational object then returns its numerator" do + @numbers.each do |number| + number.numerator.should == Rational(number).numerator + end + end + + it "works with Numeric subclasses" do + rational = mock_numeric('rational') + rational.should_receive(:numerator).and_return(:numerator) + numeric = mock_numeric('numeric') + numeric.should_receive(:to_r).and_return(rational) + numeric.numerator.should == :numerator + end +end diff --git a/spec/ruby/core/numeric/numeric_spec.rb b/spec/ruby/core/numeric/numeric_spec.rb new file mode 100644 index 0000000000..2bcf2a1175 --- /dev/null +++ b/spec/ruby/core/numeric/numeric_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' + +describe "Numeric" do + it "includes Comparable" do + Numeric.include?(Comparable).should == true + end +end diff --git a/spec/ruby/core/numeric/phase_spec.rb b/spec/ruby/core/numeric/phase_spec.rb new file mode 100644 index 0000000000..bc1995303f --- /dev/null +++ b/spec/ruby/core/numeric/phase_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/arg' + +describe "Numeric#phase" do + it_behaves_like :numeric_arg, :phase +end diff --git a/spec/ruby/core/numeric/polar_spec.rb b/spec/ruby/core/numeric/polar_spec.rb new file mode 100644 index 0000000000..b594e408b2 --- /dev/null +++ b/spec/ruby/core/numeric/polar_spec.rb @@ -0,0 +1,50 @@ +require_relative '../../spec_helper' + +describe "Numeric#polar" do + before :each do + @pos_numbers = [ + 1, + 3898172610**9, + 987.18273, + Float::MAX, + Rational(13,7), + infinity_value, + ] + @neg_numbers = @pos_numbers.map {|n| -n} + @numbers = @pos_numbers + @neg_numbers + @numbers.push(0, 0.0) + end + + it "returns a two-element Array" do + @numbers.each do |number| + number.polar.should be_an_instance_of(Array) + number.polar.size.should == 2 + end + end + + it "sets the first value to the absolute value of self" do + @numbers.each do |number| + number.polar.first.should == number.abs + end + end + + it "sets the last value to 0 if self is positive" do + (@numbers - @neg_numbers).each do |number| + number.should >= 0 + number.polar.last.should == 0 + end + end + + it "sets the last value to Pi if self is negative" do + @neg_numbers.each do |number| + number.should < 0 + number.polar.last.should == Math::PI + end + end + + it "returns [NaN, NaN] if self is NaN" do + nan_value.polar.size.should == 2 + nan_value.polar.first.nan?.should be_true + nan_value.polar.last.nan?.should be_true + end +end diff --git a/spec/ruby/core/numeric/positive_spec.rb b/spec/ruby/core/numeric/positive_spec.rb new file mode 100644 index 0000000000..3b831b4d34 --- /dev/null +++ b/spec/ruby/core/numeric/positive_spec.rb @@ -0,0 +1,41 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Numeric#positive?" do + describe "on positive numbers" do + it "returns true" do + 1.positive?.should be_true + 0.1.positive?.should be_true + end + end + + describe "on zero" do + it "returns false" do + 0.positive?.should be_false + 0.0.positive?.should be_false + end + end + + describe "on negative numbers" do + it "returns false" do + -1.positive?.should be_false + -0.1.positive?.should be_false + end + end +end + +describe "Numeric#positive?" do + before(:each) do + @obj = NumericSpecs::Subclass.new + end + + it "returns true if self is greater than 0" do + @obj.should_receive(:>).with(0).and_return(true) + @obj.should.positive? + end + + it "returns false if self is less than 0" do + @obj.should_receive(:>).with(0).and_return(false) + @obj.should_not.positive? + end +end diff --git a/spec/ruby/core/numeric/quo_spec.rb b/spec/ruby/core/numeric/quo_spec.rb new file mode 100644 index 0000000000..6e3ce7a374 --- /dev/null +++ b/spec/ruby/core/numeric/quo_spec.rb @@ -0,0 +1,63 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Numeric#quo" do + it "returns the result of self divided by the given Integer as a Rational" do + 5.quo(2).should eql(Rational(5,2)) + end + + it "returns the result of self divided by the given Float as a Float" do + 2.quo(2.5).should eql(0.8) + end + + it "returns the result of self divided by the given Bignum as a Float" do + 45.quo(bignum_value).should be_close(1.04773789668636e-08, TOLERANCE) + end + + it "raises a ZeroDivisionError when the given Integer is 0" do + -> { 0.quo(0) }.should raise_error(ZeroDivisionError) + -> { 10.quo(0) }.should raise_error(ZeroDivisionError) + -> { -10.quo(0) }.should raise_error(ZeroDivisionError) + -> { bignum_value.quo(0) }.should raise_error(ZeroDivisionError) + -> { (-bignum_value).quo(0) }.should raise_error(ZeroDivisionError) + end + + it "calls #to_r to convert the object to a Rational" do + obj = NumericSpecs::Subclass.new + obj.should_receive(:to_r).and_return(Rational(1)) + + obj.quo(19).should == Rational(1, 19) + end + + it "raises a TypeError of #to_r does not return a Rational" do + obj = NumericSpecs::Subclass.new + obj.should_receive(:to_r).and_return(1) + + -> { obj.quo(19) }.should raise_error(TypeError) + end + + it "raises a TypeError when given a non-Integer" do + -> { + (obj = mock('x')).should_not_receive(:to_int) + 13.quo(obj) + }.should raise_error(TypeError) + -> { 13.quo("10") }.should raise_error(TypeError) + -> { 13.quo(:symbol) }.should raise_error(TypeError) + end + + it "returns the result of calling self#/ with other" do + obj = NumericSpecs::Subclass.new + obj.should_receive(:to_r).and_return(19.quo(20)) + + obj.quo(19).should == 1.quo(20) + end + + it "raises a ZeroDivisionError if the given argument is zero and not a Float" do + -> { 1.quo(0) }.should raise_error(ZeroDivisionError) + end + + it "returns infinity if the given argument is zero and is a Float" do + (1.quo(0.0)).to_s.should == 'Infinity' + (-1.quo(0.0)).to_s.should == '-Infinity' + end +end diff --git a/spec/ruby/core/numeric/real_spec.rb b/spec/ruby/core/numeric/real_spec.rb new file mode 100644 index 0000000000..2d2499bcce --- /dev/null +++ b/spec/ruby/core/numeric/real_spec.rb @@ -0,0 +1,37 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Numeric#real" do + before :each do + @numbers = [ + 20, # Integer + 398.72, # Float + Rational(3, 4), # Rational + bignum_value, # Bignum + infinity_value, + nan_value + ].map{ |n| [n, -n] }.flatten + end + + it "returns self" do + @numbers.each do |number| + if number.to_f.nan? + number.real.nan?.should be_true + else + number.real.should == number + end + end + end + + it "raises an ArgumentError if given any arguments" do + @numbers.each do |number| + -> { number.real(number) }.should raise_error(ArgumentError) + end + end +end + +describe "Numeric#real?" do + it "returns true" do + NumericSpecs::Subclass.new.should.real? + end +end diff --git a/spec/ruby/core/numeric/rect_spec.rb b/spec/ruby/core/numeric/rect_spec.rb new file mode 100644 index 0000000000..79a144c5a4 --- /dev/null +++ b/spec/ruby/core/numeric/rect_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/rect' + +describe "Numeric#rect" do + it_behaves_like :numeric_rect, :rect +end diff --git a/spec/ruby/core/numeric/rectangular_spec.rb b/spec/ruby/core/numeric/rectangular_spec.rb new file mode 100644 index 0000000000..2c68985a16 --- /dev/null +++ b/spec/ruby/core/numeric/rectangular_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/rect' + +describe "Numeric#rectangular" do + it_behaves_like :numeric_rect, :rectangular +end diff --git a/spec/ruby/core/numeric/remainder_spec.rb b/spec/ruby/core/numeric/remainder_spec.rb new file mode 100644 index 0000000000..674fa22d8e --- /dev/null +++ b/spec/ruby/core/numeric/remainder_spec.rb @@ -0,0 +1,70 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Numeric#remainder" do + before :each do + @obj = NumericSpecs::Subclass.new + @result = mock("Numeric#% result") + @other = mock("Passed Object") + ruby_version_is "3.3" do + @other.should_receive(:coerce).with(@obj).and_return([@obj, @other]) + end + end + + it "returns the result of calling self#% with other if self is 0" do + @obj.should_receive(:%).with(@other).and_return(@result) + @result.should_receive(:==).with(0).and_return(true) + + @obj.remainder(@other).should equal(@result) + end + + it "returns the result of calling self#% with other if self and other are greater than 0" do + @obj.should_receive(:%).with(@other).and_return(@result) + @result.should_receive(:==).with(0).and_return(false) + + @obj.should_receive(:<).with(0).and_return(false) + + @obj.should_receive(:>).with(0).and_return(true) + @other.should_receive(:<).with(0).and_return(false) + + @obj.remainder(@other).should equal(@result) + end + + it "returns the result of calling self#% with other if self and other are less than 0" do + @obj.should_receive(:%).with(@other).and_return(@result) + @result.should_receive(:==).with(0).and_return(false) + + @obj.should_receive(:<).with(0).and_return(true) + @other.should_receive(:>).with(0).and_return(false) + + @obj.should_receive(:>).with(0).and_return(false) + + @obj.remainder(@other).should equal(@result) + end + + it "returns the result of calling self#% with other - other if self is greater than 0 and other is less than 0" do + @obj.should_receive(:%).with(@other).and_return(@result) + @result.should_receive(:==).with(0).and_return(false) + + @obj.should_receive(:<).with(0).and_return(false) + + @obj.should_receive(:>).with(0).and_return(true) + @other.should_receive(:<).with(0).and_return(true) + + @result.should_receive(:-).with(@other).and_return(:result) + + @obj.remainder(@other).should == :result + end + + it "returns the result of calling self#% with other - other if self is less than 0 and other is greater than 0" do + @obj.should_receive(:%).with(@other).and_return(@result) + @result.should_receive(:==).with(0).and_return(false) + + @obj.should_receive(:<).with(0).and_return(true) + @other.should_receive(:>).with(0).and_return(true) + + @result.should_receive(:-).with(@other).and_return(:result) + + @obj.remainder(@other).should == :result + end +end diff --git a/spec/ruby/core/numeric/round_spec.rb b/spec/ruby/core/numeric/round_spec.rb new file mode 100644 index 0000000000..47c5837693 --- /dev/null +++ b/spec/ruby/core/numeric/round_spec.rb @@ -0,0 +1,14 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Numeric#round" do + before :each do + @obj = NumericSpecs::Subclass.new + end + + it "converts self to a Float (using #to_f) and returns the #round'ed result" do + @obj.should_receive(:to_f).and_return(2 - TOLERANCE, TOLERANCE - 2) + @obj.round.should == 2 + @obj.round.should == -2 + end +end diff --git a/spec/ruby/core/numeric/shared/abs.rb b/spec/ruby/core/numeric/shared/abs.rb new file mode 100644 index 0000000000..c3dadccfd6 --- /dev/null +++ b/spec/ruby/core/numeric/shared/abs.rb @@ -0,0 +1,19 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' + +describe :numeric_abs, shared: true do + before :each do + @obj = NumericSpecs::Subclass.new + end + + it "returns self when self is greater than 0" do + @obj.should_receive(:<).with(0).and_return(false) + @obj.send(@method).should == @obj + end + + it "returns self\#@- when self is less than 0" do + @obj.should_receive(:<).with(0).and_return(true) + @obj.should_receive(:-@).and_return(:absolute_value) + @obj.send(@method).should == :absolute_value + end +end diff --git a/spec/ruby/core/numeric/shared/arg.rb b/spec/ruby/core/numeric/shared/arg.rb new file mode 100644 index 0000000000..c8e7ad8333 --- /dev/null +++ b/spec/ruby/core/numeric/shared/arg.rb @@ -0,0 +1,38 @@ +require_relative '../../../spec_helper' + +describe :numeric_arg, shared: true do + before :each do + @numbers = [ + 20, + Rational(3, 4), + bignum_value, + infinity_value + ] + end + + it "returns 0 if positive" do + @numbers.each do |number| + number.send(@method).should == 0 + end + end + + it "returns Pi if negative" do + @numbers.each do |number| + (0-number).send(@method).should == Math::PI + end + end + + describe "with a Numeric subclass" do + it "returns 0 if self#<(0) returns false" do + numeric = mock_numeric('positive') + numeric.should_receive(:<).with(0).and_return(false) + numeric.send(@method).should == 0 + end + + it "returns Pi if self#<(0) returns true" do + numeric = mock_numeric('positive') + numeric.should_receive(:<).with(0).and_return(true) + numeric.send(@method).should == Math::PI + end + end +end diff --git a/spec/ruby/core/numeric/shared/conj.rb b/spec/ruby/core/numeric/shared/conj.rb new file mode 100644 index 0000000000..6d5197ecab --- /dev/null +++ b/spec/ruby/core/numeric/shared/conj.rb @@ -0,0 +1,20 @@ +require_relative '../../../spec_helper' + +describe :numeric_conj, shared: true do + before :each do + @numbers = [ + 20, # Integer + 398.72, # Float + Rational(3, 4), # Rational + bignum_value, + infinity_value, + nan_value + ] + end + + it "returns self" do + @numbers.each do |number| + number.send(@method).should equal(number) + end + end +end diff --git a/spec/ruby/core/numeric/shared/imag.rb b/spec/ruby/core/numeric/shared/imag.rb new file mode 100644 index 0000000000..4f117e243a --- /dev/null +++ b/spec/ruby/core/numeric/shared/imag.rb @@ -0,0 +1,26 @@ +require_relative '../../../spec_helper' + +describe :numeric_imag, shared: true do + before :each do + @numbers = [ + 20, # Integer + 398.72, # Float + Rational(3, 4), # Rational + bignum_value, # Bignum + infinity_value, + nan_value + ].map{|n| [n,-n]}.flatten + end + + it "returns 0" do + @numbers.each do |number| + number.send(@method).should == 0 + end + end + + it "raises an ArgumentError if given any arguments" do + @numbers.each do |number| + -> { number.send(@method, number) }.should raise_error(ArgumentError) + end + end +end diff --git a/spec/ruby/core/numeric/shared/rect.rb b/spec/ruby/core/numeric/shared/rect.rb new file mode 100644 index 0000000000..120a69b1c4 --- /dev/null +++ b/spec/ruby/core/numeric/shared/rect.rb @@ -0,0 +1,48 @@ +require_relative '../../../spec_helper' + +describe :numeric_rect, shared: true do + before :each do + @numbers = [ + 20, # Integer + 398.72, # Float + Rational(3, 4), # Rational + 99999999**99, # Bignum + infinity_value, + nan_value + ] + end + + it "returns an Array" do + @numbers.each do |number| + number.send(@method).should be_an_instance_of(Array) + end + end + + it "returns a two-element Array" do + @numbers.each do |number| + number.send(@method).size.should == 2 + end + end + + it "returns self as the first element" do + @numbers.each do |number| + if Float === number and number.nan? + number.send(@method).first.nan?.should be_true + else + number.send(@method).first.should == number + end + end + end + + it "returns 0 as the last element" do + @numbers.each do |number| + number.send(@method).last.should == 0 + end + end + + it "raises an ArgumentError if given any arguments" do + @numbers.each do |number| + -> { number.send(@method, number) }.should raise_error(ArgumentError) + end + end +end diff --git a/spec/ruby/core/numeric/shared/step.rb b/spec/ruby/core/numeric/shared/step.rb new file mode 100644 index 0000000000..977ec6de02 --- /dev/null +++ b/spec/ruby/core/numeric/shared/step.rb @@ -0,0 +1,410 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' + +# Describes Numeric#step shared specs between different argument styles. +# To be able to do it, the @step ivar must contain a Proc that transforms +# the step call arguments passed as positional arguments to the style of +# arguments pretended to test. +describe :numeric_step, shared: true do + before :each do + ScratchPad.record [] + @prc = -> x { ScratchPad << x } + end + + it "defaults to step = 1" do + @step.call(1, 5, &@prc) + ScratchPad.recorded.should eql [1, 2, 3, 4, 5] + end + + it "defaults to an infinite limit with a step size of 1 for Integers" do + 1.step.first(5).should == [1, 2, 3, 4, 5] + end + + it "defaults to an infinite limit with a step size of 1.0 for Floats" do + 1.0.step.first(5).should == [1.0, 2.0, 3.0, 4.0, 5.0] + end + + describe "when self, stop and step are Integers" do + it "yields only Integers" do + @step.call(1, 5, 1) { |x| x.should be_an_instance_of(Integer) } + end + + describe "with a positive step" do + it "yields while increasing self by step until stop is reached" do + @step.call(1, 5, 1, &@prc) + ScratchPad.recorded.should eql [1, 2, 3, 4, 5] + end + + it "yields once when self equals stop" do + @step.call(1, 1, 1, &@prc) + ScratchPad.recorded.should eql [1] + end + + it "does not yield when self is greater than stop" do + @step.call(2, 1, 1, &@prc) + ScratchPad.recorded.should eql [] + end + end + + describe "with a negative step" do + it "yields while decreasing self by step until stop is reached" do + @step.call(5, 1, -1, &@prc) + ScratchPad.recorded.should eql [5, 4, 3, 2, 1] + end + + it "yields once when self equals stop" do + @step.call(5, 5, -1, &@prc) + ScratchPad.recorded.should eql [5] + end + + it "does not yield when self is less than stop" do + @step.call(1, 5, -1, &@prc) + ScratchPad.recorded.should == [] + end + end + end + + describe "when at least one of self, stop or step is a Float" do + it "yields Floats even if only self is a Float" do + @step.call(1.5, 5, 1) { |x| x.should be_an_instance_of(Float) } + end + + it "yields Floats even if only stop is a Float" do + @step.call(1, 5.0, 1) { |x| x.should be_an_instance_of(Float) } + end + + it "yields Floats even if only step is a Float" do + @step.call(1, 5, 1.0) { |x| x.should be_an_instance_of(Float) } + end + + describe "with a positive step" do + it "yields while increasing self by step while < stop" do + @step.call(1.5, 5, 1, &@prc) + ScratchPad.recorded.should eql [1.5, 2.5, 3.5, 4.5] + end + + it "yields once when self equals stop" do + @step.call(1.5, 1.5, 1, &@prc) + ScratchPad.recorded.should eql [1.5] + end + + it "does not yield when self is greater than stop" do + @step.call(2.5, 1.5, 1, &@prc) + ScratchPad.recorded.should == [] + end + + it "is careful about not yielding a value greater than limit" do + # As 9*1.3+1.0 == 12.700000000000001 > 12.7, we test: + @step.call(1.0, 12.7, 1.3, &@prc) + ScratchPad.recorded.should eql [1.0, 2.3, 3.6, 4.9, 6.2, 7.5, 8.8, 10.1, 11.4, 12.7] + end + end + + describe "with a negative step" do + it "yields while decreasing self by step while self > stop" do + @step.call(5, 1.5, -1, &@prc) + ScratchPad.recorded.should eql [5.0, 4.0, 3.0, 2.0] + end + + it "yields once when self equals stop" do + @step.call(1.5, 1.5, -1, &@prc) + ScratchPad.recorded.should eql [1.5] + end + + it "does not yield when self is less than stop" do + @step.call(1, 5, -1.5, &@prc) + ScratchPad.recorded.should == [] + end + + it "is careful about not yielding a value smaller than limit" do + # As -9*1.3-1.0 == -12.700000000000001 < -12.7, we test: + @step.call(-1.0, -12.7, -1.3, &@prc) + ScratchPad.recorded.should eql [-1.0, -2.3, -3.6, -4.9, -6.2, -7.5, -8.8, -10.1, -11.4, -12.7] + end + end + + describe "with a positive Infinity step" do + it "yields once if self < stop" do + @step.call(42, 100, infinity_value, &@prc) + ScratchPad.recorded.should eql [42.0] + end + + it "yields once when stop is Infinity" do + @step.call(42, infinity_value, infinity_value, &@prc) + ScratchPad.recorded.should eql [42.0] + end + + it "yields once when self equals stop" do + @step.call(42, 42, infinity_value, &@prc) + ScratchPad.recorded.should eql [42.0] + end + + it "yields once when self and stop are Infinity" do + # @step.call(infinity_value, infinity_value, infinity_value, &@prc) + @step.call(infinity_value, infinity_value, infinity_value, &@prc) + ScratchPad.recorded.should == [infinity_value] + end + + it "does not yield when self > stop" do + @step.call(100, 42, infinity_value, &@prc) + ScratchPad.recorded.should == [] + end + + it "does not yield when stop is -Infinity" do + @step.call(42, -infinity_value, infinity_value, &@prc) + ScratchPad.recorded.should == [] + end + end + + describe "with a negative Infinity step" do + it "yields once if self > stop" do + @step.call(42, 6, -infinity_value, &@prc) + ScratchPad.recorded.should eql [42.0] + end + + it "yields once if stop is -Infinity" do + @step.call(42, -infinity_value, -infinity_value, &@prc) + ScratchPad.recorded.should eql [42.0] + end + + it "yields once when self equals stop" do + @step.call(42, 42, -infinity_value, &@prc) + ScratchPad.recorded.should eql [42.0] + end + + it "yields once when self and stop are Infinity" do + @step.call(infinity_value, infinity_value, -infinity_value, &@prc) + ScratchPad.recorded.should == [infinity_value] + end + + it "does not yield when self > stop" do + @step.call(42, 100, -infinity_value, &@prc) + ScratchPad.recorded.should == [] + end + + it "does not yield when stop is Infinity" do + @step.call(42, infinity_value, -infinity_value, &@prc) + ScratchPad.recorded.should == [] + end + end + + describe "with a Infinity stop and a positive step" do + it "does not yield when self is infinity" do + @step.call(infinity_value, infinity_value, 1, &@prc) + ScratchPad.recorded.should == [] + end + end + + describe "with a Infinity stop and a negative step" do + it "does not yield when self is negative infinity" do + @step.call(-infinity_value, infinity_value, -1, &@prc) + ScratchPad.recorded.should == [] + end + + it "does not yield when self is positive infinity" do + @step.call(infinity_value, infinity_value, -1, &@prc) + ScratchPad.recorded.should == [] + end + end + + describe "with a negative Infinity stop and a positive step" do + it "does not yield when self is negative infinity" do + @step.call(-infinity_value, -infinity_value, 1, &@prc) + ScratchPad.recorded.should == [] + end + end + + describe "with a negative Infinity stop and a negative step" do + it "does not yield when self is negative infinity" do + @step.call(-infinity_value, -infinity_value, -1, &@prc) + ScratchPad.recorded.should == [] + end + end + + end + + describe "when step is a String" do + describe "with self and stop as Integers" do + it "raises an ArgumentError when step is a numeric representation" do + -> { @step.call(1, 5, "1") {} }.should raise_error(ArgumentError) + -> { @step.call(1, 5, "0.1") {} }.should raise_error(ArgumentError) + -> { @step.call(1, 5, "1/3") {} }.should raise_error(ArgumentError) + end + it "raises an ArgumentError with step as an alphanumeric string" do + -> { @step.call(1, 5, "foo") {} }.should raise_error(ArgumentError) + end + end + + describe "with self and stop as Floats" do + it "raises an ArgumentError when step is a numeric representation" do + -> { @step.call(1.1, 5.1, "1") {} }.should raise_error(ArgumentError) + -> { @step.call(1.1, 5.1, "0.1") {} }.should raise_error(ArgumentError) + -> { @step.call(1.1, 5.1, "1/3") {} }.should raise_error(ArgumentError) + end + it "raises an ArgumentError with step as an alphanumeric string" do + -> { @step.call(1.1, 5.1, "foo") {} }.should raise_error(ArgumentError) + end + end + end + + it "does not rescue ArgumentError exceptions" do + -> { @step.call(1, 2) { raise ArgumentError, "" }}.should raise_error(ArgumentError) + end + + it "does not rescue TypeError exceptions" do + -> { @step.call(1, 2) { raise TypeError, "" } }.should raise_error(TypeError) + end + + describe "when no block is given" do + step_enum_class = Enumerator::ArithmeticSequence + + it "returns an #{step_enum_class} when not passed a block and self > stop" do + @step.call(1, 0, 2).should be_an_instance_of(step_enum_class) + end + + it "returns an #{step_enum_class} when not passed a block and self < stop" do + @step.call(1, 2, 3).should be_an_instance_of(step_enum_class) + end + + it "returns an #{step_enum_class} that uses the given step" do + @step.call(0, 5, 2).to_a.should eql [0, 2, 4] + end + + describe "when step is a String" do + describe "with self and stop as Integers" do + it "returns an Enumerator" do + @step.call(1, 5, "foo").should be_an_instance_of(Enumerator) + end + end + + describe "with self and stop as Floats" do + it "returns an Enumerator" do + @step.call(1.1, 5.1, "foo").should be_an_instance_of(Enumerator) + end + end + end + + describe "returned Enumerator" do + describe "size" do + describe "when step is a String" do + describe "with self and stop as Integers" do + it "raises an ArgumentError when step is a numeric representation" do + -> { @step.call(1, 5, "1").size }.should raise_error(ArgumentError) + -> { @step.call(1, 5, "0.1").size }.should raise_error(ArgumentError) + -> { @step.call(1, 5, "1/3").size }.should raise_error(ArgumentError) + end + it "raises an ArgumentError with step as an alphanumeric string" do + -> { @step.call(1, 5, "foo").size }.should raise_error(ArgumentError) + end + end + + describe "with self and stop as Floats" do + it "raises an ArgumentError when step is a numeric representation" do + -> { @step.call(1.1, 5.1, "1").size }.should raise_error(ArgumentError) + -> { @step.call(1.1, 5.1, "0.1").size }.should raise_error(ArgumentError) + -> { @step.call(1.1, 5.1, "1/3").size }.should raise_error(ArgumentError) + end + it "raises an ArgumentError with step as an alphanumeric string" do + -> { @step.call(1.1, 5.1, "foo").size }.should raise_error(ArgumentError) + end + end + end + + describe "when self, stop and step are Integers and step is positive" do + it "returns the difference between self and stop divided by the number of steps" do + @step.call(5, 10, 11).size.should == 1 + @step.call(5, 10, 6).size.should == 1 + @step.call(5, 10, 5).size.should == 2 + @step.call(5, 10, 4).size.should == 2 + @step.call(5, 10, 2).size.should == 3 + @step.call(5, 10, 1).size.should == 6 + @step.call(5, 10).size.should == 6 + @step.call(10, 10, 1).size.should == 1 + end + + it "returns 0 if value > limit" do + @step.call(11, 10, 1).size.should == 0 + end + end + + describe "when self, stop and step are Integers and step is negative" do + it "returns the difference between self and stop divided by the number of steps" do + @step.call(10, 5, -11).size.should == 1 + @step.call(10, 5, -6).size.should == 1 + @step.call(10, 5, -5).size.should == 2 + @step.call(10, 5, -4).size.should == 2 + @step.call(10, 5, -2).size.should == 3 + @step.call(10, 5, -1).size.should == 6 + @step.call(10, 10, -1).size.should == 1 + end + + it "returns 0 if value < limit" do + @step.call(10, 11, -1).size.should == 0 + end + end + + describe "when self, stop or step is a Float" do + describe "and step is positive" do + it "returns the difference between self and stop divided by the number of steps" do + @step.call(5, 10, 11.0).size.should == 1 + @step.call(5, 10, 6.0).size.should == 1 + @step.call(5, 10, 5.0).size.should == 2 + @step.call(5, 10, 4.0).size.should == 2 + @step.call(5, 10, 2.0).size.should == 3 + @step.call(5, 10, 0.5).size.should == 11 + @step.call(5, 10, 1.0).size.should == 6 + @step.call(5, 10.5).size.should == 6 + @step.call(10, 10, 1.0).size.should == 1 + end + + it "returns 0 if value > limit" do + @step.call(10, 5.5).size.should == 0 + @step.call(11, 10, 1.0).size.should == 0 + @step.call(11, 10, 1.5).size.should == 0 + @step.call(10, 5, infinity_value).size.should == 0 + end + + it "returns 1 if step is infinity_value" do + @step.call(5, 10, infinity_value).size.should == 1 + end + end + + describe "and step is negative" do + it "returns the difference between self and stop divided by the number of steps" do + @step.call(10, 5, -11.0).size.should == 1 + @step.call(10, 5, -6.0).size.should == 1 + @step.call(10, 5, -5.0).size.should == 2 + @step.call(10, 5, -4.0).size.should == 2 + @step.call(10, 5, -2.0).size.should == 3 + @step.call(10, 5, -0.5).size.should == 11 + @step.call(10, 5, -1.0).size.should == 6 + @step.call(10, 10, -1.0).size.should == 1 + end + + it "returns 0 if value < limit" do + @step.call(10, 11, -1.0).size.should == 0 + @step.call(10, 11, -1.5).size.should == 0 + @step.call(5, 10, -infinity_value).size.should == 0 + end + + it "returns 1 if step is infinity_value" do + @step.call(10, 5, -infinity_value).size.should == 1 + end + end + end + + describe "when stop is not passed" do + it "returns infinity_value" do + @step.call(1).size.should == infinity_value + end + end + + describe "when stop is nil" do + it "returns infinity_value" do + @step.call(1, nil, 5).size.should == infinity_value + end + end + end + end + end +end diff --git a/spec/ruby/core/numeric/singleton_method_added_spec.rb b/spec/ruby/core/numeric/singleton_method_added_spec.rb new file mode 100644 index 0000000000..2091398e5d --- /dev/null +++ b/spec/ruby/core/numeric/singleton_method_added_spec.rb @@ -0,0 +1,41 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Numeric#singleton_method_added" do + before :all do + class ::NumericSpecs::Subclass + # We want restore default Numeric behaviour for this particular test + remove_method :singleton_method_added + end + end + + after :all do + class ::NumericSpecs::Subclass + # Allow mocking methods again + def singleton_method_added(val) + end + end + end + + it "raises a TypeError when trying to define a singleton method on a Numeric" do + -> do + a = NumericSpecs::Subclass.new + def a.test; end + end.should raise_error(TypeError) + + -> do + a = 1 + def a.test; end + end.should raise_error(TypeError) + + -> do + a = 1.5 + def a.test; end + end.should raise_error(TypeError) + + -> do + a = bignum_value + def a.test; end + end.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/numeric/step_spec.rb b/spec/ruby/core/numeric/step_spec.rb new file mode 100644 index 0000000000..1705fb1b4e --- /dev/null +++ b/spec/ruby/core/numeric/step_spec.rb @@ -0,0 +1,121 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/step' + +describe "Numeric#step" do + + describe 'with positional args' do + it "raises an ArgumentError when step is 0" do + -> { 1.step(5, 0) {} }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when step is 0.0" do + -> { 1.step(2, 0.0) {} }.should raise_error(ArgumentError) + end + + before :all do + # This lambda definition limits to return the arguments it receives. + # It's needed to test numeric_step behaviour with positional arguments. + @step = -> receiver, *args, &block { receiver.step(*args, &block) } + end + it_behaves_like :numeric_step, :step + + describe "when no block is given" do + step_enum_class = Enumerator::ArithmeticSequence + + describe "returned #{step_enum_class}" do + describe "size" do + it "defaults to an infinite size" do + enum = 1.step + enum.size.should == Float::INFINITY + end + end + + describe "type" do + it "returns an instance of Enumerator::ArithmeticSequence" do + 1.step(10).class.should == Enumerator::ArithmeticSequence + end + end + end + end + end + + describe 'with keyword arguments' do + describe "when no block is given" do + describe "returned Enumerator" do + describe "size" do + it "should return infinity_value when limit is nil" do + 1.step(by: 42).size.should == infinity_value + end + + it "should return infinity_value when ascending towards a limit of Float::INFINITY" do + 1.step(to: Float::INFINITY, by: 42).size.should == infinity_value + end + + it "should return infinity_value when descending towards a limit of -Float::INFINITY" do + 1.step(to: -Float::INFINITY, by: -42).size.should == infinity_value + end + + it "should return 1 when the both limit and step are Float::INFINITY" do + 1.step(to: Float::INFINITY, by: Float::INFINITY).size.should == 1 + end + + it "should return 1 when the both limit and step are -Float::INFINITY" do + 1.step(to: -Float::INFINITY, by: -Float::INFINITY).size.should == 1 + end + end + end + end + + before :all do + # This lambda transforms a positional step method args into keyword arguments. + # It's needed to test numeric_step behaviour with keyword arguments. + @step = -> receiver, *args, &block do + kw_args = { to: args[0] } + kw_args[:by] = args[1] if args.size == 2 + receiver.step(**kw_args, &block) + end + end + it_behaves_like :numeric_step, :step + end + + describe 'with mixed arguments' do + it " raises an ArgumentError when step is 0" do + -> { 1.step(5, by: 0) { break } }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when step is 0.0" do + -> { 1.step(2, by: 0.0) { break } }.should raise_error(ArgumentError) + end + + it "raises a ArgumentError when limit and to are defined" do + -> { 1.step(5, 1, to: 5) { break } }.should raise_error(ArgumentError) + end + + it "raises a ArgumentError when step and by are defined" do + -> { 1.step(5, 1, by: 5) { break } }.should raise_error(ArgumentError) + end + + describe "when no block is given" do + describe "returned Enumerator" do + describe "size" do + end + end + end + + before :all do + # This lambda definition transforms a positional step method args into + # a mix of positional and keyword arguments. + # It's needed to test numeric_step behaviour with positional mixed with + # keyword arguments. + @step = -> receiver, *args, &block do + if args.size == 2 + receiver.step(args[0], by: args[1], &block) + else + receiver.step(*args, &block) + end + end + end + it_behaves_like :numeric_step, :step + end +end diff --git a/spec/ruby/core/numeric/to_c_spec.rb b/spec/ruby/core/numeric/to_c_spec.rb new file mode 100644 index 0000000000..75245a612e --- /dev/null +++ b/spec/ruby/core/numeric/to_c_spec.rb @@ -0,0 +1,45 @@ +require_relative '../../spec_helper' + +describe "Numeric#to_c" do + before :all do + @numbers = [ + 0, + 29871, + 99999999999999**99, + -72628191273, + Rational(2,3), + Rational(1.898), + Rational(-238), + 29282.2827, + -2927.00091, + 0.0, + 12.0, + Float::MAX, + infinity_value, + nan_value + ] + end + + it "returns a Complex object" do + @numbers.each do |number| + number.to_c.should be_an_instance_of(Complex) + end + end + + it "uses self as the real component" do + @numbers.each do |number| + real = number.to_c.real + if Float === number and number.nan? + real.nan?.should be_true + else + real.should == number + end + end + end + + it "uses 0 as the imaginary component" do + @numbers.each do |number| + number.to_c.imag.should == 0 + end + end +end diff --git a/spec/ruby/core/numeric/to_int_spec.rb b/spec/ruby/core/numeric/to_int_spec.rb new file mode 100644 index 0000000000..3cc39a6d40 --- /dev/null +++ b/spec/ruby/core/numeric/to_int_spec.rb @@ -0,0 +1,10 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Numeric#to_int" do + it "returns self#to_i" do + obj = NumericSpecs::Subclass.new + obj.should_receive(:to_i).and_return(:result) + obj.to_int.should == :result + end +end diff --git a/spec/ruby/core/numeric/truncate_spec.rb b/spec/ruby/core/numeric/truncate_spec.rb new file mode 100644 index 0000000000..f1592334c5 --- /dev/null +++ b/spec/ruby/core/numeric/truncate_spec.rb @@ -0,0 +1,14 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Numeric#truncate" do + before :each do + @obj = NumericSpecs::Subclass.new + end + + it "converts self to a Float (using #to_f) and returns the #truncate'd result" do + @obj.should_receive(:to_f).and_return(2.5555, -2.3333) + @obj.truncate.should == 2 + @obj.truncate.should == -2 + end +end diff --git a/spec/ruby/core/numeric/uminus_spec.rb b/spec/ruby/core/numeric/uminus_spec.rb new file mode 100644 index 0000000000..39065fa392 --- /dev/null +++ b/spec/ruby/core/numeric/uminus_spec.rb @@ -0,0 +1,31 @@ +require_relative '../../spec_helper' + +describe "Numeric#-@" do + it "returns the same value with opposite sign (integers)" do + 0.send(:-@).should == 0 + 100.send(:-@).should == -100 + -100.send(:-@).should == 100 + end + + it "returns the same value with opposite sign (floats)" do + 34.56.send(:-@).should == -34.56 + -34.56.send(:-@).should == 34.56 + end + + it "returns the same value with opposite sign (two complement)" do + 2147483648.send(:-@).should == -2147483648 + -2147483648.send(:-@).should == 2147483648 + 9223372036854775808.send(:-@).should == -9223372036854775808 + -9223372036854775808.send(:-@).should == 9223372036854775808 + end + + describe "with a Numeric subclass" do + it "calls #coerce(0) on self, then subtracts the second element of the result from the first" do + ten = mock_numeric('10') + zero = mock_numeric('0') + ten.should_receive(:coerce).with(0).and_return([zero, ten]) + zero.should_receive(:-).with(ten).and_return(-10) + ten.send(:-@).should == -10 + end + end +end diff --git a/spec/ruby/core/numeric/uplus_spec.rb b/spec/ruby/core/numeric/uplus_spec.rb new file mode 100644 index 0000000000..88cf5e037b --- /dev/null +++ b/spec/ruby/core/numeric/uplus_spec.rb @@ -0,0 +1,9 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Numeric#+@" do + it "returns self" do + obj = NumericSpecs::Subclass.new + obj.send(:+@).should == obj + end +end diff --git a/spec/ruby/core/numeric/zero_spec.rb b/spec/ruby/core/numeric/zero_spec.rb new file mode 100644 index 0000000000..0fb7619bcd --- /dev/null +++ b/spec/ruby/core/numeric/zero_spec.rb @@ -0,0 +1,18 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Numeric#zero?" do + before :each do + @obj = NumericSpecs::Subclass.new + end + + it "returns true if self is 0" do + @obj.should_receive(:==).with(0).and_return(true) + @obj.should.zero? + end + + it "returns false if self is not 0" do + @obj.should_receive(:==).with(0).and_return(false) + @obj.should_not.zero? + end +end diff --git a/spec/ruby/core/objectspace/_id2ref_spec.rb b/spec/ruby/core/objectspace/_id2ref_spec.rb new file mode 100644 index 0000000000..1ae3230bdf --- /dev/null +++ b/spec/ruby/core/objectspace/_id2ref_spec.rb @@ -0,0 +1,65 @@ +require_relative '../../spec_helper' + +ruby_version_is "4.0" do + describe "ObjectSpace._id2ref" do + it "is deprecated" do + id = nil.object_id + -> { + ObjectSpace._id2ref(id) + }.should complain(/warning: ObjectSpace\._id2ref is deprecated/) + end + end +end + +ruby_version_is ""..."4.0" do + describe "ObjectSpace._id2ref" do + it "converts an object id to a reference to the object" do + s = "I am a string" + r = ObjectSpace._id2ref(s.object_id) + r.should == s + end + + it "retrieves true by object_id" do + ObjectSpace._id2ref(true.object_id).should == true + end + + it "retrieves false by object_id" do + ObjectSpace._id2ref(false.object_id).should == false + end + + it "retrieves nil by object_id" do + ObjectSpace._id2ref(nil.object_id).should == nil + end + + it "retrieves a small Integer by object_id" do + ObjectSpace._id2ref(1.object_id).should == 1 + ObjectSpace._id2ref((-42).object_id).should == -42 + end + + it "retrieves a large Integer by object_id" do + obj = 1 << 88 + ObjectSpace._id2ref(obj.object_id).should.equal?(obj) + end + + it "retrieves a Symbol by object_id" do + ObjectSpace._id2ref(:sym.object_id).should.equal?(:sym) + end + + it "retrieves a String by object_id" do + obj = "str" + ObjectSpace._id2ref(obj.object_id).should.equal?(obj) + end + + it "retrieves a frozen literal String by object_id" do + ObjectSpace._id2ref("frozen string literal _id2ref".freeze.object_id).should.equal?("frozen string literal _id2ref".freeze) + end + + it "retrieves an Encoding by object_id" do + ObjectSpace._id2ref(Encoding::UTF_8.object_id).should.equal?(Encoding::UTF_8) + end + + it 'raises RangeError when an object could not be found' do + proc { ObjectSpace._id2ref(1 << 60) }.should raise_error(RangeError) + end + end +end diff --git a/spec/ruby/core/objectspace/count_objects_spec.rb b/spec/ruby/core/objectspace/count_objects_spec.rb new file mode 100644 index 0000000000..e9831a3a42 --- /dev/null +++ b/spec/ruby/core/objectspace/count_objects_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../spec_helper' + +describe "ObjectSpace.count_objects" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/objectspace/define_finalizer_spec.rb b/spec/ruby/core/objectspace/define_finalizer_spec.rb new file mode 100644 index 0000000000..0f4b54c345 --- /dev/null +++ b/spec/ruby/core/objectspace/define_finalizer_spec.rb @@ -0,0 +1,215 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +# Why do we not test that finalizers are run by the GC? The documentation +# says that finalizers are never guaranteed to be run, so we can't +# spec that they are. On some implementations of Ruby the finalizers may +# run asynchronously, meaning that we can't predict when they'll run, +# even if they were guaranteed to do so. Even on MRI finalizers can be +# very unpredictable, due to conservative stack scanning and references +# left in unused memory. + +describe "ObjectSpace.define_finalizer" do + it "raises an ArgumentError if the action does not respond to call" do + -> { + ObjectSpace.define_finalizer(Object.new, mock("ObjectSpace.define_finalizer no #call")) + }.should raise_error(ArgumentError) + end + + it "accepts an object and a proc" do + handler = -> id { id } + ObjectSpace.define_finalizer(Object.new, handler).should == [0, handler] + end + + it "accepts an object and a bound method" do + handler = mock("callable") + def handler.finalize(id) end + finalize = handler.method(:finalize) + ObjectSpace.define_finalizer(Object.new, finalize).should == [0, finalize] + end + + it "accepts an object and a callable" do + handler = mock("callable") + def handler.call(id) end + ObjectSpace.define_finalizer(Object.new, handler).should == [0, handler] + end + + it "accepts an object and a block" do + handler = -> id { id } + ObjectSpace.define_finalizer(Object.new, &handler).should == [0, handler] + end + + it "raises ArgumentError trying to define a finalizer on a non-reference" do + -> { + ObjectSpace.define_finalizer(:blah) { 1 } + }.should raise_error(ArgumentError) + end + + # see [ruby-core:24095] + it "calls finalizer on process termination" do + code = <<-RUBY + def scoped + Proc.new { puts "finalizer run" } + end + handler = scoped + obj = +"Test" + ObjectSpace.define_finalizer(obj, handler) + exit 0 + RUBY + + ruby_exe(code, :args => "2>&1").should include("finalizer run\n") + end + + it "warns if the finalizer has the object as the receiver" do + code = <<-RUBY + class CapturesSelf + def initialize + ObjectSpace.define_finalizer(self, proc { + puts "finalizer run" + }) + end + end + CapturesSelf.new + exit 0 + RUBY + + ruby_exe(code, :args => "2>&1").should include("warning: finalizer references object to be finalized\n") + end + + it "warns if the finalizer is a method bound to the receiver" do + code = <<-RUBY + class CapturesSelf + def initialize + ObjectSpace.define_finalizer(self, method(:finalize)) + end + def finalize(id) + puts "finalizer run" + end + end + CapturesSelf.new + exit 0 + RUBY + + ruby_exe(code, :args => "2>&1").should include("warning: finalizer references object to be finalized\n") + end + + it "warns if the finalizer was a block in the receiver" do + code = <<-RUBY + class CapturesSelf + def initialize + ObjectSpace.define_finalizer(self) do + puts "finalizer run" + end + end + end + CapturesSelf.new + exit 0 + RUBY + + ruby_exe(code, :args => "2>&1").should include("warning: finalizer references object to be finalized\n") + end + + it "calls a finalizer at exit even if it is self-referencing" do + code = <<-RUBY + obj = +"Test" + handler = Proc.new { puts "finalizer run" } + ObjectSpace.define_finalizer(obj, handler) + exit 0 + RUBY + + ruby_exe(code).should include("finalizer run\n") + end + + it "calls a finalizer at exit even if it is indirectly self-referencing" do + code = <<-RUBY + class CapturesSelf + def initialize + ObjectSpace.define_finalizer(self, finalizer(self)) + end + def finalizer(zelf) + proc do + puts "finalizer run" + end + end + end + CapturesSelf.new + exit 0 + RUBY + + ruby_exe(code, :args => "2>&1").should include("finalizer run\n") + end + + it "calls a finalizer defined in a finalizer running at exit" do + code = <<-RUBY + obj = +"Test" + handler = Proc.new do + obj2 = +"Test" + handler2 = Proc.new { puts "finalizer 2 run" } + ObjectSpace.define_finalizer(obj2, handler2) + exit 0 + end + ObjectSpace.define_finalizer(obj, handler) + exit 0 + RUBY + + ruby_exe(code, :args => "2>&1").should include("finalizer 2 run\n") + end + + it "allows multiple finalizers with different 'callables' to be defined" do + code = <<-'RUBY' + obj = Object.new + + ObjectSpace.define_finalizer(obj, Proc.new { STDOUT.write "finalized1\n" }) + ObjectSpace.define_finalizer(obj, Proc.new { STDOUT.write "finalized2\n" }) + + exit 0 + RUBY + + ruby_exe(code).lines.sort.should == ["finalized1\n", "finalized2\n"] + end + + it "defines same finalizer only once" do + code = <<~RUBY + obj = Object.new + p = proc { |id| print "ok" } + ObjectSpace.define_finalizer(obj, p.dup) + ObjectSpace.define_finalizer(obj, p.dup) + RUBY + + ruby_exe(code).should == "ok" + end + + it "returns the defined finalizer" do + obj = Object.new + p = proc { |id| } + p2 = p.dup + + ret = ObjectSpace.define_finalizer(obj, p) + ret.should == [0, p] + ret[1].should.equal?(p) + + ret = ObjectSpace.define_finalizer(obj, p2) + ret.should == [0, p] + ret[1].should.equal?(p) + end + + describe "when $VERBOSE is not nil" do + it "warns if an exception is raised in finalizer" do + code = <<-RUBY + ObjectSpace.define_finalizer(Object.new) { raise "finalizing" } + RUBY + + ruby_exe(code, args: "2>&1").should include("warning: Exception in finalizer", "finalizing") + end + end + + describe "when $VERBOSE is nil" do + it "does not warn even if an exception is raised in finalizer" do + code = <<-RUBY + ObjectSpace.define_finalizer(Object.new) { raise "finalizing" } + RUBY + + ruby_exe(code, args: "2>&1", options: "-W0").should == "" + end + end +end diff --git a/spec/ruby/core/objectspace/each_object_spec.rb b/spec/ruby/core/objectspace/each_object_spec.rb new file mode 100644 index 0000000000..09a582afaf --- /dev/null +++ b/spec/ruby/core/objectspace/each_object_spec.rb @@ -0,0 +1,213 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "ObjectSpace.each_object" do + it "calls the block once for each living, non-immediate object in the Ruby process" do + klass = Class.new + new_obj = klass.new + + yields = 0 + count = ObjectSpace.each_object(klass) do |obj| + obj.should == new_obj + yields += 1 + end + count.should == 1 + yields.should == 1 + + # this is needed to prevent the new_obj from being GC'd too early + new_obj.should_not == nil + end + + it "calls the block once for each class, module in the Ruby process" do + klass = Class.new + mod = Module.new + + [klass, mod].each do |k| + yields = 0 + got_it = false + count = ObjectSpace.each_object(k.class) do |obj| + got_it = true if obj == k + yields += 1 + end + got_it.should == true + count.should == yields + end + end + + it "returns an enumerator if not given a block" do + klass = Class.new + new_obj = klass.new + + counter = ObjectSpace.each_object(klass) + counter.should be_an_instance_of(Enumerator) + counter.each{}.should == 1 + # this is needed to prevent the new_obj from being GC'd too early + new_obj.should_not == nil + end + + it "finds an object stored in a global variable" do + $object_space_global_variable = ObjectSpaceFixtures::ObjectToBeFound.new(:global) + ObjectSpaceFixtures.to_be_found_symbols.should include(:global) + end + + it "finds an object stored in a top-level constant" do + ObjectSpaceFixtures.to_be_found_symbols.should include(:top_level_constant) + end + + it "finds an object stored in a second-level constant" do + ObjectSpaceFixtures.to_be_found_symbols.should include(:second_level_constant) + end + + it "finds an object stored in a local variable" do + local = ObjectSpaceFixtures::ObjectToBeFound.new(:local) + ObjectSpaceFixtures.to_be_found_symbols.should include(:local) + end + + it "finds an object stored in a local variable captured in a block explicitly" do + proc = Proc.new { + local_in_block = ObjectSpaceFixtures::ObjectToBeFound.new(:local_in_block_explicit) + Proc.new { local_in_block } + }.call + + ObjectSpaceFixtures.to_be_found_symbols.should include(:local_in_block_explicit) + end + + it "finds an object stored in a local variable captured in a block implicitly" do + proc = Proc.new { + local_in_block = ObjectSpaceFixtures::ObjectToBeFound.new(:local_in_block_implicit) + Proc.new { } + }.call + + ObjectSpaceFixtures.to_be_found_symbols.should include(:local_in_block_implicit) + end + + it "finds an object stored in a local variable captured in by a method defined with a block" do + ObjectSpaceFixtures.to_be_found_symbols.should include(:captured_by_define_method) + end + + it "finds an object stored in a local variable captured in a Proc#binding" do + binding = Proc.new { + local_in_proc_binding = ObjectSpaceFixtures::ObjectToBeFound.new(:local_in_proc_binding) + Proc.new { }.binding + }.call + + ObjectSpaceFixtures.to_be_found_symbols.should include(:local_in_proc_binding) + end + + it "finds an object stored in a local variable captured in a Kernel#binding" do + b = Proc.new { + local_in_kernel_binding = ObjectSpaceFixtures::ObjectToBeFound.new(:local_in_kernel_binding) + binding + }.call + + ObjectSpaceFixtures.to_be_found_symbols.should include(:local_in_kernel_binding) + end + + it "finds an object stored in a local variable set in a binding manually" do + b = binding + b.eval("local = ObjectSpaceFixtures::ObjectToBeFound.new(:local_in_manual_binding)") + ObjectSpaceFixtures.to_be_found_symbols.should include(:local_in_manual_binding) + end + + it "finds an object stored in an array" do + array = [ObjectSpaceFixtures::ObjectToBeFound.new(:array)] + ObjectSpaceFixtures.to_be_found_symbols.should include(:array) + end + + it "finds an object stored in a hash key" do + hash = {ObjectSpaceFixtures::ObjectToBeFound.new(:hash_key) => :value} + ObjectSpaceFixtures.to_be_found_symbols.should include(:hash_key) + end + + it "finds an object stored in a hash value" do + hash = {a: ObjectSpaceFixtures::ObjectToBeFound.new(:hash_value)} + ObjectSpaceFixtures.to_be_found_symbols.should include(:hash_value) + end + + it "finds an object stored in an instance variable" do + local = ObjectSpaceFixtures::ObjectWithInstanceVariable.new + ObjectSpaceFixtures.to_be_found_symbols.should include(:instance_variable) + end + + it "finds an object stored in a thread local" do + thread = Thread.new {} + thread.thread_variable_set(:object_space_thread_local, ObjectSpaceFixtures::ObjectToBeFound.new(:thread_local)) + ObjectSpaceFixtures.to_be_found_symbols.should include(:thread_local) + thread.join + end + + it "finds an object stored in a fiber local" do + Thread.current[:object_space_fiber_local] = ObjectSpaceFixtures::ObjectToBeFound.new(:fiber_local) + ObjectSpaceFixtures.to_be_found_symbols.should include(:fiber_local) + end + + it "finds an object captured in an at_exit handler" do + Proc.new { + local = ObjectSpaceFixtures::ObjectToBeFound.new(:at_exit) + + at_exit do + local + end + }.call + + ObjectSpaceFixtures.to_be_found_symbols.should include(:at_exit) + end + + it "finds an object captured in finalizer" do + alive = Object.new + + Proc.new { + local = ObjectSpaceFixtures::ObjectToBeFound.new(:finalizer) + + ObjectSpace.define_finalizer(alive, Proc.new { + local + }) + }.call + + ObjectSpaceFixtures.to_be_found_symbols.should include(:finalizer) + + alive.should_not be_nil + end + + describe "on singleton classes" do + before :each do + @klass = Class.new + instance = @klass.new + @sclass = instance.singleton_class + @meta = @klass.singleton_class + end + + it "does not walk hidden metaclasses" do + klass = Class.new.singleton_class + ancestors = ObjectSpace.each_object(Class).select { |c| klass.is_a? c } + hidden = ancestors.find { |h| h.inspect.include? klass.inspect } + hidden.should == nil + end + + it "walks singleton classes" do + @sclass.should be_kind_of(@meta) + ObjectSpace.each_object(@meta).to_a.should include(@sclass) + end + end + + it "walks a class and its normal descendants when passed the class's singleton class" do + a = Class.new + b = Class.new(a) + c = Class.new(a) + d = Class.new(b) + + c_instance = c.new + c_sclass = c_instance.singleton_class + + expected = [ a, b, c, d ] + + expected << c_sclass + c_sclass.should be_kind_of(a.singleton_class) + + b.extend Enumerable # included modules should not be walked + + classes = ObjectSpace.each_object(a.singleton_class).to_a + + classes.sort_by(&:object_id).should == expected.sort_by(&:object_id) + end +end diff --git a/spec/ruby/core/objectspace/fixtures/classes.rb b/spec/ruby/core/objectspace/fixtures/classes.rb new file mode 100644 index 0000000000..612156c180 --- /dev/null +++ b/spec/ruby/core/objectspace/fixtures/classes.rb @@ -0,0 +1,64 @@ +module ObjectSpaceFixtures + def self.garbage + blah + end + + def self.blah + o = "hello" + @garbage_objid = o.object_id + return o + end + + @last_objid = nil + + def self.last_objid + @last_objid + end + + def self.garbage_objid + @garbage_objid + end + + def self.make_finalizer + proc { |obj_id| @last_objid = obj_id } + end + + def self.define_finalizer + handler = -> obj { ScratchPad.record :finalized } + ObjectSpace.define_finalizer "#{rand 5}", handler + end + + def self.scoped(wr) + return Proc.new { wr.write "finalized"; wr.close } + end + + class ObjectToBeFound + attr_reader :name + + def initialize(name) + @name = name + end + end + + class ObjectWithInstanceVariable + def initialize + @instance_variable = ObjectToBeFound.new(:instance_variable) + end + end + + def self.to_be_found_symbols + ObjectSpace.each_object(ObjectToBeFound).map do |o| + o.name + end + end + + o = ObjectToBeFound.new(:captured_by_define_method) + define_method :capturing_method do + o + end + + SECOND_LEVEL_CONSTANT = ObjectToBeFound.new(:second_level_constant) + +end + +OBJECT_SPACE_TOP_LEVEL_CONSTANT = ObjectSpaceFixtures::ObjectToBeFound.new(:top_level_constant) diff --git a/spec/ruby/core/objectspace/garbage_collect_spec.rb b/spec/ruby/core/objectspace/garbage_collect_spec.rb new file mode 100644 index 0000000000..521eaa8785 --- /dev/null +++ b/spec/ruby/core/objectspace/garbage_collect_spec.rb @@ -0,0 +1,22 @@ +require_relative '../../spec_helper' + +describe "ObjectSpace.garbage_collect" do + + it "can be invoked without any exceptions" do + -> { ObjectSpace.garbage_collect }.should_not raise_error + end + + it "accepts keyword arguments" do + ObjectSpace.garbage_collect(full_mark: true, immediate_sweep: true).should == nil + end + + it "ignores the supplied block" do + -> { ObjectSpace.garbage_collect {} }.should_not raise_error + end + + it "always returns nil" do + ObjectSpace.garbage_collect.should == nil + ObjectSpace.garbage_collect.should == nil + end + +end diff --git a/spec/ruby/core/objectspace/undefine_finalizer_spec.rb b/spec/ruby/core/objectspace/undefine_finalizer_spec.rb new file mode 100644 index 0000000000..f57d5a7845 --- /dev/null +++ b/spec/ruby/core/objectspace/undefine_finalizer_spec.rb @@ -0,0 +1,33 @@ +require_relative '../../spec_helper' + +describe "ObjectSpace.undefine_finalizer" do + it "removes finalizers for an object" do + code = <<~RUBY + obj = Object.new + ObjectSpace.define_finalizer(obj, proc { |id| puts "hello" }) + ObjectSpace.undefine_finalizer(obj) + RUBY + + ruby_exe(code).should.empty? + end + + it "should not remove finalizers for a frozen object" do + code = <<~RUBY + obj = Object.new + ObjectSpace.define_finalizer(obj, proc { |id| print "ok" }) + obj.freeze + begin + ObjectSpace.undefine_finalizer(obj) + rescue + end + RUBY + + ruby_exe(code).should == "ok" + end + + it "should raise when removing finalizers for a frozen object" do + obj = Object.new + obj.freeze + -> { ObjectSpace.undefine_finalizer(obj) }.should raise_error(FrozenError) + end +end diff --git a/spec/ruby/core/objectspace/weakkeymap/clear_spec.rb b/spec/ruby/core/objectspace/weakkeymap/clear_spec.rb new file mode 100644 index 0000000000..8050e2c307 --- /dev/null +++ b/spec/ruby/core/objectspace/weakkeymap/clear_spec.rb @@ -0,0 +1,27 @@ +require_relative '../../../spec_helper' + +ruby_version_is '3.3' do + describe "ObjectSpace::WeakKeyMap#clear" do + it "removes all the entries" do + m = ObjectSpace::WeakKeyMap.new + + key = Object.new + value = Object.new + m[key] = value + + key2 = Object.new + value2 = Object.new + m[key2] = value2 + + m.clear + + m.key?(key).should == false + m.key?(key2).should == false + end + + it "returns self" do + m = ObjectSpace::WeakKeyMap.new + m.clear.should.equal?(m) + end + end +end diff --git a/spec/ruby/core/objectspace/weakkeymap/delete_spec.rb b/spec/ruby/core/objectspace/weakkeymap/delete_spec.rb new file mode 100644 index 0000000000..3cd61355d6 --- /dev/null +++ b/spec/ruby/core/objectspace/weakkeymap/delete_spec.rb @@ -0,0 +1,51 @@ +require_relative '../../../spec_helper' + +ruby_version_is '3.3' do + describe "ObjectSpace::WeakKeyMap#delete" do + it "removes the entry and returns the deleted value" do + m = ObjectSpace::WeakKeyMap.new + key = Object.new + value = Object.new + m[key] = value + + m.delete(key).should == value + m.key?(key).should == false + end + + it "uses equality semantic" do + m = ObjectSpace::WeakKeyMap.new + key = "foo".upcase + value = Object.new + m[key] = value + + m.delete("foo".upcase).should == value + m.key?(key).should == false + end + + it "calls supplied block if the key is not found" do + key = Object.new + m = ObjectSpace::WeakKeyMap.new + return_value = m.delete(key) do |yielded_key| + yielded_key.should == key + 5 + end + return_value.should == 5 + end + + it "returns nil if the key is not found when no block is given" do + m = ObjectSpace::WeakKeyMap.new + m.delete(Object.new).should == nil + end + + it "returns nil when a key cannot be garbage collected" do + map = ObjectSpace::WeakKeyMap.new + + map.delete(1).should == nil + map.delete(1.0).should == nil + map.delete(:a).should == nil + map.delete(true).should == nil + map.delete(false).should == nil + map.delete(nil).should == nil + end + end +end diff --git a/spec/ruby/core/objectspace/weakkeymap/element_reference_spec.rb b/spec/ruby/core/objectspace/weakkeymap/element_reference_spec.rb new file mode 100644 index 0000000000..51368e8d3b --- /dev/null +++ b/spec/ruby/core/objectspace/weakkeymap/element_reference_spec.rb @@ -0,0 +1,107 @@ +require_relative '../../../spec_helper' +require_relative 'fixtures/classes' + +ruby_version_is "3.3" do + describe "ObjectSpace::WeakKeyMap#[]" do + it "is faithful to the map's content" do + map = ObjectSpace::WeakKeyMap.new + key1, key2 = %w[a b].map(&:upcase) + ref1, ref2 = %w[x y] + map[key1] = ref1 + map[key1].should == ref1 + map[key1] = ref1 + map[key1].should == ref1 + map[key2] = ref2 + map[key1].should == ref1 + map[key2].should == ref2 + end + + it "compares keys with #eql? semantics" do + map = ObjectSpace::WeakKeyMap.new + key = [1.0] + map[key] = "x" + map[[1]].should == nil + map[[1.0]].should == "x" + key.should == [1.0] # keep the key alive until here to keep the map entry + + map = ObjectSpace::WeakKeyMap.new + key = [1] + map[key] = "x" + map[[1.0]].should == nil + map[[1]].should == "x" + key.should == [1] # keep the key alive until here to keep the map entry + + map = ObjectSpace::WeakKeyMap.new + key1, key2 = %w[a a].map(&:upcase) + ref = "x" + map[key1] = ref + map[key2].should == ref + end + + it "compares key via #hash first" do + x = mock('0') + x.should_receive(:hash).and_return(0) + + map = ObjectSpace::WeakKeyMap.new + key = 'foo' + map[key] = :bar + map[x].should == nil + end + + it "does not compare keys with different #hash values via #eql?" do + x = mock('x') + x.should_not_receive(:eql?) + x.stub!(:hash).and_return(0) + + y = mock('y') + y.should_not_receive(:eql?) + y.stub!(:hash).and_return(1) + + map = ObjectSpace::WeakKeyMap.new + map[y] = 1 + map[x].should == nil + end + + it "compares keys with the same #hash value via #eql?" do + x = mock('x') + x.should_receive(:eql?).and_return(true) + x.stub!(:hash).and_return(42) + + y = mock('y') + y.should_not_receive(:eql?) + y.stub!(:hash).and_return(42) + + map = ObjectSpace::WeakKeyMap.new + map[y] = 1 + map[x].should == 1 + end + + it "finds a value via an identical key even when its #eql? isn't reflexive" do + x = mock('x') + x.should_receive(:hash).at_least(1).and_return(42) + x.stub!(:eql?).and_return(false) # Stubbed for clarity and latitude in implementation; not actually sent by MRI. + + map = ObjectSpace::WeakKeyMap.new + map[x] = :x + map[x].should == :x + end + + it "supports keys with private #hash method" do + key = WeakKeyMapSpecs::KeyWithPrivateHash.new + map = ObjectSpace::WeakKeyMap.new + map[key] = 42 + map[key].should == 42 + end + + it "returns nil and does not raise error when a key cannot be garbage collected" do + map = ObjectSpace::WeakKeyMap.new + + map[1].should == nil + map[1.0].should == nil + map[:a].should == nil + map[true].should == nil + map[false].should == nil + map[nil].should == nil + end + end +end diff --git a/spec/ruby/core/objectspace/weakkeymap/element_set_spec.rb b/spec/ruby/core/objectspace/weakkeymap/element_set_spec.rb new file mode 100644 index 0000000000..8db8d780c7 --- /dev/null +++ b/spec/ruby/core/objectspace/weakkeymap/element_set_spec.rb @@ -0,0 +1,82 @@ +require_relative '../../../spec_helper' + +ruby_version_is "3.3" do + describe "ObjectSpace::WeakKeyMap#[]=" do + def should_accept(map, key, value) + (map[key] = value).should == value + map.should.key?(key) + map[key].should == value + end + + it "is correct" do + map = ObjectSpace::WeakKeyMap.new + key1, key2 = %w[a b].map(&:upcase) + ref1, ref2 = %w[x y] + should_accept(map, key1, ref1) + should_accept(map, key1, ref1) + should_accept(map, key2, ref2) + map[key1].should == ref1 + end + + it "requires the keys to implement #hash" do + map = ObjectSpace::WeakKeyMap.new + -> { map[BasicObject.new] = 1 }.should raise_error(NoMethodError, /undefined method [`']hash' for an instance of BasicObject/) + end + + it "accepts frozen keys or values" do + map = ObjectSpace::WeakKeyMap.new + x = Object.new + should_accept(map, x, true) + should_accept(map, x, false) + should_accept(map, x, 42) + should_accept(map, x, :foo) + + y = Object.new.freeze + should_accept(map, x, y) + should_accept(map, y, x) + end + + it "does not duplicate and freeze String keys (like Hash#[]= does)" do + map = ObjectSpace::WeakKeyMap.new + key = +"a" + map[key] = 1 + + map.getkey("a").should.equal? key + map.getkey("a").should_not.frozen? + + key.should == "a" # keep the key alive until here to keep the map entry + end + + context "a key cannot be garbage collected" do + it "raises ArgumentError when Integer is used as a key" do + map = ObjectSpace::WeakKeyMap.new + -> { map[1] = "x" }.should raise_error(ArgumentError, /WeakKeyMap (keys )?must be garbage collectable/) + end + + it "raises ArgumentError when Float is used as a key" do + map = ObjectSpace::WeakKeyMap.new + -> { map[1.0] = "x" }.should raise_error(ArgumentError, /WeakKeyMap (keys )?must be garbage collectable/) + end + + it "raises ArgumentError when Symbol is used as a key" do + map = ObjectSpace::WeakKeyMap.new + -> { map[:a] = "x" }.should raise_error(ArgumentError, /WeakKeyMap (keys )?must be garbage collectable/) + end + + it "raises ArgumentError when true is used as a key" do + map = ObjectSpace::WeakKeyMap.new + -> { map[true] = "x" }.should raise_error(ArgumentError, /WeakKeyMap (keys )?must be garbage collectable/) + end + + it "raises ArgumentError when false is used as a key" do + map = ObjectSpace::WeakKeyMap.new + -> { map[false] = "x" }.should raise_error(ArgumentError, /WeakKeyMap (keys )?must be garbage collectable/) + end + + it "raises ArgumentError when nil is used as a key" do + map = ObjectSpace::WeakKeyMap.new + -> { map[nil] = "x" }.should raise_error(ArgumentError, /WeakKeyMap (keys )?must be garbage collectable/) + end + end + end +end diff --git a/spec/ruby/core/objectspace/weakkeymap/fixtures/classes.rb b/spec/ruby/core/objectspace/weakkeymap/fixtures/classes.rb new file mode 100644 index 0000000000..0fd04551b5 --- /dev/null +++ b/spec/ruby/core/objectspace/weakkeymap/fixtures/classes.rb @@ -0,0 +1,5 @@ +module WeakKeyMapSpecs + class KeyWithPrivateHash + private :hash + end +end diff --git a/spec/ruby/core/objectspace/weakkeymap/getkey_spec.rb b/spec/ruby/core/objectspace/weakkeymap/getkey_spec.rb new file mode 100644 index 0000000000..8a2dbf809d --- /dev/null +++ b/spec/ruby/core/objectspace/weakkeymap/getkey_spec.rb @@ -0,0 +1,28 @@ +require_relative '../../../spec_helper' + +ruby_version_is "3.3" do + describe "ObjectSpace::WeakKeyMap#getkey" do + it "returns the existing equal key" do + map = ObjectSpace::WeakKeyMap.new + key1, key2 = %w[a a].map(&:upcase) + + map[key1] = true + map.getkey(key2).should equal(key1) + map.getkey("X").should == nil + + key1.should == "A" # keep the key alive until here to keep the map entry + key2.should == "A" # keep the key alive until here to keep the map entry + end + + it "returns nil when a key cannot be garbage collected" do + map = ObjectSpace::WeakKeyMap.new + + map.getkey(1).should == nil + map.getkey(1.0).should == nil + map.getkey(:a).should == nil + map.getkey(true).should == nil + map.getkey(false).should == nil + map.getkey(nil).should == nil + end + end +end diff --git a/spec/ruby/core/objectspace/weakkeymap/inspect_spec.rb b/spec/ruby/core/objectspace/weakkeymap/inspect_spec.rb new file mode 100644 index 0000000000..319f050970 --- /dev/null +++ b/spec/ruby/core/objectspace/weakkeymap/inspect_spec.rb @@ -0,0 +1,21 @@ +require_relative '../../../spec_helper' + +ruby_version_is "3.3" do + describe "ObjectSpace::WeakKeyMap#inspect" do + it "only displays size in output" do + map = ObjectSpace::WeakKeyMap.new + key1, key2, key3 = "foo", "bar", "bar" + map.inspect.should =~ /\A\#<ObjectSpace::WeakKeyMap:0x\h+ size=0>\z/ + map[key1] = 1 + map.inspect.should =~ /\A\#<ObjectSpace::WeakKeyMap:0x\h+ size=1>\z/ + map[key2] = 2 + map.inspect.should =~ /\A\#<ObjectSpace::WeakKeyMap:0x\h+ size=2>\z/ + map[key3] = 3 + map.inspect.should =~ /\A\#<ObjectSpace::WeakKeyMap:0x\h+ size=2>\z/ + + key1.should == "foo" # keep the key alive until here to keep the map entry + key2.should == "bar" # keep the key alive until here to keep the map entry + key3.should == "bar" # keep the key alive until here to keep the map entry + end + end +end diff --git a/spec/ruby/core/objectspace/weakkeymap/key_spec.rb b/spec/ruby/core/objectspace/weakkeymap/key_spec.rb new file mode 100644 index 0000000000..a9a2e12432 --- /dev/null +++ b/spec/ruby/core/objectspace/weakkeymap/key_spec.rb @@ -0,0 +1,44 @@ +require_relative '../../../spec_helper' + +ruby_version_is "3.3" do + describe "ObjectSpace::WeakKeyMap#key?" do + it "recognizes keys in use" do + map = ObjectSpace::WeakKeyMap.new + key1, key2 = %w[a b].map(&:upcase) + ref1, ref2 = %w[x y] + + map[key1] = ref1 + map.key?(key1).should == true + map[key1] = ref1 + map.key?(key1).should == true + map[key2] = ref2 + map.key?(key2).should == true + end + + it "matches using equality semantics" do + map = ObjectSpace::WeakKeyMap.new + key1, key2 = %w[a a].map(&:upcase) + ref = "x" + map[key1] = ref + map.key?(key2).should == true + end + + it "reports true if the pair exists and the value is nil" do + map = ObjectSpace::WeakKeyMap.new + key = Object.new + map[key] = nil + map.key?(key).should == true + end + + it "returns false when a key cannot be garbage collected" do + map = ObjectSpace::WeakKeyMap.new + + map.key?(1).should == false + map.key?(1.0).should == false + map.key?(:a).should == false + map.key?(true).should == false + map.key?(false).should == false + map.key?(nil).should == false + end + end +end diff --git a/spec/ruby/core/objectspace/weakmap/delete_spec.rb b/spec/ruby/core/objectspace/weakmap/delete_spec.rb new file mode 100644 index 0000000000..302de264fb --- /dev/null +++ b/spec/ruby/core/objectspace/weakmap/delete_spec.rb @@ -0,0 +1,30 @@ +require_relative '../../../spec_helper' + +ruby_version_is '3.3' do + describe "ObjectSpace::WeakMap#delete" do + it "removes the entry and returns the deleted value" do + m = ObjectSpace::WeakMap.new + key = Object.new + value = Object.new + m[key] = value + + m.delete(key).should == value + m.key?(key).should == false + end + + it "calls supplied block if the key is not found" do + key = Object.new + m = ObjectSpace::WeakMap.new + return_value = m.delete(key) do |yielded_key| + yielded_key.should == key + 5 + end + return_value.should == 5 + end + + it "returns nil if the key is not found when no block is given" do + m = ObjectSpace::WeakMap.new + m.delete(Object.new).should == nil + end + end +end diff --git a/spec/ruby/core/objectspace/weakmap/each_key_spec.rb b/spec/ruby/core/objectspace/weakmap/each_key_spec.rb new file mode 100644 index 0000000000..df971deeb9 --- /dev/null +++ b/spec/ruby/core/objectspace/weakmap/each_key_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../../spec_helper' +require_relative 'shared/members' +require_relative 'shared/each' + +describe "ObjectSpace::WeakMap#each_key" do + it_behaves_like :weakmap_members, -> map { a = []; map.each_key{ |k| a << k }; a }, %w[A B] +end + +describe "ObjectSpace::WeakMap#each_key" do + it_behaves_like :weakmap_each, :each_key +end diff --git a/spec/ruby/core/objectspace/weakmap/each_pair_spec.rb b/spec/ruby/core/objectspace/weakmap/each_pair_spec.rb new file mode 100644 index 0000000000..ea29edbd2f --- /dev/null +++ b/spec/ruby/core/objectspace/weakmap/each_pair_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../../spec_helper' +require_relative 'shared/members' +require_relative 'shared/each' + +describe "ObjectSpace::WeakMap#each_pair" do + it_behaves_like :weakmap_members, -> map { a = []; map.each_pair{ |k,v| a << "#{k}#{v}" }; a }, %w[Ax By] +end + +describe "ObjectSpace::WeakMap#each_key" do + it_behaves_like :weakmap_each, :each_pair +end diff --git a/spec/ruby/core/objectspace/weakmap/each_spec.rb b/spec/ruby/core/objectspace/weakmap/each_spec.rb new file mode 100644 index 0000000000..46fcb66a6f --- /dev/null +++ b/spec/ruby/core/objectspace/weakmap/each_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../../spec_helper' +require_relative 'shared/members' +require_relative 'shared/each' + +describe "ObjectSpace::WeakMap#each" do + it_behaves_like :weakmap_members, -> map { a = []; map.each{ |k,v| a << "#{k}#{v}" }; a }, %w[Ax By] +end + +describe "ObjectSpace::WeakMap#each_key" do + it_behaves_like :weakmap_each, :each +end diff --git a/spec/ruby/core/objectspace/weakmap/each_value_spec.rb b/spec/ruby/core/objectspace/weakmap/each_value_spec.rb new file mode 100644 index 0000000000..65a1a7f6fe --- /dev/null +++ b/spec/ruby/core/objectspace/weakmap/each_value_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../../spec_helper' +require_relative 'shared/members' +require_relative 'shared/each' + +describe "ObjectSpace::WeakMap#each_value" do + it_behaves_like :weakmap_members, -> map { a = []; map.each_value{ |k| a << k }; a }, %w[x y] +end + +describe "ObjectSpace::WeakMap#each_key" do + it_behaves_like :weakmap_each, :each_value +end diff --git a/spec/ruby/core/objectspace/weakmap/element_reference_spec.rb b/spec/ruby/core/objectspace/weakmap/element_reference_spec.rb new file mode 100644 index 0000000000..cb3174cbfa --- /dev/null +++ b/spec/ruby/core/objectspace/weakmap/element_reference_spec.rb @@ -0,0 +1,24 @@ +require_relative '../../../spec_helper' + +describe "ObjectSpace::WeakMap#[]" do + it "is faithful to the map's content" do + map = ObjectSpace::WeakMap.new + key1, key2 = %w[a b].map(&:upcase) + ref1, ref2 = %w[x y] + map[key1] = ref1 + map[key1].should == ref1 + map[key1] = ref1 + map[key1].should == ref1 + map[key2] = ref2 + map[key1].should == ref1 + map[key2].should == ref2 + end + + it "matches using identity semantics" do + map = ObjectSpace::WeakMap.new + key1, key2 = %w[a a].map(&:upcase) + ref = "x" + map[key1] = ref + map[key2].should == nil + end +end diff --git a/spec/ruby/core/objectspace/weakmap/element_set_spec.rb b/spec/ruby/core/objectspace/weakmap/element_set_spec.rb new file mode 100644 index 0000000000..8588877158 --- /dev/null +++ b/spec/ruby/core/objectspace/weakmap/element_set_spec.rb @@ -0,0 +1,38 @@ +require_relative '../../../spec_helper' + +describe "ObjectSpace::WeakMap#[]=" do + def should_accept(map, key, value) + (map[key] = value).should == value + map.should.key?(key) + map[key].should == value + end + + it "is correct" do + map = ObjectSpace::WeakMap.new + key1, key2 = %w[a b].map(&:upcase) + ref1, ref2 = %w[x y] + should_accept(map, key1, ref1) + should_accept(map, key1, ref1) + should_accept(map, key2, ref2) + map[key1].should == ref1 + end + + it "accepts primitive or frozen keys or values" do + map = ObjectSpace::WeakMap.new + x = Object.new + should_accept(map, true, x) + should_accept(map, false, x) + should_accept(map, nil, x) + should_accept(map, 42, x) + should_accept(map, :foo, x) + + should_accept(map, x, true) + should_accept(map, x, false) + should_accept(map, x, 42) + should_accept(map, x, :foo) + + y = Object.new.freeze + should_accept(map, x, y) + should_accept(map, y, x) + end +end diff --git a/spec/ruby/core/objectspace/weakmap/include_spec.rb b/spec/ruby/core/objectspace/weakmap/include_spec.rb new file mode 100644 index 0000000000..54ca6b3030 --- /dev/null +++ b/spec/ruby/core/objectspace/weakmap/include_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../../spec_helper' +require_relative 'shared/include' + +describe "ObjectSpace::WeakMap#include?" do + it_behaves_like :weakmap_include?, :include? +end diff --git a/spec/ruby/core/objectspace/weakmap/inspect_spec.rb b/spec/ruby/core/objectspace/weakmap/inspect_spec.rb new file mode 100644 index 0000000000..f064f6e3ea --- /dev/null +++ b/spec/ruby/core/objectspace/weakmap/inspect_spec.rb @@ -0,0 +1,25 @@ +require_relative '../../../spec_helper' + +describe "ObjectSpace::WeakMap#inspect" do + it "displays object pointers in output" do + map = ObjectSpace::WeakMap.new + # important to test with BasicObject (without Kernel) here to test edge cases + key1, key2 = [BasicObject.new, Object.new] + ref1, ref2 = [BasicObject.new, Object.new] + map.inspect.should =~ /\A\#<ObjectSpace::WeakMap:0x\h+>\z/ + map[key1] = ref1 + map.inspect.should =~ /\A\#<ObjectSpace::WeakMap:0x\h+: \#<BasicObject:0x\h+> => \#<BasicObject:0x\h+>>\z/ + map[key1] = ref1 + map.inspect.should =~ /\A\#<ObjectSpace::WeakMap:0x\h+: \#<BasicObject:0x\h+> => \#<BasicObject:0x\h+>>\z/ + map[key2] = ref2 + + regexp1 = /\A\#<ObjectSpace::WeakMap:0x\h+: \#<BasicObject:0x\h+> => \#<BasicObject:0x\h+>, \#<Object:0x\h+> => \#<Object:0x\h+>>\z/ + regexp2 = /\A\#<ObjectSpace::WeakMap:0x\h+: \#<Object:0x\h+> => \#<Object:0x\h+>, \#<BasicObject:0x\h+> => \#<BasicObject:0x\h+>>\z/ + str = map.inspect + if str =~ regexp1 + str.should =~ regexp1 + else + str.should =~ regexp2 + end + end +end diff --git a/spec/ruby/core/objectspace/weakmap/key_spec.rb b/spec/ruby/core/objectspace/weakmap/key_spec.rb new file mode 100644 index 0000000000..999685ff95 --- /dev/null +++ b/spec/ruby/core/objectspace/weakmap/key_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../../spec_helper' +require_relative 'shared/include' + +describe "ObjectSpace::WeakMap#key?" do + it_behaves_like :weakmap_include?, :key? +end diff --git a/spec/ruby/core/objectspace/weakmap/keys_spec.rb b/spec/ruby/core/objectspace/weakmap/keys_spec.rb new file mode 100644 index 0000000000..7b1494bdd7 --- /dev/null +++ b/spec/ruby/core/objectspace/weakmap/keys_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../../spec_helper' +require_relative 'shared/members' + +describe "ObjectSpace::WeakMap#keys" do + it_behaves_like :weakmap_members, -> map { map.keys }, %w[A B] +end diff --git a/spec/ruby/core/objectspace/weakmap/length_spec.rb b/spec/ruby/core/objectspace/weakmap/length_spec.rb new file mode 100644 index 0000000000..3a935648b1 --- /dev/null +++ b/spec/ruby/core/objectspace/weakmap/length_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../../spec_helper' +require_relative 'shared/size' + +describe "ObjectSpace::WeakMap#length" do + it_behaves_like :weakmap_size, :length +end diff --git a/spec/ruby/core/objectspace/weakmap/member_spec.rb b/spec/ruby/core/objectspace/weakmap/member_spec.rb new file mode 100644 index 0000000000..cefb190ce7 --- /dev/null +++ b/spec/ruby/core/objectspace/weakmap/member_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../../spec_helper' +require_relative 'shared/include' + +describe "ObjectSpace::WeakMap#member?" do + it_behaves_like :weakmap_include?, :member? +end diff --git a/spec/ruby/core/objectspace/weakmap/shared/each.rb b/spec/ruby/core/objectspace/weakmap/shared/each.rb new file mode 100644 index 0000000000..3d43a19347 --- /dev/null +++ b/spec/ruby/core/objectspace/weakmap/shared/each.rb @@ -0,0 +1,10 @@ +describe :weakmap_each, shared: true do + it "must take a block, except when empty" do + map = ObjectSpace::WeakMap.new + key = "a".upcase + ref = "x" + map.send(@method).should == map + map[key] = ref + -> { map.send(@method) }.should raise_error(LocalJumpError) + end +end diff --git a/spec/ruby/core/objectspace/weakmap/shared/include.rb b/spec/ruby/core/objectspace/weakmap/shared/include.rb new file mode 100644 index 0000000000..1770eeac8b --- /dev/null +++ b/spec/ruby/core/objectspace/weakmap/shared/include.rb @@ -0,0 +1,30 @@ +describe :weakmap_include?, shared: true do + it "recognizes keys in use" do + map = ObjectSpace::WeakMap.new + key1, key2 = %w[a b].map(&:upcase) + ref1, ref2 = %w[x y] + + map[key1] = ref1 + map.send(@method, key1).should == true + map[key1] = ref1 + map.send(@method, key1).should == true + map[key2] = ref2 + map.send(@method, key2).should == true + end + + it "matches using identity semantics" do + map = ObjectSpace::WeakMap.new + key1, key2 = %w[a a].map(&:upcase) + ref = "x" + map[key1] = ref + map.send(@method, key2).should == false + end + + it "reports true if the pair exists and the value is nil" do + map = ObjectSpace::WeakMap.new + key = Object.new + map[key] = nil + map.size.should == 1 + map.send(@method, key).should == true + end +end diff --git a/spec/ruby/core/objectspace/weakmap/shared/members.rb b/spec/ruby/core/objectspace/weakmap/shared/members.rb new file mode 100644 index 0000000000..57226c8d7a --- /dev/null +++ b/spec/ruby/core/objectspace/weakmap/shared/members.rb @@ -0,0 +1,14 @@ +describe :weakmap_members, shared: true do + it "is correct" do + map = ObjectSpace::WeakMap.new + key1, key2 = %w[a b].map(&:upcase) + ref1, ref2 = %w[x y] + @method.call(map).should == [] + map[key1] = ref1 + @method.call(map).should == @object[0..0] + map[key1] = ref1 + @method.call(map).should == @object[0..0] + map[key2] = ref2 + @method.call(map).sort.should == @object + end +end diff --git a/spec/ruby/core/objectspace/weakmap/shared/size.rb b/spec/ruby/core/objectspace/weakmap/shared/size.rb new file mode 100644 index 0000000000..1064f99d1b --- /dev/null +++ b/spec/ruby/core/objectspace/weakmap/shared/size.rb @@ -0,0 +1,14 @@ +describe :weakmap_size, shared: true do + it "is correct" do + map = ObjectSpace::WeakMap.new + key1, key2 = %w[a b].map(&:upcase) + ref1, ref2 = %w[x y] + map.send(@method).should == 0 + map[key1] = ref1 + map.send(@method).should == 1 + map[key1] = ref1 + map.send(@method).should == 1 + map[key2] = ref2 + map.send(@method).should == 2 + end +end diff --git a/spec/ruby/core/objectspace/weakmap/size_spec.rb b/spec/ruby/core/objectspace/weakmap/size_spec.rb new file mode 100644 index 0000000000..1446abaa24 --- /dev/null +++ b/spec/ruby/core/objectspace/weakmap/size_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../../spec_helper' +require_relative 'shared/size' + +describe "ObjectSpace::WeakMap#size" do + it_behaves_like :weakmap_size, :size +end diff --git a/spec/ruby/core/objectspace/weakmap/values_spec.rb b/spec/ruby/core/objectspace/weakmap/values_spec.rb new file mode 100644 index 0000000000..6f6f90d0ba --- /dev/null +++ b/spec/ruby/core/objectspace/weakmap/values_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../../spec_helper' +require_relative 'shared/members' + +describe "ObjectSpace::WeakMap#values" do + it_behaves_like :weakmap_members, -> map { map.values }, %w[x y] +end diff --git a/spec/ruby/core/objectspace/weakmap_spec.rb b/spec/ruby/core/objectspace/weakmap_spec.rb new file mode 100644 index 0000000000..2f3f93c291 --- /dev/null +++ b/spec/ruby/core/objectspace/weakmap_spec.rb @@ -0,0 +1,12 @@ +require_relative '../../spec_helper' + +describe "ObjectSpace::WeakMap" do + + # Note that we can't really spec the most important aspect of this class: that entries get removed when the values + # become unreachable. This is because Ruby does not offer a way to reliable invoke GC (GC.start is not enough, neither + # on MRI or on alternative implementations). + + it "includes Enumerable" do + ObjectSpace::WeakMap.include?(Enumerable).should == true + end +end diff --git a/spec/ruby/core/proc/allocate_spec.rb b/spec/ruby/core/proc/allocate_spec.rb new file mode 100644 index 0000000000..54e1b69df9 --- /dev/null +++ b/spec/ruby/core/proc/allocate_spec.rb @@ -0,0 +1,9 @@ +require_relative '../../spec_helper' + +describe "Proc.allocate" do + it "raises a TypeError" do + -> { + Proc.allocate + }.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/proc/arity_spec.rb b/spec/ruby/core/proc/arity_spec.rb new file mode 100644 index 0000000000..5c7728cb30 --- /dev/null +++ b/spec/ruby/core/proc/arity_spec.rb @@ -0,0 +1,656 @@ +require_relative '../../spec_helper' + +describe "Proc#arity" do + SpecEvaluate.desc = "for definition" + + context "for instances created with -> () { }" do + context "returns zero" do + evaluate <<-ruby do + @a = -> () {} + ruby + + @a.arity.should == 0 + end + + evaluate <<-ruby do + @a = -> (&b) {} + ruby + + @a.arity.should == 0 + end + end + + context "returns positive values" do + evaluate <<-ruby do + @a = -> (a) { } + @b = -> (a, b) { } + @c = -> (a, b, c) { } + @d = -> (a, b, c, d) { } + ruby + + @a.arity.should == 1 + @b.arity.should == 2 + @c.arity.should == 3 + @d.arity.should == 4 + end + + evaluate <<-ruby do + @a = -> (a:) { } + @b = -> (a:, b:) { } + @c = -> (a: 1, b:, c:, d: 2) { } + ruby + + @a.arity.should == 1 + @b.arity.should == 1 + @c.arity.should == 1 + end + + evaluate <<-ruby do + @a = -> (a, b:) { } + @b = -> (a, b:, &l) { } + ruby + + @a.arity.should == 2 + @b.arity.should == 2 + end + + evaluate <<-ruby do + @a = -> (a, b, c:, d: 1) { } + @b = -> (a, b, c:, d: 1, **k, &l) { } + ruby + + @a.arity.should == 3 + @b.arity.should == 3 + end + + evaluate <<-ruby do + @a = -> ((a, (*b, c))) { } + @b = -> (a, (*b, c), d, (*e), (*)) { } + ruby + + @a.arity.should == 1 + @b.arity.should == 5 + end + end + + context "returns negative values" do + evaluate <<-ruby do + @a = -> (a=1) { } + @b = -> (a=1, b=2) { } + ruby + + @a.arity.should == -1 + @b.arity.should == -1 + end + + evaluate <<-ruby do + @a = -> (a, b=1) { } + @b = -> (a, b, c=1, d=2) { } + ruby + + @a.arity.should == -2 + @b.arity.should == -3 + end + + evaluate <<-ruby do + @a = -> (a=1, *b) { } + @b = -> (a=1, b=2, *c) { } + ruby + + @a.arity.should == -1 + @b.arity.should == -1 + end + + evaluate <<-ruby do + @a = -> (*) { } + @b = -> (*a) { } + ruby + + @a.arity.should == -1 + @b.arity.should == -1 + end + + evaluate <<-ruby do + @a = -> (a, *) { } + @b = -> (a, *b) { } + @c = -> (a, b, *c) { } + @d = -> (a, b, c, *d) { } + ruby + + @a.arity.should == -2 + @b.arity.should == -2 + @c.arity.should == -3 + @d.arity.should == -4 + end + + evaluate <<-ruby do + @a = -> (*a, b) { } + @b = -> (*a, b, c) { } + @c = -> (*a, b, c, d) { } + ruby + + @a.arity.should == -2 + @b.arity.should == -3 + @c.arity.should == -4 + end + + evaluate <<-ruby do + @a = -> (a, *b, c) { } + @b = -> (a, b, *c, d, e) { } + ruby + + @a.arity.should == -3 + @b.arity.should == -5 + end + + evaluate <<-ruby do + @a = -> (a, b=1, c=2, *d, e, f) { } + @b = -> (a, b, c=1, *d, e, f, g) { } + ruby + + @a.arity.should == -4 + @b.arity.should == -6 + end + + evaluate <<-ruby do + @a = -> (a: 1) { } + @b = -> (a: 1, b: 2) { } + ruby + + @a.arity.should == -1 + @b.arity.should == -1 + end + + evaluate <<-ruby do + @a = -> (a=1, b: 2) { } + @b = -> (*a, b: 1) { } + @c = -> (a=1, b: 2) { } + @d = -> (a=1, *b, c: 2, &l) { } + ruby + + @a.arity.should == -1 + @b.arity.should == -1 + @c.arity.should == -1 + @d.arity.should == -1 + end + + evaluate <<-ruby do + @a = -> (**k, &l) { } + @b= -> (*a, **k) { } + @c = ->(a: 1, b: 2, **k) { } + ruby + + @a.arity.should == -1 + @b.arity.should == -1 + @c.arity.should == -1 + end + + evaluate <<-ruby do + @a = -> (a=1, *b, c:, d: 2, **k, &l) { } + ruby + + @a.arity.should == -2 + end + + evaluate <<-ruby do + @a = -> (a, b=1, *c, d, e:, f: 2, **k, &l) { } + @b = -> (a, b=1, *c, d:, e:, f: 2, **k, &l) { } + @c = -> (a=0, b=1, *c, d, e:, f: 2, **k, &l) { } + @d = -> (a=0, b=1, *c, d:, e:, f: 2, **k, &l) { } + ruby + + @a.arity.should == -4 + @b.arity.should == -3 + @c.arity.should == -3 + @d.arity.should == -2 + end + end + end + + context "for instances created with lambda { || }" do + context "returns zero" do + evaluate <<-ruby do + @a = lambda { } + @b = lambda { || } + ruby + + @a.arity.should == 0 + @b.arity.should == 0 + end + + evaluate <<-ruby do + @a = lambda { |&b| } + ruby + + @a.arity.should == 0 + end + end + + context "returns positive values" do + evaluate <<-ruby do + @a = lambda { |a| } + @b = lambda { |a, b| } + @c = lambda { |a, b, c| } + @d = lambda { |a, b, c, d| } + ruby + + @a.arity.should == 1 + @b.arity.should == 2 + @c.arity.should == 3 + @d.arity.should == 4 + end + + evaluate <<-ruby do + @a = lambda { |a:| } + @b = lambda { |a:, b:| } + @c = lambda { |a: 1, b:, c:, d: 2| } + ruby + + @a.arity.should == 1 + @b.arity.should == 1 + @c.arity.should == 1 + end + + evaluate <<-ruby do + @a = lambda { |a, b:| } + @b = lambda { |a, b:, &l| } + ruby + + @a.arity.should == 2 + @b.arity.should == 2 + end + + evaluate <<-ruby do + @a = lambda { |a, b, c:, d: 1| } + @b = lambda { |a, b, c:, d: 1, **k, &l| } + ruby + + @a.arity.should == 3 + @b.arity.should == 3 + end + + # implicit rest + evaluate <<-ruby do + @a = lambda { |a, | } + ruby + + @a.arity.should == 1 + end + end + + context "returns negative values" do + evaluate <<-ruby do + @a = lambda { |a=1| } + @b = lambda { |a=1, b=2| } + ruby + + @a.arity.should == -1 + @b.arity.should == -1 + end + + evaluate <<-ruby do + @a = lambda { |a, b=1| } + @b = lambda { |a, b, c=1, d=2| } + ruby + + @a.arity.should == -2 + @b.arity.should == -3 + end + + evaluate <<-ruby do + @a = lambda { |a=1, *b| } + @b = lambda { |a=1, b=2, *c| } + ruby + + @a.arity.should == -1 + @b.arity.should == -1 + end + + evaluate <<-ruby do + @a = lambda { |*| } + @b = lambda { |*a| } + ruby + + @a.arity.should == -1 + @b.arity.should == -1 + end + + evaluate <<-ruby do + @a = lambda { |a, *| } + @b = lambda { |a, *b| } + @c = lambda { |a, b, *c| } + @d = lambda { |a, b, c, *d| } + ruby + + @a.arity.should == -2 + @b.arity.should == -2 + @c.arity.should == -3 + @d.arity.should == -4 + end + + evaluate <<-ruby do + @a = lambda { |*a, b| } + @b = lambda { |*a, b, c| } + @c = lambda { |*a, b, c, d| } + ruby + + @a.arity.should == -2 + @b.arity.should == -3 + @c.arity.should == -4 + end + + evaluate <<-ruby do + @a = lambda { |a, *b, c| } + @b = lambda { |a, b, *c, d, e| } + ruby + + @a.arity.should == -3 + @b.arity.should == -5 + end + + evaluate <<-ruby do + @a = lambda { |a, b=1, c=2, *d, e, f| } + @b = lambda { |a, b, c=1, *d, e, f, g| } + ruby + + @a.arity.should == -4 + @b.arity.should == -6 + end + + evaluate <<-ruby do + @a = lambda { |a: 1| } + @b = lambda { |a: 1, b: 2| } + ruby + + @a.arity.should == -1 + @b.arity.should == -1 + end + + evaluate <<-ruby do + @a = lambda { |a=1, b: 2| } + @b = lambda { |*a, b: 1| } + @c = lambda { |a=1, b: 2| } + @d = lambda { |a=1, *b, c: 2, &l| } + ruby + + @a.arity.should == -1 + @b.arity.should == -1 + @c.arity.should == -1 + @d.arity.should == -1 + end + + evaluate <<-ruby do + @a = lambda { |**k, &l| } + @b = lambda { |*a, **k| } + @c = lambda { |a: 1, b: 2, **k| } + ruby + + @a.arity.should == -1 + @b.arity.should == -1 + @c.arity.should == -1 + end + + evaluate <<-ruby do + @a = lambda { |a=1, *b, c:, d: 2, **k, &l| } + ruby + + @a.arity.should == -2 + end + + evaluate <<-ruby do + @a = lambda { |(a, (*b, c)), d=1| } + @b = lambda { |a, (*b, c), d, (*e), (*), **k| } + @c = lambda { |a, (b, c), *, d:, e: 2, **| } + ruby + + @a.arity.should == -2 + @b.arity.should == -6 + @c.arity.should == -4 + end + + evaluate <<-ruby do + @a = lambda { |a, b=1, *c, d, e:, f: 2, **k, &l| } + @b = lambda { |a, b=1, *c, d:, e:, f: 2, **k, &l| } + @c = lambda { |a=0, b=1, *c, d, e:, f: 2, **k, &l| } + @d = lambda { |a=0, b=1, *c, d:, e:, f: 2, **k, &l| } + ruby + + @a.arity.should == -4 + @b.arity.should == -3 + @c.arity.should == -3 + @d.arity.should == -2 + end + end + end + + context "for instances created with proc { || }" do + context "returns zero" do + evaluate <<-ruby do + @a = proc { } + @b = proc { || } + ruby + + @a.arity.should == 0 + @b.arity.should == 0 + end + + evaluate <<-ruby do + @a = proc { |&b| } + ruby + + @a.arity.should == 0 + end + + evaluate <<-ruby do + @a = proc { |a=1| } + @b = proc { |a=1, b=2| } + ruby + + @a.arity.should == 0 + @b.arity.should == 0 + end + + evaluate <<-ruby do + @a = proc { |a: 1| } + @b = proc { |a: 1, b: 2| } + ruby + + @a.arity.should == 0 + @b.arity.should == 0 + end + + evaluate <<-ruby do + @a = proc { |**k, &l| } + @b = proc { |a: 1, b: 2, **k| } + ruby + + @a.arity.should == 0 + @b.arity.should == 0 + end + + evaluate <<-ruby do + @a = proc { |a=1, b: 2| } + @b = proc { |a=1, b: 2| } + ruby + + @a.arity.should == 0 + @b.arity.should == 0 + end + end + + context "returns positive values" do + evaluate <<-ruby do + @a = proc { |a| } + @b = proc { |a, b| } + @c = proc { |a, b, c| } + @d = proc { |a, b, c, d| } + ruby + + @a.arity.should == 1 + @b.arity.should == 2 + @c.arity.should == 3 + @d.arity.should == 4 + end + + evaluate <<-ruby do + @a = proc { |a, b=1| } + @b = proc { |a, b, c=1, d=2| } + ruby + + @a.arity.should == 1 + @b.arity.should == 2 + end + + evaluate <<-ruby do + @a = lambda { |a:| } + @b = lambda { |a:, b:| } + @c = lambda { |a: 1, b:, c:, d: 2| } + ruby + + @a.arity.should == 1 + @b.arity.should == 1 + @c.arity.should == 1 + end + + evaluate <<-ruby do + @a = proc { |a, b:| } + @b = proc { |a, b:, &l| } + ruby + + @a.arity.should == 2 + @b.arity.should == 2 + end + + evaluate <<-ruby do + @a = proc { |a, b, c:, d: 1| } + @b = proc { |a, b, c:, d: 1, **k, &l| } + ruby + + @a.arity.should == 3 + @b.arity.should == 3 + end + + evaluate <<-ruby do + @a = proc { |(a, (*b, c)), d=1| } + @b = proc { |a, (*b, c), d, (*e), (*), **k| } + ruby + + @a.arity.should == 1 + @b.arity.should == 5 + end + + # implicit rest + evaluate <<-ruby do + @a = proc { |a, | } + ruby + + @a.arity.should == 1 + end + end + + context "returns negative values" do + evaluate <<-ruby do + @a = proc { |a=1, *b| } + @b = proc { |a=1, b=2, *c| } + ruby + + @a.arity.should == -1 + @b.arity.should == -1 + end + + evaluate <<-ruby do + @a = proc { |*| } + @b = proc { |*a| } + ruby + + @a.arity.should == -1 + @b.arity.should == -1 + end + + evaluate <<-ruby do + @a = proc { |a, *| } + @b = proc { |a, *b| } + @c = proc { |a, b, *c| } + @d = proc { |a, b, c, *d| } + ruby + + @a.arity.should == -2 + @b.arity.should == -2 + @c.arity.should == -3 + @d.arity.should == -4 + end + + evaluate <<-ruby do + @a = proc { |*a, b| } + @b = proc { |*a, b, c| } + @c = proc { |*a, b, c, d| } + ruby + + @a.arity.should == -2 + @b.arity.should == -3 + @c.arity.should == -4 + end + + evaluate <<-ruby do + @a = proc { |a, *b, c| } + @b = proc { |a, b, *c, d, e| } + ruby + + @a.arity.should == -3 + @b.arity.should == -5 + end + + evaluate <<-ruby do + @a = proc { |a, b=1, c=2, *d, e, f| } + @b = proc { |a, b, c=1, *d, e, f, g| } + ruby + + @a.arity.should == -4 + @b.arity.should == -6 + end + + evaluate <<-ruby do + @a = proc { |*a, b: 1| } + @b = proc { |a=1, *b, c: 2, &l| } + ruby + + @a.arity.should == -1 + @b.arity.should == -1 + end + + evaluate <<-ruby do + @a = proc { |*a, **k| } + ruby + + @a.arity.should == -1 + end + + evaluate <<-ruby do + @a = proc { |a=1, *b, c:, d: 2, **k, &l| } + ruby + + @a.arity.should == -2 + end + + evaluate <<-ruby do + @a = proc { |a, (b, c), *, d:, e: 2, **| } + ruby + + @a.arity.should == -4 + end + + evaluate <<-ruby do + @a = proc { |a, b=1, *c, d, e:, f: 2, **k, &l| } + @b = proc { |a, b=1, *c, d:, e:, f: 2, **k, &l| } + @c = proc { |a=0, b=1, *c, d, e:, f: 2, **k, &l| } + @d = proc { |a=0, b=1, *c, d:, e:, f: 2, **k, &l| } + ruby + + @a.arity.should == -4 + @b.arity.should == -3 + @c.arity.should == -3 + @d.arity.should == -2 + end + end + end +end diff --git a/spec/ruby/core/proc/binding_spec.rb b/spec/ruby/core/proc/binding_spec.rb new file mode 100644 index 0000000000..86ab6bd400 --- /dev/null +++ b/spec/ruby/core/proc/binding_spec.rb @@ -0,0 +1,21 @@ +require_relative '../../spec_helper' + +describe "Proc#binding" do + it "returns a Binding instance" do + [Proc.new{}, -> {}, proc {}].each { |p| + p.binding.should be_kind_of(Binding) + } + end + + it "returns the binding associated with self" do + obj = mock('binding') + def obj.test_binding(some, params) + -> {} + end + + lambdas_binding = obj.test_binding(1, 2).binding + + eval("some", lambdas_binding).should == 1 + eval("params", lambdas_binding).should == 2 + end +end diff --git a/spec/ruby/core/proc/block_pass_spec.rb b/spec/ruby/core/proc/block_pass_spec.rb new file mode 100644 index 0000000000..411c0bf3db --- /dev/null +++ b/spec/ruby/core/proc/block_pass_spec.rb @@ -0,0 +1,21 @@ +require_relative '../../spec_helper' + +describe "Proc as a block pass argument" do + def revivify(&b) + b + end + + it "remains the same object if re-vivified by the target method" do + p = Proc.new {} + p2 = revivify(&p) + p.should equal p2 + p.should == p2 + end + + it "remains the same object if reconstructed with Proc.new" do + p = Proc.new {} + p2 = Proc.new(&p) + p.should equal p2 + p.should == p2 + end +end diff --git a/spec/ruby/core/proc/call_spec.rb b/spec/ruby/core/proc/call_spec.rb new file mode 100644 index 0000000000..6ec2fc8682 --- /dev/null +++ b/spec/ruby/core/proc/call_spec.rb @@ -0,0 +1,16 @@ +require_relative '../../spec_helper' +require_relative 'shared/call' +require_relative 'shared/call_arguments' + +describe "Proc#call" do + it_behaves_like :proc_call, :call + it_behaves_like :proc_call_block_args, :call +end + +describe "Proc#call on a Proc created with Proc.new" do + it_behaves_like :proc_call_on_proc_new, :call +end + +describe "Proc#call on a Proc created with Kernel#lambda or Kernel#proc" do + it_behaves_like :proc_call_on_proc_or_lambda, :call +end diff --git a/spec/ruby/core/proc/case_compare_spec.rb b/spec/ruby/core/proc/case_compare_spec.rb new file mode 100644 index 0000000000..f11513cdb9 --- /dev/null +++ b/spec/ruby/core/proc/case_compare_spec.rb @@ -0,0 +1,16 @@ +require_relative '../../spec_helper' +require_relative 'shared/call' +require_relative 'shared/call_arguments' + +describe "Proc#===" do + it_behaves_like :proc_call, :=== + it_behaves_like :proc_call_block_args, :=== +end + +describe "Proc#=== on a Proc created with Proc.new" do + it_behaves_like :proc_call_on_proc_new, :=== +end + +describe "Proc#=== on a Proc created with Kernel#lambda or Kernel#proc" do + it_behaves_like :proc_call_on_proc_or_lambda, :=== +end diff --git a/spec/ruby/core/proc/clone_spec.rb b/spec/ruby/core/proc/clone_spec.rb new file mode 100644 index 0000000000..730dc421a8 --- /dev/null +++ b/spec/ruby/core/proc/clone_spec.rb @@ -0,0 +1,30 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' +require_relative 'shared/dup' + +describe "Proc#clone" do + it_behaves_like :proc_dup, :clone + + ruby_bug "cloning a frozen proc is broken on Ruby 3.3", "3.3"..."3.4" do + it "preserves frozen status" do + proc = Proc.new { } + proc.freeze + proc.frozen?.should == true + proc.clone.frozen?.should == true + end + end + + ruby_version_is "3.3" do + it "calls #initialize_clone on subclass" do + obj = ProcSpecs::MyProc2.new(:a, 2) { } + dup = obj.clone + + dup.should_not equal(obj) + dup.class.should == ProcSpecs::MyProc2 + + dup.first.should == :a + dup.second.should == 2 + dup.initializer.should == :clone + end + end +end diff --git a/spec/ruby/core/proc/compose_spec.rb b/spec/ruby/core/proc/compose_spec.rb new file mode 100644 index 0000000000..9e9b57e06f --- /dev/null +++ b/spec/ruby/core/proc/compose_spec.rb @@ -0,0 +1,142 @@ +require_relative '../../spec_helper' +require_relative 'shared/compose' + +describe "Proc#<<" do + it "returns a Proc that is the composition of self and the passed Proc" do + upcase = proc { |s| s.upcase } + succ = proc { |s| s.succ } + + (succ << upcase).call('Ruby').should == "RUBZ" + end + + it "calls passed Proc with arguments and then calls self with result" do + f = proc { |x| x * x } + g = proc { |x| x + x } + + (f << g).call(2).should == 16 + (g << f).call(2).should == 8 + end + + it "accepts any callable object" do + inc = proc { |n| n + 1 } + + double = Object.new + def double.call(n); n * 2; end + + (inc << double).call(3).should == 7 + end + + it_behaves_like :proc_compose, :<<, -> { proc { |s| s.upcase } } + + describe "composition" do + it "is a Proc" do + f = proc { |x| x * x } + g = proc { |x| x + x } + + (f << g).is_a?(Proc).should == true + (f << g).should_not.lambda? + end + + it "is a lambda when parameter is lambda" do + f = -> x { x * x } + g = proc { |x| x + x } + lambda_proc = -> x { x } + + # lambda << proc + (f << g).is_a?(Proc).should == true + (f << g).should_not.lambda? + + # lambda << lambda + (f << lambda_proc).is_a?(Proc).should == true + (f << lambda_proc).should.lambda? + + # proc << lambda + (g << f).is_a?(Proc).should == true + (g << f).should.lambda? + end + + it "may accept multiple arguments" do + inc = proc { |n| n + 1 } + mul = proc { |n, m| n * m } + + (inc << mul).call(2, 3).should == 7 + end + + it "passes blocks to the second proc" do + ScratchPad.record [] + one = proc { |&arg| arg.call :one if arg } + two = proc { |&arg| arg.call :two if arg } + (one << two).call { |x| ScratchPad << x } + ScratchPad.recorded.should == [:two] + end + end +end + +describe "Proc#>>" do + it "returns a Proc that is the composition of self and the passed Proc" do + upcase = proc { |s| s.upcase } + succ = proc { |s| s.succ } + + (succ >> upcase).call('Ruby').should == "RUBZ" + end + + it "calls passed Proc with arguments and then calls self with result" do + f = proc { |x| x * x } + g = proc { |x| x + x } + + (f >> g).call(2).should == 8 + (g >> f).call(2).should == 16 + end + + it "accepts any callable object" do + inc = proc { |n| n + 1 } + + double = Object.new + def double.call(n); n * 2; end + + (inc >> double).call(3).should == 8 + end + + it_behaves_like :proc_compose, :>>, -> { proc { |s| s.upcase } } + + describe "composition" do + it "is a Proc" do + f = proc { |x| x * x } + g = proc { |x| x + x } + + (f >> g).is_a?(Proc).should == true + (f >> g).should_not.lambda? + end + + it "is a Proc when other is lambda" do + f = proc { |x| x * x } + g = -> x { x + x } + + (f >> g).is_a?(Proc).should == true + (f >> g).should_not.lambda? + end + + it "is a lambda when self is lambda" do + f = -> x { x * x } + g = proc { |x| x + x } + + (f >> g).is_a?(Proc).should == true + (f >> g).should.lambda? + end + + it "may accept multiple arguments" do + inc = proc { |n| n + 1 } + mul = proc { |n, m| n * m } + + (mul >> inc).call(2, 3).should == 7 + end + + it "passes blocks to the first proc" do + ScratchPad.record [] + one = proc { |&arg| arg.call :one if arg } + two = proc { |&arg| arg.call :two if arg } + (one >> two).call { |x| ScratchPad << x } + ScratchPad.recorded.should == [:one] + end + end +end diff --git a/spec/ruby/core/proc/curry_spec.rb b/spec/ruby/core/proc/curry_spec.rb new file mode 100644 index 0000000000..6daabe0ee1 --- /dev/null +++ b/spec/ruby/core/proc/curry_spec.rb @@ -0,0 +1,179 @@ +require_relative '../../spec_helper' + +describe "Proc#curry" do + before :each do + @proc_add = Proc.new {|x,y,z| (x||0) + (y||0) + (z||0) } + @lambda_add = -> x, y, z { (x||0) + (y||0) + (z||0) } + end + + it "returns a Proc when called on a proc" do + p = proc { true } + p.curry.should be_an_instance_of(Proc) + end + + it "returns a Proc when called on a lambda" do + p = -> { true } + p.curry.should be_an_instance_of(Proc) + end + + it "calls the curried proc with the arguments if sufficient arguments have been given" do + @proc_add.curry[1][2][3].should == 6 + @lambda_add.curry[1][2][3].should == 6 + end + + it "returns a Proc that consumes the remainder of the arguments unless sufficient arguments have been given" do + proc2 = @proc_add.curry[1][2] + proc2.should be_an_instance_of(Proc) + proc2.call(3).should == 6 + + lambda2 = @lambda_add.curry[1][2] + lambda2.should be_an_instance_of(Proc) + lambda2.call(3).should == 6 + + @proc_add.curry.call(1,2,3).should == 6 + @lambda_add.curry.call(1,2,3).should == 6 + end + + it "can be called multiple times on the same Proc" do + @proc_add.curry + -> { @proc_add.curry }.should_not raise_error + + @lambda_add.curry + -> { @lambda_add.curry }.should_not raise_error + end + + it "can be passed superfluous arguments if created from a proc" do + @proc_add.curry[1,2,3,4].should == 6 + + @proc_add.curry[1,2].curry[3,4,5,6].should == 6 + end + + it "raises an ArgumentError if passed superfluous arguments when created from a lambda" do + -> { @lambda_add.curry[1,2,3,4] }.should raise_error(ArgumentError) + -> { @lambda_add.curry[1,2].curry[3,4,5,6] }.should raise_error(ArgumentError) + end + + it "returns Procs with arities of -1" do + @proc_add.curry.arity.should == -1 + @lambda_add.curry.arity.should == -1 + l = -> *a { } + l.curry.arity.should == -1 + end + + it "produces Procs that raise ArgumentError for #binding" do + -> do + @proc_add.curry.binding + end.should raise_error(ArgumentError) + end + + it "produces Procs that return [[:rest]] for #parameters" do + @proc_add.curry.parameters.should == [[:rest]] + end + + it "produces Procs that return nil for #source_location" do + @proc_add.curry.source_location.should == nil + end + + it "produces Procs that can be passed as the block for instance_exec" do + curried = @proc_add.curry.call(1, 2) + + instance_exec(3, &curried).should == 6 + end + + it "combines arguments and calculates incoming arity accurately for successively currying" do + l = -> a, b, c { a+b+c } + l1 = l.curry.call(1) + # the l1 currying seems unnecessary, but it triggered the original issue + l2 = l1.curry.call(2) + + l2.curry.call(3).should == 6 + l1.curry.call(2,3).should == 6 + end +end + +describe "Proc#curry with arity argument" do + before :each do + @proc_add = proc { |x,y,z| (x||0) + (y||0) + (z||0) } + @lambda_add = -> x, y, z { (x||0) + (y||0) + (z||0) } + end + + it "accepts an optional Integer argument for the arity" do + -> { @proc_add.curry(3) }.should_not raise_error + -> { @lambda_add.curry(3) }.should_not raise_error + end + + it "returns a Proc when called on a proc" do + @proc_add.curry(3).should be_an_instance_of(Proc) + end + + it "returns a Proc when called on a lambda" do + @lambda_add.curry(3).should be_an_instance_of(Proc) + end + + # [ruby-core:24127] + it "retains the lambda-ness of the Proc on which its called" do + @lambda_add.curry(3).lambda?.should be_true + @proc_add.curry(3).lambda?.should be_false + end + + it "raises an ArgumentError if called on a lambda that requires more than _arity_ arguments" do + -> { @lambda_add.curry(2) }.should raise_error(ArgumentError) + -> { -> x, y, z, *more{}.curry(2) }.should raise_error(ArgumentError) + end + + it 'returns a Proc if called on a lambda that requires fewer than _arity_ arguments but may take more' do + -> a, b, c, d=nil, e=nil {}.curry(4).should be_an_instance_of(Proc) + -> a, b, c, d=nil, *e {}.curry(4).should be_an_instance_of(Proc) + -> a, b, c, *d {}.curry(4).should be_an_instance_of(Proc) + end + + it "raises an ArgumentError if called on a lambda that requires fewer than _arity_ arguments" do + -> { @lambda_add.curry(4) }.should raise_error(ArgumentError) + -> { -> { true }.curry(1) }.should raise_error(ArgumentError) + -> { -> a, b=nil {}.curry(5) }.should raise_error(ArgumentError) + -> { -> a, &b {}.curry(2) }.should raise_error(ArgumentError) + -> { -> a, b=nil, &c {}.curry(3) }.should raise_error(ArgumentError) + end + + it "calls the curried proc with the arguments if _arity_ arguments have been given" do + @proc_add.curry(3)[1][2][3].should == 6 + @lambda_add.curry(3)[1][2][3].should == 6 + end + + it "returns a Proc that consumes the remainder of the arguments when fewer than _arity_ arguments are given" do + proc2 = @proc_add.curry(3)[1][2] + proc2.should be_an_instance_of(Proc) + proc2.call(3).should == 6 + + lambda2 = @lambda_add.curry(3)[1][2] + lambda2.should be_an_instance_of(Proc) + lambda2.call(3).should == 6 + end + + it "can be specified multiple times on the same Proc" do + @proc_add.curry(2) + -> { @proc_add.curry(1) }.should_not raise_error + + @lambda_add.curry(3) + -> { @lambda_add.curry(3) }.should_not raise_error + end + + it "can be passed more than _arity_ arguments if created from a proc" do + @proc_add.curry(3)[1,2,3,4].should == 6 + + @proc_add.curry(3)[1,2].curry(3)[3,4,5,6].should == 6 + end + + it "raises an ArgumentError if passed more than _arity_ arguments when created from a lambda" do + -> { @lambda_add.curry(3)[1,2,3,4] }.should raise_error(ArgumentError) + -> { @lambda_add.curry(3)[1,2].curry(3)[3,4,5,6] }.should raise_error(ArgumentError) + end + + it "returns Procs with arities of -1 regardless of the value of _arity_" do + @proc_add.curry(1).arity.should == -1 + @proc_add.curry(2).arity.should == -1 + @lambda_add.curry(3).arity.should == -1 + l = -> *a { } + l.curry(3).arity.should == -1 + end +end diff --git a/spec/ruby/core/proc/dup_spec.rb b/spec/ruby/core/proc/dup_spec.rb new file mode 100644 index 0000000000..716357d1f0 --- /dev/null +++ b/spec/ruby/core/proc/dup_spec.rb @@ -0,0 +1,28 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' +require_relative 'shared/dup' + +describe "Proc#dup" do + it_behaves_like :proc_dup, :dup + + it "resets frozen status" do + proc = Proc.new { } + proc.freeze + proc.frozen?.should == true + proc.dup.frozen?.should == false + end + + ruby_version_is "3.3" do + it "calls #initialize_dup on subclass" do + obj = ProcSpecs::MyProc2.new(:a, 2) { } + dup = obj.dup + + dup.should_not equal(obj) + dup.class.should == ProcSpecs::MyProc2 + + dup.first.should == :a + dup.second.should == 2 + dup.initializer.should == :dup + end + end +end diff --git a/spec/ruby/core/proc/element_reference_spec.rb b/spec/ruby/core/proc/element_reference_spec.rb new file mode 100644 index 0000000000..81ceb91af5 --- /dev/null +++ b/spec/ruby/core/proc/element_reference_spec.rb @@ -0,0 +1,27 @@ +require_relative '../../spec_helper' +require_relative 'shared/call' +require_relative 'shared/call_arguments' +require_relative 'fixtures/proc_aref' +require_relative 'fixtures/proc_aref_frozen' + +describe "Proc#[]" do + it_behaves_like :proc_call, :[] + it_behaves_like :proc_call_block_args, :[] +end + +describe "Proc#call on a Proc created with Proc.new" do + it_behaves_like :proc_call_on_proc_new, :call +end + +describe "Proc#call on a Proc created with Kernel#lambda or Kernel#proc" do + it_behaves_like :proc_call_on_proc_or_lambda, :call +end + +describe "Proc#[] with frozen_string_literal: true/false" do + it "doesn't duplicate frozen strings" do + ProcArefSpecs.aref.frozen?.should be_false + ProcArefSpecs.aref_freeze.frozen?.should be_true + ProcArefFrozenSpecs.aref.frozen?.should be_true + ProcArefFrozenSpecs.aref_freeze.frozen?.should be_true + end +end diff --git a/spec/ruby/core/proc/eql_spec.rb b/spec/ruby/core/proc/eql_spec.rb new file mode 100644 index 0000000000..ad8f6749fc --- /dev/null +++ b/spec/ruby/core/proc/eql_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/equal' + +describe "Proc#eql?" do + it_behaves_like :proc_equal, :eql? +end diff --git a/spec/ruby/core/proc/equal_value_spec.rb b/spec/ruby/core/proc/equal_value_spec.rb new file mode 100644 index 0000000000..ec7f274732 --- /dev/null +++ b/spec/ruby/core/proc/equal_value_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/equal' + +describe "Proc#==" do + it_behaves_like :proc_equal, :== +end diff --git a/spec/ruby/core/proc/fixtures/common.rb b/spec/ruby/core/proc/fixtures/common.rb new file mode 100644 index 0000000000..dfe67d7ba8 --- /dev/null +++ b/spec/ruby/core/proc/fixtures/common.rb @@ -0,0 +1,72 @@ +module ProcSpecs + class ToAryAsNil + def to_ary + nil + end + end + def self.new_proc_in_method + Proc.new + end + + def self.new_proc_from_amp(&block) + block + end + + def self.proc_for_1 + proc { 1 } + end + + class ProcSubclass < Proc + end + + def self.new_proc_subclass_in_method + ProcSubclass.new + end + + class MyProc < Proc + end + + class MyProc2 < Proc + def initialize(a, b) + @first = a + @second = b + end + + attr_reader :first, :second, :initializer + + def initialize_copy(other) + super + @initializer = :copy + @first = other.first + @second = other.second + end + + def initialize_dup(other) + super + @initializer = :dup + @first = other.first + @second = other.second + end + + def initialize_clone(other, **options) + super + @initializer = :clone + @first = other.first + @second = other.second + end + end + + class Arity + def arity_check(&block) + pn = Proc.new(&block).arity + pr = proc(&block).arity + lm = lambda(&block).arity + + if pn == pr and pr == lm + return pn + else + return :arity_check_failed + end + end + end +end diff --git a/spec/ruby/core/proc/fixtures/proc_aref.rb b/spec/ruby/core/proc/fixtures/proc_aref.rb new file mode 100644 index 0000000000..8ee355b14c --- /dev/null +++ b/spec/ruby/core/proc/fixtures/proc_aref.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: false +module ProcArefSpecs + def self.aref + proc {|a| a }["sometext"] + end + + def self.aref_freeze + proc {|a| a }["sometext".freeze] + end +end diff --git a/spec/ruby/core/proc/fixtures/proc_aref_frozen.rb b/spec/ruby/core/proc/fixtures/proc_aref_frozen.rb new file mode 100644 index 0000000000..50a330ba4f --- /dev/null +++ b/spec/ruby/core/proc/fixtures/proc_aref_frozen.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true +module ProcArefFrozenSpecs + def self.aref + proc {|a| a }["sometext"] + end + + def self.aref_freeze + proc {|a| a }["sometext".freeze] + end +end diff --git a/spec/ruby/core/proc/fixtures/source_location.rb b/spec/ruby/core/proc/fixtures/source_location.rb new file mode 100644 index 0000000000..5572094630 --- /dev/null +++ b/spec/ruby/core/proc/fixtures/source_location.rb @@ -0,0 +1,55 @@ +module ProcSpecs + class SourceLocation + def self.my_proc + proc { true } + end + + def self.my_lambda + -> { true } + end + + def self.my_proc_new + Proc.new { true } + end + + def self.my_method + method(__method__).to_proc + end + + def self.my_multiline_proc + proc do + 'a'.upcase + 1 + 22 + end + end + + def self.my_multiline_lambda + -> do + 'a'.upcase + 1 + 22 + end + end + + def self.my_multiline_proc_new + Proc.new do + 'a'.upcase + 1 + 22 + end + end + + def self.my_detached_proc + body = proc { true } + proc(&body) + end + + def self.my_detached_lambda + body = -> { true } + suppress_warning {lambda(&body)} + end + + def self.my_detached_proc_new + body = Proc.new { true } + Proc.new(&body) + end + end +end diff --git a/spec/ruby/core/proc/hash_spec.rb b/spec/ruby/core/proc/hash_spec.rb new file mode 100644 index 0000000000..ebe0fde1a0 --- /dev/null +++ b/spec/ruby/core/proc/hash_spec.rb @@ -0,0 +1,17 @@ +require_relative '../../spec_helper' + +describe "Proc#hash" do + it "is provided" do + proc {}.respond_to?(:hash).should be_true + -> {}.respond_to?(:hash).should be_true + end + + it "returns an Integer" do + proc { 1 + 489 }.hash.should be_kind_of(Integer) + end + + it "is stable" do + body = proc { :foo } + proc(&body).hash.should == proc(&body).hash + end +end diff --git a/spec/ruby/core/proc/inspect_spec.rb b/spec/ruby/core/proc/inspect_spec.rb new file mode 100644 index 0000000000..f53d34116f --- /dev/null +++ b/spec/ruby/core/proc/inspect_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/to_s' + +describe "Proc#inspect" do + it_behaves_like :proc_to_s, :inspect +end diff --git a/spec/ruby/core/proc/lambda_spec.rb b/spec/ruby/core/proc/lambda_spec.rb new file mode 100644 index 0000000000..5c3c38fc2a --- /dev/null +++ b/spec/ruby/core/proc/lambda_spec.rb @@ -0,0 +1,62 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' + +describe "Proc#lambda?" do + it "returns true if the Proc was created from a block with the lambda keyword" do + -> {}.lambda?.should be_true + end + + it "returns false if the Proc was created from a block with the proc keyword" do + proc {}.lambda?.should be_false + end + + it "returns false if the Proc was created from a block with Proc.new" do + Proc.new {}.lambda?.should be_false + end + + ruby_version_is ""..."3.3" do + it "is preserved when passing a Proc with & to the lambda keyword" do + suppress_warning {lambda(&->{})}.lambda?.should be_true + suppress_warning {lambda(&proc{})}.lambda?.should be_false + end + end + + it "is preserved when passing a Proc with & to the proc keyword" do + proc(&->{}).lambda?.should be_true + proc(&proc{}).lambda?.should be_false + end + + it "is preserved when passing a Proc with & to Proc.new" do + Proc.new(&->{}).lambda?.should be_true + Proc.new(&proc{}).lambda?.should be_false + end + + it "returns false if the Proc was created from a block with &" do + ProcSpecs.new_proc_from_amp{}.lambda?.should be_false + end + + it "is preserved when the Proc was passed using &" do + ProcSpecs.new_proc_from_amp(&->{}).lambda?.should be_true + ProcSpecs.new_proc_from_amp(&proc{}).lambda?.should be_false + ProcSpecs.new_proc_from_amp(&Proc.new{}).lambda?.should be_false + end + + it "returns true for a Method converted to a Proc" do + m = :foo.method(:to_s) + m.to_proc.lambda?.should be_true + ProcSpecs.new_proc_from_amp(&m).lambda?.should be_true + end + + # [ruby-core:24127] + it "is preserved when a Proc is curried" do + ->{}.curry.lambda?.should be_true + proc{}.curry.lambda?.should be_false + Proc.new{}.curry.lambda?.should be_false + end + + it "is preserved when a curried Proc is called without enough arguments" do + -> x, y{}.curry.call(42).lambda?.should be_true + proc{|x,y|}.curry.call(42).lambda?.should be_false + Proc.new{|x,y|}.curry.call(42).lambda?.should be_false + end +end diff --git a/spec/ruby/core/proc/new_spec.rb b/spec/ruby/core/proc/new_spec.rb new file mode 100644 index 0000000000..b2b7387756 --- /dev/null +++ b/spec/ruby/core/proc/new_spec.rb @@ -0,0 +1,178 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' + +describe "Proc.new with an associated block" do + it "returns a proc that represents the block" do + Proc.new { }.call.should == nil + Proc.new { "hello" }.call.should == "hello" + end + + describe "called on a subclass of Proc" do + before :each do + @subclass = Class.new(Proc) do + attr_reader :ok + def initialize + @ok = true + super + end + end + end + + it "returns an instance of the subclass" do + proc = @subclass.new {"hello"} + + proc.class.should == @subclass + proc.call.should == "hello" + proc.ok.should == true + end + + # JRUBY-5026 + describe "using a reified block parameter" do + it "returns an instance of the subclass" do + cls = Class.new do + def self.subclass=(subclass) + @subclass = subclass + end + def self.foo(&block) + @subclass.new(&block) + end + end + cls.subclass = @subclass + proc = cls.foo {"hello"} + + proc.class.should == @subclass + proc.call.should == "hello" + proc.ok.should == true + end + end + end + + # JRUBY-5261; Proc sets up the block during .new, not in #initialize + describe "called on a subclass of Proc that does not 'super' in 'initialize'" do + before :each do + @subclass = Class.new(Proc) do + attr_reader :ok + def initialize + @ok = true + end + end + end + + it "still constructs a functional proc" do + proc = @subclass.new {'ok'} + proc.call.should == 'ok' + proc.ok.should == true + end + end + + it "raises a LocalJumpError when context of the block no longer exists" do + def some_method + Proc.new { return } + end + res = some_method() + + -> { res.call }.should raise_error(LocalJumpError) + end + + it "returns from within enclosing method when 'return' is used in the block" do + # we essentially verify that the created instance behaves like proc, + # not like lambda. + def some_method + Proc.new { return :proc_return_value }.call + :method_return_value + end + some_method.should == :proc_return_value + end + + it "returns a subclass of Proc" do + obj = ProcSpecs::MyProc.new { } + obj.should be_kind_of(ProcSpecs::MyProc) + end + + it "calls initialize on the Proc object" do + obj = ProcSpecs::MyProc2.new(:a, 2) { } + obj.first.should == :a + obj.second.should == 2 + end +end + +describe "Proc.new with a block argument" do + it "returns the passed proc created from a block" do + passed_prc = Proc.new { "hello".size } + prc = Proc.new(&passed_prc) + + prc.should equal(passed_prc) + prc.call.should == 5 + end + + it "returns the passed proc created from a method" do + method = "hello".method(:size) + passed_prc = Proc.new(&method) + prc = Proc.new(&passed_prc) + + prc.should equal(passed_prc) + prc.call.should == 5 + end + + it "returns the passed proc created from a symbol" do + passed_prc = Proc.new(&:size) + prc = Proc.new(&passed_prc) + + prc.should equal(passed_prc) + prc.call("hello").should == 5 + end +end + +describe "Proc.new with a block argument called indirectly from a subclass" do + it "returns the passed proc created from a block" do + passed_prc = ProcSpecs::MyProc.new { "hello".size } + passed_prc.class.should == ProcSpecs::MyProc + prc = ProcSpecs::MyProc.new(&passed_prc) + + prc.should equal(passed_prc) + prc.call.should == 5 + end + + it "returns the passed proc created from a method" do + method = "hello".method(:size) + passed_prc = ProcSpecs::MyProc.new(&method) + passed_prc.class.should == ProcSpecs::MyProc + prc = ProcSpecs::MyProc.new(&passed_prc) + + prc.should equal(passed_prc) + prc.call.should == 5 + end + + it "returns the passed proc created from a symbol" do + passed_prc = ProcSpecs::MyProc.new(&:size) + passed_prc.class.should == ProcSpecs::MyProc + prc = ProcSpecs::MyProc.new(&passed_prc) + + prc.should equal(passed_prc) + prc.call("hello").should == 5 + end +end + +describe "Proc.new without a block" do + it "raises an ArgumentError" do + -> { Proc.new }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if invoked from within a method with no block" do + -> { ProcSpecs.new_proc_in_method }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if invoked on a subclass from within a method with no block" do + -> { ProcSpecs.new_proc_subclass_in_method }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when passed no block" do + def some_method + Proc.new + end + + -> { ProcSpecs.new_proc_in_method { "hello" } }.should raise_error(ArgumentError, 'tried to create Proc object without a block') + -> { ProcSpecs.new_proc_subclass_in_method { "hello" } }.should raise_error(ArgumentError, 'tried to create Proc object without a block') + -> { some_method { "hello" } }.should raise_error(ArgumentError, 'tried to create Proc object without a block') + end +end diff --git a/spec/ruby/core/proc/parameters_spec.rb b/spec/ruby/core/proc/parameters_spec.rb new file mode 100644 index 0000000000..cf8a8f5b12 --- /dev/null +++ b/spec/ruby/core/proc/parameters_spec.rb @@ -0,0 +1,175 @@ +require_relative '../../spec_helper' + +describe "Proc#parameters" do + it "returns an empty Array for a proc expecting no parameters" do + proc {}.parameters.should == [] + end + + it "returns an Array of Arrays for a proc expecting parameters" do + p = proc {|x| } + p.parameters.should be_an_instance_of(Array) + p.parameters.first.should be_an_instance_of(Array) + end + + it "sets the first element of each sub-Array to :opt for optional arguments" do + proc {|x| }.parameters.first.first.should == :opt + proc {|y,*x| }.parameters.first.first.should == :opt + end + + it "regards named parameters in procs as optional" do + proc {|x| }.parameters.first.first.should == :opt + end + + it "sets the first element of each sub-Array to :req for required argument if lambda keyword used" do + proc {|x| }.parameters(lambda: true).first.first.should == :req + proc {|y,*x| }.parameters(lambda: true).first.first.should == :req + end + + it "regards named parameters in procs as required if lambda keyword used" do + proc {|x| }.parameters(lambda: true).first.first.should == :req + end + + it "regards named parameters in lambda as optional if lambda: false keyword used" do + -> x { }.parameters(lambda: false).first.first.should == :opt + end + + it "regards named parameters in procs and lambdas as required if lambda keyword is truthy" do + proc {|x| }.parameters(lambda: 123).first.first.should == :req + -> x { }.parameters(lambda: 123).first.first.should == :req + end + + it "ignores the lambda keyword if it is nil" do + proc {|x|}.parameters(lambda: nil).first.first.should == :opt + -> x { }.parameters(lambda: nil).first.first.should == :req + end + + it "regards optional keyword parameters in procs as optional" do + proc {|x: :y| }.parameters.first.first.should == :key + end + + it "regards parameters with default values as optional" do + -> x=1 { }.parameters.first.first.should == :opt + proc {|x=1| }.parameters.first.first.should == :opt + end + + it "sets the first element of each sub-Array to :req for required arguments" do + -> x, y=[] { }.parameters.first.first.should == :req + -> y, *x { }.parameters.first.first.should == :req + end + + it "regards named parameters in lambdas as required" do + -> x { }.parameters.first.first.should == :req + end + + it "regards keyword parameters in lambdas as required" do + -> x: { }.parameters.first.first.should == :keyreq + end + + it "sets the first element of each sub-Array to :rest for parameters prefixed with asterisks" do + -> *x { }.parameters.first.first.should == :rest + -> x, *y { }.parameters.last.first.should == :rest + proc {|*x| }.parameters.first.first.should == :rest + proc {|x,*y| }.parameters.last.first.should == :rest + end + + it "sets the first element of each sub-Array to :keyrest for parameters prefixed with double asterisks" do + -> **x { }.parameters.first.first.should == :keyrest + -> x, **y { }.parameters.last.first.should == :keyrest + proc {|**x| }.parameters.first.first.should == :keyrest + proc {|x,**y| }.parameters.last.first.should == :keyrest + end + + it "sets the first element of each sub-Array to :block for parameters prefixed with ampersands" do + -> &x { }.parameters.first.first.should == :block + -> x, &y { }.parameters.last.first.should == :block + proc {|&x| }.parameters.first.first.should == :block + proc {|x,&y| }.parameters.last.first.should == :block + end + + it "sets the second element of each sub-Array to the name of the argument" do + -> x { }.parameters.first.last.should == :x + -> x=Math::PI { }.parameters.first.last.should == :x + -> an_argument, glark, &foo { }.parameters[1].last.should == :glark + -> *rest { }.parameters.first.last.should == :rest + -> &block { }.parameters.first.last.should == :block + proc {|x| }.parameters.first.last.should == :x + proc {|x=Math::PI| }.parameters.first.last.should == :x + proc {|an_argument, glark, &foo| }.parameters[1].last.should == :glark + proc {|*rest| }.parameters.first.last.should == :rest + proc {|&block| }.parameters.first.last.should == :block + end + + it "ignores unnamed rest arguments" do + -> x {}.parameters.should == [[:req, :x]] + end + + it "ignores implicit rest arguments" do + proc { |x, | }.parameters.should == [[:opt, :x]] + -> x { }.parameters.should == [[:req, :x]] + end + + it "adds rest arg with name * for \"star\" argument" do + -> * {}.parameters.should == [[:rest, :*]] + end + + it "adds keyrest arg with ** as a name for \"double star\" argument" do + -> ** {}.parameters.should == [[:keyrest, :**]] + end + + it "adds block arg with name & for anonymous block argument" do + -> & {}.parameters.should == [[:block, :&]] + end + + it "does not add locals as block options with a block and splat" do + -> *args, &blk do + local_is_not_parameter = {} + end.parameters.should == [[:rest, :args], [:block, :blk]] + proc do |*args, &blk| + local_is_not_parameter = {} + end.parameters.should == [[:rest, :args], [:block, :blk]] + end + + it "returns all parameters defined with the name _ as _" do + proc = proc {|_, _, _ = 1, *_, _:, _: 2, **_, &_| } + proc.parameters.should == [ + [:opt, :_], + [:opt, :_], + [:opt, :_], + [:rest, :_], + [:keyreq, :_], + [:key, :_], + [:keyrest, :_], + [:block, :_] + ] + + lambda = -> _, _, _ = 1, *_, _:, _: 2, **_, &_ {} + lambda.parameters.should == [ + [:req, :_], + [:req, :_], + [:opt, :_], + [:rest, :_], + [:keyreq, :_], + [:key, :_], + [:keyrest, :_], + [:block, :_] + ] + end + + it "returns :nokey for **nil parameter" do + proc { |**nil| }.parameters.should == [[:nokey]] + end + + ruby_version_is "3.4"..."4.0" do + it "handles the usage of `it` as a parameter" do + eval("proc { it }").parameters.should == [[:opt, nil]] + eval("lambda { it }").parameters.should == [[:req]] + end + end + + ruby_version_is "4.0" do + it "handles the usage of `it` as a parameter" do + eval("proc { it }").parameters.should == [[:opt]] + eval("lambda { it }").parameters.should == [[:req]] + end + end +end diff --git a/spec/ruby/core/proc/ruby2_keywords_spec.rb b/spec/ruby/core/proc/ruby2_keywords_spec.rb new file mode 100644 index 0000000000..d7f8f592e1 --- /dev/null +++ b/spec/ruby/core/proc/ruby2_keywords_spec.rb @@ -0,0 +1,66 @@ +require_relative '../../spec_helper' + +describe "Proc#ruby2_keywords" do + it "marks the final hash argument as keyword hash" do + f = -> *a { a.last } + f.ruby2_keywords + + last = f.call(1, 2, a: "a") + Hash.ruby2_keywords_hash?(last).should == true + end + + it "applies to the underlying method and applies across duplication" do + f1 = -> *a { a.last } + f1.ruby2_keywords + f2 = f1.dup + + Hash.ruby2_keywords_hash?(f1.call(1, 2, a: "a")).should == true + Hash.ruby2_keywords_hash?(f2.call(1, 2, a: "a")).should == true + + f3 = -> *a { a.last } + f4 = f3.dup + f3.ruby2_keywords + + Hash.ruby2_keywords_hash?(f3.call(1, 2, a: "a")).should == true + Hash.ruby2_keywords_hash?(f4.call(1, 2, a: "a")).should == true + end + + it "returns self" do + f = -> *a { } + f.ruby2_keywords.should equal f + end + + it "prints warning when a proc does not accept argument splat" do + f = -> a, b, c { } + + -> { + f.ruby2_keywords + }.should complain(/Skipping set of ruby2_keywords flag for/) + end + + it "prints warning when a proc accepts keywords" do + f = -> *a, b: { } + + -> { + f.ruby2_keywords + }.should complain(/Skipping set of ruby2_keywords flag for/) + end + + it "prints warning when a proc accepts keyword splat" do + f = -> *a, **b { } + + -> { + f.ruby2_keywords + }.should complain(/Skipping set of ruby2_keywords flag for/) + end + + ruby_version_is "4.0" do + it "prints warning when a proc accepts post arguments" do + f = -> *a, b { } + + -> { + f.ruby2_keywords + }.should complain(/Skipping set of ruby2_keywords flag for/) + end + end +end diff --git a/spec/ruby/core/proc/shared/call.rb b/spec/ruby/core/proc/shared/call.rb new file mode 100644 index 0000000000..dbec34df4b --- /dev/null +++ b/spec/ruby/core/proc/shared/call.rb @@ -0,0 +1,99 @@ +require_relative '../fixtures/common' + +describe :proc_call, shared: true do + it "invokes self" do + Proc.new { "test!" }.send(@method).should == "test!" + -> { "test!" }.send(@method).should == "test!" + proc { "test!" }.send(@method).should == "test!" + end + + it "sets self's parameters to the given values" do + Proc.new { |a, b| a + b }.send(@method, 1, 2).should == 3 + Proc.new { |*args| args }.send(@method, 1, 2, 3, 4).should == [1, 2, 3, 4] + Proc.new { |_, *args| args }.send(@method, 1, 2, 3).should == [2, 3] + + -> a, b { a + b }.send(@method, 1, 2).should == 3 + -> *args { args }.send(@method, 1, 2, 3, 4).should == [1, 2, 3, 4] + -> _, *args { args }.send(@method, 1, 2, 3).should == [2, 3] + + proc { |a, b| a + b }.send(@method, 1, 2).should == 3 + proc { |*args| args }.send(@method, 1, 2, 3, 4).should == [1, 2, 3, 4] + proc { |_, *args| args }.send(@method, 1, 2, 3).should == [2, 3] + end +end + + +describe :proc_call_on_proc_new, shared: true do + it "replaces missing arguments with nil" do + Proc.new { |a, b| [a, b] }.send(@method).should == [nil, nil] + Proc.new { |a, b| [a, b] }.send(@method, 1).should == [1, nil] + end + + it "silently ignores extra arguments" do + Proc.new { |a, b| a + b }.send(@method, 1, 2, 5).should == 3 + end + + it "auto-explodes a single Array argument" do + p = Proc.new { |a, b| [a, b] } + p.send(@method, 1, 2).should == [1, 2] + p.send(@method, [1, 2]).should == [1, 2] + p.send(@method, [1, 2, 3]).should == [1, 2] + p.send(@method, [1, 2, 3], 4).should == [[1, 2, 3], 4] + end +end + +describe :proc_call_on_proc_or_lambda, shared: true do + it "ignores excess arguments when self is a proc" do + a = proc {|x| x}.send(@method, 1, 2) + a.should == 1 + + a = proc {|x| x}.send(@method, 1, 2, 3) + a.should == 1 + + a = proc {|x:| x}.send(@method, 2, x: 1) + a.should == 1 + end + + it "will call #to_ary on argument and return self if return is nil" do + argument = ProcSpecs::ToAryAsNil.new + result = proc { |x, _| x }.send(@method, argument) + result.should == argument + end + + it "substitutes nil for missing arguments when self is a proc" do + proc {|x,y| [x,y]}.send(@method).should == [nil,nil] + + a = proc {|x,y| [x, y]}.send(@method, 1) + a.should == [1,nil] + end + + it "raises an ArgumentError on excess arguments when self is a lambda" do + -> { + -> x { x }.send(@method, 1, 2) + }.should raise_error(ArgumentError) + + -> { + -> x { x }.send(@method, 1, 2, 3) + }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError on missing arguments when self is a lambda" do + -> { + -> x { x }.send(@method) + }.should raise_error(ArgumentError) + + -> { + -> x, y { [x,y] }.send(@method, 1) + }.should raise_error(ArgumentError) + end + + it "treats a single Array argument as a single argument when self is a lambda" do + -> a { a }.send(@method, [1, 2]).should == [1, 2] + -> a, b { [a, b] }.send(@method, [1, 2], 3).should == [[1,2], 3] + end + + it "treats a single Array argument as a single argument when self is a proc" do + proc { |a| a }.send(@method, [1, 2]).should == [1, 2] + proc { |a, b| [a, b] }.send(@method, [1, 2], 3).should == [[1,2], 3] + end +end diff --git a/spec/ruby/core/proc/shared/call_arguments.rb b/spec/ruby/core/proc/shared/call_arguments.rb new file mode 100644 index 0000000000..91ada3439e --- /dev/null +++ b/spec/ruby/core/proc/shared/call_arguments.rb @@ -0,0 +1,29 @@ +describe :proc_call_block_args, shared: true do + it "can receive block arguments" do + Proc.new {|&b| b.send(@method)}.send(@method) {1 + 1}.should == 2 + -> &b { b.send(@method)}.send(@method) {1 + 1}.should == 2 + proc {|&b| b.send(@method)}.send(@method) {1 + 1}.should == 2 + end + + it "yields to the block given at declaration and not to the block argument" do + proc_creator = Object.new + def proc_creator.create + Proc.new do |&b| + yield + end + end + a_proc = proc_creator.create { 7 } + a_proc.send(@method) { 3 }.should == 7 + end + + it "can call its block argument declared with a block argument" do + proc_creator = Object.new + def proc_creator.create(method_name) + Proc.new do |&b| + yield + b.send(method_name) + end + end + a_proc = proc_creator.create(@method) { 7 } + a_proc.call { 3 }.should == 10 + end +end diff --git a/spec/ruby/core/proc/shared/compose.rb b/spec/ruby/core/proc/shared/compose.rb new file mode 100644 index 0000000000..3d3f3b310d --- /dev/null +++ b/spec/ruby/core/proc/shared/compose.rb @@ -0,0 +1,22 @@ +describe :proc_compose, shared: true do + it "raises TypeError if passed not callable object" do + lhs = @object.call + not_callable = Object.new + + -> { + lhs.send(@method, not_callable) + }.should raise_error(TypeError, "callable object is expected") + + end + + it "does not try to coerce argument with #to_proc" do + lhs = @object.call + + succ = Object.new + def succ.to_proc(s); s.succ; end + + -> { + lhs.send(@method, succ) + }.should raise_error(TypeError, "callable object is expected") + end +end diff --git a/spec/ruby/core/proc/shared/dup.rb b/spec/ruby/core/proc/shared/dup.rb new file mode 100644 index 0000000000..1266337f94 --- /dev/null +++ b/spec/ruby/core/proc/shared/dup.rb @@ -0,0 +1,39 @@ +describe :proc_dup, shared: true do + it "returns a copy of self" do + a = -> { "hello" } + b = a.send(@method) + + a.should_not equal(b) + + a.call.should == b.call + end + + it "returns an instance of subclass" do + cl = Class.new(Proc) + + cl.new{}.send(@method).class.should == cl + end + + ruby_version_is "3.4" do + it "copies instance variables" do + proc = -> { "hello" } + proc.instance_variable_set(:@ivar, 1) + cl = proc.send(@method) + cl.instance_variables.should == [:@ivar] + end + + it "copies the finalizer" do + code = <<-'RUBY' + obj = Proc.new { } + + ObjectSpace.define_finalizer(obj, Proc.new { STDOUT.write "finalized\n" }) + + obj.clone + + exit 0 + RUBY + + ruby_exe(code).lines.sort.should == ["finalized\n", "finalized\n"] + end + end +end diff --git a/spec/ruby/core/proc/shared/equal.rb b/spec/ruby/core/proc/shared/equal.rb new file mode 100644 index 0000000000..d0503fb064 --- /dev/null +++ b/spec/ruby/core/proc/shared/equal.rb @@ -0,0 +1,83 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/common' + +describe :proc_equal, shared: true do + it "is a public method" do + Proc.should have_public_instance_method(@method, false) + end + + it "returns true if self and other are the same object" do + p = proc { :foo } + p.send(@method, p).should be_true + + p = Proc.new { :foo } + p.send(@method, p).should be_true + + p = -> { :foo } + p.send(@method, p).should be_true + end + + it "returns true if other is a dup of the original" do + p = proc { :foo } + p.send(@method, p.dup).should be_true + + p = Proc.new { :foo } + p.send(@method, p.dup).should be_true + + p = -> { :foo } + p.send(@method, p.dup).should be_true + end + + # identical here means the same method invocation. + it "returns false when bodies are the same but capture env is not identical" do + a = ProcSpecs.proc_for_1 + b = ProcSpecs.proc_for_1 + + a.send(@method, b).should be_false + end + + it "returns false if procs are distinct but have the same body and environment" do + p = proc { :foo } + p2 = proc { :foo } + p.send(@method, p2).should be_false + end + + it "returns false if lambdas are distinct but have same body and environment" do + x = -> { :foo } + x2 = -> { :foo } + x.send(@method, x2).should be_false + end + + it "returns false if using comparing lambda to proc, even with the same body and env" do + p = -> { :foo } + p2 = proc { :foo } + p.send(@method, p2).should be_false + + x = proc { :bar } + x2 = -> { :bar } + x.send(@method, x2).should be_false + end + + it "returns false if other is not a Proc" do + p = proc { :foo } + p.send(@method, []).should be_false + + p = Proc.new { :foo } + p.send(@method, Object.new).should be_false + + p = -> { :foo } + p.send(@method, :foo).should be_false + end + + it "returns false if self and other are both procs but have different bodies" do + p = proc { :bar } + p2 = proc { :foo } + p.send(@method, p2).should be_false + end + + it "returns false if self and other are both lambdas but have different bodies" do + p = -> { :foo } + p2 = -> { :bar } + p.send(@method, p2).should be_false + end +end diff --git a/spec/ruby/core/proc/shared/to_s.rb b/spec/ruby/core/proc/shared/to_s.rb new file mode 100644 index 0000000000..a52688a89f --- /dev/null +++ b/spec/ruby/core/proc/shared/to_s.rb @@ -0,0 +1,60 @@ +describe :proc_to_s, shared: true do + describe "for a proc created with Proc.new" do + it "returns a description including file and line number" do + Proc.new { "hello" }.send(@method).should =~ /^#<Proc:([^ ]*?) #{Regexp.escape __FILE__}:#{__LINE__ }>$/ + end + + it "has a binary encoding" do + Proc.new { "hello" }.send(@method).encoding.should == Encoding::BINARY + end + end + + describe "for a proc created with lambda" do + it "returns a description including '(lambda)' and including file and line number" do + -> { "hello" }.send(@method).should =~ /^#<Proc:([^ ]*?) #{Regexp.escape __FILE__}:#{__LINE__ } \(lambda\)>$/ + end + + it "has a binary encoding" do + -> { "hello" }.send(@method).encoding.should == Encoding::BINARY + end + end + + describe "for a proc created with proc" do + it "returns a description including file and line number" do + proc { "hello" }.send(@method).should =~ /^#<Proc:([^ ]*?) #{Regexp.escape __FILE__}:#{__LINE__ }>$/ + end + + it "has a binary encoding" do + proc { "hello" }.send(@method).encoding.should == Encoding::BINARY + end + end + + describe "for a proc created with UnboundMethod#to_proc" do + it "returns a description including '(lambda)' and optionally including file and line number" do + def hello; end + s = method("hello").to_proc.send(@method) + if s.include? __FILE__ + s.should =~ /^#<Proc:([^ ]*?) #{Regexp.escape __FILE__}:#{__LINE__ - 3} \(lambda\)>$/ + else + s.should =~ /^#<Proc:([^ ]*?) \(lambda\)>$/ + end + end + + it "has a binary encoding" do + def hello; end + method("hello").to_proc.send(@method).encoding.should == Encoding::BINARY + end + end + + describe "for a proc created with Symbol#to_proc" do + it "returns a description including '(&:symbol)'" do + proc = :foobar.to_proc + proc.send(@method).should.include?('(&:foobar)') + end + + it "has a binary encoding" do + proc = :foobar.to_proc + proc.send(@method).encoding.should == Encoding::BINARY + end + end +end diff --git a/spec/ruby/core/proc/source_location_spec.rb b/spec/ruby/core/proc/source_location_spec.rb new file mode 100644 index 0000000000..fd33f21a26 --- /dev/null +++ b/spec/ruby/core/proc/source_location_spec.rb @@ -0,0 +1,104 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/source_location' + +describe "Proc#source_location" do + before :each do + @proc = ProcSpecs::SourceLocation.my_proc + @lambda = ProcSpecs::SourceLocation.my_lambda + @proc_new = ProcSpecs::SourceLocation.my_proc_new + @method = ProcSpecs::SourceLocation.my_method + end + + it "returns an Array" do + @proc.source_location.should be_an_instance_of(Array) + @proc_new.source_location.should be_an_instance_of(Array) + @lambda.source_location.should be_an_instance_of(Array) + @method.source_location.should be_an_instance_of(Array) + end + + it "sets the first value to the path of the file in which the proc was defined" do + file = @proc.source_location[0] + file.should be_an_instance_of(String) + file.should == File.realpath('fixtures/source_location.rb', __dir__) + + file = @proc_new.source_location[0] + file.should be_an_instance_of(String) + file.should == File.realpath('fixtures/source_location.rb', __dir__) + + file = @lambda.source_location[0] + file.should be_an_instance_of(String) + file.should == File.realpath('fixtures/source_location.rb', __dir__) + + file = @method.source_location[0] + file.should be_an_instance_of(String) + file.should == File.realpath('fixtures/source_location.rb', __dir__) + end + + it "sets the second value to an Integer representing the line on which the proc was defined" do + line = @proc.source_location[1] + line.should be_an_instance_of(Integer) + line.should == 4 + + line = @proc_new.source_location[1] + line.should be_an_instance_of(Integer) + line.should == 12 + + line = @lambda.source_location[1] + line.should be_an_instance_of(Integer) + line.should == 8 + + line = @method.source_location[1] + line.should be_an_instance_of(Integer) + line.should == 15 + end + + it "works even if the proc was created on the same line" do + ruby_version_is(""..."4.1") do + proc { true }.source_location.should == [__FILE__, __LINE__] + Proc.new { true }.source_location.should == [__FILE__, __LINE__] + -> { true }.source_location.should == [__FILE__, __LINE__] + end + ruby_version_is("4.1") do + proc { true }.source_location.should == [__FILE__, __LINE__, 11, __LINE__, 19] + Proc.new { true }.source_location.should == [__FILE__, __LINE__, 15, __LINE__, 23] + -> { true }.source_location.should == [__FILE__, __LINE__, 6, __LINE__, 17] + end + end + + it "returns the first line of a multi-line proc (i.e. the line containing 'proc do')" do + ProcSpecs::SourceLocation.my_multiline_proc.source_location[1].should == 20 + ProcSpecs::SourceLocation.my_multiline_proc_new.source_location[1].should == 34 + ProcSpecs::SourceLocation.my_multiline_lambda.source_location[1].should == 27 + end + + it "returns the location of the proc's body; not necessarily the proc itself" do + ProcSpecs::SourceLocation.my_detached_proc.source_location[1].should == 41 + ProcSpecs::SourceLocation.my_detached_proc_new.source_location[1].should == 51 + ProcSpecs::SourceLocation.my_detached_lambda.source_location[1].should == 46 + end + + it "returns the same value for a proc-ified method as the method reports" do + method = ProcSpecs::SourceLocation.method(:my_proc) + proc = method.to_proc + + method.source_location.should == proc.source_location + end + + it "returns nil for a core method that has been proc-ified" do + method = [].method(:<<) + proc = method.to_proc + + proc.source_location.should == nil + end + + it "works for eval with a given line" do + proc = eval('-> {}', nil, "foo", 100) + location = proc.source_location + ruby_version_is(""..."4.1") do + location.should == ["foo", 100] + end + ruby_version_is("4.1") do + location.should == ["foo", 100, 0, 100, 5] + end + end +end diff --git a/spec/ruby/core/proc/to_proc_spec.rb b/spec/ruby/core/proc/to_proc_spec.rb new file mode 100644 index 0000000000..ffaa34929b --- /dev/null +++ b/spec/ruby/core/proc/to_proc_spec.rb @@ -0,0 +1,9 @@ +require_relative '../../spec_helper' + +describe "Proc#to_proc" do + it "returns self" do + [Proc.new {}, -> {}, proc {}].each { |p| + p.to_proc.should equal(p) + } + end +end diff --git a/spec/ruby/core/proc/to_s_spec.rb b/spec/ruby/core/proc/to_s_spec.rb new file mode 100644 index 0000000000..5e9c46b6b8 --- /dev/null +++ b/spec/ruby/core/proc/to_s_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/to_s' + +describe "Proc#to_s" do + it_behaves_like :proc_to_s, :to_s +end diff --git a/spec/ruby/core/proc/yield_spec.rb b/spec/ruby/core/proc/yield_spec.rb new file mode 100644 index 0000000000..365d5b04bd --- /dev/null +++ b/spec/ruby/core/proc/yield_spec.rb @@ -0,0 +1,16 @@ +require_relative '../../spec_helper' +require_relative 'shared/call' +require_relative 'shared/call_arguments' + +describe "Proc#yield" do + it_behaves_like :proc_call, :yield + it_behaves_like :proc_call_block_args, :yield +end + +describe "Proc#yield on a Proc created with Proc.new" do + it_behaves_like :proc_call_on_proc_new, :yield +end + +describe "Proc#yield on a Proc created with Kernel#lambda or Kernel#proc" do + it_behaves_like :proc_call_on_proc_or_lambda, :yield +end diff --git a/spec/ruby/core/process/_fork_spec.rb b/spec/ruby/core/process/_fork_spec.rb new file mode 100644 index 0000000000..e1f45e2656 --- /dev/null +++ b/spec/ruby/core/process/_fork_spec.rb @@ -0,0 +1,24 @@ +require_relative '../../spec_helper' + +describe "Process._fork" do + it "for #respond_to? returns the same as Process.respond_to?(:fork)" do + Process.respond_to?(:_fork).should == Process.respond_to?(:fork) + end + + # Using respond_to? in a guard here is OK because the correct semantics + # are that _fork is implemented if and only if fork is (see above). + guard_not -> { Process.respond_to?(:fork) } do + it "raises a NotImplementedError when called" do + -> { Process._fork }.should raise_error(NotImplementedError) + end + end + + guard -> { Process.respond_to?(:fork) } do + it "is called by Process#fork" do + Process.should_receive(:_fork).once.and_return(42) + + pid = Process.fork {} + pid.should equal(42) + end + end +end diff --git a/spec/ruby/core/process/abort_spec.rb b/spec/ruby/core/process/abort_spec.rb new file mode 100644 index 0000000000..1b6ad1da43 --- /dev/null +++ b/spec/ruby/core/process/abort_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/process/abort' + +describe "Process.abort" do + it_behaves_like :process_abort, :abort, Process +end diff --git a/spec/ruby/core/process/argv0_spec.rb b/spec/ruby/core/process/argv0_spec.rb new file mode 100644 index 0000000000..f5aba719e9 --- /dev/null +++ b/spec/ruby/core/process/argv0_spec.rb @@ -0,0 +1,25 @@ +require_relative '../../spec_helper' + +describe "Process.argv0" do + it "returns a String" do + Process.argv0.should be_kind_of(String) + end + + it "is the path given as the main script and the same as __FILE__" do + script = "fixtures/argv0.rb" + + Dir.chdir(__dir__) do + ruby_exe(script).should == "#{script}\n#{script}\nOK" + end + end + + ruby_bug "#19597", ""..."3.3" do + it "returns a frozen object" do + Process.argv0.should.frozen? + end + end + + it "returns every time the same object" do + Process.argv0.should.equal?(Process.argv0) + end +end diff --git a/spec/ruby/core/process/clock_getres_spec.rb b/spec/ruby/core/process/clock_getres_spec.rb new file mode 100644 index 0000000000..85aa2b25f1 --- /dev/null +++ b/spec/ruby/core/process/clock_getres_spec.rb @@ -0,0 +1,33 @@ +require_relative '../../spec_helper' + +describe "Process.clock_getres" do + # These are documented + + it "with :GETTIMEOFDAY_BASED_CLOCK_REALTIME reports 1 microsecond" do + Process.clock_getres(:GETTIMEOFDAY_BASED_CLOCK_REALTIME, :nanosecond).should == 1_000 + end + + it "with :TIME_BASED_CLOCK_REALTIME reports 1 second" do + Process.clock_getres(:TIME_BASED_CLOCK_REALTIME, :nanosecond).should == 1_000_000_000 + end + + platform_is_not :windows do + it "with :GETRUSAGE_BASED_CLOCK_PROCESS_CPUTIME_ID reports 1 microsecond" do + Process.clock_getres(:GETRUSAGE_BASED_CLOCK_PROCESS_CPUTIME_ID, :nanosecond).should == 1_000 + end + end + + # These are observed + + platform_is :linux, :darwin, :windows do + it "with Process::CLOCK_REALTIME reports at least 10 millisecond" do + Process.clock_getres(Process::CLOCK_REALTIME, :nanosecond).should <= 10_000_000 + end + end + + platform_is :linux, :darwin, :windows do + it "with Process::CLOCK_MONOTONIC reports at least 10 millisecond" do + Process.clock_getres(Process::CLOCK_MONOTONIC, :nanosecond).should <= 10_000_000 + end + end +end diff --git a/spec/ruby/core/process/clock_gettime_spec.rb b/spec/ruby/core/process/clock_gettime_spec.rb new file mode 100644 index 0000000000..6c1a52f21e --- /dev/null +++ b/spec/ruby/core/process/clock_gettime_spec.rb @@ -0,0 +1,152 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/clocks' + +describe "Process.clock_gettime" do + ProcessSpecs.clock_constants.each do |name, value| + it "can be called with Process::#{name}" do + Process.clock_gettime(value).should be_an_instance_of(Float) + end + end + + describe 'time units' do + it 'handles a fixed set of time units' do + [:nanosecond, :microsecond, :millisecond, :second].each do |unit| + Process.clock_gettime(Process::CLOCK_MONOTONIC, unit).should be_kind_of(Integer) + end + + [:float_microsecond, :float_millisecond, :float_second].each do |unit| + Process.clock_gettime(Process::CLOCK_MONOTONIC, unit).should be_an_instance_of(Float) + end + end + + it 'raises an ArgumentError for an invalid time unit' do + -> { Process.clock_gettime(Process::CLOCK_MONOTONIC, :bad) }.should raise_error(ArgumentError) + end + + it 'defaults to :float_second' do + t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC) + t2 = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_second) + + t1.should be_an_instance_of(Float) + t2.should be_an_instance_of(Float) + t2.should be_close(t1, TIME_TOLERANCE) + end + + it 'uses the default time unit (:float_second) when passed nil' do + t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC, nil) + t2 = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_second) + + t1.should be_an_instance_of(Float) + t2.should be_an_instance_of(Float) + t2.should be_close(t1, TIME_TOLERANCE) + end + end + + describe "supports the platform clocks mentioned in the documentation" do + it "CLOCK_REALTIME" do + Process.clock_gettime(Process::CLOCK_REALTIME).should be_an_instance_of(Float) + end + + it "CLOCK_MONOTONIC" do + Process.clock_gettime(Process::CLOCK_MONOTONIC).should be_an_instance_of(Float) + end + + # These specs need macOS 10.12+ / darwin 16+ + guard -> { platform_is_not(:darwin) or kernel_version_is '16' } do + platform_is :linux, :openbsd, :darwin do + it "CLOCK_PROCESS_CPUTIME_ID" do + Process.clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID).should be_an_instance_of(Float) + end + end + + platform_is :linux, :freebsd, :openbsd, :darwin do + it "CLOCK_THREAD_CPUTIME_ID" do + Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID).should be_an_instance_of(Float) + end + end + + platform_is :linux, :darwin do + it "CLOCK_MONOTONIC_RAW" do + Process.clock_gettime(Process::CLOCK_MONOTONIC_RAW).should be_an_instance_of(Float) + end + end + + platform_is :darwin do + it "CLOCK_MONOTONIC_RAW_APPROX" do + Process.clock_gettime(Process::CLOCK_MONOTONIC_RAW_APPROX).should be_an_instance_of(Float) + end + + it "CLOCK_UPTIME_RAW and CLOCK_UPTIME_RAW_APPROX" do + Process.clock_gettime(Process::CLOCK_UPTIME_RAW).should be_an_instance_of(Float) + Process.clock_gettime(Process::CLOCK_UPTIME_RAW_APPROX).should be_an_instance_of(Float) + end + end + end + + platform_is :freebsd do + it "CLOCK_VIRTUAL" do + Process.clock_gettime(Process::CLOCK_VIRTUAL).should be_an_instance_of(Float) + end + + it "CLOCK_PROF" do + Process.clock_gettime(Process::CLOCK_PROF).should be_an_instance_of(Float) + end + end + + platform_is :freebsd, :openbsd do + it "CLOCK_UPTIME" do + Process.clock_gettime(Process::CLOCK_UPTIME).should be_an_instance_of(Float) + end + end + + platform_is :freebsd do + it "CLOCK_REALTIME_FAST and CLOCK_REALTIME_PRECISE" do + Process.clock_gettime(Process::CLOCK_REALTIME_FAST).should be_an_instance_of(Float) + Process.clock_gettime(Process::CLOCK_REALTIME_PRECISE).should be_an_instance_of(Float) + end + + it "CLOCK_MONOTONIC_FAST and CLOCK_MONOTONIC_PRECISE" do + Process.clock_gettime(Process::CLOCK_MONOTONIC_FAST).should be_an_instance_of(Float) + Process.clock_gettime(Process::CLOCK_MONOTONIC_PRECISE).should be_an_instance_of(Float) + end + + it "CLOCK_UPTIME_FAST and CLOCK_UPTIME_PRECISE" do + Process.clock_gettime(Process::CLOCK_UPTIME_FAST).should be_an_instance_of(Float) + Process.clock_gettime(Process::CLOCK_UPTIME_PRECISE).should be_an_instance_of(Float) + end + + it "CLOCK_SECOND" do + Process.clock_gettime(Process::CLOCK_SECOND).should be_an_instance_of(Float) + end + end + + guard -> { platform_is :linux and kernel_version_is '2.6.32' } do + it "CLOCK_REALTIME_COARSE" do + Process.clock_gettime(Process::CLOCK_REALTIME_COARSE).should be_an_instance_of(Float) + end + + it "CLOCK_MONOTONIC_COARSE" do + Process.clock_gettime(Process::CLOCK_MONOTONIC_COARSE).should be_an_instance_of(Float) + end + end + + guard -> { platform_is :linux and kernel_version_is '2.6.39' } do + it "CLOCK_BOOTTIME" do + skip "No Process::CLOCK_BOOTTIME" unless defined?(Process::CLOCK_BOOTTIME) + Process.clock_gettime(Process::CLOCK_BOOTTIME).should be_an_instance_of(Float) + end + end + + guard -> { platform_is "x86_64-linux" and kernel_version_is '3.0' } do + it "CLOCK_REALTIME_ALARM" do + skip "No Process::CLOCK_REALTIME_ALARM" unless defined?(Process::CLOCK_REALTIME_ALARM) + Process.clock_gettime(Process::CLOCK_REALTIME_ALARM).should be_an_instance_of(Float) + end + + it "CLOCK_BOOTTIME_ALARM" do + skip "No Process::CLOCK_BOOTTIME_ALARM" unless defined?(Process::CLOCK_BOOTTIME_ALARM) + Process.clock_gettime(Process::CLOCK_BOOTTIME_ALARM).should be_an_instance_of(Float) + end + end + end +end diff --git a/spec/ruby/core/process/constants_spec.rb b/spec/ruby/core/process/constants_spec.rb new file mode 100644 index 0000000000..57cacadef2 --- /dev/null +++ b/spec/ruby/core/process/constants_spec.rb @@ -0,0 +1,114 @@ +require_relative '../../spec_helper' + +describe "Process::Constants" do + platform_is :darwin, :netbsd, :freebsd do + it "are all present on BSD-like systems" do + %i[ + WNOHANG + WUNTRACED + PRIO_PROCESS + PRIO_PGRP + PRIO_USER + RLIM_INFINITY + RLIMIT_CPU + RLIMIT_FSIZE + RLIMIT_DATA + RLIMIT_STACK + RLIMIT_CORE + RLIMIT_RSS + RLIMIT_MEMLOCK + RLIMIT_NPROC + RLIMIT_NOFILE + ].each do |const| + Process.const_defined?(const).should be_true + Process.const_get(const).should be_an_instance_of(Integer) + end + end + end + + platform_is :darwin do + it "are all present on Darwin" do + %i[ + RLIM_SAVED_MAX + RLIM_SAVED_CUR + RLIMIT_AS + ].each do |const| + Process.const_defined?(const).should be_true + Process.const_get(const).should be_an_instance_of(Integer) + end + end + end + + platform_is :linux do + it "are all present on Linux" do + %i[ + WNOHANG + WUNTRACED + PRIO_PROCESS + PRIO_PGRP + PRIO_USER + RLIMIT_CPU + RLIMIT_FSIZE + RLIMIT_DATA + RLIMIT_STACK + RLIMIT_CORE + RLIMIT_RSS + RLIMIT_NPROC + RLIMIT_NOFILE + RLIMIT_MEMLOCK + RLIMIT_AS + RLIM_INFINITY + RLIM_SAVED_MAX + RLIM_SAVED_CUR + ].each do |const| + Process.const_defined?(const).should be_true + Process.const_get(const).should be_an_instance_of(Integer) + end + end + end + + platform_is :netbsd, :freebsd do + it "are all present on NetBSD and FreeBSD" do + %i[ + RLIMIT_SBSIZE + RLIMIT_AS + ].each do |const| + Process.const_defined?(const).should be_true + Process.const_get(const).should be_an_instance_of(Integer) + end + end + end + + platform_is :freebsd do + it "are all present on FreeBSD" do + %i[ + RLIMIT_NPTS + ].each do |const| + Process.const_defined?(const).should be_true + Process.const_get(const).should be_an_instance_of(Integer) + end + end + end + + platform_is :windows do + it "does not define RLIMIT constants" do + %i[ + RLIMIT_CPU + RLIMIT_FSIZE + RLIMIT_DATA + RLIMIT_STACK + RLIMIT_CORE + RLIMIT_RSS + RLIMIT_NPROC + RLIMIT_NOFILE + RLIMIT_MEMLOCK + RLIMIT_AS + RLIM_INFINITY + RLIM_SAVED_MAX + RLIM_SAVED_CUR + ].each do |const| + Process.const_defined?(const).should be_false + end + end + end +end diff --git a/spec/ruby/core/process/daemon_spec.rb b/spec/ruby/core/process/daemon_spec.rb new file mode 100644 index 0000000000..20b0d743b9 --- /dev/null +++ b/spec/ruby/core/process/daemon_spec.rb @@ -0,0 +1,118 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' + +platform_is_not :windows do + # macOS 15 is not working this examples + return if /darwin/ =~ RUBY_PLATFORM && /15/ =~ `sw_vers -productVersion` + + describe :process_daemon_keep_stdio_open_false, shared: true do + it "redirects stdout to /dev/null" do + @daemon.invoke("keep_stdio_open_false_stdout", @object).should == "" + end + + it "redirects stderr to /dev/null" do + @daemon.invoke("keep_stdio_open_false_stderr", @object).should == "" + end + + it "redirects stdin to /dev/null" do + @daemon.invoke("keep_stdio_open_false_stdin", @object).should == "" + end + + it "does not close open files" do + @daemon.invoke("keep_stdio_open_files", @object).should == "false" + end + end + + describe :process_daemon_keep_stdio_open_true, shared: true do + it "does not redirect stdout to /dev/null" do + @daemon.invoke("keep_stdio_open_true_stdout", @object).should == "writing to stdout" + end + + it "does not redirect stderr to /dev/null" do + @daemon.invoke("keep_stdio_open_true_stderr", @object).should == "writing to stderr" + end + + it "does not redirect stdin to /dev/null" do + @daemon.invoke("keep_stdio_open_true_stdin", @object).should == "reading from stdin" + end + + it "does not close open files" do + @daemon.invoke("keep_stdio_open_files", @object).should == "false" + end + end + + describe "Process.daemon" do + before :each do + @invoke_dir = Dir.pwd + @daemon = ProcessSpecs::Daemonizer.new + end + + after :each do + rm_r @daemon.input, @daemon.data if @daemon + end + + it "returns 0" do + @daemon.invoke("return_value").should == "0" + end + + it "has a different PID after daemonizing" do + parent, daemon = @daemon.invoke("pid").split(":") + parent.should_not == daemon + end + + it "has a different process group after daemonizing" do + parent, daemon = @daemon.invoke("process_group").split(":") + parent.should_not == daemon + end + + it "does not run existing at_exit handlers when daemonizing" do + @daemon.invoke("daemonizing_at_exit").should == "not running at_exit" + end + + it "runs at_exit handlers when the daemon exits" do + @daemon.invoke("daemon_at_exit").should == "running at_exit" + end + + it "changes directory to the root directory if the first argument is not given" do + @daemon.invoke("stay_in_dir").should == "/" + end + + it "changes directory to the root directory if the first argument is false" do + @daemon.invoke("stay_in_dir", [false]).should == "/" + end + + it "changes directory to the root directory if the first argument is nil" do + @daemon.invoke("stay_in_dir", [nil]).should == "/" + end + + it "does not change to the root directory if the first argument is true" do + @daemon.invoke("stay_in_dir", [true]).should == @invoke_dir + end + + describe "when the second argument is not given" do + it_behaves_like :process_daemon_keep_stdio_open_false, nil, [false] + end + + describe "when the second argument is false" do + it_behaves_like :process_daemon_keep_stdio_open_false, nil, [false, false] + end + + describe "when the second argument is nil" do + it_behaves_like :process_daemon_keep_stdio_open_false, nil, [false, nil] + end + + describe "when the second argument is true" do + it_behaves_like :process_daemon_keep_stdio_open_true, nil, [false, true] + end + end +end + +platform_is :windows do + describe "Process.daemon" do + it "raises a NotImplementedError" do + -> { + Process.daemon + }.should raise_error(NotImplementedError) + end + end +end diff --git a/spec/ruby/core/process/detach_spec.rb b/spec/ruby/core/process/detach_spec.rb new file mode 100644 index 0000000000..f13bda1f5d --- /dev/null +++ b/spec/ruby/core/process/detach_spec.rb @@ -0,0 +1,81 @@ +require_relative '../../spec_helper' + +describe "Process.detach" do + platform_is_not :windows do + it "returns a thread" do + pid = Process.fork { Process.exit! } + thr = Process.detach(pid) + thr.should be_kind_of(Thread) + thr.join + end + + it "produces the exit Process::Status as the thread value" do + pid = Process.fork { Process.exit! } + thr = Process.detach(pid) + thr.join + + status = thr.value + status.should be_kind_of(Process::Status) + status.pid.should == pid + end + + platform_is_not :openbsd do + it "reaps the child process's status automatically" do + pid = Process.fork { Process.exit! } + Process.detach(pid).join + -> { Process.waitpid(pid) }.should raise_error(Errno::ECHILD) + end + end + + it "sets the :pid thread-local to the PID" do + pid = Process.fork { Process.exit! } + thr = Process.detach(pid) + thr.join + + thr[:pid].should == pid + end + + it "provides a #pid method on the returned thread which returns the PID" do + pid = Process.fork { Process.exit! } + thr = Process.detach(pid) + thr.join + + thr.pid.should == pid + end + + it "tolerates not existing child process pid" do + # Use a value that is close to the INT_MAX (pid usually is signed int). + # It should (at least) be greater than allowed pid limit value that depends on OS. + pid_not_existing = 2.pow(30) + + # Check that there is no a child process with this hardcoded pid. + # Command `kill 0 pid`: + # - returns "1" if a process exists and + # - raises Errno::ESRCH otherwise + -> { Process.kill(0, pid_not_existing) }.should raise_error(Errno::ESRCH) + + thr = Process.detach(pid_not_existing) + thr.join + + thr.should be_kind_of(Thread) + end + + it "calls #to_int to implicitly convert non-Integer pid to Integer" do + pid = MockObject.new('mock-enumerable') + pid.should_receive(:to_int).and_return(100500) + + Process.detach(pid).join + end + + it "raises TypeError when pid argument does not have #to_int method" do + -> { Process.detach(Object.new) }.should raise_error(TypeError, "no implicit conversion of Object into Integer") + end + + it "raises TypeError when #to_int returns non-Integer value" do + pid = MockObject.new('mock-enumerable') + pid.should_receive(:to_int).and_return(:symbol) + + -> { Process.detach(pid) }.should raise_error(TypeError, "can't convert MockObject to Integer (MockObject#to_int gives Symbol)") + end + end +end diff --git a/spec/ruby/core/process/egid_spec.rb b/spec/ruby/core/process/egid_spec.rb new file mode 100644 index 0000000000..a67b623d5c --- /dev/null +++ b/spec/ruby/core/process/egid_spec.rb @@ -0,0 +1,58 @@ +require_relative '../../spec_helper' + +describe "Process.egid" do + it "returns the effective group ID for this process" do + Process.egid.should be_kind_of(Integer) + end + + it "also goes by Process::GID.eid" do + Process::GID.eid.should == Process.egid + end + + it "also goes by Process::Sys.getegid" do + Process::Sys.getegid.should == Process.egid + end +end + +describe "Process.egid=" do + + platform_is_not :windows do + it "raises TypeError if not passed an Integer or String" do + -> { Process.egid = Object.new }.should raise_error(TypeError) + end + + it "sets the effective group id to its own gid if given the username corresponding to its own gid" do + raise unless Process.gid == Process.egid + + require "etc" + group = Etc.getgrgid(Process.gid).name + + Process.egid = group + Process.egid.should == Process.gid + end + + as_user do + it "raises Errno::ERPERM if run by a non superuser trying to set the root group id" do + -> { Process.egid = 0 }.should raise_error(Errno::EPERM) + end + + platform_is :linux do + it "raises Errno::ERPERM if run by a non superuser trying to set the group id from group name" do + -> { Process.egid = "root" }.should raise_error(Errno::EPERM) + end + end + end + + as_superuser do + context "when ran by a superuser" do + it "sets the effective group id for the current process if run by a superuser" do + code = <<-RUBY + Process.egid = 1 + puts Process.egid + RUBY + ruby_exe(code).should == "1\n" + end + end + end + end +end diff --git a/spec/ruby/core/process/euid_spec.rb b/spec/ruby/core/process/euid_spec.rb new file mode 100644 index 0000000000..c1ec4171d0 --- /dev/null +++ b/spec/ruby/core/process/euid_spec.rb @@ -0,0 +1,56 @@ +require_relative '../../spec_helper' + +describe "Process.euid" do + it "returns the effective user ID for this process" do + Process.euid.should be_kind_of(Integer) + end + + it "also goes by Process::UID.eid" do + Process::UID.eid.should == Process.euid + end + + it "also goes by Process::Sys.geteuid" do + Process::Sys.geteuid.should == Process.euid + end +end + +describe "Process.euid=" do + + platform_is_not :windows do + it "raises TypeError if not passed an Integer" do + -> { Process.euid = Object.new }.should raise_error(TypeError) + end + + it "sets the effective user id to its own uid if given the username corresponding to its own uid" do + raise unless Process.uid == Process.euid + + require "etc" + user = Etc.getpwuid(Process.uid).name + + Process.euid = user + Process.euid.should == Process.uid + end + + as_user do + it "raises Errno::ERPERM if run by a non superuser trying to set the superuser id" do + -> { Process.euid = 0 }.should raise_error(Errno::EPERM) + end + + it "raises Errno::ERPERM if run by a non superuser trying to set the superuser id from username" do + -> { Process.euid = "root" }.should raise_error(Errno::EPERM) + end + end + + as_superuser do + describe "if run by a superuser" do + it "sets the effective user id for the current process if run by a superuser" do + code = <<-RUBY + Process.euid = 1 + puts Process.euid + RUBY + ruby_exe(code).should == "1\n" + end + end + end + end +end diff --git a/spec/ruby/core/process/exec_spec.rb b/spec/ruby/core/process/exec_spec.rb new file mode 100644 index 0000000000..0f371b39c8 --- /dev/null +++ b/spec/ruby/core/process/exec_spec.rb @@ -0,0 +1,241 @@ +require_relative '../../spec_helper' + +describe "Process.exec" do + it "raises Errno::ENOENT for an empty string" do + -> { Process.exec "" }.should raise_error(Errno::ENOENT) + end + + it "raises Errno::ENOENT for a command which does not exist" do + -> { Process.exec "bogus-noent-script.sh" }.should raise_error(Errno::ENOENT) + end + + it "raises an ArgumentError if the command includes a null byte" do + -> { Process.exec "\000" }.should raise_error(ArgumentError) + end + + unless File.executable?(__FILE__) # Some FS (e.g. vboxfs) locate all files executable + platform_is_not :windows do + it "raises Errno::EACCES when the file does not have execute permissions" do + -> { Process.exec __FILE__ }.should raise_error(Errno::EACCES) + end + end + + platform_is :windows do + it "raises Errno::EACCES or Errno::ENOEXEC when the file is not an executable file" do + -> { Process.exec __FILE__ }.should raise_error(SystemCallError) { |e| + [Errno::EACCES, Errno::ENOEXEC].should include(e.class) + } + end + end + end + + it "raises Errno::EACCES when passed a directory" do + -> { Process.exec __dir__ }.should raise_error(Errno::EACCES) + end + + it "runs the specified command, replacing current process" do + ruby_exe('Process.exec "echo hello"; puts "fail"').should == "hello\n" + end + + it "sets the current directory when given the :chdir option" do + tmpdir = tmp("")[0..-2] + platform_is_not :windows do + ruby_exe("Process.exec(\"pwd\", chdir: #{tmpdir.inspect})").should == "#{tmpdir}\n" + end + platform_is :windows do + ruby_exe("Process.exec(\"cd\", chdir: #{tmpdir.inspect})").tr('\\', '/').should == "#{tmpdir}\n" + end + end + + it "flushes STDOUT upon exit when it's not set to sync" do + ruby_exe("STDOUT.sync = false; STDOUT.write 'hello'").should == "hello" + end + + it "flushes STDERR upon exit when it's not set to sync" do + ruby_exe("STDERR.sync = false; STDERR.write 'hello'", args: "2>&1").should == "hello" + end + + describe "with a single argument" do + before :each do + @dir = tmp("exec_with_dir", false) + Dir.mkdir @dir + + @name = "some_file" + @path = tmp("exec_with_dir/#{@name}", false) + touch @path + end + + after :each do + rm_r @path + rm_r @dir + end + + platform_is_not :windows do + it "subjects the specified command to shell expansion" do + result = Dir.chdir(@dir) do + ruby_exe('Process.exec "echo *"') + end + result.chomp.should == @name + end + + it "creates an argument array with shell parsing semantics for whitespace" do + ruby_exe('Process.exec "echo a b c d"').should == "a b c d\n" + end + end + + platform_is :windows do + # There is no shell expansion on Windows + it "does not subject the specified command to shell expansion on Windows" do + result = Dir.chdir(@dir) do + ruby_exe('Process.exec "echo *"') + end + result.should == "*\n" + end + + it "does not create an argument array with shell parsing semantics for whitespace on Windows" do + ruby_exe('Process.exec "echo a b c d"').should == "a b c d\n" + end + end + + end + + describe "with multiple arguments" do + it "does not subject the arguments to shell expansion" do + cmd = '"echo", "*"' + platform_is :windows do + cmd = '"cmd.exe", "/C", "echo", "*"' + end + ruby_exe("Process.exec #{cmd}").should == "*\n" + end + end + + describe "(environment variables)" do + before :each do + ENV["FOO"] = "FOO" + end + + after :each do + ENV["FOO"] = nil + end + + var = '$FOO' + platform_is :windows do + var = '%FOO%' + end + + it "sets environment variables in the child environment" do + ruby_exe('Process.exec({"FOO" => "BAR"}, "echo ' + var + '")').should == "BAR\n" + end + + it "unsets environment variables whose value is nil" do + platform_is_not :windows do + ruby_exe('Process.exec({"FOO" => nil}, "echo ' + var + '")').should == "\n" + end + platform_is :windows do + # On Windows, echo-ing a non-existent env var is treated as echo-ing any other string of text + ruby_exe('Process.exec({"FOO" => nil}, "echo ' + var + '")').should == var + "\n" + end + end + + it "coerces environment argument using to_hash" do + ruby_exe('o = Object.new; def o.to_hash; {"FOO" => "BAR"}; end; Process.exec(o, "echo ' + var + '")').should == "BAR\n" + end + + it "unsets other environment variables when given a true :unsetenv_others option" do + platform_is_not :windows do + ruby_exe('Process.exec("echo ' + var + '", unsetenv_others: true)').should == "\n" + end + platform_is :windows do + ruby_exe('Process.exec("' + ENV['COMSPEC'].gsub('\\', '\\\\\\') + ' /C echo ' + var + '", unsetenv_others: true)').should == var + "\n" + end + end + end + + describe "with a command array" do + it "uses the first element as the command name and the second as the argv[0] value" do + platform_is_not :windows do + ruby_exe('Process.exec(["/bin/sh", "argv_zero"], "-c", "echo $0")').should == "argv_zero\n" + end + platform_is :windows do + ruby_exe('Process.exec(["cmd.exe", "/C"], "/C", "echo", "argv_zero")').should == "argv_zero\n" + end + end + + it "coerces the argument using to_ary" do + platform_is_not :windows do + ruby_exe('o = Object.new; def o.to_ary; ["/bin/sh", "argv_zero"]; end; Process.exec(o, "-c", "echo $0")').should == "argv_zero\n" + end + platform_is :windows do + ruby_exe('o = Object.new; def o.to_ary; ["cmd.exe", "/C"]; end; Process.exec(o, "/C", "echo", "argv_zero")').should == "argv_zero\n" + end + end + + it "raises an ArgumentError if the Array does not have exactly two elements" do + -> { Process.exec([]) }.should raise_error(ArgumentError) + -> { Process.exec([:a]) }.should raise_error(ArgumentError) + -> { Process.exec([:a, :b, :c]) }.should raise_error(ArgumentError) + end + end + + platform_is_not :windows do + describe "with an options Hash" do + describe "with Integer option keys" do + before :each do + @name = tmp("exec_fd_map.txt") + @child_fd_file = tmp("child_fd_file.txt") + end + + after :each do + rm_r @name, @child_fd_file + end + + it "maps the key to a file descriptor in the child that inherits the file descriptor from the parent specified by the value" do + map_fd_fixture = fixture __FILE__, "map_fd.rb" + cmd = <<-EOC + f = File.open(#{@name.inspect}, "w+") + File.open(#{__FILE__.inspect}, "r") do |io| + child_fd = io.fileno + File.open(#{@child_fd_file.inspect}, "w") { |io| io.print child_fd } + Process.exec "#{ruby_cmd(map_fd_fixture)} \#{child_fd}", { child_fd => f } + end + EOC + + ruby_exe(cmd) + child_fd = IO.read(@child_fd_file).to_i + child_fd.to_i.should > STDERR.fileno + + File.read(@name).should == "writing to fd: #{child_fd}" + end + + it "lets the process after exec have specified file descriptor despite close_on_exec" do + map_fd_fixture = fixture __FILE__, "map_fd.rb" + cmd = <<-EOC + f = File.open(#{@name.inspect}, 'w+') + puts(f.fileno, f.close_on_exec?) + STDOUT.flush + Process.exec("#{ruby_cmd(map_fd_fixture)} \#{f.fileno}", f.fileno => f.fileno) + EOC + + output = ruby_exe(cmd) + child_fd, close_on_exec = output.split + + child_fd.to_i.should > STDERR.fileno + close_on_exec.should == 'true' + File.read(@name).should == "writing to fd: #{child_fd}" + end + + it "sets close_on_exec to false on specified fd even when it fails" do + cmd = <<-EOC + f = File.open(#{__FILE__.inspect}, 'r') + puts(f.close_on_exec?) + Process.exec('/', f.fileno => f.fileno) rescue nil + puts(f.close_on_exec?) + EOC + + output = ruby_exe(cmd) + output.split.should == ['true', 'false'] + end + end + end + end +end diff --git a/spec/ruby/core/process/exit_spec.rb b/spec/ruby/core/process/exit_spec.rb new file mode 100644 index 0000000000..4f7dc94407 --- /dev/null +++ b/spec/ruby/core/process/exit_spec.rb @@ -0,0 +1,10 @@ +require_relative '../../spec_helper' +require_relative '../../shared/process/exit' + +describe "Process.exit" do + it_behaves_like :process_exit, :exit, Process +end + +describe "Process.exit!" do + it_behaves_like :process_exit!, :exit!, "Process" +end diff --git a/spec/ruby/core/process/fixtures/argv0.rb b/spec/ruby/core/process/fixtures/argv0.rb new file mode 100644 index 0000000000..847a3e903e --- /dev/null +++ b/spec/ruby/core/process/fixtures/argv0.rb @@ -0,0 +1,6 @@ +puts Process.argv0 +puts __FILE__ + +if Process.argv0 == __FILE__ + print "OK" +end diff --git a/spec/ruby/core/process/fixtures/clocks.rb b/spec/ruby/core/process/fixtures/clocks.rb new file mode 100644 index 0000000000..5757e280be --- /dev/null +++ b/spec/ruby/core/process/fixtures/clocks.rb @@ -0,0 +1,18 @@ +module ProcessSpecs + def self.clock_constants + clocks = [] + + platform_is_not :windows do + clocks += Process.constants.select { |c| c.to_s.start_with?('CLOCK_') } + + # These require CAP_WAKE_ALARM and are not documented in + # Process#clock_gettime. They return EINVAL if the permission + # is not granted. + clocks -= [:CLOCK_BOOTTIME_ALARM, :CLOCK_REALTIME_ALARM] + end + + clocks.sort.map { |c| + [c, Process.const_get(c)] + } + end +end diff --git a/spec/ruby/core/process/fixtures/common.rb b/spec/ruby/core/process/fixtures/common.rb new file mode 100644 index 0000000000..f49513d262 --- /dev/null +++ b/spec/ruby/core/process/fixtures/common.rb @@ -0,0 +1,88 @@ +module ProcessSpecs + def self.use_system_ruby(context) + if defined?(MSpecScript::SYSTEM_RUBY) + context.send(:before, :all) do + @ruby = ::RUBY_EXE + suppress_warning { + Object.const_set(:RUBY_EXE, MSpecScript::SYSTEM_RUBY) + } + end + + context.send(:after, :all) do + suppress_warning { + Object.const_set(:RUBY_EXE, @ruby) + } + end + end + end + + class Daemonizer + attr_reader :input, :data + + def initialize + # Fast feedback for implementations without Process.daemon + raise NotImplementedError, "Process.daemon is not implemented" unless Process.respond_to? :daemon + + @script = fixture __FILE__, "daemon.rb" + @input = tmp("process_daemon_input_file") + @data = tmp("process_daemon_data_file") + @args = [] + end + + def wait_for_daemon + sleep 0.001 until File.exist?(@data) and File.size?(@data) + end + + def invoke(behavior, arguments=[]) + args = Marshal.dump(arguments).unpack("H*") + args << @input << @data << behavior + + ruby_exe @script, args: args + + wait_for_daemon + + return unless File.exist? @data + + File.open(@data, "rb") { |f| return f.read.chomp } + end + end + + class Signalizer + attr_reader :pid_file, :pid + + def initialize(scenario=nil) + platform_is :windows do + fail "not supported on windows" + end + @script = fixture __FILE__, "kill.rb" + @pid = nil + @pid_file = tmp("process_kill_signal_file") + rm_r @pid_file + + @thread = Thread.new do + Thread.current.abort_on_exception = true + args = [@pid_file] + args << scenario if scenario + @result = ruby_exe @script, args: args + end + Thread.pass while @thread.status and !File.exist?(@pid_file) + while @thread.status && (@pid.nil? || @pid == 0) + @pid = IO.read(@pid_file).chomp.to_i + end + end + + def wait_on_result + @thread.join + end + + def cleanup + wait_on_result + rm_r pid_file + end + + def result + wait_on_result + @result.chomp if @result + end + end +end diff --git a/spec/ruby/core/process/fixtures/daemon.rb b/spec/ruby/core/process/fixtures/daemon.rb new file mode 100644 index 0000000000..772df2d09e --- /dev/null +++ b/spec/ruby/core/process/fixtures/daemon.rb @@ -0,0 +1,111 @@ +module ProcessSpecs + class Daemon + def initialize(argv) + args, @input, @data, @behavior = argv + @args = Marshal.load [args].pack("H*") + @no_at_exit = false + end + + def run + send @behavior + + # Exit without running any at_exit handlers + exit!(0) if @no_at_exit + end + + def write(data) + File.open(@data, "wb") { |f| f.puts data } + end + + def daemonizing_at_exit + at_exit do + write "running at_exit" + end + + @no_at_exit = true + Process.daemon + write "not running at_exit" + end + + def return_value + write Process.daemon.to_s + end + + def pid + parent = Process.pid + Process.daemon + daemon = Process.pid + write "#{parent}:#{daemon}" + end + + def process_group + parent = Process.getpgrp + Process.daemon + daemon = Process.getpgrp + write "#{parent}:#{daemon}" + end + + def daemon_at_exit + at_exit do + write "running at_exit" + end + + Process.daemon + end + + def stay_in_dir + Process.daemon(*@args) + write Dir.pwd + end + + def keep_stdio_open_false_stdout + Process.daemon(*@args) + $stdout.write "writing to stdout" + write "" + end + + def keep_stdio_open_false_stderr + Process.daemon(*@args) + $stderr.write "writing to stderr" + write "" + end + + def keep_stdio_open_false_stdin + Process.daemon(*@args) + + # Reading from /dev/null will return right away. If STDIN were not + # /dev/null, reading would block and the spec would hang. This is not a + # perfect way to spec the behavior but it works. + write $stdin.read + end + + def keep_stdio_open_true_stdout + $stdout.reopen @data + Process.daemon(*@args) + $stdout.write "writing to stdout" + end + + def keep_stdio_open_true_stderr + $stderr.reopen @data + Process.daemon(*@args) + $stderr.write "writing to stderr" + end + + def keep_stdio_open_true_stdin + File.open(@input, "w") { |f| f.puts "reading from stdin" } + + $stdin.reopen @input, "r" + Process.daemon(*@args) + write $stdin.read + end + + def keep_stdio_open_files + file = File.open @input, "w" + + Process.daemon(*@args) + write file.closed? + end + end +end + +ProcessSpecs::Daemon.new(ARGV).run diff --git a/spec/ruby/core/process/fixtures/in.txt b/spec/ruby/core/process/fixtures/in.txt new file mode 100644 index 0000000000..cf52303bdc --- /dev/null +++ b/spec/ruby/core/process/fixtures/in.txt @@ -0,0 +1 @@ +stdin diff --git a/spec/ruby/core/process/fixtures/kill.rb b/spec/ruby/core/process/fixtures/kill.rb new file mode 100644 index 0000000000..b922a043f1 --- /dev/null +++ b/spec/ruby/core/process/fixtures/kill.rb @@ -0,0 +1,43 @@ +pid_file = ARGV.shift +scenario = ARGV.shift + +# We must do this first otherwise there will be a race with the process that +# creates this process and the TERM signal below could go to that process +# instead, which will likely abort the specs process. +Process.setsid if scenario + +mutex = Mutex.new + +Signal.trap(:TERM) do + if mutex.try_lock + STDOUT.puts "signaled" + STDOUT.flush + $signaled = true + end +end + +File.open(pid_file, "wb") { |f| f.puts Process.pid } + +if scenario + # We are sending a signal to the process group + process = "Process.getpgrp" + + case scenario + when "self" + signal = %["SIGTERM"] + process = "0" + when "group_numeric" + signal = %[-Signal.list["TERM"]] + when "group_short_string" + signal = %["-TERM"] + when "group_full_string" + signal = %["-SIGTERM"] + else + raise "unknown scenario: #{scenario.inspect}" + end + + code = "Process.kill(#{signal}, #{process})" + system(ENV["RUBY_EXE"], *ENV["RUBY_FLAGS"].split(' '), "-e", code) +end + +sleep 0.001 until mutex.locked? and $signaled diff --git a/spec/ruby/core/process/fixtures/map_fd.rb b/spec/ruby/core/process/fixtures/map_fd.rb new file mode 100644 index 0000000000..3ed887486b --- /dev/null +++ b/spec/ruby/core/process/fixtures/map_fd.rb @@ -0,0 +1,9 @@ +fd = ARGV.shift.to_i + +f = File.for_fd(fd) +f.autoclose = false +begin + f.write "writing to fd: #{fd}" +ensure + f.close +end diff --git a/spec/ruby/core/process/fixtures/setpriority.rb b/spec/ruby/core/process/fixtures/setpriority.rb new file mode 100644 index 0000000000..cf22d85b12 --- /dev/null +++ b/spec/ruby/core/process/fixtures/setpriority.rb @@ -0,0 +1,12 @@ +case ARGV[0] +when "process" + which = Process::PRIO_PROCESS +when "group" + Process.setpgrp + which = Process::PRIO_PGRP +end + +priority = Process.getpriority(which, 0) +p priority +p Process.setpriority(which, 0, priority + 1) +p Process.getpriority(which, 0) diff --git a/spec/ruby/core/process/fork_spec.rb b/spec/ruby/core/process/fork_spec.rb new file mode 100644 index 0000000000..a4f765247d --- /dev/null +++ b/spec/ruby/core/process/fork_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/process/fork' + +describe "Process.fork" do + it_behaves_like :process_fork, :fork, Process +end diff --git a/spec/ruby/core/process/getpgid_spec.rb b/spec/ruby/core/process/getpgid_spec.rb new file mode 100644 index 0000000000..c1dd007b16 --- /dev/null +++ b/spec/ruby/core/process/getpgid_spec.rb @@ -0,0 +1,17 @@ +require_relative '../../spec_helper' + +describe "Process.getpgid" do + platform_is_not :windows do + it "coerces the argument to an Integer" do + Process.getpgid(mock_int(Process.pid)).should == Process.getpgrp + end + + it "returns the process group ID for the given process id" do + Process.getpgid(Process.pid).should == Process.getpgrp + end + + it "returns the process group ID for the calling process id when passed 0" do + Process.getpgid(0).should == Process.getpgrp + end + end +end diff --git a/spec/ruby/core/process/getpgrp_spec.rb b/spec/ruby/core/process/getpgrp_spec.rb new file mode 100644 index 0000000000..e1d1c5f92d --- /dev/null +++ b/spec/ruby/core/process/getpgrp_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' + +# see setpgrp_spec.rb + +describe "Process.getpgrp" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/process/getpriority_spec.rb b/spec/ruby/core/process/getpriority_spec.rb new file mode 100644 index 0000000000..a35e5956a5 --- /dev/null +++ b/spec/ruby/core/process/getpriority_spec.rb @@ -0,0 +1,23 @@ +require_relative '../../spec_helper' + +describe "Process.getpriority" do + platform_is_not :windows do + + it "coerces arguments to Integers" do + ret = Process.getpriority mock_int(Process::PRIO_PROCESS), mock_int(0) + ret.should be_kind_of(Integer) + end + + it "gets the scheduling priority for a specified process" do + Process.getpriority(Process::PRIO_PROCESS, 0).should be_kind_of(Integer) + end + + it "gets the scheduling priority for a specified process group" do + Process.getpriority(Process::PRIO_PGRP, 0).should be_kind_of(Integer) + end + + it "gets the scheduling priority for a specified user" do + Process.getpriority(Process::PRIO_USER, 0).should be_kind_of(Integer) + end + end +end diff --git a/spec/ruby/core/process/getrlimit_spec.rb b/spec/ruby/core/process/getrlimit_spec.rb new file mode 100644 index 0000000000..883b8fac48 --- /dev/null +++ b/spec/ruby/core/process/getrlimit_spec.rb @@ -0,0 +1,100 @@ +require_relative '../../spec_helper' + +platform_is :aix do + # In AIX, if getrlimit(2) is called multiple times with RLIMIT_DATA, + # the first call and the subsequent calls return slightly different + # values of rlim_cur, even if the process does nothing between + # the calls. This behavior causes some of the tests in this spec + # to fail, so call Process.getrlimit(:DATA) once and discard the result. + # Subsequent calls to Process.getrlimit(:DATA) should return + # a consistent value of rlim_cur. + Process.getrlimit(:DATA) +end + +describe "Process.getrlimit" do + platform_is_not :windows do + it "returns a two-element Array of Integers" do + result = Process.getrlimit Process::RLIMIT_CORE + result.size.should == 2 + result.first.should be_kind_of(Integer) + result.last.should be_kind_of(Integer) + end + + context "when passed an Object" do + before do + @resource = Process::RLIMIT_CORE + end + + it "calls #to_int to convert to an Integer" do + obj = mock("process getrlimit integer") + obj.should_receive(:to_int).and_return(@resource) + + Process.getrlimit(obj).should == Process.getrlimit(@resource) + end + + it "raises a TypeError if #to_int does not return an Integer" do + obj = mock("process getrlimit integer") + obj.should_receive(:to_int).and_return(nil) + + -> { Process.getrlimit(obj) }.should raise_error(TypeError) + end + end + + context "when passed a Symbol" do + it "coerces the short name into the full RLIMIT_ prefixed name" do + Process.constants.grep(/\ARLIMIT_/) do |fullname| + short = fullname[/\ARLIMIT_(.+)/, 1] + Process.getrlimit(short.to_sym).should == Process.getrlimit(Process.const_get(fullname)) + end + end + + it "raises ArgumentError when passed an unknown resource" do + -> { Process.getrlimit(:FOO) }.should raise_error(ArgumentError) + end + end + + context "when passed a String" do + it "coerces the short name into the full RLIMIT_ prefixed name" do + Process.constants.grep(/\ARLIMIT_/) do |fullname| + short = fullname[/\ARLIMIT_(.+)/, 1] + Process.getrlimit(short).should == Process.getrlimit(Process.const_get(fullname)) + end + end + + it "raises ArgumentError when passed an unknown resource" do + -> { Process.getrlimit("FOO") }.should raise_error(ArgumentError) + end + end + + context "when passed on Object" do + before do + @resource = Process::RLIMIT_CORE + end + + it "calls #to_str to convert to a String" do + obj = mock("process getrlimit string") + obj.should_receive(:to_str).and_return("CORE") + obj.should_not_receive(:to_int) + + Process.getrlimit(obj).should == Process.getrlimit(@resource) + end + + it "calls #to_int if #to_str does not return a String" do + obj = mock("process getrlimit string") + obj.should_receive(:to_str).and_return(nil) + obj.should_receive(:to_int).and_return(@resource) + + Process.getrlimit(obj).should == Process.getrlimit(@resource) + end + end + end + + platform_is :windows do + it "is not implemented" do + Process.respond_to?(:getrlimit).should be_false + -> do + Process.getrlimit(nil) + end.should raise_error NotImplementedError + end + end +end diff --git a/spec/ruby/core/process/gid/change_privilege_spec.rb b/spec/ruby/core/process/gid/change_privilege_spec.rb new file mode 100644 index 0000000000..4ada277514 --- /dev/null +++ b/spec/ruby/core/process/gid/change_privilege_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../../spec_helper' + +describe "Process::GID.change_privilege" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/process/gid/eid_spec.rb b/spec/ruby/core/process/gid/eid_spec.rb new file mode 100644 index 0000000000..3f2186bb6a --- /dev/null +++ b/spec/ruby/core/process/gid/eid_spec.rb @@ -0,0 +1,9 @@ +require_relative '../../../spec_helper' + +describe "Process::GID.eid" do + it "needs to be reviewed for spec completeness" +end + +describe "Process::GID.eid=" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/process/gid/grant_privilege_spec.rb b/spec/ruby/core/process/gid/grant_privilege_spec.rb new file mode 100644 index 0000000000..10da7f8a26 --- /dev/null +++ b/spec/ruby/core/process/gid/grant_privilege_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../../spec_helper' + +describe "Process::GID.grant_privilege" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/process/gid/re_exchange_spec.rb b/spec/ruby/core/process/gid/re_exchange_spec.rb new file mode 100644 index 0000000000..9642c81c5b --- /dev/null +++ b/spec/ruby/core/process/gid/re_exchange_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../../spec_helper' + +describe "Process::GID.re_exchange" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/process/gid/re_exchangeable_spec.rb b/spec/ruby/core/process/gid/re_exchangeable_spec.rb new file mode 100644 index 0000000000..1c38f903d5 --- /dev/null +++ b/spec/ruby/core/process/gid/re_exchangeable_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../../spec_helper' + +describe "Process::GID.re_exchangeable?" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/process/gid/rid_spec.rb b/spec/ruby/core/process/gid/rid_spec.rb new file mode 100644 index 0000000000..ad66c94e72 --- /dev/null +++ b/spec/ruby/core/process/gid/rid_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../../spec_helper' + +describe "Process::GID.rid" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/process/gid/sid_available_spec.rb b/spec/ruby/core/process/gid/sid_available_spec.rb new file mode 100644 index 0000000000..8d86b3e9d2 --- /dev/null +++ b/spec/ruby/core/process/gid/sid_available_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../../spec_helper' + +describe "Process::GID.sid_available?" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/process/gid/switch_spec.rb b/spec/ruby/core/process/gid/switch_spec.rb new file mode 100644 index 0000000000..b162f1e782 --- /dev/null +++ b/spec/ruby/core/process/gid/switch_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../../spec_helper' + +describe "Process::GID.switch" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/process/gid_spec.rb b/spec/ruby/core/process/gid_spec.rb new file mode 100644 index 0000000000..ca935ed520 --- /dev/null +++ b/spec/ruby/core/process/gid_spec.rb @@ -0,0 +1,22 @@ +require_relative '../../spec_helper' + +describe "Process.gid" do + platform_is_not :windows do + it "returns the correct gid for the user executing this process" do + current_gid_according_to_unix = `id -gr`.to_i + Process.gid.should == current_gid_according_to_unix + end + end + + it "also goes by Process::GID.rid" do + Process::GID.rid.should == Process.gid + end + + it "also goes by Process::Sys.getgid" do + Process::Sys.getgid.should == Process.gid + end +end + +describe "Process.gid=" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/process/groups_spec.rb b/spec/ruby/core/process/groups_spec.rb new file mode 100644 index 0000000000..33e0f9d7b3 --- /dev/null +++ b/spec/ruby/core/process/groups_spec.rb @@ -0,0 +1,67 @@ +require_relative '../../spec_helper' + +describe "Process.groups" do + platform_is_not :windows do + it "gets an Array of the gids of groups in the supplemental group access list" do + groups = `id -G`.scan(/\d+/).map { |i| i.to_i } + # Include the standard `id` command output. On macOS, GNU + # coreutils `id` is limited to NGROUPS_MAX groups, because of + # the backward compatibility of getgroups(2). + (groups |= `/usr/bin/id -G`.scan(/\d+/).map { |i| i.to_i }) rescue nil + gid = Process.gid + + expected = (groups.sort - [gid]).uniq.sort + actual = (Process.groups - [gid]).uniq.sort + actual.should == expected + end + end +end + +describe "Process.groups=" do + platform_is_not :windows, :android do + as_superuser do + it "sets the list of gids of groups in the supplemental group access list" do + groups = Process.groups + Process.groups = [] + Process.groups.should == [] + Process.groups = groups + Process.groups.sort.should == groups.sort + end + end + + as_user do + platform_is :aix do + it "sets the list of gids of groups in the supplemental group access list" do + # setgroups() is not part of the POSIX standard, + # so its behavior varies from OS to OS. AIX allows a non-root + # process to set the supplementary group IDs, as long as + # they are presently in its supplementary group IDs. + # The order of the following tests matters. + # After this process executes "Process.groups = []" + # it should no longer be able to set any supplementary + # group IDs, even if it originally belonged to them. + # It should only be able to set its primary group ID. + groups = Process.groups + Process.groups = groups + Process.groups.sort.should == groups.sort + Process.groups = [] + Process.groups.should == [] + Process.groups = [ Process.gid ] + Process.groups.should == [ Process.gid ] + supplementary = groups - [ Process.gid ] + if supplementary.length > 0 + -> { Process.groups = supplementary }.should raise_error(Errno::EPERM) + end + end + end + + platform_is_not :aix do + it "raises Errno::EPERM" do + -> { + Process.groups = [0] + }.should raise_error(Errno::EPERM) + end + end + end + end +end diff --git a/spec/ruby/core/process/initgroups_spec.rb b/spec/ruby/core/process/initgroups_spec.rb new file mode 100644 index 0000000000..ffc7f282b6 --- /dev/null +++ b/spec/ruby/core/process/initgroups_spec.rb @@ -0,0 +1,22 @@ +require_relative '../../spec_helper' + +describe "Process.initgroups" do + platform_is_not :windows, :android do + as_user do + it "initializes the supplemental group access list" do + name = `id -un`.strip + groups = Process.groups + gid = groups.max.to_i + 1 + augmented_groups = `id -G`.scan(/\d+/).map {|i| i.to_i} << gid + if Process.uid == 0 + Process.groups = [] + Process.initgroups(name, gid).sort.should == augmented_groups.sort + Process.groups.sort.should == augmented_groups.sort + Process.groups = groups + else + -> { Process.initgroups(name, gid) }.should raise_error(Errno::EPERM) + end + end + end + end +end diff --git a/spec/ruby/core/process/kill_spec.rb b/spec/ruby/core/process/kill_spec.rb new file mode 100644 index 0000000000..af9c9e6deb --- /dev/null +++ b/spec/ruby/core/process/kill_spec.rb @@ -0,0 +1,132 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' + +describe "Process.kill" do + ProcessSpecs.use_system_ruby(self) + + before :each do + @pid = Process.pid + end + + it "raises an ArgumentError for unknown signals" do + -> { Process.kill("FOO", @pid) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if passed a lowercase signal name" do + -> { Process.kill("term", @pid) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if signal is not an Integer or String" do + signal = mock("process kill signal") + signal.should_not_receive(:to_int) + + -> { Process.kill(signal, @pid) }.should raise_error(ArgumentError) + end + + it "raises Errno::ESRCH if the process does not exist" do + pid = Process.spawn(*ruby_exe, "-e", "sleep 10") + Process.kill("SIGKILL", pid) + Process.wait(pid) + -> { + Process.kill("SIGKILL", pid) + }.should raise_error(Errno::ESRCH) + end + + it "checks for existence and permissions to signal a process, but does not actually signal it, when using signal 0" do + Process.kill(0, @pid).should == 1 + end +end + +platform_is_not :windows do + describe "Process.kill" do + ProcessSpecs.use_system_ruby(self) + + before :each do + @sp = ProcessSpecs::Signalizer.new + end + + after :each do + @sp.cleanup + end + + it "accepts a Symbol as a signal name" do + Process.kill(:SIGTERM, @sp.pid) + @sp.result.should == "signaled" + end + + it "accepts a String as signal name" do + Process.kill("SIGTERM", @sp.pid) + @sp.result.should == "signaled" + end + + it "accepts a signal name without the 'SIG' prefix" do + Process.kill("TERM", @sp.pid) + @sp.result.should == "signaled" + end + + it "accepts a signal name with the 'SIG' prefix" do + Process.kill("SIGTERM", @sp.pid) + @sp.result.should == "signaled" + end + + it "accepts an Integer as a signal value" do + Process.kill(15, @sp.pid) + @sp.result.should == "signaled" + end + + it "calls #to_int to coerce the pid to an Integer" do + Process.kill("SIGTERM", mock_int(@sp.pid)) + @sp.result.should == "signaled" + end + end + + describe "Process.kill" do + ProcessSpecs.use_system_ruby(self) + + before :each do + @sp1 = ProcessSpecs::Signalizer.new + @sp2 = ProcessSpecs::Signalizer.new + end + + after :each do + @sp1.cleanup + @sp2.cleanup + end + + it "signals multiple processes" do + Process.kill("SIGTERM", @sp1.pid, @sp2.pid) + @sp1.result.should == "signaled" + @sp2.result.should == "signaled" + end + + it "returns the number of processes signaled" do + Process.kill("SIGTERM", @sp1.pid, @sp2.pid).should == 2 + end + end + + describe "Process.kill" do + after :each do + @sp.cleanup if @sp + end + + it "signals the process group if the PID is zero" do + @sp = ProcessSpecs::Signalizer.new "self" + @sp.result.should == "signaled" + end + + it "signals the process group if the signal number is negative" do + @sp = ProcessSpecs::Signalizer.new "group_numeric" + @sp.result.should == "signaled" + end + + it "signals the process group if the short signal name starts with a minus sign" do + @sp = ProcessSpecs::Signalizer.new "group_short_string" + @sp.result.should == "signaled" + end + + it "signals the process group if the full signal name starts with a minus sign" do + @sp = ProcessSpecs::Signalizer.new "group_full_string" + @sp.result.should == "signaled" + end + end +end diff --git a/spec/ruby/core/process/last_status_spec.rb b/spec/ruby/core/process/last_status_spec.rb new file mode 100644 index 0000000000..2372f2aae3 --- /dev/null +++ b/spec/ruby/core/process/last_status_spec.rb @@ -0,0 +1,18 @@ +require_relative '../../spec_helper' + +describe 'Process#last_status' do + it 'returns the status of the last executed child process in the current thread' do + pid = Process.wait Process.spawn("exit 0") + Process.last_status.pid.should == pid + end + + it 'returns nil if no child process has been ever executed in the current thread' do + Thread.new do + Process.last_status.should == nil + end.join + end + + it 'raises an ArgumentError if any arguments are provided' do + -> { Process.last_status(1) }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/process/maxgroups_spec.rb b/spec/ruby/core/process/maxgroups_spec.rb new file mode 100644 index 0000000000..2549a7a971 --- /dev/null +++ b/spec/ruby/core/process/maxgroups_spec.rb @@ -0,0 +1,19 @@ +require_relative '../../spec_helper' + +platform_is_not :windows do + describe "Process.maxgroups" do + it "returns the maximum number of gids allowed in the supplemental group access list" do + Process.maxgroups.should be_kind_of(Integer) + end + + it "sets the maximum number of gids allowed in the supplemental group access list" do + n = Process.maxgroups + begin + Process.maxgroups = n - 1 + Process.maxgroups.should == n - 1 + ensure + Process.maxgroups = n + end + end + end +end diff --git a/spec/ruby/core/process/pid_spec.rb b/spec/ruby/core/process/pid_spec.rb new file mode 100644 index 0000000000..2d008bc4b7 --- /dev/null +++ b/spec/ruby/core/process/pid_spec.rb @@ -0,0 +1,9 @@ +require_relative '../../spec_helper' + +describe "Process.pid" do + it "returns the process id of this process" do + pid = Process.pid + pid.should be_kind_of(Integer) + Process.pid.should == pid + end +end diff --git a/spec/ruby/core/process/ppid_spec.rb b/spec/ruby/core/process/ppid_spec.rb new file mode 100644 index 0000000000..47c32a8591 --- /dev/null +++ b/spec/ruby/core/process/ppid_spec.rb @@ -0,0 +1,9 @@ +require_relative '../../spec_helper' + +describe "Process.ppid" do + platform_is_not :windows do + it "returns the process id of the parent of this process" do + ruby_exe("puts Process.ppid").should == "#{Process.pid}\n" + end + end +end diff --git a/spec/ruby/core/process/set_proctitle_spec.rb b/spec/ruby/core/process/set_proctitle_spec.rb new file mode 100644 index 0000000000..d022f2021c --- /dev/null +++ b/spec/ruby/core/process/set_proctitle_spec.rb @@ -0,0 +1,23 @@ +require_relative '../../spec_helper' + +# Note that there's no way to get the current process title defined as a spec +# somewhere. Process.setproctitle explicitly does not change `$0` so the only +# way to get the process title is to shell out. +describe 'Process.setproctitle' do + platform_is :linux, :darwin do + before :each do + @old_title = $0 + end + + after :each do + Process.setproctitle(@old_title) + end + + it 'should set the process title' do + title = 'rubyspec-proctitle-test' + + Process.setproctitle(title).should == title + `ps -ocommand= -p#{$$}`.should include(title) + end + end +end diff --git a/spec/ruby/core/process/setpgid_spec.rb b/spec/ruby/core/process/setpgid_spec.rb new file mode 100644 index 0000000000..be724e9007 --- /dev/null +++ b/spec/ruby/core/process/setpgid_spec.rb @@ -0,0 +1,29 @@ +require_relative '../../spec_helper' + +describe "Process.setpgid" do + platform_is_not :windows do + # Must use fork as setpgid(2) gives EACCESS after execve() + it "sets the process group id of the specified process" do + rd, wr = IO.pipe + + pid = Process.fork do + wr.close + rd.read + rd.close + Process.exit! + end + + rd.close + + begin + Process.getpgid(pid).should == Process.getpgrp + Process.setpgid(mock_int(pid), mock_int(pid)).should == 0 + Process.getpgid(pid).should == pid + ensure + wr.write ' ' + wr.close + Process.wait pid + end + end + end +end diff --git a/spec/ruby/core/process/setpgrp_spec.rb b/spec/ruby/core/process/setpgrp_spec.rb new file mode 100644 index 0000000000..800668008d --- /dev/null +++ b/spec/ruby/core/process/setpgrp_spec.rb @@ -0,0 +1,37 @@ +require_relative '../../spec_helper' + +# TODO: put these in the right files. +describe "Process.setpgrp and Process.getpgrp" do + platform_is_not :windows do + it "sets and gets the process group ID of the calling process" do + # there are two synchronization points here: + # One for the child to let the parent know that it has finished + # setting its process group; + # and another for the parent to let the child know that it's ok to die. + read1, write1 = IO.pipe + read2, write2 = IO.pipe + pid = Process.fork do + read1.close + write2.close + Process.setpgrp + write1 << Process.getpgrp + write1.close + read2.read(1) + read2.close + Process.exit! + end + write1.close + read2.close + pgid = read1.read # wait for child to change process groups + read1.close + + begin + Process.getpgid(pid).should == pgid.to_i + ensure + write2 << "!" + write2.close + Process.wait pid + end + end + end +end diff --git a/spec/ruby/core/process/setpriority_spec.rb b/spec/ruby/core/process/setpriority_spec.rb new file mode 100644 index 0000000000..4d60973429 --- /dev/null +++ b/spec/ruby/core/process/setpriority_spec.rb @@ -0,0 +1,60 @@ +require_relative '../../spec_helper' + +describe "Process.setpriority" do + platform_is_not :windows do + it "sets the scheduling priority for a specified process" do + priority = Process.getpriority(Process::PRIO_PROCESS, 0) + + out = ruby_exe(fixture(__FILE__, "setpriority.rb"), args: "process") + out = out.lines.map { |l| Integer(l) } + pr = out[0] + out.should == [pr, 0, pr+1] + + Process.getpriority(Process::PRIO_PROCESS, 0).should == priority + end + + # Darwin and FreeBSD don't seem to handle these at all, getting all out of + # whack with either permission errors or just the wrong value + platform_is_not :darwin, :freebsd do + it "sets the scheduling priority for a specified process group" do + priority = Process.getpriority(Process::PRIO_PGRP, 0) + + out = ruby_exe(fixture(__FILE__, "setpriority.rb"), args: "group") + out = out.lines.map { |l| Integer(l) } + pr = out[0] + out.should == [pr, 0, pr+1] + + Process.getpriority(Process::PRIO_PGRP, 0).should == priority + end + end + + as_superuser do + guard -> { + prio = Process.getpriority(Process::PRIO_USER, 0) + # The nice value is a value in the range -20 to 19. + # This test tries to change the nice value to +-1, so it cannot run if prio == -20 || prio == 19. + if -20 < prio && prio < 19 + begin + # Check if we can lower the nice value or not. + # + # We are not always able to do it even as a root. + # Docker container is not always able to do it depending upon the configuration, + # which cannot know from the container itself. + Process.setpriority(Process::PRIO_USER, 0, prio - 1) + Process.setpriority(Process::PRIO_USER, 0, prio) + true + rescue Errno::EACCES + false + end + end + } do + it "sets the scheduling priority for a specified user" do + prio = Process.getpriority(Process::PRIO_USER, 0) + Process.setpriority(Process::PRIO_USER, 0, prio + 1).should == 0 + Process.getpriority(Process::PRIO_USER, 0).should == (prio + 1) + Process.setpriority(Process::PRIO_USER, 0, prio).should == 0 + end + end + end + end +end diff --git a/spec/ruby/core/process/setrlimit_spec.rb b/spec/ruby/core/process/setrlimit_spec.rb new file mode 100644 index 0000000000..ba8d1e04ca --- /dev/null +++ b/spec/ruby/core/process/setrlimit_spec.rb @@ -0,0 +1,237 @@ +require_relative '../../spec_helper' + +describe "Process.setrlimit" do + platform_is_not :windows do + context "when passed an Object" do + before do + @resource = Process::RLIMIT_CORE + @limit, @max = Process.getrlimit @resource + end + + it "calls #to_int to convert resource to an Integer" do + Process.setrlimit(mock_int(@resource), @limit, @max).should be_nil + end + + it "raises a TypeError if #to_int for resource does not return an Integer" do + obj = mock("process getrlimit integer") + obj.should_receive(:to_int).and_return(nil) + + -> { Process.setrlimit(obj, @limit, @max) }.should raise_error(TypeError) + end + + it "calls #to_int to convert the soft limit to an Integer" do + Process.setrlimit(@resource, mock_int(@limit), @max).should be_nil + end + + it "raises a TypeError if #to_int for resource does not return an Integer" do + obj = mock("process getrlimit integer") + obj.should_receive(:to_int).and_return(nil) + + -> { Process.setrlimit(@resource, obj, @max) }.should raise_error(TypeError) + end + + it "calls #to_int to convert the hard limit to an Integer" do + Process.setrlimit(@resource, @limit, mock_int(@max)).should be_nil + end + + it "raises a TypeError if #to_int for resource does not return an Integer" do + obj = mock("process getrlimit integer") + obj.should_receive(:to_int).and_return(nil) + + -> { Process.setrlimit(@resource, @limit, obj) }.should raise_error(TypeError) + end + end + + context "when passed a Symbol" do + platform_is_not :openbsd do + it "coerces :AS into RLIMIT_AS" do + Process.setrlimit(:AS, *Process.getrlimit(Process::RLIMIT_AS)).should be_nil + end + end + + it "coerces :CORE into RLIMIT_CORE" do + Process.setrlimit(:CORE, *Process.getrlimit(Process::RLIMIT_CORE)).should be_nil + end + + it "coerces :CPU into RLIMIT_CPU" do + Process.setrlimit(:CPU, *Process.getrlimit(Process::RLIMIT_CPU)).should be_nil + end + + it "coerces :DATA into RLIMIT_DATA" do + Process.setrlimit(:DATA, *Process.getrlimit(Process::RLIMIT_DATA)).should be_nil + end + + it "coerces :FSIZE into RLIMIT_FSIZE" do + Process.setrlimit(:FSIZE, *Process.getrlimit(Process::RLIMIT_FSIZE)).should be_nil + end + + it "coerces :NOFILE into RLIMIT_NOFILE" do + Process.setrlimit(:NOFILE, *Process.getrlimit(Process::RLIMIT_NOFILE)).should be_nil + end + + it "coerces :STACK into RLIMIT_STACK" do + Process.setrlimit(:STACK, *Process.getrlimit(Process::RLIMIT_STACK)).should be_nil + end + + platform_is_not :aix do + it "coerces :MEMLOCK into RLIMIT_MEMLOCK" do + Process.setrlimit(:MEMLOCK, *Process.getrlimit(Process::RLIMIT_MEMLOCK)).should be_nil + end + end + + it "coerces :NPROC into RLIMIT_NPROC" do + Process.setrlimit(:NPROC, *Process.getrlimit(Process::RLIMIT_NPROC)).should be_nil + end + + it "coerces :RSS into RLIMIT_RSS" do + Process.setrlimit(:RSS, *Process.getrlimit(Process::RLIMIT_RSS)).should be_nil + end + + platform_is :netbsd, :freebsd do + it "coerces :SBSIZE into RLIMIT_SBSIZE" do + Process.setrlimit(:SBSIZE, *Process.getrlimit(Process::RLIMIT_SBSIZE)).should be_nil + end + end + + platform_is :linux do + it "coerces :RTPRIO into RLIMIT_RTPRIO" do + Process.setrlimit(:RTPRIO, *Process.getrlimit(Process::RLIMIT_RTPRIO)).should be_nil + end + + guard -> { defined?(Process::RLIMIT_RTTIME) } do + it "coerces :RTTIME into RLIMIT_RTTIME" do + Process.setrlimit(:RTTIME, *Process.getrlimit(Process::RLIMIT_RTTIME)).should be_nil + end + end + + it "coerces :SIGPENDING into RLIMIT_SIGPENDING" do + Process.setrlimit(:SIGPENDING, *Process.getrlimit(Process::RLIMIT_SIGPENDING)).should be_nil + end + + it "coerces :MSGQUEUE into RLIMIT_MSGQUEUE" do + Process.setrlimit(:MSGQUEUE, *Process.getrlimit(Process::RLIMIT_MSGQUEUE)).should be_nil + end + + it "coerces :NICE into RLIMIT_NICE" do + Process.setrlimit(:NICE, *Process.getrlimit(Process::RLIMIT_NICE)).should be_nil + end + end + + it "raises ArgumentError when passed an unknown resource" do + -> { Process.setrlimit(:FOO, 1, 1) }.should raise_error(ArgumentError) + end + end + + context "when passed a String" do + platform_is_not :openbsd do + it "coerces 'AS' into RLIMIT_AS" do + Process.setrlimit("AS", *Process.getrlimit(Process::RLIMIT_AS)).should be_nil + end + end + + it "coerces 'CORE' into RLIMIT_CORE" do + Process.setrlimit("CORE", *Process.getrlimit(Process::RLIMIT_CORE)).should be_nil + end + + it "coerces 'CPU' into RLIMIT_CPU" do + Process.setrlimit("CPU", *Process.getrlimit(Process::RLIMIT_CPU)).should be_nil + end + + it "coerces 'DATA' into RLIMIT_DATA" do + Process.setrlimit("DATA", *Process.getrlimit(Process::RLIMIT_DATA)).should be_nil + end + + it "coerces 'FSIZE' into RLIMIT_FSIZE" do + Process.setrlimit("FSIZE", *Process.getrlimit(Process::RLIMIT_FSIZE)).should be_nil + end + + it "coerces 'NOFILE' into RLIMIT_NOFILE" do + Process.setrlimit("NOFILE", *Process.getrlimit(Process::RLIMIT_NOFILE)).should be_nil + end + + it "coerces 'STACK' into RLIMIT_STACK" do + Process.setrlimit("STACK", *Process.getrlimit(Process::RLIMIT_STACK)).should be_nil + end + + platform_is_not :aix do + it "coerces 'MEMLOCK' into RLIMIT_MEMLOCK" do + Process.setrlimit("MEMLOCK", *Process.getrlimit(Process::RLIMIT_MEMLOCK)).should be_nil + end + end + + it "coerces 'NPROC' into RLIMIT_NPROC" do + Process.setrlimit("NPROC", *Process.getrlimit(Process::RLIMIT_NPROC)).should be_nil + end + + it "coerces 'RSS' into RLIMIT_RSS" do + Process.setrlimit("RSS", *Process.getrlimit(Process::RLIMIT_RSS)).should be_nil + end + + platform_is :netbsd, :freebsd do + it "coerces 'SBSIZE' into RLIMIT_SBSIZE" do + Process.setrlimit("SBSIZE", *Process.getrlimit(Process::RLIMIT_SBSIZE)).should be_nil + end + end + + platform_is :linux do + it "coerces 'RTPRIO' into RLIMIT_RTPRIO" do + Process.setrlimit("RTPRIO", *Process.getrlimit(Process::RLIMIT_RTPRIO)).should be_nil + end + + guard -> { defined?(Process::RLIMIT_RTTIME) } do + it "coerces 'RTTIME' into RLIMIT_RTTIME" do + Process.setrlimit("RTTIME", *Process.getrlimit(Process::RLIMIT_RTTIME)).should be_nil + end + end + + it "coerces 'SIGPENDING' into RLIMIT_SIGPENDING" do + Process.setrlimit("SIGPENDING", *Process.getrlimit(Process::RLIMIT_SIGPENDING)).should be_nil + end + + it "coerces 'MSGQUEUE' into RLIMIT_MSGQUEUE" do + Process.setrlimit("MSGQUEUE", *Process.getrlimit(Process::RLIMIT_MSGQUEUE)).should be_nil + end + + it "coerces 'NICE' into RLIMIT_NICE" do + Process.setrlimit("NICE", *Process.getrlimit(Process::RLIMIT_NICE)).should be_nil + end + end + + it "raises ArgumentError when passed an unknown resource" do + -> { Process.setrlimit("FOO", 1, 1) }.should raise_error(ArgumentError) + end + end + + context "when passed on Object" do + before do + @resource = Process::RLIMIT_CORE + @limit, @max = Process.getrlimit @resource + end + + it "calls #to_str to convert to a String" do + obj = mock("process getrlimit string") + obj.should_receive(:to_str).and_return("CORE") + obj.should_not_receive(:to_int) + + Process.setrlimit(obj, @limit, @max).should be_nil + end + + it "calls #to_int if #to_str does not return a String" do + obj = mock("process getrlimit string") + obj.should_receive(:to_str).and_return(nil) + obj.should_receive(:to_int).and_return(@resource) + + Process.setrlimit(obj, @limit, @max).should be_nil + end + end + end + + platform_is :windows do + it "is not implemented" do + Process.respond_to?(:setrlimit).should be_false + -> do + Process.setrlimit(nil, nil) + end.should raise_error NotImplementedError + end + end +end diff --git a/spec/ruby/core/process/setsid_spec.rb b/spec/ruby/core/process/setsid_spec.rb new file mode 100644 index 0000000000..c83f912066 --- /dev/null +++ b/spec/ruby/core/process/setsid_spec.rb @@ -0,0 +1,16 @@ +require_relative '../../spec_helper' + +describe "Process.setsid" do + platform_is_not :windows do + it "establishes this process as a new session and process group leader" do + sid = Process.getsid + + out = ruby_exe("p Process.getsid; p Process.setsid; p Process.getsid").lines + out[0].should == "#{sid}\n" + out[1].should == out[2] + out[2].should_not == "#{sid}\n" + + sid.should == Process.getsid + end + end +end diff --git a/spec/ruby/core/process/spawn_spec.rb b/spec/ruby/core/process/spawn_spec.rb new file mode 100644 index 0000000000..283a7f033d --- /dev/null +++ b/spec/ruby/core/process/spawn_spec.rb @@ -0,0 +1,756 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' + +newline = "\n" +platform_is :windows do + newline = "\r\n" +end + +describe :process_spawn_does_not_close_std_streams, shared: true do + it "does not close STDIN" do + code = "puts STDIN.read" + cmd = "Process.wait Process.spawn(#{ruby_cmd(code).inspect}, #{@options.inspect})" + ruby_exe(cmd, args: "< #{fixture(__FILE__, "in.txt")} > #{@name}") + File.binread(@name).should == %[stdin#{newline}] + end + + it "does not close STDOUT" do + code = "STDOUT.puts 'hello'" + cmd = "Process.wait Process.spawn(#{ruby_cmd(code).inspect}, #{@options.inspect})" + ruby_exe(cmd, args: "> #{@name}") + File.binread(@name).should == "hello#{newline}" + end + + it "does not close STDERR" do + code = "STDERR.puts 'hello'" + cmd = "Process.wait Process.spawn(#{ruby_cmd(code).inspect}, #{@options.inspect})" + ruby_exe(cmd, args: "2> #{@name}") + File.binread(@name).should =~ /hello#{newline}/ + end +end + +describe "Process.spawn" do + ProcessSpecs.use_system_ruby(self) + + before :each do + @name = tmp("process_spawn.txt") + @var = "$FOO" + platform_is :windows do + @var = "%FOO%" + end + end + + after :each do + rm_r @name + end + + it "executes the given command" do + -> { Process.wait Process.spawn("echo spawn") }.should output_to_fd("spawn\n") + end + + it "returns the process ID of the new process as an Integer" do + pid = Process.spawn(*ruby_exe, "-e", "exit") + Process.wait pid + pid.should be_an_instance_of(Integer) + end + + it "returns immediately" do + start = Time.now + pid = Process.spawn(*ruby_exe, "-e", "sleep 10") + (Time.now - start).should < 5 + Process.kill :KILL, pid + Process.wait pid + end + + # argv processing + + describe "with a single argument" do + platform_is_not :windows do + it "subjects the specified command to shell expansion" do + -> { Process.wait Process.spawn("echo *") }.should_not output_to_fd("*\n") + end + + it "creates an argument array with shell parsing semantics for whitespace" do + -> { Process.wait Process.spawn("echo a b c d") }.should output_to_fd("a b c d\n") + end + end + + platform_is :windows do + # There is no shell expansion on Windows + it "does not subject the specified command to shell expansion on Windows" do + -> { Process.wait Process.spawn("echo *") }.should output_to_fd("*\n") + end + + it "does not create an argument array with shell parsing semantics for whitespace on Windows" do + -> { Process.wait Process.spawn("echo a b c d") }.should output_to_fd("a b c d\n") + end + end + + it "calls #to_str to convert the argument to a String" do + o = mock("to_str") + o.should_receive(:to_str).and_return("echo foo") + -> { Process.wait Process.spawn(o) }.should output_to_fd("foo\n") + end + + it "raises an ArgumentError if the command includes a null byte" do + -> { Process.spawn "\000" }.should raise_error(ArgumentError) + end + + it "raises a TypeError if the argument does not respond to #to_str" do + -> { Process.spawn :echo }.should raise_error(TypeError) + end + end + + describe "with multiple arguments" do + it "does not subject the arguments to shell expansion" do + -> { Process.wait Process.spawn("echo", "*") }.should output_to_fd("*\n") + end + + it "preserves whitespace in passed arguments" do + out = "a b c d\n" + platform_is :windows do + # The echo command on Windows takes quotes literally + out = "\"a b c d\"\n" + end + -> { Process.wait Process.spawn("echo", "a b c d") }.should output_to_fd(out) + end + + it "calls #to_str to convert the arguments to Strings" do + o = mock("to_str") + o.should_receive(:to_str).and_return("foo") + -> { Process.wait Process.spawn("echo", o) }.should output_to_fd("foo\n") + end + + it "raises an ArgumentError if an argument includes a null byte" do + -> { Process.spawn "echo", "\000" }.should raise_error(ArgumentError) + end + + it "raises a TypeError if an argument does not respond to #to_str" do + -> { Process.spawn "echo", :foo }.should raise_error(TypeError) + end + end + + describe "with a command array" do + it "uses the first element as the command name and the second as the argv[0] value" do + platform_is_not :windows do + -> { Process.wait Process.spawn(["/bin/sh", "argv_zero"], "-c", "echo $0") }.should output_to_fd("argv_zero\n") + end + platform_is :windows do + -> { Process.wait Process.spawn(["cmd.exe", "/C"], "/C", "echo", "argv_zero") }.should output_to_fd("argv_zero\n") + end + end + + it "does not subject the arguments to shell expansion" do + -> { Process.wait Process.spawn(["echo", "echo"], "*") }.should output_to_fd("*\n") + end + + it "preserves whitespace in passed arguments" do + out = "a b c d\n" + platform_is :windows do + # The echo command on Windows takes quotes literally + out = "\"a b c d\"\n" + end + -> { Process.wait Process.spawn(["echo", "echo"], "a b c d") }.should output_to_fd(out) + end + + it "calls #to_ary to convert the argument to an Array" do + o = mock("to_ary") + platform_is_not :windows do + o.should_receive(:to_ary).and_return(["/bin/sh", "argv_zero"]) + -> { Process.wait Process.spawn(o, "-c", "echo $0") }.should output_to_fd("argv_zero\n") + end + platform_is :windows do + o.should_receive(:to_ary).and_return(["cmd.exe", "/C"]) + -> { Process.wait Process.spawn(o, "/C", "echo", "argv_zero") }.should output_to_fd("argv_zero\n") + end + end + + it "calls #to_str to convert the first element to a String" do + o = mock("to_str") + o.should_receive(:to_str).and_return("echo") + -> { Process.wait Process.spawn([o, "echo"], "foo") }.should output_to_fd("foo\n") + end + + it "calls #to_str to convert the second element to a String" do + o = mock("to_str") + o.should_receive(:to_str).and_return("echo") + -> { Process.wait Process.spawn(["echo", o], "foo") }.should output_to_fd("foo\n") + end + + it "raises an ArgumentError if the Array does not have exactly two elements" do + -> { Process.spawn([]) }.should raise_error(ArgumentError) + -> { Process.spawn([:a]) }.should raise_error(ArgumentError) + -> { Process.spawn([:a, :b, :c]) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if the Strings in the Array include a null byte" do + -> { Process.spawn ["\000", "echo"] }.should raise_error(ArgumentError) + -> { Process.spawn ["echo", "\000"] }.should raise_error(ArgumentError) + end + + it "raises a TypeError if an element in the Array does not respond to #to_str" do + -> { Process.spawn ["echo", :echo] }.should raise_error(TypeError) + -> { Process.spawn [:echo, "echo"] }.should raise_error(TypeError) + end + end + + # env handling + + after :each do + ENV.delete("FOO") + end + + it "sets environment variables in the child environment" do + Process.wait Process.spawn({"FOO" => "BAR"}, "echo #{@var}>#{@name}") + File.read(@name).should == "BAR\n" + end + + it "unsets environment variables whose value is nil" do + ENV["FOO"] = "BAR" + -> do + Process.wait Process.spawn({"FOO" => nil}, ruby_cmd("p ENV['FOO']")) + end.should output_to_fd("nil\n") + end + + platform_is_not :windows do + it "uses the passed env['PATH'] to search the executable" do + dir = tmp("spawn_path_dir") + mkdir_p dir + begin + exe = 'process-spawn-executable-in-path' + path = "#{dir}/#{exe}" + File.write(path, "#!/bin/sh\necho $1") + File.chmod(0755, path) + + env = { "PATH" => "#{dir}#{File::PATH_SEPARATOR}#{ENV['PATH']}" } + Process.wait Process.spawn(env, exe, 'OK', out: @name) + $?.should.success? + File.read(@name).should == "OK\n" + ensure + rm_r dir + end + end + end + + it "calls #to_hash to convert the environment" do + o = mock("to_hash") + o.should_receive(:to_hash).and_return({"FOO" => "BAR"}) + Process.wait Process.spawn(o, "echo #{@var}>#{@name}") + File.read(@name).should == "BAR\n" + end + + it "calls #to_str to convert the environment keys" do + o = mock("to_str") + o.should_receive(:to_str).and_return("FOO") + Process.wait Process.spawn({o => "BAR"}, "echo #{@var}>#{@name}") + File.read(@name).should == "BAR\n" + end + + it "calls #to_str to convert the environment values" do + o = mock("to_str") + o.should_receive(:to_str).and_return("BAR") + Process.wait Process.spawn({"FOO" => o}, "echo #{@var}>#{@name}") + File.read(@name).should == "BAR\n" + end + + it "raises an ArgumentError if an environment key includes an equals sign" do + -> do + Process.spawn({"FOO=" => "BAR"}, "echo #{@var}>#{@name}") + end.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if an environment key includes a null byte" do + -> do + Process.spawn({"\000" => "BAR"}, "echo #{@var}>#{@name}") + end.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if an environment value includes a null byte" do + -> do + Process.spawn({"FOO" => "\000"}, "echo #{@var}>#{@name}") + end.should raise_error(ArgumentError) + end + + # :unsetenv_others + + before :each do + @minimal_env = { + "PATH" => ENV["PATH"], + "HOME" => ENV["HOME"] + } + @common_env_spawn_args = [@minimal_env, "echo #{@var}>#{@name}"] + end + + platform_is_not :windows do + it "unsets other environment variables when given a true :unsetenv_others option" do + ENV["FOO"] = "BAR" + Process.wait Process.spawn(*@common_env_spawn_args, unsetenv_others: true) + $?.success?.should be_true + File.read(@name).should == "\n" + end + end + + it "does not unset other environment variables when given a false :unsetenv_others option" do + ENV["FOO"] = "BAR" + Process.wait Process.spawn(*@common_env_spawn_args, unsetenv_others: false) + $?.success?.should be_true + File.read(@name).should == "BAR\n" + end + + platform_is_not :windows do + it "does not unset environment variables included in the environment hash" do + env = @minimal_env.merge({"FOO" => "BAR"}) + Process.wait Process.spawn(env, "echo #{@var}>#{@name}", unsetenv_others: true) + $?.success?.should be_true + File.read(@name).should == "BAR\n" + end + end + + # :pgroup + + platform_is_not :windows do + it "joins the current process group by default" do + -> do + Process.wait Process.spawn(ruby_cmd("print Process.getpgid(Process.pid)")) + end.should output_to_fd(Process.getpgid(Process.pid).to_s) + end + + it "joins the current process if pgroup: false" do + -> do + Process.wait Process.spawn(ruby_cmd("print Process.getpgid(Process.pid)"), pgroup: false) + end.should output_to_fd(Process.getpgid(Process.pid).to_s) + end + + it "joins the current process if pgroup: nil" do + -> do + Process.wait Process.spawn(ruby_cmd("print Process.getpgid(Process.pid)"), pgroup: nil) + end.should output_to_fd(Process.getpgid(Process.pid).to_s) + end + + it "joins a new process group if pgroup: true" do + process = -> do + Process.wait Process.spawn(ruby_cmd("print Process.getpgid(Process.pid)"), pgroup: true) + end + + process.should_not output_to_fd(Process.getpgid(Process.pid).to_s) + process.should output_to_fd(/\d+/) + end + + it "joins a new process group if pgroup: 0" do + process = -> do + Process.wait Process.spawn(ruby_cmd("print Process.getpgid(Process.pid)"), pgroup: 0) + end + + process.should_not output_to_fd(Process.getpgid(Process.pid).to_s) + process.should output_to_fd(/\d+/) + end + + it "joins the specified process group if pgroup: pgid" do + pgid = Process.getpgid(Process.pid) + # The process group is not available on all platforms. + # See "man proc" - /proc/[pid]/stat - (5) pgrp + # In Travis aarch64 environment, the value is 0. + # + # $ cat /proc/[pid]/stat + # 19179 (ruby) S 19160 0 0 ... + unless pgid.zero? + -> do + Process.wait Process.spawn(ruby_cmd("print Process.getpgid(Process.pid)"), pgroup: pgid) + end.should output_to_fd(pgid.to_s) + else + skip "The process group is not available." + end + end + + it "raises an ArgumentError if given a negative :pgroup option" do + -> { Process.spawn("echo", pgroup: -1) }.should raise_error(ArgumentError) + end + + it "raises a TypeError if given a symbol as :pgroup option" do + -> { Process.spawn("echo", pgroup: :true) }.should raise_error(TypeError) + end + end + + platform_is :windows do + it "raises an ArgumentError if given :pgroup option" do + -> { Process.spawn("echo", pgroup: false) }.should raise_error(ArgumentError) + end + end + + # :rlimit_core + # :rlimit_cpu + # :rlimit_data + + # :chdir + + it "uses the current working directory as its working directory" do + -> do + Process.wait Process.spawn(ruby_cmd("print Dir.pwd")) + end.should output_to_fd(Dir.pwd) + end + + describe "when passed :chdir" do + before do + @dir = tmp("spawn_chdir", false) + Dir.mkdir @dir + end + + after do + rm_r @dir + end + + it "changes to the directory passed for :chdir" do + -> do + Process.wait Process.spawn(ruby_cmd("print Dir.pwd"), chdir: @dir) + end.should output_to_fd(@dir) + end + + it "calls #to_path to convert the :chdir value" do + dir = mock("spawn_to_path") + dir.should_receive(:to_path).and_return(@dir) + + -> do + Process.wait Process.spawn(ruby_cmd("print Dir.pwd"), chdir: dir) + end.should output_to_fd(@dir) + end + end + + # chdir + + platform_is :linux do + describe "inside Dir.chdir" do + def child_pids(pid) + `pgrep -P #{pid}`.lines.map { |child| Integer(child) } + end + + it "does not create extra process without chdir" do + pid = Process.spawn("sleep 10") + begin + child_pids(pid).size.should == 0 + ensure + Process.kill("TERM", pid) + Process.wait(pid) + end + end + + it "kills extra chdir processes" do + pid = nil + Dir.chdir("/") do + pid = Process.spawn("sleep 10") + end + + children = child_pids(pid) + children.size.should <= 1 + + Process.kill("TERM", pid) + Process.wait(pid) + + if children.size > 0 + # wait a bit for children to die + sleep(1) + + children.each do |child| + -> do + Process.kill("TERM", child) + end.should raise_error(Errno::ESRCH) + end + end + end + end + end + + # :umask + + it "uses the current umask by default" do + -> do + Process.wait Process.spawn(ruby_cmd("print File.umask")) + end.should output_to_fd(File.umask.to_s) + end + + platform_is_not :windows do + it "sets the umask if given the :umask option" do + -> do + Process.wait Process.spawn(ruby_cmd("print File.umask"), umask: 146) + end.should output_to_fd("146") + end + end + + # redirection + + it 'redirects to the wrapped IO using wrapped_io.to_io if out: wrapped_io' do + File.open(@name, 'w') do |file| + -> do + wrapped_io = mock('wrapped IO') + wrapped_io.should_receive(:to_io).and_return(file) + Process.wait Process.spawn('echo Hello World', out: wrapped_io) + end.should output_to_fd("Hello World\n", file) + end + end + + it "redirects STDOUT to the given file descriptor if out: Integer" do + File.open(@name, 'w') do |file| + -> do + Process.wait Process.spawn("echo glark", out: file.fileno) + end.should output_to_fd("glark\n", file) + end + end + + it "redirects STDOUT to the given file if out: IO" do + File.open(@name, 'w') do |file| + -> do + Process.wait Process.spawn("echo glark", out: file) + end.should output_to_fd("glark\n", file) + end + end + + it "redirects STDOUT to the given file if out: String" do + Process.wait Process.spawn("echo glark", out: @name) + File.read(@name).should == "glark\n" + end + + it "redirects STDOUT to the given file if out: [String name, String mode]" do + Process.wait Process.spawn("echo glark", out: [@name, 'w']) + File.read(@name).should == "glark\n" + end + + it "redirects STDERR to the given file descriptor if err: Integer" do + File.open(@name, 'w') do |file| + -> do + Process.wait Process.spawn("echo glark>&2", err: file.fileno) + end.should output_to_fd("glark\n", file) + end + end + + it "redirects STDERR to the given file descriptor if err: IO" do + File.open(@name, 'w') do |file| + -> do + Process.wait Process.spawn("echo glark>&2", err: file) + end.should output_to_fd("glark\n", file) + end + end + + it "redirects STDERR to the given file if err: String" do + Process.wait Process.spawn("echo glark>&2", err: @name) + File.read(@name).should == "glark\n" + end + + it "redirects STDERR to child STDOUT if :err => [:child, :out]" do + File.open(@name, 'w') do |file| + -> do + Process.wait Process.spawn("echo glark>&2", :out => file, :err => [:child, :out]) + end.should output_to_fd("glark\n", file) + end + end + + it "redirects both STDERR and STDOUT to the given file descriptor" do + File.open(@name, 'w') do |file| + -> do + Process.wait Process.spawn(ruby_cmd("print(:glark); STDOUT.flush; STDERR.print(:bang)"), + [:out, :err] => file.fileno) + end.should output_to_fd("glarkbang", file) + end + end + + it "redirects both STDERR and STDOUT to the given IO" do + File.open(@name, 'w') do |file| + -> do + Process.wait Process.spawn(ruby_cmd("print(:glark); STDOUT.flush; STDERR.print(:bang)"), + [:out, :err] => file) + end.should output_to_fd("glarkbang", file) + end + end + + it "redirects both STDERR and STDOUT at the time to the given name" do + touch @name + Process.wait Process.spawn(ruby_cmd("print(:glark); STDOUT.flush; STDERR.print(:bang)"), [:out, :err] => @name) + File.read(@name).should == "glarkbang" + end + + platform_is_not :windows, :android do + it "closes STDERR in the child if :err => :close" do + File.open(@name, 'w') do |file| + -> do + code = "begin; STDOUT.puts 'out'; STDERR.puts 'hello'; rescue => e; puts 'rescued'; end" + Process.wait Process.spawn(ruby_cmd(code), :out => file, :err => :close) + end.should output_to_fd("out\nrescued\n", file) + end + end + end + + platform_is_not :windows do + it "redirects non-default file descriptor to itself" do + File.open(@name, 'w') do |file| + -> do + Process.wait Process.spawn( + ruby_cmd("f = IO.new(#{file.fileno}, 'w'); f.print(:bang); f.flush"), file.fileno => file.fileno) + end.should output_to_fd("bang", file) + end + end + end + + it "redirects default file descriptor to itself" do + -> do + Process.wait Process.spawn( + ruby_cmd("f = IO.new(#{STDOUT.fileno}, 'w'); f.print(:bang); f.flush"), STDOUT.fileno => STDOUT.fileno) + end.should output_to_fd("bang", STDOUT) + end + + # :close_others + + platform_is_not :windows do + context "defaults :close_others to" do + it "false" do + IO.pipe do |r, w| + w.close_on_exec = false + code = "io = IO.new(#{w.fileno}); io.puts('inherited'); io.close" + pid = Process.spawn(ruby_cmd(code)) + w.close + Process.wait(pid) + r.read.should == "inherited\n" + end + end + end + + context "when passed close_others: true" do + before :each do + @options = { close_others: true } + end + + it "closes file descriptors >= 3 in the child process even if fds are set close_on_exec=false" do + touch @name + IO.pipe do |r, w| + r.close_on_exec = false + w.close_on_exec = false + + begin + pid = Process.spawn(ruby_cmd("while File.exist? '#{@name}'; sleep 0.1; end"), @options) + w.close + r.read(1).should == nil + ensure + rm_r @name + Process.wait(pid) if pid + end + end + end + + it_should_behave_like :process_spawn_does_not_close_std_streams + end + + context "when passed close_others: false" do + before :each do + @options = { close_others: false } + end + + it "closes file descriptors >= 3 in the child process because they are set close_on_exec by default" do + touch @name + IO.pipe do |r, w| + begin + pid = Process.spawn(ruby_cmd("while File.exist? '#{@name}'; sleep 0.1; end"), @options) + w.close + r.read(1).should == nil + ensure + rm_r @name + Process.wait(pid) if pid + end + end + end + + it "does not close file descriptors >= 3 in the child process if fds are set close_on_exec=false" do + IO.pipe do |r, w| + r.close_on_exec = false + w.close_on_exec = false + + code = "fd = IO.for_fd(#{w.fileno}); fd.autoclose = false; fd.write 'abc'; fd.close" + pid = Process.spawn(ruby_cmd(code), @options) + begin + w.close + r.read.should == 'abc' + ensure + Process.wait(pid) + end + end + end + + it_should_behave_like :process_spawn_does_not_close_std_streams + end + end + + # error handling + + it "raises an ArgumentError if passed no command arguments" do + -> { Process.spawn }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if passed env or options but no command arguments" do + -> { Process.spawn({}) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if passed env and options but no command arguments" do + -> { Process.spawn({}, {}) }.should raise_error(ArgumentError) + end + + it "raises an Errno::ENOENT for an empty string" do + -> { Process.spawn "" }.should raise_error(Errno::ENOENT) + end + + it "raises an Errno::ENOENT if the command does not exist" do + -> { Process.spawn "nonesuch" }.should raise_error(Errno::ENOENT) + end + + unless File.executable?(__FILE__) # Some FS (e.g. vboxfs) locate all files executable + platform_is_not :windows do + it "raises an Errno::EACCES when the file does not have execute permissions" do + -> { Process.spawn __FILE__ }.should raise_error(Errno::EACCES) + end + end + + platform_is :windows do + it "raises Errno::EACCES or Errno::ENOEXEC when the file is not an executable file" do + -> { Process.spawn __FILE__ }.should raise_error(SystemCallError) { |e| + [Errno::EACCES, Errno::ENOEXEC].should include(e.class) + } + end + end + end + + it "raises an Errno::EACCES or Errno::EISDIR when passed a directory" do + -> { Process.spawn __dir__ }.should raise_error(SystemCallError) { |e| + [Errno::EACCES, Errno::EISDIR].should include(e.class) + } + end + + it "raises an ArgumentError when passed a string key in options" do + -> { Process.spawn("echo", "chdir" => Dir.pwd) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when passed an unknown option key" do + -> { Process.spawn("echo", nonesuch: :foo) }.should raise_error(ArgumentError) + end + + platform_is_not :windows, :aix do + describe "with Integer option keys" do + before :each do + @name = tmp("spawn_fd_map.txt") + @io = new_io @name, "w+" + @io.sync = true + end + + after :each do + @io.close unless @io.closed? + rm_r @name + end + + it "maps the key to a file descriptor in the child that inherits the file descriptor from the parent specified by the value" do + File.open(__FILE__, "r") do |f| + child_fd = f.fileno + args = ruby_cmd(fixture(__FILE__, "map_fd.rb"), args: [child_fd.to_s]) + pid = Process.spawn(*args, { child_fd => @io }) + Process.waitpid pid + @io.rewind + + @io.read.should == "writing to fd: #{child_fd}" + end + end + end + end +end diff --git a/spec/ruby/core/process/status/bit_and_spec.rb b/spec/ruby/core/process/status/bit_and_spec.rb new file mode 100644 index 0000000000..a805364629 --- /dev/null +++ b/spec/ruby/core/process/status/bit_and_spec.rb @@ -0,0 +1,38 @@ +require_relative '../../../spec_helper' + +ruby_version_is ""..."4.0" do + + describe "Process::Status#&" do + it "returns a bitwise and of the integer status of an exited child" do + suppress_warning do + ruby_exe("exit(29)", exit_status: 29) + ($? & 0).should == 0 + ($? & $?.to_i).should == $?.to_i + + # Actual value is implementation specific + platform_is :linux do + # 29 == 0b11101 + ($? & 0b1011100000000).should == 0b1010100000000 + end + end + end + + ruby_version_is "3.3"..."4.0" do + it "raises an ArgumentError if mask is negative" do + suppress_warning do + ruby_exe("exit(0)") + -> { + $? & -1 + }.should raise_error(ArgumentError, 'negative mask value: -1') + end + end + + it "shows a deprecation warning" do + ruby_exe("exit(0)") + -> { + $? & 0 + }.should complain(/warning: Process::Status#& is deprecated and will be removed .*use other Process::Status predicates instead/) + end + end + end +end diff --git a/spec/ruby/core/process/status/coredump_spec.rb b/spec/ruby/core/process/status/coredump_spec.rb new file mode 100644 index 0000000000..fbbaf926f7 --- /dev/null +++ b/spec/ruby/core/process/status/coredump_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../../spec_helper' + +describe "Process::Status#coredump?" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/process/status/equal_value_spec.rb b/spec/ruby/core/process/status/equal_value_spec.rb new file mode 100644 index 0000000000..d8a2be26b8 --- /dev/null +++ b/spec/ruby/core/process/status/equal_value_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../../spec_helper' + +describe "Process::Status#==" do + it "returns true when compared to the integer status of an exited child" do + ruby_exe("exit(29)", exit_status: 29) + $?.to_i.should == $? + $?.should == $?.to_i + end + + it "returns true when compared to the integer status of a terminated child" do + ruby_exe("Process.kill(:KILL, $$); exit(29)", exit_status: platform_is(:windows) ? 0 : :SIGKILL) + $?.to_i.should == $? + $?.should == $?.to_i + end +end diff --git a/spec/ruby/core/process/status/exited_spec.rb b/spec/ruby/core/process/status/exited_spec.rb new file mode 100644 index 0000000000..a61292b146 --- /dev/null +++ b/spec/ruby/core/process/status/exited_spec.rb @@ -0,0 +1,32 @@ +require_relative '../../../spec_helper' + +describe "Process::Status#exited?" do + describe "for a child that exited normally" do + before :each do + ruby_exe("exit(0)") + end + + it "returns true" do + $?.exited?.should be_true + end + end + + + describe "for a terminated child" do + before :each do + ruby_exe("Process.kill(:KILL, $$); exit(42)", exit_status: platform_is(:windows) ? 0 : :SIGKILL) + end + + platform_is_not :windows do + it "returns false" do + $?.exited?.should be_false + end + end + + platform_is :windows do + it "always returns true" do + $?.exited?.should be_true + end + end + end +end diff --git a/spec/ruby/core/process/status/exitstatus_spec.rb b/spec/ruby/core/process/status/exitstatus_spec.rb new file mode 100644 index 0000000000..5c86c2b3c8 --- /dev/null +++ b/spec/ruby/core/process/status/exitstatus_spec.rb @@ -0,0 +1,25 @@ +require_relative '../../../spec_helper' + +describe "Process::Status#exitstatus" do + before :each do + ruby_exe("exit(42)", exit_status: 42) + end + + it "returns the process exit code" do + $?.exitstatus.should == 42 + end + + describe "for a child that raised SignalException" do + before :each do + ruby_exe("Process.kill(:KILL, $$); exit(42)", exit_status: platform_is(:windows) ? 0 : :SIGKILL) + end + + platform_is_not :windows do + # The exitstatus is not set in these cases. See the termsig_spec + # for info on where the signal number (SIGTERM) is available. + it "returns nil" do + $?.exitstatus.should == nil + end + end + end +end diff --git a/spec/ruby/core/process/status/inspect_spec.rb b/spec/ruby/core/process/status/inspect_spec.rb new file mode 100644 index 0000000000..03f0479f2c --- /dev/null +++ b/spec/ruby/core/process/status/inspect_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../../spec_helper' + +describe "Process::Status#inspect" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/process/status/pid_spec.rb b/spec/ruby/core/process/status/pid_spec.rb new file mode 100644 index 0000000000..9965fc3bdf --- /dev/null +++ b/spec/ruby/core/process/status/pid_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../../spec_helper' + +platform_is_not :windows do + describe "Process::Status#pid" do + + before :each do + @pid = ruby_exe("print $$").to_i + end + + it "returns the pid of the process" do + $?.pid.should == @pid + end + + end +end diff --git a/spec/ruby/core/process/status/right_shift_spec.rb b/spec/ruby/core/process/status/right_shift_spec.rb new file mode 100644 index 0000000000..355aaf4c95 --- /dev/null +++ b/spec/ruby/core/process/status/right_shift_spec.rb @@ -0,0 +1,37 @@ +require_relative '../../../spec_helper' + +ruby_version_is ""..."4.0" do + + describe "Process::Status#>>" do + it "returns a right shift of the integer status of an exited child" do + suppress_warning do + ruby_exe("exit(29)", exit_status: 29) + ($? >> 0).should == $?.to_i + ($? >> 1).should == $?.to_i >> 1 + + # Actual value is implementation specific + platform_is :linux do + ($? >> 8).should == 29 + end + end + end + + ruby_version_is "3.3"..."4.0" do + it "raises an ArgumentError if shift value is negative" do + suppress_warning do + ruby_exe("exit(0)") + -> { + $? >> -1 + }.should raise_error(ArgumentError, 'negative shift value: -1') + end + end + + it "shows a deprecation warning" do + ruby_exe("exit(0)") + -> { + $? >> 0 + }.should complain(/warning: Process::Status#>> is deprecated and will be removed .*use other Process::Status attributes instead/) + end + end + end +end diff --git a/spec/ruby/core/process/status/signaled_spec.rb b/spec/ruby/core/process/status/signaled_spec.rb new file mode 100644 index 0000000000..c0de7b8006 --- /dev/null +++ b/spec/ruby/core/process/status/signaled_spec.rb @@ -0,0 +1,31 @@ +require_relative '../../../spec_helper' + +describe "Process::Status#signaled?" do + describe "for a cleanly exited child" do + before :each do + ruby_exe("exit(0)") + end + + it "returns false" do + $?.signaled?.should be_false + end + end + + describe "for a terminated child" do + before :each do + ruby_exe("Process.kill(:KILL, $$); exit(42)", exit_status: platform_is(:windows) ? 0 : :SIGKILL) + end + + platform_is_not :windows do + it "returns true" do + $?.signaled?.should be_true + end + end + + platform_is :windows do + it "always returns false" do + $?.signaled?.should be_false + end + end + end +end diff --git a/spec/ruby/core/process/status/stopped_spec.rb b/spec/ruby/core/process/status/stopped_spec.rb new file mode 100644 index 0000000000..bebd441d6f --- /dev/null +++ b/spec/ruby/core/process/status/stopped_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../../spec_helper' + +describe "Process::Status#stopped?" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/process/status/stopsig_spec.rb b/spec/ruby/core/process/status/stopsig_spec.rb new file mode 100644 index 0000000000..b2a7c5d9e2 --- /dev/null +++ b/spec/ruby/core/process/status/stopsig_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../../spec_helper' + +describe "Process::Status#stopsig" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/process/status/success_spec.rb b/spec/ruby/core/process/status/success_spec.rb new file mode 100644 index 0000000000..3589cc611f --- /dev/null +++ b/spec/ruby/core/process/status/success_spec.rb @@ -0,0 +1,41 @@ +require_relative '../../../spec_helper' + +describe "Process::Status#success?" do + describe "for a child that exited normally" do + before :each do + ruby_exe("exit(0)") + end + + it "returns true" do + $?.success?.should be_true + end + end + + describe "for a child that exited with a non zero status" do + before :each do + ruby_exe("exit(42)", exit_status: 42) + end + + it "returns false" do + $?.success?.should be_false + end + end + + describe "for a child that was terminated" do + before :each do + ruby_exe("Process.kill(:KILL, $$); exit(42)", exit_status: platform_is(:windows) ? 0 : :SIGKILL) + end + + platform_is_not :windows do + it "returns nil" do + $?.success?.should be_nil + end + end + + platform_is :windows do + it "always returns true" do + $?.success?.should be_true + end + end + end +end diff --git a/spec/ruby/core/process/status/termsig_spec.rb b/spec/ruby/core/process/status/termsig_spec.rb new file mode 100644 index 0000000000..9a22dbea71 --- /dev/null +++ b/spec/ruby/core/process/status/termsig_spec.rb @@ -0,0 +1,43 @@ +require_relative '../../../spec_helper' + +describe "Process::Status#termsig" do + describe "for a child that exited normally" do + before :each do + ruby_exe("exit(0)") + end + + it "returns nil" do + $?.termsig.should be_nil + end + end + + describe "for a child that raised SignalException" do + before :each do + ruby_exe("raise SignalException, 'SIGTERM'", exit_status: :SIGTERM) + end + + platform_is_not :windows do + it "returns the signal" do + $?.termsig.should == Signal.list["TERM"] + end + end + end + + describe "for a child that was sent a signal" do + before :each do + ruby_exe("Process.kill(:KILL, $$); exit(42)", exit_status: platform_is(:windows) ? 0 : :SIGKILL) + end + + platform_is_not :windows do + it "returns the signal" do + $?.termsig.should == Signal.list["KILL"] + end + end + + platform_is :windows do + it "always returns nil" do + $?.termsig.should be_nil + end + end + end +end diff --git a/spec/ruby/core/process/status/to_i_spec.rb b/spec/ruby/core/process/status/to_i_spec.rb new file mode 100644 index 0000000000..39f8e2d84c --- /dev/null +++ b/spec/ruby/core/process/status/to_i_spec.rb @@ -0,0 +1,13 @@ +require_relative '../../../spec_helper' + +describe "Process::Status#to_i" do + it "returns an integer when the child exits" do + ruby_exe('exit 48', exit_status: 48) + $?.to_i.should be_an_instance_of(Integer) + end + + it "returns an integer when the child is signaled" do + ruby_exe('raise SignalException, "TERM"', exit_status: platform_is(:windows) ? 3 : :SIGTERM) + $?.to_i.should be_an_instance_of(Integer) + end +end diff --git a/spec/ruby/core/process/status/to_int_spec.rb b/spec/ruby/core/process/status/to_int_spec.rb new file mode 100644 index 0000000000..fb596c1bfb --- /dev/null +++ b/spec/ruby/core/process/status/to_int_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../../spec_helper' + +describe "Process::Status#to_int" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/process/status/to_s_spec.rb b/spec/ruby/core/process/status/to_s_spec.rb new file mode 100644 index 0000000000..5c0d4cd30c --- /dev/null +++ b/spec/ruby/core/process/status/to_s_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../../spec_helper' + +describe "Process::Status#to_s" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/process/status/wait_spec.rb b/spec/ruby/core/process/status/wait_spec.rb new file mode 100644 index 0000000000..57d56209a9 --- /dev/null +++ b/spec/ruby/core/process/status/wait_spec.rb @@ -0,0 +1,100 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/common' + +describe "Process::Status.wait" do + ProcessSpecs.use_system_ruby(self) + + before :all do + begin + leaked = Process.waitall + # Ruby-space should not see PIDs used by rjit + raise "subprocesses leaked before wait specs: #{leaked}" unless leaked.empty? + rescue NotImplementedError + end + end + + it "returns a status with pid -1 if there are no child processes" do + Process::Status.wait.pid.should == -1 + end + + platform_is_not :windows do + it "returns a status with its child pid" do + pid = Process.spawn(ruby_cmd('exit')) + status = Process::Status.wait + status.should be_an_instance_of(Process::Status) + status.pid.should == pid + end + + it "should not set $? to the Process::Status" do + pid = Process.spawn(ruby_cmd('exit')) + status = Process::Status.wait + $?.should_not equal(status) + end + + it "should not change the value of $?" do + pid = Process.spawn(ruby_cmd('exit')) + Process.wait + status = $? + Process::Status.wait + status.should equal($?) + end + + it "waits for any child process if no pid is given" do + pid = Process.spawn(ruby_cmd('exit')) + Process::Status.wait.pid.should == pid + -> { Process.kill(0, pid) }.should raise_error(Errno::ESRCH) + end + + it "waits for a specific child if a pid is given" do + pid1 = Process.spawn(ruby_cmd('exit')) + pid2 = Process.spawn(ruby_cmd('exit')) + Process::Status.wait(pid2).pid.should == pid2 + Process::Status.wait(pid1).pid.should == pid1 + -> { Process.kill(0, pid1) }.should raise_error(Errno::ESRCH) + -> { Process.kill(0, pid2) }.should raise_error(Errno::ESRCH) + end + + it "coerces the pid to an Integer" do + pid1 = Process.spawn(ruby_cmd('exit')) + Process::Status.wait(mock_int(pid1)).pid.should == pid1 + -> { Process.kill(0, pid1) }.should raise_error(Errno::ESRCH) + end + + # This spec is probably system-dependent. + it "waits for a child whose process group ID is that of the calling process" do + pid1 = Process.spawn(ruby_cmd('exit'), pgroup: true) + pid2 = Process.spawn(ruby_cmd('exit')) + + Process::Status.wait(0).pid.should == pid2 + Process::Status.wait.pid.should == pid1 + end + + # This spec is probably system-dependent. + it "doesn't block if no child is available when WNOHANG is used" do + read, write = IO.pipe + pid = Process.fork do + read.close + Signal.trap("TERM") { Process.exit! } + write << 1 + write.close + sleep + end + + Process::Status.wait(pid, Process::WNOHANG).should be_nil + + # wait for the child to setup its TERM handler + write.close + read.read(1) + read.close + + Process.kill("TERM", pid) + Process::Status.wait.pid.should == pid + end + + it "always accepts flags=0" do + pid = Process.spawn(ruby_cmd('exit')) + Process::Status.wait(-1, 0).pid.should == pid + -> { Process.kill(0, pid) }.should raise_error(Errno::ESRCH) + end + end +end diff --git a/spec/ruby/core/process/sys/getegid_spec.rb b/spec/ruby/core/process/sys/getegid_spec.rb new file mode 100644 index 0000000000..5b8588f147 --- /dev/null +++ b/spec/ruby/core/process/sys/getegid_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../../spec_helper' + +describe "Process::Sys.getegid" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/process/sys/geteuid_spec.rb b/spec/ruby/core/process/sys/geteuid_spec.rb new file mode 100644 index 0000000000..0ce7fc5459 --- /dev/null +++ b/spec/ruby/core/process/sys/geteuid_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../../spec_helper' + +describe "Process::Sys.geteuid" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/process/sys/getgid_spec.rb b/spec/ruby/core/process/sys/getgid_spec.rb new file mode 100644 index 0000000000..05c0fd4c0b --- /dev/null +++ b/spec/ruby/core/process/sys/getgid_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../../spec_helper' + +describe "Process::Sys.getgid" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/process/sys/getuid_spec.rb b/spec/ruby/core/process/sys/getuid_spec.rb new file mode 100644 index 0000000000..56bcef01cc --- /dev/null +++ b/spec/ruby/core/process/sys/getuid_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../../spec_helper' + +describe "Process::Sys.getuid" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/process/sys/issetugid_spec.rb b/spec/ruby/core/process/sys/issetugid_spec.rb new file mode 100644 index 0000000000..4fc7dc3bc6 --- /dev/null +++ b/spec/ruby/core/process/sys/issetugid_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../../spec_helper' + +describe "Process::Sys.issetugid" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/process/sys/setegid_spec.rb b/spec/ruby/core/process/sys/setegid_spec.rb new file mode 100644 index 0000000000..8f20330b94 --- /dev/null +++ b/spec/ruby/core/process/sys/setegid_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../../spec_helper' + +describe "Process::Sys.setegid" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/process/sys/seteuid_spec.rb b/spec/ruby/core/process/sys/seteuid_spec.rb new file mode 100644 index 0000000000..57d3a4800d --- /dev/null +++ b/spec/ruby/core/process/sys/seteuid_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../../spec_helper' + +describe "Process::Sys.seteuid" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/process/sys/setgid_spec.rb b/spec/ruby/core/process/sys/setgid_spec.rb new file mode 100644 index 0000000000..cc712e0102 --- /dev/null +++ b/spec/ruby/core/process/sys/setgid_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../../spec_helper' + +describe "Process::Sys.setgid" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/process/sys/setregid_spec.rb b/spec/ruby/core/process/sys/setregid_spec.rb new file mode 100644 index 0000000000..57d491a707 --- /dev/null +++ b/spec/ruby/core/process/sys/setregid_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../../spec_helper' + +describe "Process::Sys.setregid" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/process/sys/setresgid_spec.rb b/spec/ruby/core/process/sys/setresgid_spec.rb new file mode 100644 index 0000000000..9be06612c1 --- /dev/null +++ b/spec/ruby/core/process/sys/setresgid_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../../spec_helper' + +describe "Process::Sys.setresgid" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/process/sys/setresuid_spec.rb b/spec/ruby/core/process/sys/setresuid_spec.rb new file mode 100644 index 0000000000..1092b349cb --- /dev/null +++ b/spec/ruby/core/process/sys/setresuid_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../../spec_helper' + +describe "Process::Sys.setresuid" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/process/sys/setreuid_spec.rb b/spec/ruby/core/process/sys/setreuid_spec.rb new file mode 100644 index 0000000000..f3451c63c8 --- /dev/null +++ b/spec/ruby/core/process/sys/setreuid_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../../spec_helper' + +describe "Process::Sys.setreuid" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/process/sys/setrgid_spec.rb b/spec/ruby/core/process/sys/setrgid_spec.rb new file mode 100644 index 0000000000..27eea2ed86 --- /dev/null +++ b/spec/ruby/core/process/sys/setrgid_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../../spec_helper' + +describe "Process::Sys.setrgid" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/process/sys/setruid_spec.rb b/spec/ruby/core/process/sys/setruid_spec.rb new file mode 100644 index 0000000000..9dbd84bf68 --- /dev/null +++ b/spec/ruby/core/process/sys/setruid_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../../spec_helper' + +describe "Process::Sys.setruid" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/process/sys/setuid_spec.rb b/spec/ruby/core/process/sys/setuid_spec.rb new file mode 100644 index 0000000000..e06c3588a5 --- /dev/null +++ b/spec/ruby/core/process/sys/setuid_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../../spec_helper' + +describe "Process::Sys.setuid" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/process/times_spec.rb b/spec/ruby/core/process/times_spec.rb new file mode 100644 index 0000000000..d3bff2cda9 --- /dev/null +++ b/spec/ruby/core/process/times_spec.rb @@ -0,0 +1,19 @@ +require_relative '../../spec_helper' + +describe "Process.times" do + it "returns a Process::Tms" do + Process.times.should be_kind_of(Process::Tms) + end + + # TODO: Intel C Compiler does not work this example + # http://rubyci.s3.amazonaws.com/icc-x64/ruby-master/log/20221013T030005Z.fail.html.gz + unless RbConfig::CONFIG['CC']&.include?("icx") + it "returns current cpu times" do + t = Process.times + user = t.utime + + 1 until Process.times.utime > user + Process.times.utime.should > user + end + end +end diff --git a/spec/ruby/core/process/tms/cstime_spec.rb b/spec/ruby/core/process/tms/cstime_spec.rb new file mode 100644 index 0000000000..9c2d9e8632 --- /dev/null +++ b/spec/ruby/core/process/tms/cstime_spec.rb @@ -0,0 +1,17 @@ +require_relative '../../../spec_helper' + +describe "Process::Tms#cstime" do + it "returns cstime attribute" do + cstime = Object.new + Process::Tms.new(nil, nil, nil, cstime).cstime.should == cstime + end +end + +describe "Process::Tms#cstime=" do + it "assigns a value to the cstime attribute" do + cstime = Object.new + tms = Process::Tms.new + tms.cstime = cstime + tms.cstime.should == cstime + end +end diff --git a/spec/ruby/core/process/tms/cutime_spec.rb b/spec/ruby/core/process/tms/cutime_spec.rb new file mode 100644 index 0000000000..0ac3ff1964 --- /dev/null +++ b/spec/ruby/core/process/tms/cutime_spec.rb @@ -0,0 +1,17 @@ +require_relative '../../../spec_helper' + +describe "Process::Tms#cutime" do + it "returns cutime attribute" do + cutime = Object.new + Process::Tms.new(nil, nil, cutime, nil).cutime.should == cutime + end +end + +describe "Process::Tms#cutime=" do + it "assigns a value to the cutime attribute" do + cutime = Object.new + tms = Process::Tms.new + tms.cutime = cutime + tms.cutime.should == cutime + end +end diff --git a/spec/ruby/core/process/tms/stime_spec.rb b/spec/ruby/core/process/tms/stime_spec.rb new file mode 100644 index 0000000000..1e8371475f --- /dev/null +++ b/spec/ruby/core/process/tms/stime_spec.rb @@ -0,0 +1,17 @@ +require_relative '../../../spec_helper' + +describe "Process::Tms#stime" do + it "returns stime attribute" do + stime = Object.new + Process::Tms.new(nil, stime, nil, nil).stime.should == stime + end +end + +describe "Process::Tms#stime=" do + it "assigns a value to the stime attribute" do + stime = Object.new + tms = Process::Tms.new + tms.stime = stime + tms.stime.should == stime + end +end diff --git a/spec/ruby/core/process/tms/utime_spec.rb b/spec/ruby/core/process/tms/utime_spec.rb new file mode 100644 index 0000000000..403a31e2e6 --- /dev/null +++ b/spec/ruby/core/process/tms/utime_spec.rb @@ -0,0 +1,17 @@ +require_relative '../../../spec_helper' + +describe "Process::Tms#utime" do + it "returns utime attribute" do + utime = Object.new + Process::Tms.new(utime, nil, nil, nil).utime.should == utime + end +end + +describe "Process::Tms#utime=" do + it "assigns a value to the ctime attribute" do + utime = Object.new + tms = Process::Tms.new + tms.utime = utime + tms.utime.should == utime + end +end diff --git a/spec/ruby/core/process/uid/change_privilege_spec.rb b/spec/ruby/core/process/uid/change_privilege_spec.rb new file mode 100644 index 0000000000..e4b552dd94 --- /dev/null +++ b/spec/ruby/core/process/uid/change_privilege_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../../spec_helper' + +describe "Process::UID.change_privilege" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/process/uid/eid_spec.rb b/spec/ruby/core/process/uid/eid_spec.rb new file mode 100644 index 0000000000..f0bb9ce762 --- /dev/null +++ b/spec/ruby/core/process/uid/eid_spec.rb @@ -0,0 +1,9 @@ +require_relative '../../../spec_helper' + +describe "Process::UID.eid" do + it "needs to be reviewed for spec completeness" +end + +describe "Process::UID.eid=" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/process/uid/grant_privilege_spec.rb b/spec/ruby/core/process/uid/grant_privilege_spec.rb new file mode 100644 index 0000000000..2b8a5c9102 --- /dev/null +++ b/spec/ruby/core/process/uid/grant_privilege_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../../spec_helper' + +describe "Process::UID.grant_privilege" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/process/uid/re_exchange_spec.rb b/spec/ruby/core/process/uid/re_exchange_spec.rb new file mode 100644 index 0000000000..c0f10f33c4 --- /dev/null +++ b/spec/ruby/core/process/uid/re_exchange_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../../spec_helper' + +describe "Process::UID.re_exchange" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/process/uid/re_exchangeable_spec.rb b/spec/ruby/core/process/uid/re_exchangeable_spec.rb new file mode 100644 index 0000000000..8200d7ecb7 --- /dev/null +++ b/spec/ruby/core/process/uid/re_exchangeable_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../../spec_helper' + +describe "Process::UID.re_exchangeable?" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/process/uid/rid_spec.rb b/spec/ruby/core/process/uid/rid_spec.rb new file mode 100644 index 0000000000..e865cbfef6 --- /dev/null +++ b/spec/ruby/core/process/uid/rid_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../../spec_helper' + +describe "Process::UID.rid" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/process/uid/sid_available_spec.rb b/spec/ruby/core/process/uid/sid_available_spec.rb new file mode 100644 index 0000000000..be7912eb68 --- /dev/null +++ b/spec/ruby/core/process/uid/sid_available_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../../spec_helper' + +describe "Process::UID.sid_available?" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/process/uid/switch_spec.rb b/spec/ruby/core/process/uid/switch_spec.rb new file mode 100644 index 0000000000..4191b97db4 --- /dev/null +++ b/spec/ruby/core/process/uid/switch_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../../spec_helper' + +describe "Process::UID.switch" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/process/uid_spec.rb b/spec/ruby/core/process/uid_spec.rb new file mode 100644 index 0000000000..a068b1a2c1 --- /dev/null +++ b/spec/ruby/core/process/uid_spec.rb @@ -0,0 +1,57 @@ +require_relative '../../spec_helper' + +describe "Process.uid" do + platform_is_not :windows do + it "returns the correct uid for the user executing this process" do + current_uid_according_to_unix = `id -ur`.to_i + Process.uid.should == current_uid_according_to_unix + end + end + + it "also goes by Process::UID.rid" do + Process::UID.rid.should == Process.uid + end + + it "also goes by Process::Sys.getuid" do + Process::Sys.getuid.should == Process.uid + end +end + +describe "Process.uid=" do + platform_is_not :windows do + it "raises TypeError if not passed an Integer" do + -> { Process.uid = Object.new }.should raise_error(TypeError) + end + + as_user do + it "raises Errno::ERPERM if run by a non privileged user trying to set the superuser id" do + -> { (Process.uid = 0)}.should raise_error(Errno::EPERM) + end + + it "raises Errno::ERPERM if run by a non privileged user trying to set the superuser id from username" do + -> { Process.uid = "root" }.should raise_error(Errno::EPERM) + end + end + + as_superuser do + describe "if run by a superuser" do + it "sets the real user id for the current process" do + code = <<-RUBY + Process.uid = 1 + puts Process.uid + RUBY + ruby_exe(code).should == "1\n" + end + + it "sets the real user id if preceded by Process.euid=id" do + code = <<-RUBY + Process.euid = 1 + Process.uid = 1 + puts Process.uid + RUBY + ruby_exe(code).should == "1\n" + end + end + end + end +end diff --git a/spec/ruby/core/process/wait2_spec.rb b/spec/ruby/core/process/wait2_spec.rb new file mode 100644 index 0000000000..8ba429dc96 --- /dev/null +++ b/spec/ruby/core/process/wait2_spec.rb @@ -0,0 +1,45 @@ +require_relative '../../spec_helper' + +describe "Process.wait2" do + before :all do + # HACK: this kludge is temporarily necessary because some + # misbehaving spec somewhere else does not clear processes + # Note: background processes are unavoidable with RJIT, + # but we shouldn't reap them from Ruby-space + begin + Process.wait(-1, Process::WNOHANG) + $stderr.puts "Leaked process before wait2 specs! Waiting for it" + leaked = Process.waitall + $stderr.puts "leaked before wait2 specs: #{leaked}" unless leaked.empty? + # Ruby-space should not see PIDs used by rjit + leaked.should be_empty + rescue Errno::ECHILD # No child processes + rescue NotImplementedError + end + end + + platform_is_not :windows do + it "returns the pid and status of child process" do + pidf = Process.fork { Process.exit! 99 } + results = Process.wait2 + results.size.should == 2 + pidw, status = results + pidf.should == pidw + status.exitstatus.should == 99 + end + end + + it "raises a StandardError if no child processes exist" do + -> { Process.wait2 }.should raise_error(Errno::ECHILD) + -> { Process.wait2 }.should raise_error(StandardError) + end + + it "returns nil if the child process is still running when given the WNOHANG flag" do + IO.popen(ruby_cmd('STDIN.getbyte'), "w") do |io| + pid, status = Process.wait2(io.pid, Process::WNOHANG) + pid.should be_nil + status.should be_nil + io.write('a') + end + end +end diff --git a/spec/ruby/core/process/wait_spec.rb b/spec/ruby/core/process/wait_spec.rb new file mode 100644 index 0000000000..385acc9928 --- /dev/null +++ b/spec/ruby/core/process/wait_spec.rb @@ -0,0 +1,91 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/common' + +describe "Process.wait" do + ProcessSpecs.use_system_ruby(self) + + before :all do + begin + leaked = Process.waitall + # Ruby-space should not see PIDs used by rjit + raise "subprocesses leaked before wait specs: #{leaked}" unless leaked.empty? + rescue NotImplementedError + end + end + + it "raises an Errno::ECHILD if there are no child processes" do + -> { Process.wait }.should raise_error(Errno::ECHILD) + end + + platform_is_not :windows do + it "returns its child pid" do + pid = Process.spawn(ruby_cmd('exit')) + Process.wait.should == pid + end + + it "sets $? to a Process::Status" do + pid = Process.spawn(ruby_cmd('exit')) + Process.wait + $?.should be_kind_of(Process::Status) + $?.pid.should == pid + end + + it "waits for any child process if no pid is given" do + pid = Process.spawn(ruby_cmd('exit')) + Process.wait.should == pid + -> { Process.kill(0, pid) }.should raise_error(Errno::ESRCH) + end + + it "waits for a specific child if a pid is given" do + pid1 = Process.spawn(ruby_cmd('exit')) + pid2 = Process.spawn(ruby_cmd('exit')) + Process.wait(pid2).should == pid2 + Process.wait(pid1).should == pid1 + -> { Process.kill(0, pid1) }.should raise_error(Errno::ESRCH) + -> { Process.kill(0, pid2) }.should raise_error(Errno::ESRCH) + end + + it "coerces the pid to an Integer" do + pid1 = Process.spawn(ruby_cmd('exit')) + Process.wait(mock_int(pid1)).should == pid1 + -> { Process.kill(0, pid1) }.should raise_error(Errno::ESRCH) + end + + # This spec is probably system-dependent. + it "waits for a child whose process group ID is that of the calling process" do + pid1 = Process.spawn(ruby_cmd('exit'), pgroup: true) + pid2 = Process.spawn(ruby_cmd('exit')) + + Process.wait(0).should == pid2 + Process.wait.should == pid1 + end + + # This spec is probably system-dependent. + it "doesn't block if no child is available when WNOHANG is used" do + read, write = IO.pipe + pid = Process.fork do + read.close + Signal.trap("TERM") { Process.exit! } + write << 1 + write.close + sleep + end + + Process.wait(pid, Process::WNOHANG).should be_nil + + # wait for the child to setup its TERM handler + write.close + read.read(1) + read.close + + Process.kill("TERM", pid) + Process.wait.should == pid + end + + it "always accepts flags=0" do + pid = Process.spawn(ruby_cmd('exit')) + Process.wait(-1, 0).should == pid + -> { Process.kill(0, pid) }.should raise_error(Errno::ESRCH) + end + end +end diff --git a/spec/ruby/core/process/waitall_spec.rb b/spec/ruby/core/process/waitall_spec.rb new file mode 100644 index 0000000000..6cf4e32bc9 --- /dev/null +++ b/spec/ruby/core/process/waitall_spec.rb @@ -0,0 +1,48 @@ +require_relative '../../spec_helper' + +describe "Process.waitall" do + before :all do + begin + Process.waitall + rescue NotImplementedError + end + end + + it "returns an empty array when there are no children" do + Process.waitall.should == [] + end + + it "takes no arguments" do + -> { Process.waitall(0) }.should raise_error(ArgumentError) + end + + platform_is_not :windows do + it "waits for all children" do + pids = [] + pids << Process.fork { Process.exit! 2 } + pids << Process.fork { Process.exit! 1 } + pids << Process.fork { Process.exit! 0 } + Process.waitall + pids.each { |pid| + -> { Process.kill(0, pid) }.should raise_error(Errno::ESRCH) + } + end + + it "returns an array of pid/status pairs" do + pids = [] + pids << Process.fork { Process.exit! 2 } + pids << Process.fork { Process.exit! 1 } + pids << Process.fork { Process.exit! 0 } + a = Process.waitall + a.should be_kind_of(Array) + a.size.should == 3 + pids.each { |pid| + pid_status = a.assoc(pid) + pid_status.should be_kind_of(Array) + pid_status.size.should == 2 + pid_status.first.should == pid + pid_status.last.should be_kind_of(Process::Status) + } + end + end +end diff --git a/spec/ruby/core/process/waitpid2_spec.rb b/spec/ruby/core/process/waitpid2_spec.rb new file mode 100644 index 0000000000..45513af667 --- /dev/null +++ b/spec/ruby/core/process/waitpid2_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../spec_helper' + +describe "Process.waitpid2" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/process/waitpid_spec.rb b/spec/ruby/core/process/waitpid_spec.rb new file mode 100644 index 0000000000..a02147b663 --- /dev/null +++ b/spec/ruby/core/process/waitpid_spec.rb @@ -0,0 +1,14 @@ +require_relative '../../spec_helper' + +describe "Process.waitpid" do + it "returns nil when the process has not yet completed and WNOHANG is specified" do + cmd = platform_is(:windows) ? "timeout" : "sleep" + pid = spawn("#{cmd} 5") + begin + Process.waitpid(pid, Process::WNOHANG).should == nil + Process.kill("KILL", pid) + ensure + Process.wait(pid) + end + end +end diff --git a/spec/ruby/core/process/warmup_spec.rb b/spec/ruby/core/process/warmup_spec.rb new file mode 100644 index 0000000000..b562d52d22 --- /dev/null +++ b/spec/ruby/core/process/warmup_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' + +describe "Process.warmup" do + ruby_version_is "3.3" do + # The behavior is entirely implementation specific. + # Other implementations are free to just make it a noop + it "is implemented" do + Process.warmup.should == true + end + end +end diff --git a/spec/ruby/core/queue/append_spec.rb b/spec/ruby/core/queue/append_spec.rb new file mode 100644 index 0000000000..34165a7506 --- /dev/null +++ b/spec/ruby/core/queue/append_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/enque' + +describe "Queue#<<" do + it_behaves_like :queue_enq, :<<, -> { Queue.new } +end diff --git a/spec/ruby/core/queue/clear_spec.rb b/spec/ruby/core/queue/clear_spec.rb new file mode 100644 index 0000000000..3245e4cb83 --- /dev/null +++ b/spec/ruby/core/queue/clear_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/clear' + +describe "Queue#clear" do + it_behaves_like :queue_clear, :clear, -> { Queue.new } +end diff --git a/spec/ruby/core/queue/close_spec.rb b/spec/ruby/core/queue/close_spec.rb new file mode 100644 index 0000000000..c0d774cd74 --- /dev/null +++ b/spec/ruby/core/queue/close_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/close' + +describe "Queue#close" do + it_behaves_like :queue_close, :close, -> { Queue.new } +end diff --git a/spec/ruby/core/queue/closed_spec.rb b/spec/ruby/core/queue/closed_spec.rb new file mode 100644 index 0000000000..10d552996d --- /dev/null +++ b/spec/ruby/core/queue/closed_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/closed' + +describe "Queue#closed?" do + it_behaves_like :queue_closed?, :closed?, -> { Queue.new } +end diff --git a/spec/ruby/core/queue/deq_spec.rb b/spec/ruby/core/queue/deq_spec.rb new file mode 100644 index 0000000000..a2784e6a63 --- /dev/null +++ b/spec/ruby/core/queue/deq_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/deque' +require_relative '../../shared/types/rb_num2dbl_fails' + +describe "Queue#deq" do + it_behaves_like :queue_deq, :deq, -> { Queue.new } +end + +describe "Queue operations with timeout" do + it_behaves_like :rb_num2dbl_fails, nil, -> v { q = Queue.new; q.push(1); q.deq(timeout: v) } +end diff --git a/spec/ruby/core/queue/empty_spec.rb b/spec/ruby/core/queue/empty_spec.rb new file mode 100644 index 0000000000..55ca777466 --- /dev/null +++ b/spec/ruby/core/queue/empty_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/empty' + +describe "Queue#empty?" do + it_behaves_like :queue_empty?, :empty?, -> { Queue.new } +end diff --git a/spec/ruby/core/queue/enq_spec.rb b/spec/ruby/core/queue/enq_spec.rb new file mode 100644 index 0000000000..c69c496fbc --- /dev/null +++ b/spec/ruby/core/queue/enq_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/enque' + +describe "Queue#enq" do + it_behaves_like :queue_enq, :enq, -> { Queue.new } +end diff --git a/spec/ruby/core/queue/freeze_spec.rb b/spec/ruby/core/queue/freeze_spec.rb new file mode 100644 index 0000000000..ced2cc52dd --- /dev/null +++ b/spec/ruby/core/queue/freeze_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/freeze' + +describe "Queue#freeze" do + it_behaves_like :queue_freeze, :freeze, -> { Queue.new } +end diff --git a/spec/ruby/core/queue/initialize_spec.rb b/spec/ruby/core/queue/initialize_spec.rb new file mode 100644 index 0000000000..592fbe2487 --- /dev/null +++ b/spec/ruby/core/queue/initialize_spec.rb @@ -0,0 +1,60 @@ +require_relative '../../spec_helper' + +describe "Queue#initialize" do + it "can be passed no arguments for an empty Queue" do + q = Queue.new + q.size.should == 0 + q.should.empty? + end + + it "is a private method" do + Queue.private_instance_methods.include?(:initialize).should == true + end + + it "adds all elements of the passed Enumerable to self" do + q = Queue.new([1, 2, 3]) + q.size.should == 3 + q.should_not.empty? + q.pop.should == 1 + q.pop.should == 2 + q.pop.should == 3 + q.should.empty? + end + + describe "converts the given argument to an Array using #to_a" do + it "uses #to_a on the provided Enumerable" do + enumerable = MockObject.new('mock-enumerable') + enumerable.should_receive(:to_a).and_return([1, 2, 3]) + q = Queue.new(enumerable) + q.size.should == 3 + q.should_not.empty? + q.pop.should == 1 + q.pop.should == 2 + q.pop.should == 3 + q.should.empty? + end + + it "raises a TypeError if the given argument can't be converted to an Array" do + -> { Queue.new(42) }.should raise_error(TypeError) + -> { Queue.new(:abc) }.should raise_error(TypeError) + end + + it "raises a NoMethodError if the given argument raises a NoMethodError during type coercion to an Array" do + enumerable = MockObject.new('mock-enumerable') + enumerable.should_receive(:to_a).and_raise(NoMethodError) + -> { Queue.new(enumerable) }.should raise_error(NoMethodError) + end + end + + it "raises TypeError if the provided Enumerable does not respond to #to_a" do + enumerable = MockObject.new('mock-enumerable') + -> { Queue.new(enumerable) }.should raise_error(TypeError, "can't convert MockObject into Array") + end + + it "raises TypeError if #to_a does not return Array" do + enumerable = MockObject.new('mock-enumerable') + enumerable.should_receive(:to_a).and_return("string") + + -> { Queue.new(enumerable) }.should raise_error(TypeError, "can't convert MockObject to Array (MockObject#to_a gives String)") + end +end diff --git a/spec/ruby/core/queue/length_spec.rb b/spec/ruby/core/queue/length_spec.rb new file mode 100644 index 0000000000..25399b2b76 --- /dev/null +++ b/spec/ruby/core/queue/length_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/length' + +describe "Queue#length" do + it_behaves_like :queue_length, :length, -> { Queue.new } +end diff --git a/spec/ruby/core/queue/num_waiting_spec.rb b/spec/ruby/core/queue/num_waiting_spec.rb new file mode 100644 index 0000000000..edc0c37a82 --- /dev/null +++ b/spec/ruby/core/queue/num_waiting_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/num_waiting' + +describe "Queue#num_waiting" do + it_behaves_like :queue_num_waiting, :num_waiting, -> { Queue.new } +end diff --git a/spec/ruby/core/queue/pop_spec.rb b/spec/ruby/core/queue/pop_spec.rb new file mode 100644 index 0000000000..3dff7db242 --- /dev/null +++ b/spec/ruby/core/queue/pop_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/deque' +require_relative '../../shared/types/rb_num2dbl_fails' + +describe "Queue#pop" do + it_behaves_like :queue_deq, :pop, -> { Queue.new } +end + +describe "Queue operations with timeout" do + it_behaves_like :rb_num2dbl_fails, nil, -> v { q = Queue.new; q.push(1); q.pop(timeout: v) } +end diff --git a/spec/ruby/core/queue/push_spec.rb b/spec/ruby/core/queue/push_spec.rb new file mode 100644 index 0000000000..e936f9d282 --- /dev/null +++ b/spec/ruby/core/queue/push_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/enque' + +describe "Queue#push" do + it_behaves_like :queue_enq, :push, -> { Queue.new } +end diff --git a/spec/ruby/core/queue/shift_spec.rb b/spec/ruby/core/queue/shift_spec.rb new file mode 100644 index 0000000000..c105da74b2 --- /dev/null +++ b/spec/ruby/core/queue/shift_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/deque' +require_relative '../../shared/types/rb_num2dbl_fails' + +describe "Queue#shift" do + it_behaves_like :queue_deq, :shift, -> { Queue.new } +end + +describe "Queue operations with timeout" do + it_behaves_like :rb_num2dbl_fails, nil, -> v { q = Queue.new; q.push(1); q.shift(timeout: v) } +end diff --git a/spec/ruby/core/queue/size_spec.rb b/spec/ruby/core/queue/size_spec.rb new file mode 100644 index 0000000000..f528dfe797 --- /dev/null +++ b/spec/ruby/core/queue/size_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/length' + +describe "Queue#size" do + it_behaves_like :queue_length, :size, -> { Queue.new } +end diff --git a/spec/ruby/core/random/bytes_spec.rb b/spec/ruby/core/random/bytes_spec.rb new file mode 100644 index 0000000000..c9be07cd3f --- /dev/null +++ b/spec/ruby/core/random/bytes_spec.rb @@ -0,0 +1,29 @@ +# encoding: binary +require_relative '../../spec_helper' +require_relative 'shared/bytes' + +describe "Random#bytes" do + it_behaves_like :random_bytes, :bytes, Random.new + + it "returns the same output for a given seed" do + Random.new(33).bytes(2).should == Random.new(33).bytes(2) + end + + it "returns the same numeric output for a given seed across all implementations and platforms" do + rnd = Random.new(33) + rnd.bytes(2).should == "\x14\\" + rnd.bytes(1000) # skip some + rnd.bytes(2).should == "\xA1p" + end + + it "returns the same numeric output for a given huge seed across all implementations and platforms" do + rnd = Random.new(2 ** (63 * 4)) + rnd.bytes(2).should == "_\x91" + rnd.bytes(1000) # skip some + rnd.bytes(2).should == "\x17\x12" + end +end + +describe "Random.bytes" do + it_behaves_like :random_bytes, :bytes, Random +end diff --git a/spec/ruby/core/random/default_spec.rb b/spec/ruby/core/random/default_spec.rb new file mode 100644 index 0000000000..9e4845986d --- /dev/null +++ b/spec/ruby/core/random/default_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' + +describe "Random::DEFAULT" do + it "is no longer defined" do + Random.should_not.const_defined?(:DEFAULT) + end +end diff --git a/spec/ruby/core/random/equal_value_spec.rb b/spec/ruby/core/random/equal_value_spec.rb new file mode 100644 index 0000000000..5f470d6a4c --- /dev/null +++ b/spec/ruby/core/random/equal_value_spec.rb @@ -0,0 +1,37 @@ +require_relative '../../spec_helper' + +describe "Random#==" do + it "returns true if the two objects have the same state" do + a = Random.new(42) + b = Random.new(42) + a.send(:state).should == b.send(:state) + a.should == b + end + + it "returns false if the two objects have different state" do + a = Random.new + b = Random.new + a.send(:state).should_not == b.send(:state) + a.should_not == b + end + + it "returns true if the two objects have the same seed" do + a = Random.new(42) + b = Random.new(42.5) + a.seed.should == b.seed + a.should == b + end + + it "returns false if the two objects have a different seed" do + a = Random.new(42) + b = Random.new(41) + a.seed.should_not == b.seed + a.should_not == b + end + + it "returns false if the other object is not a Random" do + a = Random.new(42) + a.should_not == 42 + a.should_not == [a] + end +end diff --git a/spec/ruby/core/random/fixtures/classes.rb b/spec/ruby/core/random/fixtures/classes.rb new file mode 100644 index 0000000000..9c8d113e24 --- /dev/null +++ b/spec/ruby/core/random/fixtures/classes.rb @@ -0,0 +1,15 @@ +module RandomSpecs + CustomRangeInteger = Struct.new(:value) do + def to_int; value; end + def <=>(other); to_int <=> other.to_int; end + def -(other); self.class.new(to_int - other.to_int); end + def +(other); self.class.new(to_int + other.to_int); end + end + + CustomRangeFloat = Struct.new(:value) do + def to_f; value; end + def <=>(other); to_f <=> other.to_f; end + def -(other); to_f - other.to_f; end + def +(other); self.class.new(to_f + other.to_f); end + end +end diff --git a/spec/ruby/core/random/new_seed_spec.rb b/spec/ruby/core/random/new_seed_spec.rb new file mode 100644 index 0000000000..7a93da99aa --- /dev/null +++ b/spec/ruby/core/random/new_seed_spec.rb @@ -0,0 +1,24 @@ +require_relative '../../spec_helper' + +describe "Random.new_seed" do + it "returns an Integer" do + Random.new_seed.should be_an_instance_of(Integer) + end + + it "returns an arbitrary seed value each time" do + bigs = 200.times.map { Random.new_seed } + bigs.uniq.size.should == 200 + end + + it "is not affected by Kernel#srand" do + begin + srand 25 + a = Random.new_seed + srand 25 + b = Random.new_seed + a.should_not == b + ensure + srand Random.new_seed + end + end +end diff --git a/spec/ruby/core/random/new_spec.rb b/spec/ruby/core/random/new_spec.rb new file mode 100644 index 0000000000..69210cef03 --- /dev/null +++ b/spec/ruby/core/random/new_spec.rb @@ -0,0 +1,38 @@ +require_relative "../../spec_helper" +describe "Random.new" do + it "returns a new instance of Random" do + Random.new.should be_an_instance_of(Random) + end + + it "uses a random seed value if none is supplied" do + Random.new.seed.should be_an_instance_of(Integer) + end + + it "returns Random instances initialized with different seeds" do + first = Random.new + second = Random.new + (0..20).map { first.rand }.should_not == (0..20).map { second.rand } + end + + it "accepts an Integer seed value as an argument" do + Random.new(2).seed.should == 2 + end + + it "accepts (and truncates) a Float seed value as an argument" do + Random.new(3.4).seed.should == 3 + end + + it "accepts (and converts to Integer) a Rational seed value as an argument" do + Random.new(Rational(20,2)).seed.should == 10 + end + + it "accepts (and converts to Integer) a Complex (without imaginary part) seed value as an argument" do + Random.new(Complex(20)).seed.should == 20 + end + + it "raises a RangeError if passed a Complex (with imaginary part) seed value as an argument" do + -> do + Random.new(Complex(20,2)) + end.should raise_error(RangeError) + end +end diff --git a/spec/ruby/core/random/rand_spec.rb b/spec/ruby/core/random/rand_spec.rb new file mode 100644 index 0000000000..9244177ab5 --- /dev/null +++ b/spec/ruby/core/random/rand_spec.rb @@ -0,0 +1,224 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/rand' + +describe "Random.rand" do + it_behaves_like :random_number, :rand, Random.new + it_behaves_like :random_number, :rand, Random + + it "returns a Float >= 0 if no max argument is passed" do + floats = 200.times.map { Random.rand } + floats.min.should >= 0 + end + + it "returns a Float < 1 if no max argument is passed" do + floats = 200.times.map { Random.rand } + floats.max.should < 1 + end + + it "returns the same sequence for a given seed if no max argument is passed" do + Random.srand 33 + floats_a = 20.times.map { Random.rand } + Random.srand 33 + floats_b = 20.times.map { Random.rand } + floats_a.should == floats_b + end + + it "returns an Integer >= 0 if an Integer argument is passed" do + ints = 200.times.map { Random.rand(34) } + ints.min.should >= 0 + end + + it "returns an Integer < the max argument if an Integer argument is passed" do + ints = 200.times.map { Random.rand(55) } + ints.max.should < 55 + end + + it "returns the same sequence for a given seed if an Integer argument is passed" do + Random.srand 33 + floats_a = 20.times.map { Random.rand(90) } + Random.srand 33 + floats_b = 20.times.map { Random.rand(90) } + floats_a.should == floats_b + end + + it "coerces arguments to Integers with #to_int" do + obj = mock_numeric('int') + obj.should_receive(:to_int).and_return(99) + Random.rand(obj).should be_kind_of(Integer) + end +end + +describe "Random#rand with Fixnum" do + it "returns an Integer" do + Random.new.rand(20).should be_an_instance_of(Integer) + end + + it "returns a Fixnum greater than or equal to 0" do + prng = Random.new + ints = 20.times.map { prng.rand(5) } + ints.min.should >= 0 + end + + it "returns a Fixnum less than the argument" do + prng = Random.new + ints = 20.times.map { prng.rand(5) } + ints.max.should <= 4 + end + + it "returns the same sequence for a given seed" do + prng = Random.new 33 + a = 20.times.map { prng.rand(90) } + prng = Random.new 33 + b = 20.times.map { prng.rand(90) } + a.should == b + end + + it "eventually returns all possible values" do + prng = Random.new 33 + 100.times.map{ prng.rand(10) }.uniq.sort.should == (0...10).to_a + end + + it "raises an ArgumentError when the argument is 0" do + -> do + Random.new.rand(0) + end.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when the argument is negative" do + -> do + Random.new.rand(-12) + end.should raise_error(ArgumentError) + end +end + +describe "Random#rand with Bignum" do + it "typically returns a Bignum" do + rnd = Random.new(1) + 10.times.map{ rnd.rand(bignum_value*2) }.max.should be_an_instance_of(Integer) + end + + it "returns a Bignum greater than or equal to 0" do + prng = Random.new + bigs = 20.times.map { prng.rand(bignum_value) } + bigs.min.should >= 0 + end + + it "returns a Bignum less than the argument" do + prng = Random.new + bigs = 20.times.map { prng.rand(bignum_value) } + bigs.max.should < bignum_value + end + + it "returns the same sequence for a given seed" do + prng = Random.new 33 + a = 20.times.map { prng.rand(bignum_value) } + prng = Random.new 33 + b = 20.times.map { prng.rand(bignum_value) } + a.should == b + end + + it "raises an ArgumentError when the argument is negative" do + -> do + Random.new.rand(-bignum_value) + end.should raise_error(ArgumentError) + end +end + +describe "Random#rand with Float" do + it "returns a Float" do + Random.new.rand(20.43).should be_an_instance_of(Float) + end + + it "returns a Float greater than or equal to 0.0" do + prng = Random.new + floats = 20.times.map { prng.rand(5.2) } + floats.min.should >= 0.0 + end + + it "returns a Float less than the argument" do + prng = Random.new + floats = 20.times.map { prng.rand(4.30) } + floats.max.should < 4.30 + end + + it "returns the same sequence for a given seed" do + prng = Random.new 33 + a = 20.times.map { prng.rand(89.2928) } + prng = Random.new 33 + b = 20.times.map { prng.rand(89.2928) } + a.should == b + end + + it "raises an ArgumentError when the argument is negative" do + -> do + Random.new.rand(-1.234567) + end.should raise_error(ArgumentError) + end +end + +describe "Random#rand with Range" do + it "returns an element from the Range" do + Random.new.rand(20..43).should be_an_instance_of(Integer) + end + + it "supports custom object types" do + rand(RandomSpecs::CustomRangeInteger.new(1)..RandomSpecs::CustomRangeInteger.new(42)).should be_an_instance_of(RandomSpecs::CustomRangeInteger) + rand(RandomSpecs::CustomRangeFloat.new(1.0)..RandomSpecs::CustomRangeFloat.new(42.0)).should be_an_instance_of(RandomSpecs::CustomRangeFloat) + rand(Time.now..Time.now).should be_an_instance_of(Time) + end + + it "returns an object that is a member of the Range" do + prng = Random.new + r = 20..30 + 20.times { r.member?(prng.rand(r)).should be_true } + end + + it "works with inclusive ranges" do + prng = Random.new 33 + r = 3..5 + 40.times.map { prng.rand(r) }.uniq.sort.should == [3,4,5] + end + + it "works with exclusive ranges" do + prng = Random.new 33 + r = 3...5 + 20.times.map { prng.rand(r) }.uniq.sort.should == [3,4] + end + + it "returns the same sequence for a given seed" do + prng = Random.new 33 + a = 20.times.map { prng.rand(76890.028..800000.00) } + prng = Random.new 33 + b = 20.times.map { prng.rand(76890.028..800000.00) } + a.should == b + end + + it "eventually returns all possible values" do + prng = Random.new 33 + 100.times.map{ prng.rand(10..20) }.uniq.sort.should == (10..20).to_a + 100.times.map{ prng.rand(10...20) }.uniq.sort.should == (10...20).to_a + end + + it "considers Integers as Floats if one end point is a float" do + Random.new(42).rand(0.0..1).should be_kind_of(Float) + Random.new(42).rand(0..1.0).should be_kind_of(Float) + end + + it "returns a float within a given float range" do + Random.new(42).rand(0.0...100.0).should == 37.454011884736246 + Random.new(42).rand(-100.0...0.0).should == -62.545988115263754 + end + + it "raises an ArgumentError when the startpoint lacks #+ and #- methods" do + -> do + Random.new.rand(Object.new..67) + end.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when the endpoint lacks #+ and #- methods" do + -> do + Random.new.rand(68..Object.new) + end.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/random/random_number_spec.rb b/spec/ruby/core/random/random_number_spec.rb new file mode 100644 index 0000000000..bad81aeff6 --- /dev/null +++ b/spec/ruby/core/random/random_number_spec.rb @@ -0,0 +1,8 @@ +require_relative '../../spec_helper' +require_relative 'shared/rand' + +describe "Random.random_number" do + it_behaves_like :random_number, :random_number, Random.new + + it_behaves_like :random_number, :random_number, Random +end diff --git a/spec/ruby/core/random/seed_spec.rb b/spec/ruby/core/random/seed_spec.rb new file mode 100644 index 0000000000..bf4524fdd9 --- /dev/null +++ b/spec/ruby/core/random/seed_spec.rb @@ -0,0 +1,29 @@ +require_relative '../../spec_helper' + +describe "Random#seed" do + it "returns an Integer" do + Random.new.seed.should be_kind_of(Integer) + end + + it "returns an arbitrary seed if the constructor was called without arguments" do + Random.new.seed.should_not == Random.new.seed + end + + it "returns the same generated seed when repeatedly called on the same object" do + prng = Random.new + prng.seed.should == prng.seed + end + + it "returns the seed given in the constructor" do + prng = Random.new(36788) + prng.seed.should == prng.seed + prng.seed.should == 36788 + end + + it "returns the given seed coerced with #to_int" do + obj = mock_numeric('int') + obj.should_receive(:to_int).and_return(34) + prng = Random.new(obj) + prng.seed.should == 34 + end +end diff --git a/spec/ruby/core/random/shared/bytes.rb b/spec/ruby/core/random/shared/bytes.rb new file mode 100644 index 0000000000..9afad3b7f8 --- /dev/null +++ b/spec/ruby/core/random/shared/bytes.rb @@ -0,0 +1,17 @@ +describe :random_bytes, shared: true do + it "returns a String" do + @object.send(@method, 1).should be_an_instance_of(String) + end + + it "returns a String of the length given as argument" do + @object.send(@method, 15).length.should == 15 + end + + it "returns a binary String" do + @object.send(@method, 15).encoding.should == Encoding::BINARY + end + + it "returns a random binary String" do + @object.send(@method, 12).should_not == @object.send(@method, 12) + end +end diff --git a/spec/ruby/core/random/shared/rand.rb b/spec/ruby/core/random/shared/rand.rb new file mode 100644 index 0000000000..d3b24b8851 --- /dev/null +++ b/spec/ruby/core/random/shared/rand.rb @@ -0,0 +1,9 @@ +describe :random_number, shared: true do + it "returns a Float if no max argument is passed" do + @object.send(@method).should be_kind_of(Float) + end + + it "returns an Integer if an Integer argument is passed" do + @object.send(@method, 20).should be_kind_of(Integer) + end +end diff --git a/spec/ruby/core/random/srand_spec.rb b/spec/ruby/core/random/srand_spec.rb new file mode 100644 index 0000000000..1ef8e32cfb --- /dev/null +++ b/spec/ruby/core/random/srand_spec.rb @@ -0,0 +1,39 @@ +require_relative '../../spec_helper' + +describe "Random.srand" do + it "returns an arbitrary seed if .srand wasn't called previously with an argument and no argument is supplied this time" do + Random.srand # Reset to random seed in case .srand was called previously + Random.srand.should_not == Random.srand + end + + it "returns the previous argument to .srand if one was given and no argument is supplied" do + Random.srand 34 + Random.srand.should == 34 + end + + it "returns an arbitrary seed if .srand wasn't called previously with an argument and 0 is supplied this time" do + Random.srand # Reset to random seed in case .srand was called previously + Random.srand(0).should_not == Random.srand(0) + end + + it "returns the previous argument to .srand if one was given and 0 is supplied" do + Random.srand 34 + Random.srand(0).should == 34 + end + + it "seeds Random.rand such that its return value is deterministic" do + Random.srand 176542 + a = 20.times.map { Random.rand } + Random.srand 176542 + b = 20.times.map { Random.rand } + a.should == b + end + + it "seeds Kernel.rand such that its return value is deterministic" do + Random.srand 176542 + a = 20.times.map { Kernel.rand } + Random.srand 176542 + b = 20.times.map { Kernel.rand } + a.should == b + end +end diff --git a/spec/ruby/core/random/urandom_spec.rb b/spec/ruby/core/random/urandom_spec.rb new file mode 100644 index 0000000000..6f180e54ac --- /dev/null +++ b/spec/ruby/core/random/urandom_spec.rb @@ -0,0 +1,25 @@ +require_relative '../../spec_helper' + +describe "Random.urandom" do + it "returns a String" do + Random.urandom(1).should be_an_instance_of(String) + end + + it "returns a String of the length given as argument" do + Random.urandom(15).length.should == 15 + end + + it "raises an ArgumentError on a negative size" do + -> { + Random.urandom(-1) + }.should raise_error(ArgumentError) + end + + it "returns a binary String" do + Random.urandom(15).encoding.should == Encoding::BINARY + end + + it "returns a random binary String" do + Random.urandom(12).should_not == Random.urandom(12) + end +end diff --git a/spec/ruby/core/range/begin_spec.rb b/spec/ruby/core/range/begin_spec.rb new file mode 100644 index 0000000000..ab82b45b7e --- /dev/null +++ b/spec/ruby/core/range/begin_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/begin' + +describe "Range#begin" do + it_behaves_like :range_begin, :begin +end diff --git a/spec/ruby/core/range/bsearch_spec.rb b/spec/ruby/core/range/bsearch_spec.rb new file mode 100644 index 0000000000..5254ab756c --- /dev/null +++ b/spec/ruby/core/range/bsearch_spec.rb @@ -0,0 +1,466 @@ +require_relative '../../spec_helper' +require_relative '../enumerable/shared/enumeratorized' + +describe "Range#bsearch" do + it "returns an Enumerator when not passed a block" do + (0..1).bsearch.should be_an_instance_of(Enumerator) + end + + it_behaves_like :enumeratorized_with_unknown_size, :bsearch, (1..3) + + it "raises a TypeError if the block returns an Object" do + -> { (0..1).bsearch { Object.new } }.should raise_error(TypeError, "wrong argument type Object (must be numeric, true, false or nil)") + end + + it "raises a TypeError if the block returns a String and boundaries are Integer values" do + -> { (0..1).bsearch { "1" } }.should raise_error(TypeError, "wrong argument type String (must be numeric, true, false or nil)") + end + + it "raises a TypeError if the block returns a String and boundaries are Float values" do + -> { (0.0..1.0).bsearch { "1" } }.should raise_error(TypeError, "wrong argument type String (must be numeric, true, false or nil)") + end + + it "raises a TypeError if the Range has Object values" do + value = mock("range bsearch") + r = Range.new value, value + + -> { r.bsearch { true } }.should raise_error(TypeError, "can't do binary search for MockObject") + end + + it "raises a TypeError if the Range has String values" do + -> { ("a".."e").bsearch { true } }.should raise_error(TypeError, "can't do binary search for String") + end + + it "raises TypeError when non-Numeric begin/end and block not passed" do + -> { ("a".."e").bsearch }.should raise_error(TypeError, "can't do binary search for String") + end + + context "with Integer values" do + context "with a block returning true or false" do + it "returns nil if the block returns false for every element" do + (0...3).bsearch { |x| x > 3 }.should be_nil + end + + it "returns nil if the block returns nil for every element" do + (0..3).bsearch { |x| nil }.should be_nil + end + + it "returns minimum element if the block returns true for every element" do + (-2..4).bsearch { |x| x < 4 }.should == -2 + end + + it "returns the smallest element for which block returns true" do + (0..4).bsearch { |x| x >= 2 }.should == 2 + (-1..4).bsearch { |x| x >= 1 }.should == 1 + end + + it "returns the last element if the block returns true for the last element" do + (0..4).bsearch { |x| x >= 4 }.should == 4 + (0...4).bsearch { |x| x >= 3 }.should == 3 + end + end + + context "with a block returning negative, zero, positive numbers" do + it "returns nil if the block returns less than zero for every element" do + (0..3).bsearch { |x| x <=> 5 }.should be_nil + end + + it "returns nil if the block returns greater than zero for every element" do + (0..3).bsearch { |x| x <=> -1 }.should be_nil + + end + + it "returns nil if the block never returns zero" do + (0..3).bsearch { |x| x < 2 ? 1 : -1 }.should be_nil + end + + it "accepts (+/-)Float::INFINITY from the block" do + (0..4).bsearch { |x| Float::INFINITY }.should be_nil + (0..4).bsearch { |x| -Float::INFINITY }.should be_nil + end + + it "returns an element at an index for which block returns 0.0" do + result = (0..4).bsearch { |x| x < 2 ? 1.0 : x > 2 ? -1.0 : 0.0 } + result.should == 2 + end + + it "returns an element at an index for which block returns 0" do + result = (0..4).bsearch { |x| x < 1 ? 1 : x > 3 ? -1 : 0 } + [1, 2].should include(result) + end + end + + it "returns nil for empty ranges" do + (0...0).bsearch { true }.should == nil + (0...0).bsearch { false }.should == nil + (0...0).bsearch { 1 }.should == nil + (0...0).bsearch { 0 }.should == nil + (0...0).bsearch { -1 }.should == nil + + (4..2).bsearch { true }.should == nil + (4..2).bsearch { 1 }.should == nil + (4..2).bsearch { 0 }.should == nil + (4..2).bsearch { -1 }.should == nil + end + + it "returns enumerator when block not passed" do + (0...3).bsearch.kind_of?(Enumerator).should == true + end + end + + context "with Float values" do + context "with a block returning true or false" do + it "returns nil if the block returns false for every element" do + (0.1...2.3).bsearch { |x| x > 3 }.should be_nil + end + + it "returns nil if the block returns nil for every element" do + (-0.0..2.3).bsearch { |x| nil }.should be_nil + end + + it "returns minimum element if the block returns true for every element" do + (-0.2..4.8).bsearch { |x| x < 5 }.should == -0.2 + end + + it "returns the smallest element for which block returns true" do + (0..4.2).bsearch { |x| x >= 2 }.should == 2 + (-1.2..4.3).bsearch { |x| x >= 1 }.should == 1 + end + + it "returns a boundary element if appropriate" do + (1.0..3.0).bsearch { |x| x >= 3.0 }.should == 3.0 + (1.0...3.0).bsearch { |x| x >= 3.0.prev_float }.should == 3.0.prev_float + (1.0..3.0).bsearch { |x| x >= 1.0 }.should == 1.0 + (1.0...3.0).bsearch { |x| x >= 1.0 }.should == 1.0 + end + + it "works with infinity bounds" do + inf = Float::INFINITY + (0..inf).bsearch { |x| x == inf }.should == inf + (0...inf).bsearch { |x| x == inf }.should == nil + (-inf..0).bsearch { |x| x != -inf }.should == -Float::MAX + (-inf...0).bsearch { |x| x != -inf }.should == -Float::MAX + (inf..inf).bsearch { |x| true }.should == inf + (inf...inf).bsearch { |x| true }.should == nil + (-inf..-inf).bsearch { |x| true }.should == -inf + (-inf...-inf).bsearch { |x| true }.should == nil + (inf..0).bsearch { true }.should == nil + (inf...0).bsearch { true }.should == nil + (0..-inf).bsearch { true }.should == nil + (0...-inf).bsearch { true }.should == nil + (inf..-inf).bsearch { true }.should == nil + (inf...-inf).bsearch { true }.should == nil + (0..inf).bsearch { |x| x >= 3 }.should == 3.0 + (0...inf).bsearch { |x| x >= 3 }.should == 3.0 + (-inf..0).bsearch { |x| x >= -3 }.should == -3.0 + (-inf...0).bsearch { |x| x >= -3 }.should == -3.0 + (-inf..inf).bsearch { |x| x >= 3 }.should == 3.0 + (-inf...inf).bsearch { |x| x >= 3 }.should == 3.0 + (0..inf).bsearch { |x| x >= Float::MAX }.should == Float::MAX + (0...inf).bsearch { |x| x >= Float::MAX }.should == Float::MAX + end + end + + context "with a block returning negative, zero, positive numbers" do + it "returns nil if the block returns less than zero for every element" do + (-2.0..3.2).bsearch { |x| x <=> 5 }.should be_nil + end + + it "returns nil if the block returns greater than zero for every element" do + (0.3..3.0).bsearch { |x| x <=> -1 }.should be_nil + end + + it "returns nil if the block never returns zero" do + (0.2..2.3).bsearch { |x| x < 2 ? 1 : -1 }.should be_nil + end + + it "accepts (+/-)Float::INFINITY from the block" do + (0.1..4.5).bsearch { |x| Float::INFINITY }.should be_nil + (-5.0..4.0).bsearch { |x| -Float::INFINITY }.should be_nil + end + + it "returns an element at an index for which block returns 0.0" do + result = (0.0..4.0).bsearch { |x| x < 2 ? 1.0 : x > 2 ? -1.0 : 0.0 } + result.should == 2 + end + + it "returns an element at an index for which block returns 0" do + result = (0.1..4.9).bsearch { |x| x < 1 ? 1 : x > 3 ? -1 : 0 } + result.should >= 1 + result.should <= 3 + end + + it "returns an element at an index for which block returns 0 (small numbers)" do + result = (0.1..0.3).bsearch { |x| x < 0.1 ? 1 : x > 0.3 ? -1 : 0 } + result.should >= 0.1 + result.should <= 0.3 + end + + it "returns a boundary element if appropriate" do + (1.0..3.0).bsearch { |x| 3.0 - x }.should == 3.0 + (1.0...3.0).bsearch { |x| 3.0.prev_float - x }.should == 3.0.prev_float + (1.0..3.0).bsearch { |x| 1.0 - x }.should == 1.0 + (1.0...3.0).bsearch { |x| 1.0 - x }.should == 1.0 + end + + it "works with infinity bounds" do + inf = Float::INFINITY + (0..inf).bsearch { |x| x == inf ? 0 : 1 }.should == inf + (0...inf).bsearch { |x| x == inf ? 0 : 1 }.should == nil + (-inf...0).bsearch { |x| x == -inf ? 0 : -1 }.should == -inf + (-inf..0).bsearch { |x| x == -inf ? 0 : -1 }.should == -inf + (inf..inf).bsearch { 0 }.should == inf + (inf...inf).bsearch { 0 }.should == nil + (-inf..-inf).bsearch { 0 }.should == -inf + (-inf...-inf).bsearch { 0 }.should == nil + (inf..0).bsearch { 0 }.should == nil + (inf...0).bsearch { 0 }.should == nil + (0..-inf).bsearch { 0 }.should == nil + (0...-inf).bsearch { 0 }.should == nil + (inf..-inf).bsearch { 0 }.should == nil + (inf...-inf).bsearch { 0 }.should == nil + (-inf..inf).bsearch { |x| 3 - x }.should == 3.0 + (-inf...inf).bsearch { |x| 3 - x }.should == 3.0 + (0...inf).bsearch { |x| x >= Float::MAX ? 0 : 1 }.should == Float::MAX + end + end + + it "returns enumerator when block not passed" do + (0.1...2.3).bsearch.kind_of?(Enumerator).should == true + end + end + + context "with endless ranges and Integer values" do + context "with a block returning true or false" do + it "returns minimum element if the block returns true for every element" do + eval("(-2..)").bsearch { |x| true }.should == -2 + end + + it "returns the smallest element for which block returns true" do + eval("(0..)").bsearch { |x| x >= 2 }.should == 2 + eval("(-1..)").bsearch { |x| x >= 1 }.should == 1 + end + end + + context "with a block returning negative, zero, positive numbers" do + it "returns nil if the block returns less than zero for every element" do + eval("(0..)").bsearch { |x| -1 }.should be_nil + end + + it "returns nil if the block never returns zero" do + eval("(0..)").bsearch { |x| x > 5 ? -1 : 1 }.should be_nil + end + + it "accepts -Float::INFINITY from the block" do + eval("(0..)").bsearch { |x| -Float::INFINITY }.should be_nil + end + + it "returns an element at an index for which block returns 0.0" do + result = eval("(0..)").bsearch { |x| x < 2 ? 1.0 : x > 2 ? -1.0 : 0.0 } + result.should == 2 + end + + it "returns an element at an index for which block returns 0" do + result = eval("(0..)").bsearch { |x| x < 1 ? 1 : x > 3 ? -1 : 0 } + [1, 2, 3].should include(result) + end + end + + it "returns enumerator when block not passed" do + eval("(-2..)").bsearch.kind_of?(Enumerator).should == true + end + end + + context "with endless ranges and Float values" do + context "with a block returning true or false" do + it "returns nil if the block returns false for every element" do + eval("(0.1..)").bsearch { |x| x < 0.0 }.should be_nil + eval("(0.1...)").bsearch { |x| x < 0.0 }.should be_nil + end + + it "returns nil if the block returns nil for every element" do + eval("(-0.0..)").bsearch { |x| nil }.should be_nil + eval("(-0.0...)").bsearch { |x| nil }.should be_nil + end + + it "returns minimum element if the block returns true for every element" do + eval("(-0.2..)").bsearch { |x| true }.should == -0.2 + eval("(-0.2...)").bsearch { |x| true }.should == -0.2 + end + + it "returns the smallest element for which block returns true" do + eval("(0..)").bsearch { |x| x >= 2 }.should == 2 + eval("(-1.2..)").bsearch { |x| x >= 1 }.should == 1 + end + + it "works with infinity bounds" do + inf = Float::INFINITY + eval("(inf..)").bsearch { |x| true }.should == inf + eval("(inf...)").bsearch { |x| true }.should == nil + eval("(-inf..)").bsearch { |x| true }.should == -inf + eval("(-inf...)").bsearch { |x| true }.should == -inf + end + end + + context "with a block returning negative, zero, positive numbers" do + it "returns nil if the block returns less than zero for every element" do + eval("(-2.0..)").bsearch { |x| -1 }.should be_nil + eval("(-2.0...)").bsearch { |x| -1 }.should be_nil + end + + it "returns nil if the block returns greater than zero for every element" do + eval("(0.3..)").bsearch { |x| 1 }.should be_nil + eval("(0.3...)").bsearch { |x| 1 }.should be_nil + end + + it "returns nil if the block never returns zero" do + eval("(0.2..)").bsearch { |x| x < 2 ? 1 : -1 }.should be_nil + end + + it "accepts (+/-)Float::INFINITY from the block" do + eval("(0.1..)").bsearch { |x| Float::INFINITY }.should be_nil + eval("(-5.0..)").bsearch { |x| -Float::INFINITY }.should be_nil + end + + it "returns an element at an index for which block returns 0.0" do + result = eval("(0.0..)").bsearch { |x| x < 2 ? 1.0 : x > 2 ? -1.0 : 0.0 } + result.should == 2 + end + + it "returns an element at an index for which block returns 0" do + result = eval("(0.1..)").bsearch { |x| x < 1 ? 1 : x > 3 ? -1 : 0 } + result.should >= 1 + result.should <= 3 + end + + it "works with infinity bounds" do + inf = Float::INFINITY + eval("(inf..)").bsearch { |x| 1 }.should == nil + eval("(inf...)").bsearch { |x| 1 }.should == nil + eval("(inf..)").bsearch { |x| x == inf ? 0 : 1 }.should == inf + eval("(inf...)").bsearch { |x| x == inf ? 0 : 1 }.should == nil + eval("(-inf..)").bsearch { |x| x == -inf ? 0 : -1 }.should == -inf + eval("(-inf...)").bsearch { |x| x == -inf ? 0 : -1 }.should == -inf + eval("(-inf..)").bsearch { |x| 3 - x }.should == 3 + eval("(-inf...)").bsearch { |x| 3 - x }.should == 3 + eval("(0.0...)").bsearch { 0 }.should != inf + end + end + + it "returns enumerator when block not passed" do + eval("(0.1..)").bsearch.kind_of?(Enumerator).should == true + end + end + + context "with beginless ranges and Integer values" do + context "with a block returning true or false" do + it "returns the smallest element for which block returns true" do + (..10).bsearch { |x| x >= 2 }.should == 2 + (...-1).bsearch { |x| x >= -10 }.should == -10 + end + end + + context "with a block returning negative, zero, positive numbers" do + it "returns nil if the block returns greater than zero for every element" do + (..0).bsearch { |x| 1 }.should be_nil + end + + it "returns nil if the block never returns zero" do + (..0).bsearch { |x| x > 5 ? -1 : 1 }.should be_nil + end + + it "accepts Float::INFINITY from the block" do + (..0).bsearch { |x| Float::INFINITY }.should be_nil + end + + it "returns an element at an index for which block returns 0.0" do + result = (..10).bsearch { |x| x < 2 ? 1.0 : x > 2 ? -1.0 : 0.0 } + result.should == 2 + end + + it "returns an element at an index for which block returns 0" do + result = (...10).bsearch { |x| x < 1 ? 1 : x > 3 ? -1 : 0 } + [1, 2, 3].should include(result) + end + end + + it "returns enumerator when block not passed" do + (..10).bsearch.kind_of?(Enumerator).should == true + end + end + + context "with beginless ranges and Float values" do + context "with a block returning true or false" do + it "returns nil if the block returns true for every element" do + (..-0.1).bsearch { |x| x > 0.0 }.should be_nil + (...-0.1).bsearch { |x| x > 0.0 }.should be_nil + end + + it "returns nil if the block returns nil for every element" do + (..-0.1).bsearch { |x| nil }.should be_nil + (...-0.1).bsearch { |x| nil }.should be_nil + end + + it "returns the smallest element for which block returns true" do + (..10).bsearch { |x| x >= 2 }.should == 2 + (..10).bsearch { |x| x >= 1 }.should == 1 + end + + it "works with infinity bounds" do + inf = Float::INFINITY + (..inf).bsearch { |x| true }.should == -inf + (...inf).bsearch { |x| true }.should == -inf + (..-inf).bsearch { |x| true }.should == -inf + (...-inf).bsearch { |x| true }.should == nil + end + end + + context "with a block returning negative, zero, positive numbers" do + it "returns nil if the block returns less than zero for every element" do + (..5.0).bsearch { |x| -1 }.should be_nil + (...5.0).bsearch { |x| -1 }.should be_nil + end + + it "returns nil if the block returns greater than zero for every element" do + (..1.1).bsearch { |x| 1 }.should be_nil + (...1.1).bsearch { |x| 1 }.should be_nil + end + + it "returns nil if the block never returns zero" do + (..6.3).bsearch { |x| x < 2 ? 1 : -1 }.should be_nil + end + + it "accepts (+/-)Float::INFINITY from the block" do + (..5.0).bsearch { |x| Float::INFINITY }.should be_nil + (..7.0).bsearch { |x| -Float::INFINITY }.should be_nil + end + + it "returns an element at an index for which block returns 0.0" do + result = (..8.0).bsearch { |x| x < 2 ? 1.0 : x > 2 ? -1.0 : 0.0 } + result.should == 2 + end + + it "returns an element at an index for which block returns 0" do + result = (..8.0).bsearch { |x| x < 1 ? 1 : x > 3 ? -1 : 0 } + result.should >= 1 + result.should <= 3 + end + + it "works with infinity bounds" do + inf = Float::INFINITY + (..-inf).bsearch { |x| 1 }.should == nil + (...-inf).bsearch { |x| 1 }.should == nil + (..inf).bsearch { |x| x == inf ? 0 : 1 }.should == inf + (...inf).bsearch { |x| x == inf ? 0 : 1 }.should == nil + (..-inf).bsearch { |x| x == -inf ? 0 : -1 }.should == -inf + (...-inf).bsearch { |x| x == -inf ? 0 : -1 }.should == nil + (..inf).bsearch { |x| 3 - x }.should == 3 + (...inf).bsearch { |x| 3 - x }.should == 3 + end + end + + it "returns enumerator when block not passed" do + (..-0.1).bsearch.kind_of?(Enumerator).should == true + end + end +end diff --git a/spec/ruby/core/range/case_compare_spec.rb b/spec/ruby/core/range/case_compare_spec.rb new file mode 100644 index 0000000000..c9b253f0a5 --- /dev/null +++ b/spec/ruby/core/range/case_compare_spec.rb @@ -0,0 +1,19 @@ +require_relative '../../spec_helper' +require_relative 'shared/cover_and_include' +require_relative 'shared/cover' + +describe "Range#===" do + it "returns the result of calling #cover? on self" do + range = RangeSpecs::WithoutSucc.new(0)..RangeSpecs::WithoutSucc.new(10) + (range === RangeSpecs::WithoutSucc.new(2)).should == true + end + + it_behaves_like :range_cover_and_include, :=== + it_behaves_like :range_cover, :=== + + ruby_bug "#19533", ""..."3.3" do + it "returns true on any value if begin and end are both nil" do + (nil..nil).should === 1 + end + end +end diff --git a/spec/ruby/core/range/clone_spec.rb b/spec/ruby/core/range/clone_spec.rb new file mode 100644 index 0000000000..cf6ce74da0 --- /dev/null +++ b/spec/ruby/core/range/clone_spec.rb @@ -0,0 +1,26 @@ +require_relative '../../spec_helper' + +describe "Range#clone" do + it "duplicates the range" do + original = (1..3) + copy = original.clone + copy.begin.should == 1 + copy.end.should == 3 + copy.should_not.exclude_end? + copy.should_not.equal? original + + original = ("a"..."z") + copy = original.clone + copy.begin.should == "a" + copy.end.should == "z" + copy.should.exclude_end? + copy.should_not.equal? original + end + + it "maintains the frozen state" do + (1..2).clone.frozen?.should == (1..2).frozen? + (1..).clone.frozen?.should == (1..).frozen? + Range.new(1, 2).clone.frozen?.should == Range.new(1, 2).frozen? + Class.new(Range).new(1, 2).clone.frozen?.should == Class.new(Range).new(1, 2).frozen? + end +end diff --git a/spec/ruby/core/range/count_spec.rb b/spec/ruby/core/range/count_spec.rb new file mode 100644 index 0000000000..24d4a9caf3 --- /dev/null +++ b/spec/ruby/core/range/count_spec.rb @@ -0,0 +1,12 @@ +require_relative '../../spec_helper' + +describe "Range#count" do + it "returns Infinity for beginless ranges without arguments or blocks" do + inf = Float::INFINITY + eval("('a'...)").count.should == inf + eval("(7..)").count.should == inf + (...'a').count.should == inf + (...nil).count.should == inf + (..10.0).count.should == inf + end +end diff --git a/spec/ruby/core/range/cover_spec.rb b/spec/ruby/core/range/cover_spec.rb new file mode 100644 index 0000000000..c05bb50614 --- /dev/null +++ b/spec/ruby/core/range/cover_spec.rb @@ -0,0 +1,14 @@ +# encoding: binary +require_relative '../../spec_helper' +require_relative 'shared/cover_and_include' +require_relative 'shared/cover' + +describe "Range#cover?" do + it_behaves_like :range_cover_and_include, :cover? + it_behaves_like :range_cover, :cover? + it_behaves_like :range_cover_subrange, :cover? + + it "covers U+9995 in the range U+0999..U+9999" do + ("\u{999}".."\u{9999}").cover?("\u{9995}").should be_true + end +end diff --git a/spec/ruby/core/range/dup_spec.rb b/spec/ruby/core/range/dup_spec.rb new file mode 100644 index 0000000000..fab3c3f1b2 --- /dev/null +++ b/spec/ruby/core/range/dup_spec.rb @@ -0,0 +1,23 @@ +require_relative '../../spec_helper' + +describe "Range#dup" do + it "duplicates the range" do + original = (1..3) + copy = original.dup + copy.begin.should == 1 + copy.end.should == 3 + copy.should_not.exclude_end? + copy.should_not.equal?(original) + + copy = ("a"..."z").dup + copy.begin.should == "a" + copy.end.should == "z" + copy.should.exclude_end? + end + + it "creates an unfrozen range" do + (1..2).dup.should_not.frozen? + (1..).dup.should_not.frozen? + Range.new(1, 2).dup.should_not.frozen? + end +end diff --git a/spec/ruby/core/range/each_spec.rb b/spec/ruby/core/range/each_spec.rb new file mode 100644 index 0000000000..f10330d61d --- /dev/null +++ b/spec/ruby/core/range/each_spec.rb @@ -0,0 +1,101 @@ +require_relative '../../spec_helper' +require_relative '../enumerable/shared/enumeratorized' + +describe "Range#each" do + it "passes each element to the given block by using #succ" do + a = [] + (-5..5).each { |i| a << i } + a.should == [-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5] + + a = [] + ('A'..'D').each { |i| a << i } + a.should == ['A','B','C','D'] + + a = [] + ('A'...'D').each { |i| a << i } + a.should == ['A','B','C'] + + a = [] + (0xfffd...0xffff).each { |i| a << i } + a.should == [0xfffd, 0xfffe] + + y = mock('y') + x = mock('x') + x.should_receive(:<=>).with(y).any_number_of_times.and_return(-1) + x.should_receive(:<=>).with(x).any_number_of_times.and_return(0) + x.should_receive(:succ).any_number_of_times.and_return(y) + y.should_receive(:<=>).with(x).any_number_of_times.and_return(1) + y.should_receive(:<=>).with(y).any_number_of_times.and_return(0) + + a = [] + (x..y).each { |i| a << i } + a.should == [x, y] + end + + it "works for non-ASCII ranges" do + a = [] + ('Σ'..'Ω').each { |i| a << i } + a.should == ["Σ", "Τ", "Î¥", "Φ", "Χ", "Ψ", "Ω"] + end + + it "works with endless ranges" do + a = [] + (-2..).each { |x| break if x > 2; a << x } + a.should == [-2, -1, 0, 1, 2] + + a = [] + (-2...).each { |x| break if x > 2; a << x } + a.should == [-2, -1, 0, 1, 2] + end + + it "works with String endless ranges" do + a = [] + ('A'..).each { |x| break if x > "D"; a << x } + a.should == ["A", "B", "C", "D"] + + a = [] + ('A'...).each { |x| break if x > "D"; a << x } + a.should == ["A", "B", "C", "D"] + end + + it "raises a TypeError beginless ranges" do + -> { (..2).each { |x| x } }.should raise_error(TypeError) + end + + it "raises a TypeError if the first element does not respond to #succ" do + -> { (0.5..2.4).each { |i| i } }.should raise_error(TypeError) + + b = mock('x') + (a = mock('1')).should_receive(:<=>).with(b).and_return(1) + + -> { (a..b).each { |i| i } }.should raise_error(TypeError) + end + + it "returns self" do + range = 1..10 + range.each{}.should equal(range) + end + + it "returns an enumerator when no block given" do + enum = (1..3).each + enum.should be_an_instance_of(Enumerator) + enum.to_a.should == [1, 2, 3] + end + + it "supports Time objects that respond to #succ" do + t = Time.utc(1970) + def t.succ; self + 1 end + t_succ = t.succ + def t_succ.succ; self + 1; end + + (t..t_succ).to_a.should == [Time.utc(1970), Time.utc(1970, nil, nil, nil, nil, 1)] + (t...t_succ).to_a.should == [Time.utc(1970)] + end + + it "passes each Symbol element by using #succ" do + (:aa..:ac).each.to_a.should == [:aa, :ab, :ac] + (:aa...:ac).each.to_a.should == [:aa, :ab] + end + + it_behaves_like :enumeratorized_with_origin_size, :each, (1..3) +end diff --git a/spec/ruby/core/range/end_spec.rb b/spec/ruby/core/range/end_spec.rb new file mode 100644 index 0000000000..9e5e6f7d43 --- /dev/null +++ b/spec/ruby/core/range/end_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/end' + +describe "Range#end" do + it_behaves_like :range_end, :end +end diff --git a/spec/ruby/core/range/eql_spec.rb b/spec/ruby/core/range/eql_spec.rb new file mode 100644 index 0000000000..fa6c71840e --- /dev/null +++ b/spec/ruby/core/range/eql_spec.rb @@ -0,0 +1,10 @@ +require_relative '../../spec_helper' +require_relative 'shared/equal_value' + +describe "Range#eql?" do + it_behaves_like :range_eql, :eql? + + it "returns false if the endpoints are not eql?" do + (0..1).should_not eql(0..1.0) + end +end diff --git a/spec/ruby/core/range/equal_value_spec.rb b/spec/ruby/core/range/equal_value_spec.rb new file mode 100644 index 0000000000..83dcf5cec8 --- /dev/null +++ b/spec/ruby/core/range/equal_value_spec.rb @@ -0,0 +1,18 @@ +require_relative '../../spec_helper' +require_relative 'shared/equal_value' + +describe "Range#==" do + it_behaves_like :range_eql, :== + + it "returns true if the endpoints are ==" do + (0..1).should == (0..1.0) + end + + it "returns true if the endpoints are == for endless ranges" do + eval("(1.0..)").should == eval("(1.0..)") + end + + it "returns true if the endpoints are == for beginless ranges" do + (...10).should == (...10) + end +end diff --git a/spec/ruby/core/range/exclude_end_spec.rb b/spec/ruby/core/range/exclude_end_spec.rb new file mode 100644 index 0000000000..c4006fea78 --- /dev/null +++ b/spec/ruby/core/range/exclude_end_spec.rb @@ -0,0 +1,19 @@ +require_relative '../../spec_helper' + +describe "Range#exclude_end?" do + it "returns false if the range does not exclude the end value" do + (-2..2).should_not.exclude_end? + ('A'..'B').should_not.exclude_end? + (0.5..2.4).should_not.exclude_end? + (0xfffd..0xffff).should_not.exclude_end? + Range.new(0, 1).should_not.exclude_end? + end + + it "returns true if the range excludes the end value" do + (0...5).should.exclude_end? + ('A'...'B').should.exclude_end? + (0.5...2.4).should.exclude_end? + (0xfffd...0xffff).should.exclude_end? + Range.new(0, 1, true).should.exclude_end? + end +end diff --git a/spec/ruby/core/range/first_spec.rb b/spec/ruby/core/range/first_spec.rb new file mode 100644 index 0000000000..2af935f439 --- /dev/null +++ b/spec/ruby/core/range/first_spec.rb @@ -0,0 +1,53 @@ +require_relative '../../spec_helper' +require_relative 'shared/begin' + +describe "Range#first" do + it_behaves_like :range_begin, :first + + it "returns the specified number of elements from the beginning" do + (0..2).first(2).should == [0, 1] + end + + it "returns an empty array for an empty Range" do + (0...0).first(2).should == [] + end + + it "returns an empty array when passed zero" do + (0..2).first(0).should == [] + end + + it "returns all elements in the range when count exceeds the number of elements" do + (0..2).first(4).should == [0, 1, 2] + end + + it "raises an ArgumentError when count is negative" do + -> { (0..2).first(-1) }.should raise_error(ArgumentError) + end + + it "calls #to_int to convert the argument" do + obj = mock_int(2) + (3..7).first(obj).should == [3, 4] + end + + it "raises a TypeError if #to_int does not return an Integer" do + obj = mock("to_int") + obj.should_receive(:to_int).and_return("1") + -> { (2..3).first(obj) }.should raise_error(TypeError) + end + + it "truncates the value when passed a Float" do + (2..9).first(2.8).should == [2, 3] + end + + it "raises a TypeError when passed nil" do + -> { (2..3).first(nil) }.should raise_error(TypeError) + end + + it "raises a TypeError when passed a String" do + -> { (2..3).first("1") }.should raise_error(TypeError) + end + + it "raises a RangeError when called on an beginless range" do + -> { (..1).first }.should raise_error(RangeError) + end +end diff --git a/spec/ruby/core/range/fixtures/classes.rb b/spec/ruby/core/range/fixtures/classes.rb new file mode 100644 index 0000000000..3a1df010b2 --- /dev/null +++ b/spec/ruby/core/range/fixtures/classes.rb @@ -0,0 +1,90 @@ +module RangeSpecs + class TenfoldSucc + include Comparable + + attr_reader :n + + def initialize(n) + @n = n + end + + def <=>(other) + @n <=> other.n + end + + def succ + self.class.new(@n * 10) + end + end + + # Custom Range classes Xs and Ys + class Custom + include Comparable + attr_reader :length + + def initialize(n) + @length = n + end + + def eql?(other) + inspect.eql? other.inspect + end + alias :== :eql? + + def inspect + 'custom' + end + + def <=>(other) + @length <=> other.length + end + end + + class WithoutSucc + include Comparable + attr_reader :n + + def initialize(n) + @n = n + end + + def eql?(other) + inspect.eql? other.inspect + end + alias :== :eql? + + def inspect + "WithoutSucc(#{@n})" + end + + def <=>(other) + @n <=> other.n + end + end + + class Xs < Custom # represent a string of 'x's + def succ + Xs.new(@length + 1) + end + + def inspect + 'x' * @length + end + end + + class Ys < Custom # represent a string of 'y's + def succ + Ys.new(@length + 1) + end + + def inspect + 'y' * @length + end + end + + class MyRange < Range + end + + class ComparisonError < RuntimeError + end +end diff --git a/spec/ruby/core/range/frozen_spec.rb b/spec/ruby/core/range/frozen_spec.rb new file mode 100644 index 0000000000..8dab5e5339 --- /dev/null +++ b/spec/ruby/core/range/frozen_spec.rb @@ -0,0 +1,25 @@ +require_relative '../../spec_helper' + +# There is no Range#frozen? method but this feels like the best place for these specs +describe "Range#frozen?" do + it "is true for literal ranges" do + (1..2).should.frozen? + (1..).should.frozen? + (..1).should.frozen? + end + + it "is true for Range.new" do + Range.new(1, 2).should.frozen? + Range.new(1, nil).should.frozen? + Range.new(nil, 1).should.frozen? + end + + it "is false for instances of a subclass of Range" do + sub_range = Class.new(Range).new(1, 2) + sub_range.should_not.frozen? + end + + it "is false for Range.allocate" do + Range.allocate.should_not.frozen? + end +end diff --git a/spec/ruby/core/range/hash_spec.rb b/spec/ruby/core/range/hash_spec.rb new file mode 100644 index 0000000000..4f2681e86e --- /dev/null +++ b/spec/ruby/core/range/hash_spec.rb @@ -0,0 +1,24 @@ +require_relative '../../spec_helper' + +describe "Range#hash" do + it "is provided" do + (0..1).respond_to?(:hash).should == true + ('A'..'Z').respond_to?(:hash).should == true + (0xfffd..0xffff).respond_to?(:hash).should == true + (0.5..2.4).respond_to?(:hash).should == true + end + + it "generates the same hash values for Ranges with the same start, end and exclude_end? values" do + (0..1).hash.should == (0..1).hash + (0...10).hash.should == (0...10).hash + (0..10).hash.should_not == (0...10).hash + end + + it "generates an Integer for the hash value" do + (0..0).hash.should be_an_instance_of(Integer) + (0..1).hash.should be_an_instance_of(Integer) + (0...10).hash.should be_an_instance_of(Integer) + (0..10).hash.should be_an_instance_of(Integer) + end + +end diff --git a/spec/ruby/core/range/include_spec.rb b/spec/ruby/core/range/include_spec.rb new file mode 100644 index 0000000000..449e18985b --- /dev/null +++ b/spec/ruby/core/range/include_spec.rb @@ -0,0 +1,14 @@ +# encoding: binary +require_relative '../../spec_helper' +require_relative 'shared/cover_and_include' +require_relative 'shared/include' +require_relative 'shared/cover' + +describe "Range#include?" do + it_behaves_like :range_cover_and_include, :include? + it_behaves_like :range_include, :include? + + it "does not include U+9995 in the range U+0999..U+9999" do + ("\u{999}".."\u{9999}").include?("\u{9995}").should be_false + end +end diff --git a/spec/ruby/core/range/initialize_spec.rb b/spec/ruby/core/range/initialize_spec.rb new file mode 100644 index 0000000000..c653caf0c6 --- /dev/null +++ b/spec/ruby/core/range/initialize_spec.rb @@ -0,0 +1,41 @@ +require_relative '../../spec_helper' + +describe "Range#initialize" do + before do + @range = Range.allocate + end + + it "is private" do + Range.should have_private_instance_method("initialize") + end + + it "initializes correctly the Range object when given 2 arguments" do + -> { @range.send(:initialize, 0, 1) }.should_not raise_error + end + + it "initializes correctly the Range object when given 3 arguments" do + -> { @range.send(:initialize, 0, 1, true) }.should_not raise_error + end + + it "raises an ArgumentError if passed without or with only one argument" do + -> { @range.send(:initialize) }.should raise_error(ArgumentError) + -> { @range.send(:initialize, 1) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if passed with four or more arguments" do + -> { @range.send(:initialize, 1, 3, 5, 7) }.should raise_error(ArgumentError) + -> { @range.send(:initialize, 1, 3, 5, 7, 9) }.should raise_error(ArgumentError) + end + + it "raises a FrozenError if called on an already initialized Range" do + -> { (0..1).send(:initialize, 1, 3) }.should raise_error(FrozenError) + -> { (0..1).send(:initialize, 1, 3, true) }.should raise_error(FrozenError) + end + + it "raises an ArgumentError if arguments don't respond to <=>" do + o1 = Object.new + o2 = Object.new + + -> { @range.send(:initialize, o1, o2) }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/range/inspect_spec.rb b/spec/ruby/core/range/inspect_spec.rb new file mode 100644 index 0000000000..072de123b7 --- /dev/null +++ b/spec/ruby/core/range/inspect_spec.rb @@ -0,0 +1,29 @@ +require_relative '../../spec_helper' + +describe "Range#inspect" do + it "provides a printable form, using #inspect to convert the start and end objects" do + ('A'..'Z').inspect.should == '"A".."Z"' + ('A'...'Z').inspect.should == '"A"..."Z"' + + (0..21).inspect.should == "0..21" + (-8..0).inspect.should == "-8..0" + (-411..959).inspect.should == "-411..959" + (0xfff..0xfffff).inspect.should == "4095..1048575" + (0.5..2.4).inspect.should == "0.5..2.4" + end + + it "works for endless ranges" do + eval("(1..)").inspect.should == "1.." + eval("(0.1...)").inspect.should == "0.1..." + end + + it "works for beginless ranges" do + (..1).inspect.should == "..1" + (...0.1).inspect.should == "...0.1" + end + + it "works for nil ... nil ranges" do + (..nil).inspect.should == "nil..nil" + eval("(nil...)").inspect.should == "nil...nil" + end +end diff --git a/spec/ruby/core/range/last_spec.rb b/spec/ruby/core/range/last_spec.rb new file mode 100644 index 0000000000..82b3e2ff53 --- /dev/null +++ b/spec/ruby/core/range/last_spec.rb @@ -0,0 +1,57 @@ +require_relative '../../spec_helper' +require_relative 'shared/end' + +describe "Range#last" do + it_behaves_like :range_end, :last + + it "returns the specified number of elements from the end" do + (1..5).last(3).should == [3, 4, 5] + end + + it "returns the specified number if elements for single element inclusive range" do + (1..1).last(1).should == [1] + end + + it "returns an empty array for an empty Range" do + (0...0).last(2).should == [] + end + + it "returns an empty array when passed zero" do + (0..2).last(0).should == [] + end + + it "returns all elements in the range when count exceeds the number of elements" do + (2..4).last(5).should == [2, 3, 4] + end + + it "raises an ArgumentError when count is negative" do + -> { (0..2).last(-1) }.should raise_error(ArgumentError) + end + + it "calls #to_int to convert the argument" do + obj = mock_int(2) + (3..7).last(obj).should == [6, 7] + end + + it "raises a TypeError if #to_int does not return an Integer" do + obj = mock("to_int") + obj.should_receive(:to_int).and_return("1") + -> { (2..3).last(obj) }.should raise_error(TypeError) + end + + it "truncates the value when passed a Float" do + (2..9).last(2.8).should == [8, 9] + end + + it "raises a TypeError when passed nil" do + -> { (2..3).last(nil) }.should raise_error(TypeError) + end + + it "raises a TypeError when passed a String" do + -> { (2..3).last("1") }.should raise_error(TypeError) + end + + it "raises a RangeError when called on an endless range" do + -> { eval("(1..)").last }.should raise_error(RangeError) + end +end diff --git a/spec/ruby/core/range/max_spec.rb b/spec/ruby/core/range/max_spec.rb new file mode 100644 index 0000000000..09371f5298 --- /dev/null +++ b/spec/ruby/core/range/max_spec.rb @@ -0,0 +1,115 @@ +require_relative '../../spec_helper' + +describe "Range#max" do + it "returns the maximum value in the range when called with no arguments" do + (1..10).max.should == 10 + (1...10).max.should == 9 + (0...2**64).max.should == 18446744073709551615 + ('f'..'l').max.should == 'l' + ('a'...'f').max.should == 'e' + end + + it "returns the maximum value in the Float range when called with no arguments" do + (303.20..908.1111).max.should == 908.1111 + end + + it "raises TypeError when called on an exclusive range and a non Integer value" do + -> { (303.20...908.1111).max }.should raise_error(TypeError) + end + + it "returns nil when the endpoint is less than the start point" do + (100..10).max.should be_nil + ('z'..'l').max.should be_nil + end + + it "returns nil when the endpoint equals the start point and the range is exclusive" do + (5...5).max.should be_nil + end + + it "returns the endpoint when the endpoint equals the start point and the range is inclusive" do + (5..5).max.should equal(5) + end + + it "returns nil when the endpoint is less than the start point in a Float range" do + (3003.20..908.1111).max.should be_nil + end + + it "returns end point when the range is Time..Time(included end point)" do + time_start = Time.now + time_end = Time.now + 1.0 + (time_start..time_end).max.should equal(time_end) + end + + it "raises TypeError when called on a Time...Time(excluded end point)" do + time_start = Time.now + time_end = Time.now + 1.0 + -> { (time_start...time_end).max }.should raise_error(TypeError) + end + + it "raises RangeError when called on an endless range" do + -> { eval("(1..)").max }.should raise_error(RangeError) + end + + it "returns the end point for beginless ranges" do + (..1).max.should == 1 + (..1.0).max.should == 1.0 + end + + ruby_version_is ""..."4.0" do + it "raises for an exclusive beginless Integer range" do + -> { + (...1).max + }.should raise_error(TypeError, 'cannot exclude end value with non Integer begin value') + end + end + + ruby_version_is "4.0" do + it "returns the end point for exclusive beginless Integer ranges" do + (...1).max.should == 0 + end + end + + it "raises for an exclusive beginless non Integer range" do + -> { + (...1.0).max + }.should raise_error(TypeError, 'cannot exclude non Integer end value') + end +end + +describe "Range#max given a block" do + it "passes each pair of values in the range to the block" do + acc = [] + (1..10).max {|a,b| acc << [a,b]; a } + acc.flatten! + (1..10).each do |value| + acc.include?(value).should be_true + end + end + + it "passes each pair of elements to the block in reversed order" do + acc = [] + (1..5).max {|a,b| acc << [a,b]; a } + acc.should == [[2,1],[3,2], [4,3], [5, 4]] + end + + it "calls #> and #< on the return value of the block" do + obj = mock('obj') + obj.should_receive(:>).exactly(2).times + obj.should_receive(:<).exactly(2).times + (1..3).max {|a,b| obj } + end + + it "returns the element the block determines to be the maximum" do + (1..3).max {|a,b| -3 }.should == 1 + end + + it "returns nil when the endpoint is less than the start point" do + (100..10).max {|x,y| x <=> y}.should be_nil + ('z'..'l').max {|x,y| x <=> y}.should be_nil + (5...5).max {|x,y| x <=> y}.should be_nil + end + + it "raises RangeError when called with custom comparison method on an beginless range" do + -> { (..1).max {|a, b| a} }.should raise_error(RangeError) + end +end diff --git a/spec/ruby/core/range/member_spec.rb b/spec/ruby/core/range/member_spec.rb new file mode 100644 index 0000000000..78299ae9e5 --- /dev/null +++ b/spec/ruby/core/range/member_spec.rb @@ -0,0 +1,10 @@ +# encoding: binary +require_relative '../../spec_helper' +require_relative 'shared/cover_and_include' +require_relative 'shared/include' +require_relative 'shared/cover' + +describe "Range#member?" do + it_behaves_like :range_cover_and_include, :member? + it_behaves_like :range_include, :member? +end diff --git a/spec/ruby/core/range/min_spec.rb b/spec/ruby/core/range/min_spec.rb new file mode 100644 index 0000000000..89310ee589 --- /dev/null +++ b/spec/ruby/core/range/min_spec.rb @@ -0,0 +1,88 @@ +require_relative '../../spec_helper' + +describe "Range#min" do + it "returns the minimum value in the range when called with no arguments" do + (1..10).min.should == 1 + ('f'..'l').min.should == 'f' + end + + it "returns the minimum value in the Float range when called with no arguments" do + (303.20..908.1111).min.should == 303.20 + end + + it "returns nil when the start point is greater than the endpoint" do + (100..10).min.should be_nil + ('z'..'l').min.should be_nil + end + + it "returns nil when the endpoint equals the start point and the range is exclusive" do + (7...7).min.should be_nil + end + + it "returns the start point when the endpoint equals the start point and the range is inclusive" do + (7..7).min.should equal(7) + end + + it "returns nil when the start point is greater than the endpoint in a Float range" do + (3003.20..908.1111).min.should be_nil + end + + it "returns start point when the range is Time..Time(included end point)" do + time_start = Time.now + time_end = Time.now + 1.0 + (time_start..time_end).min.should equal(time_start) + end + + it "returns start point when the range is Time...Time(excluded end point)" do + time_start = Time.now + time_end = Time.now + 1.0 + (time_start...time_end).min.should equal(time_start) + end + + it "returns the start point for endless ranges" do + eval("(1..)").min.should == 1 + eval("(1.0...)").min.should == 1.0 + end + + it "raises RangeError when called on an beginless range" do + -> { (..1).min }.should raise_error(RangeError) + end +end + +describe "Range#min given a block" do + it "passes each pair of values in the range to the block" do + acc = [] + (1..10).min {|a,b| acc << [a,b]; a } + acc.flatten! + (1..10).each do |value| + acc.include?(value).should be_true + end + end + + it "passes each pair of elements to the block where the first argument is the current element, and the last is the first element" do + acc = [] + (1..5).min {|a,b| acc << [a,b]; a } + acc.should == [[2, 1], [3, 1], [4, 1], [5, 1]] + end + + it "calls #> and #< on the return value of the block" do + obj = mock('obj') + obj.should_receive(:>).exactly(2).times + obj.should_receive(:<).exactly(2).times + (1..3).min {|a,b| obj } + end + + it "returns the element the block determines to be the minimum" do + (1..3).min {|a,b| -3 }.should == 3 + end + + it "returns nil when the start point is greater than the endpoint" do + (100..10).min {|x,y| x <=> y}.should be_nil + ('z'..'l').min {|x,y| x <=> y}.should be_nil + (7...7).min {|x,y| x <=> y}.should be_nil + end + + it "raises RangeError when called with custom comparison method on an endless range" do + -> { eval("(1..)").min {|a, b| a} }.should raise_error(RangeError) + end +end diff --git a/spec/ruby/core/range/minmax_spec.rb b/spec/ruby/core/range/minmax_spec.rb new file mode 100644 index 0000000000..6651ae3726 --- /dev/null +++ b/spec/ruby/core/range/minmax_spec.rb @@ -0,0 +1,130 @@ +require_relative '../../spec_helper' + +describe 'Range#minmax' do + before(:each) do + @x = mock('x') + @y = mock('y') + + @x.should_receive(:<=>).with(@y).any_number_of_times.and_return(-1) # x < y + @x.should_receive(:<=>).with(@x).any_number_of_times.and_return(0) # x == x + @y.should_receive(:<=>).with(@x).any_number_of_times.and_return(1) # y > x + @y.should_receive(:<=>).with(@y).any_number_of_times.and_return(0) # y == y + end + + describe 'on an inclusive range' do + it 'should raise RangeError on an endless range without iterating the range' do + @x.should_not_receive(:succ) + + range = (@x..) + + -> { range.minmax }.should raise_error(RangeError, 'cannot get the maximum of endless range') + end + + it 'raises RangeError or ArgumentError on a beginless range' do + range = (..@x) + + -> { range.minmax }.should raise_error(StandardError) { |e| + if RangeError === e + # error from #min + -> { raise e }.should raise_error(RangeError, 'cannot get the minimum of beginless range') + else + # error from #max + -> { raise e }.should raise_error(ArgumentError, 'comparison of NilClass with MockObject failed') + end + } + end + + it 'should return beginning of range if beginning and end are equal without iterating the range' do + @x.should_not_receive(:succ) + + (@x..@x).minmax.should == [@x, @x] + end + + it 'should return nil pair if beginning is greater than end without iterating the range' do + @y.should_not_receive(:succ) + + (@y..@x).minmax.should == [nil, nil] + end + + it 'should return the minimum and maximum values for a non-numeric range without iterating the range' do + @x.should_not_receive(:succ) + + (@x..@y).minmax.should == [@x, @y] + end + + it 'should return the minimum and maximum values for a numeric range' do + (1..3).minmax.should == [1, 3] + end + + it 'should return the minimum and maximum values for a numeric range without iterating the range' do + # We cannot set expectations on integers, + # so we "prevent" iteration by picking a value that would iterate until the spec times out. + range_end = Float::INFINITY + + (1..range_end).minmax.should == [1, range_end] + end + + it 'should return the minimum and maximum values according to the provided block by iterating the range' do + @x.should_receive(:succ).once.and_return(@y) + + (@x..@y).minmax { |x, y| - (x <=> y) }.should == [@y, @x] + end + end + + describe 'on an exclusive range' do + it 'should raise RangeError on an endless range' do + @x.should_not_receive(:succ) + range = (@x...) + + -> { range.minmax }.should raise_error(RangeError, 'cannot get the maximum of endless range') + end + + it 'should raise RangeError on a beginless range' do + range = (...@x) + + -> { range.minmax }.should raise_error(RangeError, + /cannot get the maximum of beginless range with custom comparison method|cannot get the minimum of beginless range/) + end + + it 'should return nil pair if beginning and end are equal without iterating the range' do + @x.should_not_receive(:succ) + + (@x...@x).minmax.should == [nil, nil] + end + + it 'should return nil pair if beginning is greater than end without iterating the range' do + @y.should_not_receive(:succ) + + (@y...@x).minmax.should == [nil, nil] + end + + it 'should return the minimum and maximum values for a non-numeric range by iterating the range' do + @x.should_receive(:succ).once.and_return(@y) + + (@x...@y).minmax.should == [@x, @x] + end + + it 'should return the minimum and maximum values for a numeric range' do + (1...3).minmax.should == [1, 2] + end + + it 'should return the minimum and maximum values for a numeric range without iterating the range' do + # We cannot set expectations on integers, + # so we "prevent" iteration by picking a value that would iterate until the spec times out. + range_end = bignum_value + + (1...range_end).minmax.should == [1, range_end - 1] + end + + it 'raises TypeError if the end value is not an integer' do + range = (0...Float::INFINITY) + -> { range.minmax }.should raise_error(TypeError, 'cannot exclude non Integer end value') + end + + it 'should return the minimum and maximum values according to the provided block by iterating the range' do + @x.should_receive(:succ).once.and_return(@y) + + (@x...@y).minmax { |x, y| - (x <=> y) }.should == [@x, @x] + end + end +end diff --git a/spec/ruby/core/range/new_spec.rb b/spec/ruby/core/range/new_spec.rb new file mode 100644 index 0000000000..3cab887799 --- /dev/null +++ b/spec/ruby/core/range/new_spec.rb @@ -0,0 +1,77 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Range.new" do + it "constructs a range using the given start and end" do + range = Range.new('a', 'c') + range.should == ('a'..'c') + + range.first.should == 'a' + range.last.should == 'c' + end + + it "includes the end object when the third parameter is omitted or false" do + Range.new('a', 'c').to_a.should == ['a', 'b', 'c'] + Range.new(1, 3).to_a.should == [1, 2, 3] + + Range.new('a', 'c', false).to_a.should == ['a', 'b', 'c'] + Range.new(1, 3, false).to_a.should == [1, 2, 3] + + Range.new('a', 'c', true).to_a.should == ['a', 'b'] + Range.new(1, 3, 1).to_a.should == [1, 2] + + Range.new(1, 3, mock('[1,2]')).to_a.should == [1, 2] + Range.new(1, 3, :test).to_a.should == [1, 2] + end + + it "raises an ArgumentError when the given start and end can't be compared by using #<=>" do + -> { Range.new(1, mock('x')) }.should raise_error(ArgumentError) + -> { Range.new(mock('x'), mock('y')) }.should raise_error(ArgumentError) + + b = mock('x') + (a = mock('nil')).should_receive(:<=>).with(b).and_return(nil) + -> { Range.new(a, b) }.should raise_error(ArgumentError) + end + + it "does not rescue exception raised in #<=> when compares the given start and end" do + b = mock('a') + a = mock('b') + a.should_receive(:<=>).with(b).and_raise(RangeSpecs::ComparisonError) + + -> { Range.new(a, b) }.should raise_error(RangeSpecs::ComparisonError) + end + + describe "beginless/endless range" do + it "allows beginless left boundary" do + range = Range.new(nil, 1) + range.begin.should == nil + end + + it "distinguishes ranges with included and excluded right boundary" do + range_exclude = Range.new(nil, 1, true) + range_include = Range.new(nil, 1, false) + + range_exclude.should_not == range_include + end + + it "allows endless right boundary" do + range = Range.new(1, nil) + range.end.should == nil + end + + it "distinguishes ranges with included and excluded right boundary" do + range_exclude = Range.new(1, nil, true) + range_include = Range.new(1, nil, false) + + range_exclude.should_not == range_include + end + + it "creates a frozen range if the class is Range.class" do + Range.new(1, 2).should.frozen? + end + + it "does not create a frozen range if the class is not Range.class" do + Class.new(Range).new(1, 2).should_not.frozen? + end + end +end diff --git a/spec/ruby/core/range/overlap_spec.rb b/spec/ruby/core/range/overlap_spec.rb new file mode 100644 index 0000000000..9b6fc13493 --- /dev/null +++ b/spec/ruby/core/range/overlap_spec.rb @@ -0,0 +1,89 @@ +require_relative '../../spec_helper' + +ruby_version_is '3.3' do + describe "Range#overlap?" do + it "returns true if other Range overlaps self" do + (0..2).overlap?(1..3).should == true + (1..3).overlap?(0..2).should == true + (0..2).overlap?(0..2).should == true + (0..3).overlap?(1..2).should == true + (1..2).overlap?(0..3).should == true + + ('a'..'c').overlap?('b'..'d').should == true + end + + it "returns false if other Range does not overlap self" do + (0..2).overlap?(3..4).should == false + (0..2).overlap?(-4..-1).should == false + + ('a'..'c').overlap?('d'..'f').should == false + end + + it "raises TypeError when called with non-Range argument" do + -> { + (0..2).overlap?(1) + }.should raise_error(TypeError, "wrong argument type Integer (expected Range)") + end + + it "returns true when beginningless and endless Ranges overlap" do + (0..2).overlap?(..3).should == true + (0..2).overlap?(..1).should == true + (0..2).overlap?(..0).should == true + + (..3).overlap?(0..2).should == true + (..1).overlap?(0..2).should == true + (..0).overlap?(0..2).should == true + + (0..2).overlap?(-1..).should == true + (0..2).overlap?(1..).should == true + (0..2).overlap?(2..).should == true + + (-1..).overlap?(0..2).should == true + (1..).overlap?(0..2).should == true + (2..).overlap?(0..2).should == true + + (0..).overlap?(2..).should == true + (..0).overlap?(..2).should == true + end + + it "returns false when beginningless and endless Ranges do not overlap" do + (0..2).overlap?(..-1).should == false + (0..2).overlap?(3..).should == false + + (..-1).overlap?(0..2).should == false + (3..).overlap?(0..2).should == false + end + + it "returns false when Ranges are not compatible" do + (0..2).overlap?('a'..'d').should == false + end + + it "return false when self is empty" do + (2..0).overlap?(1..3).should == false + (2...2).overlap?(1..3).should == false + (1...1).overlap?(1...1).should == false + (2..0).overlap?(2..0).should == false + + ('c'..'a').overlap?('b'..'d').should == false + ('a'...'a').overlap?('b'..'d').should == false + ('b'...'b').overlap?('b'...'b').should == false + ('c'...'a').overlap?('c'...'a').should == false + end + + it "return false when other Range is empty" do + (1..3).overlap?(2..0).should == false + (1..3).overlap?(2...2).should == false + + ('b'..'d').overlap?('c'..'a').should == false + ('b'..'d').overlap?('c'...'c').should == false + end + + it "takes into account exclusive end" do + (0...2).overlap?(2..4).should == false + (2..4).overlap?(0...2).should == false + + ('a'...'c').overlap?('c'..'e').should == false + ('c'..'e').overlap?('a'...'c').should == false + end + end +end diff --git a/spec/ruby/core/range/percent_spec.rb b/spec/ruby/core/range/percent_spec.rb new file mode 100644 index 0000000000..5ec6770ddb --- /dev/null +++ b/spec/ruby/core/range/percent_spec.rb @@ -0,0 +1,16 @@ +require_relative '../../spec_helper' + +describe "Range#%" do + it "works as a Range#step" do + aseq = (1..10) % 2 + aseq.class.should == Enumerator::ArithmeticSequence + aseq.begin.should == 1 + aseq.end.should == 10 + aseq.step.should == 2 + aseq.to_a.should == [1, 3, 5, 7, 9] + end + + it "produces an arithmetic sequence with a percent sign in #inspect" do + ((1..10) % 2).inspect.should == "((1..10).%(2))" + end +end diff --git a/spec/ruby/core/range/range_spec.rb b/spec/ruby/core/range/range_spec.rb new file mode 100644 index 0000000000..8e9433f8c1 --- /dev/null +++ b/spec/ruby/core/range/range_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' + +describe "Range" do + it "includes Enumerable" do + Range.include?(Enumerable).should == true + end +end diff --git a/spec/ruby/core/range/reverse_each_spec.rb b/spec/ruby/core/range/reverse_each_spec.rb new file mode 100644 index 0000000000..56390cc0da --- /dev/null +++ b/spec/ruby/core/range/reverse_each_spec.rb @@ -0,0 +1,103 @@ +require_relative '../../spec_helper' + +ruby_version_is "3.3" do + describe "Range#reverse_each" do + it "traverses the Range in reverse order and passes each element to block" do + a = [] + (1..3).reverse_each { |i| a << i } + a.should == [3, 2, 1] + + a = [] + (1...3).reverse_each { |i| a << i } + a.should == [2, 1] + end + + it "returns self" do + r = (1..3) + r.reverse_each { |x| }.should equal(r) + end + + it "returns an Enumerator if no block given" do + enum = (1..3).reverse_each + enum.should be_an_instance_of(Enumerator) + enum.to_a.should == [3, 2, 1] + end + + it "raises a TypeError for endless Ranges of Integers" do + -> { + (1..).reverse_each.take(3) + }.should raise_error(TypeError, "can't iterate from NilClass") + end + + it "raises a TypeError for endless Ranges of non-Integers" do + -> { + ("a"..).reverse_each.take(3) + }.should raise_error(TypeError, "can't iterate from NilClass") + end + + context "Integer boundaries" do + it "supports beginningless Ranges" do + (..5).reverse_each.take(3).should == [5, 4, 3] + end + end + + context "non-Integer boundaries" do + it "uses #succ to iterate a Range of non-Integer elements" do + y = mock('y') + x = mock('x') + + x.should_receive(:succ).any_number_of_times.and_return(y) + x.should_receive(:<=>).with(y).any_number_of_times.and_return(-1) + x.should_receive(:<=>).with(x).any_number_of_times.and_return(0) + y.should_receive(:<=>).with(x).any_number_of_times.and_return(1) + y.should_receive(:<=>).with(y).any_number_of_times.and_return(0) + + a = [] + (x..y).each { |i| a << i } + a.should == [x, y] + end + + it "uses #succ to iterate a Range of Strings" do + a = [] + ('A'..'D').reverse_each { |i| a << i } + a.should == ['D','C','B','A'] + end + + it "uses #succ to iterate a Range of Symbols" do + a = [] + (:A..:D).reverse_each { |i| a << i } + a.should == [:D, :C, :B, :A] + end + + it "raises a TypeError when `begin` value does not respond to #succ" do + -> { (Time.now..Time.now).reverse_each { |x| x } }.should raise_error(TypeError, /can't iterate from Time/) + -> { (//..//).reverse_each { |x| x } }.should raise_error(TypeError, /can't iterate from Regexp/) + -> { ([]..[]).reverse_each { |x| x } }.should raise_error(TypeError, /can't iterate from Array/) + end + + it "does not support beginningless Ranges" do + -> { + (..'a').reverse_each { |x| x } + }.should raise_error(TypeError, /can't iterate from NilClass/) + end + end + + context "when no block is given" do + describe "returned Enumerator size" do + it "returns the Range size when Range size is finite" do + (1..3).reverse_each.size.should == 3 + end + + ruby_bug "#20936", "3.4"..."4.0" do + it "returns Infinity when Range size is infinite" do + (..3).reverse_each.size.should == Float::INFINITY + end + end + + it "returns nil when Range size is unknown" do + ('a'..'z').reverse_each.size.should == nil + end + end + end + end +end diff --git a/spec/ruby/core/range/shared/begin.rb b/spec/ruby/core/range/shared/begin.rb new file mode 100644 index 0000000000..f660e3faf9 --- /dev/null +++ b/spec/ruby/core/range/shared/begin.rb @@ -0,0 +1,10 @@ +describe :range_begin, shared: true do + it "returns the first element of self" do + (-1..1).send(@method).should == -1 + (0..1).send(@method).should == 0 + (0xffff...0xfffff).send(@method).should == 65535 + ('Q'..'T').send(@method).should == 'Q' + ('Q'...'T').send(@method).should == 'Q' + (0.5..2.4).send(@method).should == 0.5 + end +end diff --git a/spec/ruby/core/range/shared/cover.rb b/spec/ruby/core/range/shared/cover.rb new file mode 100644 index 0000000000..eaefb45942 --- /dev/null +++ b/spec/ruby/core/range/shared/cover.rb @@ -0,0 +1,193 @@ +# encoding: binary +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' + +describe :range_cover, shared: true do + it "uses the range element's <=> to make the comparison" do + a = mock('a') + a.should_receive(:<=>).twice.and_return(-1,-1) + (a..'z').send(@method, 'b').should be_true + end + + it "uses a continuous inclusion test" do + ('a'..'f').send(@method, 'aa').should be_true + ('a'..'f').send(@method, 'babe').should be_true + ('a'..'f').send(@method, 'baby').should be_true + ('a'..'f').send(@method, 'ga').should be_false + (-10..-2).send(@method, -2.5).should be_true + end + + describe "on string elements" do + it "returns true if other is matched by element.succ" do + ('a'..'c').send(@method, 'b').should be_true + ('a'...'c').send(@method, 'b').should be_true + end + + it "returns true if other is not matched by element.succ" do + ('a'..'c').send(@method, 'bc').should be_true + ('a'...'c').send(@method, 'bc').should be_true + end + end + + describe "with weird succ" do + describe "when included end value" do + before :each do + @range = RangeSpecs::TenfoldSucc.new(1)..RangeSpecs::TenfoldSucc.new(99) + end + + it "returns false if other is less than first element" do + @range.send(@method, RangeSpecs::TenfoldSucc.new(0)).should be_false + end + + it "returns true if other is equal as first element" do + @range.send(@method, RangeSpecs::TenfoldSucc.new(1)).should be_true + end + + it "returns true if other is matched by element.succ" do + @range.send(@method, RangeSpecs::TenfoldSucc.new(10)).should be_true + end + + it "returns true if other is not matched by element.succ" do + @range.send(@method, RangeSpecs::TenfoldSucc.new(2)).should be_true + end + + it "returns true if other is equal as last element but not matched by element.succ" do + @range.send(@method, RangeSpecs::TenfoldSucc.new(99)).should be_true + end + + it "returns false if other is greater than last element but matched by element.succ" do + @range.send(@method, RangeSpecs::TenfoldSucc.new(100)).should be_false + end + end + + describe "when excluded end value" do + before :each do + @range = RangeSpecs::TenfoldSucc.new(1)...RangeSpecs::TenfoldSucc.new(99) + end + + it "returns false if other is less than first element" do + @range.send(@method, RangeSpecs::TenfoldSucc.new(0)).should be_false + end + + it "returns true if other is equal as first element" do + @range.send(@method, RangeSpecs::TenfoldSucc.new(1)).should be_true + end + + it "returns true if other is matched by element.succ" do + @range.send(@method, RangeSpecs::TenfoldSucc.new(10)).should be_true + end + + it "returns true if other is not matched by element.succ" do + @range.send(@method, RangeSpecs::TenfoldSucc.new(2)).should be_true + end + + it "returns false if other is equal as last element but not matched by element.succ" do + @range.send(@method, RangeSpecs::TenfoldSucc.new(99)).should be_false + end + + it "returns false if other is greater than last element but matched by element.succ" do + @range.send(@method, RangeSpecs::TenfoldSucc.new(100)).should be_false + end + end + end +end + +describe :range_cover_subrange, shared: true do + context "range argument" do + it "accepts range argument" do + (0..10).send(@method, (3..7)).should be_true + (0..10).send(@method, (3..15)).should be_false + (0..10).send(@method, (-2..7)).should be_false + + (1.1..7.9).send(@method, (2.5..6.5)).should be_true + (1.1..7.9).send(@method, (2.5..8.5)).should be_false + (1.1..7.9).send(@method, (0.5..6.5)).should be_false + + ('c'..'i').send(@method, ('d'..'f')).should be_true + ('c'..'i').send(@method, ('d'..'z')).should be_false + ('c'..'i').send(@method, ('a'..'f')).should be_false + + range_10_100 = RangeSpecs::TenfoldSucc.new(10)..RangeSpecs::TenfoldSucc.new(100) + range_20_90 = RangeSpecs::TenfoldSucc.new(20)..RangeSpecs::TenfoldSucc.new(90) + range_20_110 = RangeSpecs::TenfoldSucc.new(20)..RangeSpecs::TenfoldSucc.new(110) + range_0_90 = RangeSpecs::TenfoldSucc.new(0)..RangeSpecs::TenfoldSucc.new(90) + + range_10_100.send(@method, range_20_90).should be_true + range_10_100.send(@method, range_20_110).should be_false + range_10_100.send(@method, range_0_90).should be_false + end + + it "supports boundaries of different comparable types" do + (0..10).send(@method, (3.1..7.9)).should be_true + (0..10).send(@method, (3.1..15.9)).should be_false + (0..10).send(@method, (-2.1..7.9)).should be_false + end + + it "returns false if types are not comparable" do + (0..10).send(@method, ('a'..'z')).should be_false + (0..10).send(@method, (RangeSpecs::TenfoldSucc.new(0)..RangeSpecs::TenfoldSucc.new(100))).should be_false + end + + it "honors exclusion of right boundary (:exclude_end option)" do + # Integer + (0..10).send(@method, (0..10)).should be_true + (0...10).send(@method, (0...10)).should be_true + + (0..10).send(@method, (0...10)).should be_true + (0...10).send(@method, (0..10)).should be_false + + (0...11).send(@method, (0..10)).should be_true + (0..10).send(@method, (0...11)).should be_true + + # Float + (0..10.1).send(@method, (0..10.1)).should be_true + (0...10.1).send(@method, (0...10.1)).should be_true + + (0..10.1).send(@method, (0...10.1)).should be_true + (0...10.1).send(@method, (0..10.1)).should be_false + + (0...11.1).send(@method, (0..10.1)).should be_true + (0..10.1).send(@method, (0...11.1)).should be_false + end + end + + it "allows self to be a beginless range" do + (...10).send(@method, (3..7)).should be_true + (...10).send(@method, (3..15)).should be_false + + (..7.9).send(@method, (2.5..6.5)).should be_true + (..7.9).send(@method, (2.5..8.5)).should be_false + + (..'i').send(@method, ('d'..'f')).should be_true + (..'i').send(@method, ('d'..'z')).should be_false + end + + it "allows self to be a endless range" do + eval("(0...)").send(@method, (3..7)).should be_true + eval("(5...)").send(@method, (3..15)).should be_false + + eval("(1.1..)").send(@method, (2.5..6.5)).should be_true + eval("(3.3..)").send(@method, (2.5..8.5)).should be_false + + eval("('a'..)").send(@method, ('d'..'f')).should be_true + eval("('p'..)").send(@method, ('d'..'z')).should be_false + end + + it "accepts beginless range argument" do + (..10).send(@method, (...10)).should be_true + (0..10).send(@method, (...10)).should be_false + + (1.1..7.9).send(@method, (...10.5)).should be_false + + ('c'..'i').send(@method, (..'i')).should be_false + end + + it "accepts endless range argument" do + eval("(0..)").send(@method, eval("(0...)")).should be_true + (0..10).send(@method, eval("(0...)")).should be_false + + (1.1..7.9).send(@method, eval("(0.8...)")).should be_false + + ('c'..'i').send(@method, eval("('a'..)")).should be_false + end +end diff --git a/spec/ruby/core/range/shared/cover_and_include.rb b/spec/ruby/core/range/shared/cover_and_include.rb new file mode 100644 index 0000000000..13fc5e1790 --- /dev/null +++ b/spec/ruby/core/range/shared/cover_and_include.rb @@ -0,0 +1,86 @@ +# encoding: binary +require_relative '../../../spec_helper' + +describe :range_cover_and_include, shared: true do + it "returns true if other is an element of self" do + (0..5).send(@method, 2).should == true + (-5..5).send(@method, 0).should == true + (-1...1).send(@method, 10.5).should == false + (-10..-2).send(@method, -2.5).should == true + ('C'..'X').send(@method, 'M').should == true + ('C'..'X').send(@method, 'A').should == false + ('B'...'W').send(@method, 'W').should == false + ('B'...'W').send(@method, 'Q').should == true + (0xffff..0xfffff).send(@method, 0xffffd).should == true + (0xffff..0xfffff).send(@method, 0xfffd).should == false + (0.5..2.4).send(@method, 2).should == true + (0.5..2.4).send(@method, 2.5).should == false + (0.5..2.4).send(@method, 2.4).should == true + (0.5...2.4).send(@method, 2.4).should == false + end + + it "returns true if other is an element of self for endless ranges" do + (1..).send(@method, 2.4).should == true + (0.5...).send(@method, 2.4).should == true + end + + it "returns true if other is an element of self for beginless ranges" do + (..10).send(@method, 2.4).should == true + (...10.5).send(@method, 2.4).should == true + end + + it "returns false if values are not comparable" do + (1..10).send(@method, nil).should == false + (1...10).send(@method, nil).should == false + + (..10).send(@method, nil).should == false + (...10).send(@method, nil).should == false + + (1..).send(@method, nil).should == false + (1...).send(@method, nil).should == false + end + + it "compares values using <=>" do + rng = (1..5) + m = mock("int") + m.should_receive(:coerce).and_return([1, 2]) + m.should_receive(:<=>).and_return(1) + + rng.send(@method, m).should be_false + end + + it "raises an ArgumentError without exactly one argument" do + ->{ (1..2).send(@method) }.should raise_error(ArgumentError) + ->{ (1..2).send(@method, 1, 2) }.should raise_error(ArgumentError) + end + + it "returns true if argument is equal to the first value of the range" do + (0..5).send(@method, 0).should be_true + ('f'..'s').send(@method, 'f').should be_true + end + + it "returns true if argument is equal to the last value of the range" do + (0..5).send(@method, 5).should be_true + (0...5).send(@method, 4).should be_true + ('f'..'s').send(@method, 's').should be_true + end + + it "returns true if argument is less than the last value of the range and greater than the first value" do + (20..30).send(@method, 28).should be_true + ('e'..'h').send(@method, 'g').should be_true + end + + it "returns true if argument is sole element in the range" do + (30..30).send(@method, 30).should be_true + end + + it "returns false if range is empty" do + (30...30).send(@method, 30).should be_false + (30...30).send(@method, nil).should be_false + end + + it "returns false if the range does not contain the argument" do + ('A'..'C').send(@method, 20.9).should be_false + ('A'...'C').send(@method, 'C').should be_false + end +end diff --git a/spec/ruby/core/range/shared/end.rb b/spec/ruby/core/range/shared/end.rb new file mode 100644 index 0000000000..b26394fe31 --- /dev/null +++ b/spec/ruby/core/range/shared/end.rb @@ -0,0 +1,10 @@ +describe :range_end, shared: true do + it "end returns the last element of self" do + (-1..1).send(@method).should == 1 + (0..1).send(@method).should == 1 + ("A".."Q").send(@method).should == "Q" + ("A"..."Q").send(@method).should == "Q" + (0xffff...0xfffff).send(@method).should == 1048575 + (0.5..2.4).send(@method).should == 2.4 + end +end diff --git a/spec/ruby/core/range/shared/equal_value.rb b/spec/ruby/core/range/shared/equal_value.rb new file mode 100644 index 0000000000..363c6be558 --- /dev/null +++ b/spec/ruby/core/range/shared/equal_value.rb @@ -0,0 +1,51 @@ +require_relative '../fixtures/classes' + +describe :range_eql, shared: true do + it "returns true if other has same begin, end, and exclude_end? values" do + (0..2).send(@method, 0..2).should == true + ('G'..'M').send(@method,'G'..'M').should == true + (0.5..2.4).send(@method, 0.5..2.4).should == true + (5..10).send(@method, Range.new(5,10)).should == true + ('D'..'V').send(@method, Range.new('D','V')).should == true + (0.5..2.4).send(@method, Range.new(0.5, 2.4)).should == true + (0xffff..0xfffff).send(@method, 0xffff..0xfffff).should == true + (0xffff..0xfffff).send(@method, Range.new(0xffff,0xfffff)).should == true + + a = RangeSpecs::Xs.new(3)..RangeSpecs::Xs.new(5) + b = Range.new(RangeSpecs::Xs.new(3), RangeSpecs::Xs.new(5)) + a.send(@method, b).should == true + end + + it "returns false if one of the attributes differs" do + ('Q'..'X').send(@method, 'A'..'C').should == false + ('Q'...'X').send(@method, 'Q'..'W').should == false + ('Q'..'X').send(@method, 'Q'...'X').should == false + (0.5..2.4).send(@method, 0.5...2.4).should == false + (1482..1911).send(@method, 1482...1911).should == false + (0xffff..0xfffff).send(@method, 0xffff...0xfffff).should == false + + a = RangeSpecs::Xs.new(3)..RangeSpecs::Xs.new(5) + b = Range.new(RangeSpecs::Ys.new(3), RangeSpecs::Ys.new(5)) + a.send(@method, b).should == false + end + + it "returns false if other is not a Range" do + (1..10).send(@method, 1).should == false + (1..10).send(@method, 'a').should == false + (1..10).send(@method, mock('x')).should == false + end + + it "returns true for subclasses of Range" do + Range.new(1, 2).send(@method, RangeSpecs::MyRange.new(1, 2)).should == true + + a = Range.new(RangeSpecs::Xs.new(3), RangeSpecs::Xs.new(5)) + b = RangeSpecs::MyRange.new(RangeSpecs::Xs.new(3), RangeSpecs::Xs.new(5)) + a.send(@method, b).should == true + end + + it "works for endless Ranges" do + eval("(1..)").send(@method, eval("(1..)")).should == true + eval("(0.5...)").send(@method, eval("(0.5...)")).should == true + eval("(1..)").send(@method, eval("(1...)")).should == false + end +end diff --git a/spec/ruby/core/range/shared/include.rb b/spec/ruby/core/range/shared/include.rb new file mode 100644 index 0000000000..15a0e5fb9f --- /dev/null +++ b/spec/ruby/core/range/shared/include.rb @@ -0,0 +1,91 @@ +# encoding: binary +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' + +describe :range_include, shared: true do + describe "on string elements" do + it "returns true if other is matched by element.succ" do + ('a'..'c').send(@method, 'b').should be_true + ('a'...'c').send(@method, 'b').should be_true + end + + it "returns false if other is not matched by element.succ" do + ('a'..'c').send(@method, 'bc').should be_false + ('a'...'c').send(@method, 'bc').should be_false + end + end + + describe "with weird succ" do + describe "when included end value" do + before :each do + @range = RangeSpecs::TenfoldSucc.new(1)..RangeSpecs::TenfoldSucc.new(99) + end + + it "returns false if other is less than first element" do + @range.send(@method, RangeSpecs::TenfoldSucc.new(0)).should be_false + end + + it "returns true if other is equal as first element" do + @range.send(@method, RangeSpecs::TenfoldSucc.new(1)).should be_true + end + + it "returns true if other is matched by element.succ" do + @range.send(@method, RangeSpecs::TenfoldSucc.new(10)).should be_true + end + + it "returns false if other is not matched by element.succ" do + @range.send(@method, RangeSpecs::TenfoldSucc.new(2)).should be_false + end + + it "returns false if other is equal as last element but not matched by element.succ" do + @range.send(@method, RangeSpecs::TenfoldSucc.new(99)).should be_false + end + + it "returns false if other is greater than last element but matched by element.succ" do + @range.send(@method, RangeSpecs::TenfoldSucc.new(100)).should be_false + end + end + + describe "when excluded end value" do + before :each do + @range = RangeSpecs::TenfoldSucc.new(1)...RangeSpecs::TenfoldSucc.new(99) + end + + it "returns false if other is less than first element" do + @range.send(@method, RangeSpecs::TenfoldSucc.new(0)).should be_false + end + + it "returns true if other is equal as first element" do + @range.send(@method, RangeSpecs::TenfoldSucc.new(1)).should be_true + end + + it "returns true if other is matched by element.succ" do + @range.send(@method, RangeSpecs::TenfoldSucc.new(10)).should be_true + end + + it "returns false if other is not matched by element.succ" do + @range.send(@method, RangeSpecs::TenfoldSucc.new(2)).should be_false + end + + it "returns false if other is equal as last element but not matched by element.succ" do + @range.send(@method, RangeSpecs::TenfoldSucc.new(99)).should be_false + end + + it "returns false if other is greater than last element but matched by element.succ" do + @range.send(@method, RangeSpecs::TenfoldSucc.new(100)).should be_false + end + end + end + + describe "with Time endpoints" do + it "uses cover? logic" do + now = Time.now + range = (now..(now + 60)) + + range.include?(now).should == true + range.include?(now - 1).should == false + range.include?(now + 60).should == true + range.include?(now + 61).should == false + end + end +end diff --git a/spec/ruby/core/range/size_spec.rb b/spec/ruby/core/range/size_spec.rb new file mode 100644 index 0000000000..1a3ddd197e --- /dev/null +++ b/spec/ruby/core/range/size_spec.rb @@ -0,0 +1,92 @@ +require_relative '../../spec_helper' + +describe "Range#size" do + it "returns the number of elements in the range" do + (1..16).size.should == 16 + (1...16).size.should == 15 + end + + it "returns 0 if last is less than first" do + (16..0).size.should == 0 + end + + it 'returns Float::INFINITY for increasing, infinite ranges' do + (0..Float::INFINITY).size.should == Float::INFINITY + end + + it 'returns Float::INFINITY for endless ranges if the start is numeric' do + eval("(1..)").size.should == Float::INFINITY + end + + it 'returns nil for endless ranges if the start is not numeric' do + eval("('z'..)").size.should == nil + end + + ruby_version_is ""..."3.4" do + it 'returns Float::INFINITY for all beginless ranges if the end is numeric' do + (..1).size.should == Float::INFINITY + (...0.5).size.should == Float::INFINITY + end + + it 'returns nil for all beginless ranges if the end is not numeric' do + (...'o').size.should == nil + end + + it 'returns nil if the start and the end is both nil' do + (nil..nil).size.should == nil + end + end + + ruby_version_is ""..."3.4" do + it "returns the number of elements in the range" do + (1.0..16.0).size.should == 16 + (1.0...16.0).size.should == 15 + (1.0..15.9).size.should == 15 + (1.1..16.0).size.should == 15 + (1.1..15.9).size.should == 15 + end + + it "returns 0 if last is less than first" do + (16.0..0.0).size.should == 0 + (Float::INFINITY..0).size.should == 0 + end + + it 'returns Float::INFINITY for increasing, infinite ranges' do + (-Float::INFINITY..0).size.should == Float::INFINITY + (-Float::INFINITY..Float::INFINITY).size.should == Float::INFINITY + end + + it 'returns Float::INFINITY for endless ranges if the start is numeric' do + eval("(0.5...)").size.should == Float::INFINITY + end + + it 'returns nil for endless ranges if the start is not numeric' do + eval("([]...)").size.should == nil + end + end + + ruby_version_is "3.4" do + it 'raises TypeError if a range is not iterable' do + -> { (1.0..16.0).size }.should raise_error(TypeError, /can't iterate from/) + -> { (1.0...16.0).size }.should raise_error(TypeError, /can't iterate from/) + -> { (1.0..15.9).size }.should raise_error(TypeError, /can't iterate from/) + -> { (1.1..16.0).size }.should raise_error(TypeError, /can't iterate from/) + -> { (1.1..15.9).size }.should raise_error(TypeError, /can't iterate from/) + -> { (16.0..0.0).size }.should raise_error(TypeError, /can't iterate from/) + -> { (Float::INFINITY..0).size }.should raise_error(TypeError, /can't iterate from/) + -> { (-Float::INFINITY..0).size }.should raise_error(TypeError, /can't iterate from/) + -> { (-Float::INFINITY..Float::INFINITY).size }.should raise_error(TypeError, /can't iterate from/) + -> { (..1).size }.should raise_error(TypeError, /can't iterate from/) + -> { (...0.5).size }.should raise_error(TypeError, /can't iterate from/) + -> { (..nil).size }.should raise_error(TypeError, /can't iterate from/) + -> { (...'o').size }.should raise_error(TypeError, /can't iterate from/) + -> { eval("(0.5...)").size }.should raise_error(TypeError, /can't iterate from/) + -> { eval("([]...)").size }.should raise_error(TypeError, /can't iterate from/) + end + end + + it "returns nil if first and last are not Numeric" do + (:a..:z).size.should be_nil + ('a'..'z').size.should be_nil + end +end diff --git a/spec/ruby/core/range/step_spec.rb b/spec/ruby/core/range/step_spec.rb new file mode 100644 index 0000000000..0d0caf746d --- /dev/null +++ b/spec/ruby/core/range/step_spec.rb @@ -0,0 +1,683 @@ +require_relative '../../spec_helper' + +describe "Range#step" do + before :each do + ScratchPad.record [] + end + + it "returns self" do + r = 1..2 + r.step { }.should equal(r) + end + + ruby_version_is ""..."3.4" do + it "calls #to_int to coerce step to an Integer" do + obj = mock("Range#step") + obj.should_receive(:to_int).and_return(1) + + (1..2).step(obj) { |x| ScratchPad << x } + ScratchPad.recorded.should eql([1, 2]) + end + + it "raises a TypeError if step does not respond to #to_int" do + obj = mock("Range#step non-integer") + + -> { (1..2).step(obj) { } }.should raise_error(TypeError) + end + + it "raises a TypeError if #to_int does not return an Integer" do + obj = mock("Range#step non-integer") + obj.should_receive(:to_int).and_return("1") + + -> { (1..2).step(obj) { } }.should raise_error(TypeError) + end + + it "raises a TypeError if the first element does not respond to #succ" do + obj = mock("Range#step non-comparable") + obj.should_receive(:<=>).with(obj).and_return(1) + + -> { (obj..obj).step { |x| x } }.should raise_error(TypeError) + end + end + + ruby_version_is "3.4" do + it "calls #coerce to coerce step to an Integer" do + obj = mock("Range#step") + obj.should_receive(:coerce).at_least(:once).and_return([1, 2]) + + (1..3).step(obj) { |x| ScratchPad << x } + ScratchPad.recorded.should eql([1, 3]) + end + + it "raises a TypeError if step does not respond to #coerce" do + obj = mock("Range#step non-coercible") + + -> { (1..2).step(obj) { } }.should raise_error(TypeError) + end + end + + it "raises an ArgumentError if step is 0" do + -> { (-1..1).step(0) { |x| x } }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if step is 0.0" do + -> { (-1..1).step(0.0) { |x| x } }.should raise_error(ArgumentError) + end + + ruby_version_is "3.4" do + it "does not raise an ArgumentError if step is 0 for non-numeric ranges" do + t = Time.utc(2023, 2, 24) + -> { (t..t+1).step(0) { break } }.should_not raise_error(ArgumentError) + end + end + + ruby_version_is ""..."3.4" do + it "raises an ArgumentError if step is negative" do + -> { (-1..1).step(-2) { |x| x } }.should raise_error(ArgumentError) + end + end + + describe "with inclusive end" do + describe "and Integer values" do + it "yields Integer values incremented by 1 and less than or equal to end when not passed a step" do + (-2..2).step { |x| ScratchPad << x } + ScratchPad.recorded.should eql([-2, -1, 0, 1, 2]) + end + + it "yields Integer values incremented by an Integer step" do + (-5..5).step(2) { |x| ScratchPad << x } + ScratchPad.recorded.should eql([-5, -3, -1, 1, 3, 5]) + end + + it "yields Float values incremented by a Float step" do + (-2..2).step(1.5) { |x| ScratchPad << x } + ScratchPad.recorded.should eql([-2.0, -0.5, 1.0]) + end + + ruby_version_is "3.4" do + it "does not iterate if step is negative for forward range" do + (-1..1).step(-1) { |x| ScratchPad << x } + ScratchPad.recorded.should eql([]) + end + + it "iterates backward if step is negative for backward range" do + (1..-1).step(-1) { |x| ScratchPad << x } + ScratchPad.recorded.should eql([1, 0, -1]) + end + end + end + + describe "and Float values" do + it "yields Float values incremented by 1 and less than or equal to end when not passed a step" do + (-2.0..2.0).step { |x| ScratchPad << x } + ScratchPad.recorded.should eql([-2.0, -1.0, 0.0, 1.0, 2.0]) + end + + it "yields Float values incremented by an Integer step" do + (-5.0..5.0).step(2) { |x| ScratchPad << x } + ScratchPad.recorded.should eql([-5.0, -3.0, -1.0, 1.0, 3.0, 5.0]) + end + + it "yields Float values incremented by a Float step" do + (-1.0..1.0).step(0.5) { |x| ScratchPad << x } + ScratchPad.recorded.should eql([-1.0, -0.5, 0.0, 0.5, 1.0]) + end + + it "returns Float values of 'step * n + begin <= end'" do + (1.0..6.4).step(1.8) { |x| ScratchPad << x } + (1.0..12.7).step(1.3) { |x| ScratchPad << x } + ScratchPad.recorded.should eql([1.0, 2.8, 4.6, 6.4, 1.0, 2.3, 3.6, + 4.9, 6.2, 7.5, 8.8, 10.1, 11.4, 12.7]) + end + + it "handles infinite values at either end" do + (-Float::INFINITY..0.0).step(2) { |x| ScratchPad << x; break if ScratchPad.recorded.size == 3 } + ScratchPad.recorded.should eql([-Float::INFINITY, -Float::INFINITY, -Float::INFINITY]) + + ScratchPad.record [] + (0.0..Float::INFINITY).step(2) { |x| ScratchPad << x; break if ScratchPad.recorded.size == 3 } + ScratchPad.recorded.should eql([0.0, 2.0, 4.0]) + end + end + + describe "and Integer, Float values" do + it "yields Float values incremented by 1 and less than or equal to end when not passed a step" do + (-2..2.0).step { |x| ScratchPad << x } + ScratchPad.recorded.should eql([-2.0, -1.0, 0.0, 1.0, 2.0]) + end + + it "yields Float values incremented by an Integer step" do + (-5..5.0).step(2) { |x| ScratchPad << x } + ScratchPad.recorded.should eql([-5.0, -3.0, -1.0, 1.0, 3.0, 5.0]) + end + + it "yields Float values incremented by a Float step" do + (-1..1.0).step(0.5) { |x| ScratchPad << x } + ScratchPad.recorded.should eql([-1.0, -0.5, 0.0, 0.5, 1.0]) + end + end + + describe "and Float, Integer values" do + it "yields Float values incremented by 1 and less than or equal to end when not passed a step" do + (-2.0..2).step { |x| ScratchPad << x } + ScratchPad.recorded.should eql([-2.0, -1.0, 0.0, 1.0, 2.0]) + end + + it "yields Float values incremented by an Integer step" do + (-5.0..5).step(2) { |x| ScratchPad << x } + ScratchPad.recorded.should eql([-5.0, -3.0, -1.0, 1.0, 3.0, 5.0]) + end + + it "yields Float values incremented by a Float step" do + (-1.0..1).step(0.5) { |x| ScratchPad << x } + ScratchPad.recorded.should eql([-1.0, -0.5, 0.0, 0.5, 1.0]) + end + end + + describe "and String values" do + it "yields String values incremented by #succ and less than or equal to end when not passed a step" do + ("A".."E").step { |x| ScratchPad << x } + ScratchPad.recorded.should == ["A", "B", "C", "D", "E"] + end + + it "yields String values incremented by #succ called Integer step times" do + ("A".."G").step(2) { |x| ScratchPad << x } + ScratchPad.recorded.should == ["A", "C", "E", "G"] + end + + it "raises a TypeError when passed a Float step" do + -> { ("A".."G").step(2.0) { } }.should raise_error(TypeError) + end + + ruby_version_is ""..."3.4" do + it "calls #succ on begin and each element returned by #succ" do + obj = mock("Range#step String start") + obj.should_receive(:<=>).exactly(3).times.and_return(-1, -1, -1, 0) + obj.should_receive(:succ).exactly(2).times.and_return(obj) + + (obj..obj).step { |x| ScratchPad << x } + ScratchPad.recorded.should == [obj, obj, obj] + end + end + + ruby_version_is "3.4" do + it "yields String values adjusted by step and less than or equal to end" do + ("A".."AAA").step("A") { |x| ScratchPad << x } + ScratchPad.recorded.should == ["A", "AA", "AAA"] + end + + it "raises a TypeError when passed an incompatible type step" do + -> { ("A".."G").step([]) { } }.should raise_error(TypeError) + end + + it "calls #+ on begin and each element returned by #+" do + start = mock("Range#step String start") + stop = mock("Range#step String stop") + + mid1 = mock("Range#step String mid1") + mid2 = mock("Range#step String mid2") + + step = mock("Range#step String step") + + # Deciding on the direction of iteration + start.should_receive(:<=>).with(stop).at_least(:twice).and_return(-1) + # Deciding whether the step moves iteration in the right direction + start.should_receive(:<=>).with(mid1).and_return(-1) + # Iteration 1 + start.should_receive(:+).at_least(:once).with(step).and_return(mid1) + # Iteration 2 + mid1.should_receive(:<=>).with(stop).and_return(-1) + mid1.should_receive(:+).with(step).and_return(mid2) + # Iteration 3 + mid2.should_receive(:<=>).with(stop).and_return(0) + + (start..stop).step(step) { |x| ScratchPad << x } + ScratchPad.recorded.should == [start, mid1, mid2] + end + + it "iterates backward if the step is decreasing values, and the range is backward" do + start = mock("Range#step String start") + stop = mock("Range#step String stop") + + mid1 = mock("Range#step String mid1") + mid2 = mock("Range#step String mid2") + + step = mock("Range#step String step") + + # Deciding on the direction of iteration + start.should_receive(:<=>).with(stop).at_least(:twice).and_return(1) + # Deciding whether the step moves iteration in the right direction + start.should_receive(:<=>).with(mid1).and_return(1) + # Iteration 1 + start.should_receive(:+).at_least(:once).with(step).and_return(mid1) + # Iteration 2 + mid1.should_receive(:<=>).with(stop).and_return(1) + mid1.should_receive(:+).with(step).and_return(mid2) + # Iteration 3 + mid2.should_receive(:<=>).with(stop).and_return(0) + + (start..stop).step(step) { |x| ScratchPad << x } + ScratchPad.recorded.should == [start, mid1, mid2] + end + + it "does no iteration of the direction of the range and of the step don't match" do + start = mock("Range#step String start") + stop = mock("Range#step String stop") + + mid1 = mock("Range#step String mid1") + mid2 = mock("Range#step String mid2") + + step = mock("Range#step String step") + + # Deciding on the direction of iteration: stop > start + start.should_receive(:<=>).with(stop).at_least(:twice).and_return(1) + # Deciding whether the step moves iteration in the right direction + # start + step < start, the direction is opposite to the range's + start.should_receive(:+).with(step).and_return(mid1) + start.should_receive(:<=>).with(mid1).and_return(-1) + + (start..stop).step(step) { |x| ScratchPad << x } + ScratchPad.recorded.should == [] + end + end + end + end + + describe "with exclusive end" do + describe "and Integer values" do + it "yields Integer values incremented by 1 and less than end when not passed a step" do + (-2...2).step { |x| ScratchPad << x } + ScratchPad.recorded.should eql([-2, -1, 0, 1]) + end + + it "yields Integer values incremented by an Integer step" do + (-5...5).step(2) { |x| ScratchPad << x } + ScratchPad.recorded.should eql([-5, -3, -1, 1, 3]) + end + + it "yields Float values incremented by a Float step" do + (-2...2).step(1.5) { |x| ScratchPad << x } + ScratchPad.recorded.should eql([-2.0, -0.5, 1.0]) + end + end + + describe "and Float values" do + it "yields Float values incremented by 1 and less than end when not passed a step" do + (-2.0...2.0).step { |x| ScratchPad << x } + ScratchPad.recorded.should eql([-2.0, -1.0, 0.0, 1.0]) + end + + it "yields Float values incremented by an Integer step" do + (-5.0...5.0).step(2) { |x| ScratchPad << x } + ScratchPad.recorded.should eql([-5.0, -3.0, -1.0, 1.0, 3.0]) + end + + it "yields Float values incremented by a Float step" do + (-1.0...1.0).step(0.5) { |x| ScratchPad << x } + ScratchPad.recorded.should eql([-1.0, -0.5, 0.0, 0.5]) + end + + it "returns Float values of 'step * n + begin < end'" do + (1.0...6.4).step(1.8) { |x| ScratchPad << x } + ScratchPad.recorded.should eql([1.0, 2.8, 4.6]) + end + + it "correctly handles values near the upper limit" do # https://bugs.ruby-lang.org/issues/16612 + (1.0...55.6).step(18.2) { |x| ScratchPad << x } + ScratchPad.recorded.should eql([1.0, 19.2, 37.4, 55.599999999999994]) + + (1.0...55.6).step(18.2).size.should == 4 + end + + it "handles infinite values at either end" do + (-Float::INFINITY...0.0).step(2) { |x| ScratchPad << x; break if ScratchPad.recorded.size == 3 } + ScratchPad.recorded.should eql([-Float::INFINITY, -Float::INFINITY, -Float::INFINITY]) + + ScratchPad.record [] + (0.0...Float::INFINITY).step(2) { |x| ScratchPad << x; break if ScratchPad.recorded.size == 3 } + ScratchPad.recorded.should eql([0.0, 2.0, 4.0]) + end + end + + describe "and Integer, Float values" do + it "yields Float values incremented by 1 and less than end when not passed a step" do + (-2...2.0).step { |x| ScratchPad << x } + ScratchPad.recorded.should eql([-2.0, -1.0, 0.0, 1.0]) + end + + it "yields Float values incremented by an Integer step" do + (-5...5.0).step(2) { |x| ScratchPad << x } + ScratchPad.recorded.should eql([-5.0, -3.0, -1.0, 1.0, 3.0]) + end + + it "yields an Float and then Float values incremented by a Float step" do + (-1...1.0).step(0.5) { |x| ScratchPad << x } + ScratchPad.recorded.should eql([-1.0, -0.5, 0.0, 0.5]) + end + end + + describe "and Float, Integer values" do + it "yields Float values incremented by 1 and less than end when not passed a step" do + (-2.0...2).step { |x| ScratchPad << x } + ScratchPad.recorded.should eql([-2.0, -1.0, 0.0, 1.0]) + end + + it "yields Float values incremented by an Integer step" do + (-5.0...5).step(2) { |x| ScratchPad << x } + ScratchPad.recorded.should eql([-5.0, -3.0, -1.0, 1.0, 3.0]) + end + + it "yields Float values incremented by a Float step" do + (-1.0...1).step(0.5) { |x| ScratchPad << x } + ScratchPad.recorded.should eql([-1.0, -0.5, 0.0, 0.5]) + end + end + + describe "and String values" do + ruby_version_is ""..."3.4" do + it "yields String values incremented by #succ and less than or equal to end when not passed a step" do + ("A"..."E").step { |x| ScratchPad << x } + ScratchPad.recorded.should == ["A", "B", "C", "D"] + end + + it "yields String values incremented by #succ called Integer step times" do + ("A"..."G").step(2) { |x| ScratchPad << x } + ScratchPad.recorded.should == ["A", "C", "E"] + end + + it "raises a TypeError when passed a Float step" do + -> { ("A"..."G").step(2.0) { } }.should raise_error(TypeError) + end + end + + ruby_version_is "3.4" do + it "yields String values adjusted by step and less than or equal to end" do + ("A"..."AAA").step("A") { |x| ScratchPad << x } + ScratchPad.recorded.should == ["A", "AA"] + end + + it "raises a TypeError when passed an incompatible type step" do + -> { ("A".."G").step([]) { } }.should raise_error(TypeError) + end + end + end + end + + describe "with an endless range" do + describe "and Integer values" do + it "yield Integer values incremented by 1 when not passed a step" do + (-2..).step { |x| break if x > 2; ScratchPad << x } + ScratchPad.recorded.should eql([-2, -1, 0, 1, 2]) + + ScratchPad.record [] + (-2...).step { |x| break if x > 2; ScratchPad << x } + ScratchPad.recorded.should eql([-2, -1, 0, 1, 2]) + end + + it "yields Integer values incremented by an Integer step" do + (-5..).step(2) { |x| break if x > 3; ScratchPad << x } + ScratchPad.recorded.should eql([-5, -3, -1, 1, 3]) + + ScratchPad.record [] + (-5...).step(2) { |x| break if x > 3; ScratchPad << x } + ScratchPad.recorded.should eql([-5, -3, -1, 1, 3]) + end + + it "yields Float values incremented by a Float step" do + (-2..).step(1.5) { |x| break if x > 1.0; ScratchPad << x } + ScratchPad.recorded.should eql([-2.0, -0.5, 1.0]) + + ScratchPad.record [] + (-2..).step(1.5) { |x| break if x > 1.0; ScratchPad << x } + ScratchPad.recorded.should eql([-2.0, -0.5, 1.0]) + end + end + + describe "and Float values" do + it "yields Float values incremented by 1 and less than end when not passed a step" do + (-2.0..).step { |x| break if x > 1.5; ScratchPad << x } + ScratchPad.recorded.should eql([-2.0, -1.0, 0.0, 1.0]) + + ScratchPad.record [] + (-2.0...).step { |x| break if x > 1.5; ScratchPad << x } + ScratchPad.recorded.should eql([-2.0, -1.0, 0.0, 1.0]) + end + + it "yields Float values incremented by an Integer step" do + (-5.0..).step(2) { |x| break if x > 3.5; ScratchPad << x } + ScratchPad.recorded.should eql([-5.0, -3.0, -1.0, 1.0, 3.0]) + + ScratchPad.record [] + (-5.0...).step(2) { |x| break if x > 3.5; ScratchPad << x } + ScratchPad.recorded.should eql([-5.0, -3.0, -1.0, 1.0, 3.0]) + end + + it "yields Float values incremented by a Float step" do + (-1.0..).step(0.5) { |x| break if x > 0.6; ScratchPad << x } + ScratchPad.recorded.should eql([-1.0, -0.5, 0.0, 0.5]) + + ScratchPad.record [] + (-1.0...).step(0.5) { |x| break if x > 0.6; ScratchPad << x } + ScratchPad.recorded.should eql([-1.0, -0.5, 0.0, 0.5]) + end + + it "handles infinite values at the start" do + (-Float::INFINITY..).step(2) { |x| ScratchPad << x; break if ScratchPad.recorded.size == 3 } + ScratchPad.recorded.should eql([-Float::INFINITY, -Float::INFINITY, -Float::INFINITY]) + + ScratchPad.record [] + (-Float::INFINITY...).step(2) { |x| ScratchPad << x; break if ScratchPad.recorded.size == 3 } + ScratchPad.recorded.should eql([-Float::INFINITY, -Float::INFINITY, -Float::INFINITY]) + end + end + + describe "and String values" do + it "yields String values incremented by #succ and less than or equal to end when not passed a step" do + ('A'..).step { |x| break if x > "D"; ScratchPad << x } + ScratchPad.recorded.should == ["A", "B", "C", "D"] + + ScratchPad.record [] + ('A'...).step { |x| break if x > "D"; ScratchPad << x } + ScratchPad.recorded.should == ["A", "B", "C", "D"] + end + + it "yields String values incremented by #succ called Integer step times" do + ('A'..).step(2) { |x| break if x > "F"; ScratchPad << x } + ScratchPad.recorded.should == ["A", "C", "E"] + + ScratchPad.record [] + ('A'...).step(2) { |x| break if x > "F"; ScratchPad << x } + ScratchPad.recorded.should == ["A", "C", "E"] + end + + it "raises a TypeError when passed a Float step" do + -> { ('A'..).step(2.0) { } }.should raise_error(TypeError) + -> { ('A'...).step(2.0) { } }.should raise_error(TypeError) + end + + ruby_version_is "3.4" do + it "yields String values adjusted by step" do + ('A'..).step("A") { |x| break if x > "AAA"; ScratchPad << x } + ScratchPad.recorded.should == ["A", "AA", "AAA"] + + ScratchPad.record [] + ('A'...).step("A") { |x| break if x > "AAA"; ScratchPad << x } + ScratchPad.recorded.should == ["A", "AA", "AAA"] + end + + it "raises a TypeError when passed an incompatible type step" do + -> { ('A'..).step([]) { } }.should raise_error(TypeError) + -> { ('A'...).step([]) { } }.should raise_error(TypeError) + end + end + end + end + + describe "when no block is given" do + it "raises an ArgumentError if step is 0" do + -> { (-1..1).step(0) }.should raise_error(ArgumentError) + end + + describe "returned Enumerator" do + describe "size" do + ruby_version_is ""..."3.4" do + it "raises a TypeError if step does not respond to #to_int" do + obj = mock("Range#step non-integer") + -> { (1..2).step(obj) }.should raise_error(TypeError) + end + + it "raises a TypeError if #to_int does not return an Integer" do + obj = mock("Range#step non-integer") + obj.should_receive(:to_int).and_return("1") + -> { (1..2).step(obj) }.should raise_error(TypeError) + end + end + + ruby_version_is "3.4" do + it "does not raise if step is incompatible" do + obj = mock("Range#step non-integer") + -> { (1..2).step(obj) }.should_not raise_error + end + end + + it "returns the ceil of range size divided by the number of steps" do + (1..10).step(4).size.should == 3 + (1..10).step(3).size.should == 4 + (1..10).step(2).size.should == 5 + (1..10).step(1).size.should == 10 + (-5..5).step(2).size.should == 6 + (1...10).step(4).size.should == 3 + (1...10).step(3).size.should == 3 + (1...10).step(2).size.should == 5 + (1...10).step(1).size.should == 9 + (-5...5).step(2).size.should == 5 + end + + it "returns the ceil of range size divided by the number of steps even if step is negative" do + (-1..1).step(-1).size.should == 0 + (1..-1).step(-1).size.should == 3 + end + + it "returns the correct number of steps when one of the arguments is a float" do + (-1..1.0).step(0.5).size.should == 5 + (-1.0...1.0).step(0.5).size.should == 4 + end + + it "returns the range size when there's no step_size" do + (-2..2).step.size.should == 5 + (-2.0..2.0).step.size.should == 5 + (-2..2.0).step.size.should == 5 + (-2.0..2).step.size.should == 5 + (1.0..6.4).step(1.8).size.should == 4 + (1.0..12.7).step(1.3).size.should == 10 + (-2...2).step.size.should == 4 + (-2.0...2.0).step.size.should == 4 + (-2...2.0).step.size.should == 4 + (-2.0...2).step.size.should == 4 + (1.0...6.4).step(1.8).size.should == 3 + end + + ruby_version_is ""..."3.4" do + it "returns nil with begin and end are String" do + ("A".."E").step(2).size.should == nil + ("A"..."E").step(2).size.should == nil + ("A".."E").step.size.should == nil + ("A"..."E").step.size.should == nil + end + + it "return nil and not raises a TypeError if the first element does not respond to #succ" do + obj = mock("Range#step non-comparable") + obj.should_receive(:<=>).with(obj).and_return(1) + enum = (obj..obj).step + -> { enum.size }.should_not raise_error + enum.size.should == nil + end + end + + ruby_version_is "3.4" do + it "returns nil with begin and end are String" do + ("A".."E").step("A").size.should == nil + ("A"..."E").step("A").size.should == nil + end + + it "return nil and not raises a TypeError if the first element is not of compatible type" do + obj = mock("Range#step non-comparable") + obj.should_receive(:<=>).with(obj).and_return(1) + enum = (obj..obj).step(obj) + -> { enum.size }.should_not raise_error + enum.size.should == nil + end + end + end + + # We use .take below to ensure the enumerator works + # because that's an Enumerable method and so it uses the Enumerator behavior + # not just a method overridden in Enumerator::ArithmeticSequence. + describe "type" do + context "when both begin and end are numerics" do + it "returns an instance of Enumerator::ArithmeticSequence" do + (1..10).step.class.should == Enumerator::ArithmeticSequence + (1..10).step(3).take(4).should == [1, 4, 7, 10] + end + end + + context "when begin is not defined and end is numeric" do + it "returns an instance of Enumerator::ArithmeticSequence" do + (..10).step.class.should == Enumerator::ArithmeticSequence + end + end + + context "when range is endless" do + it "returns an instance of Enumerator::ArithmeticSequence when begin is numeric" do + (1..).step.class.should == Enumerator::ArithmeticSequence + (1..).step(2).take(3).should == [1, 3, 5] + end + + ruby_version_is ""..."3.4" do + it "returns an instance of Enumerator when begin is not numeric" do + ("a"..).step.class.should == Enumerator + ("a"..).step(2).take(3).should == %w[a c e] + end + end + + ruby_version_is "3.4" do + it "returns an instance of Enumerator when begin is not numeric" do + ("a"..).step("a").class.should == Enumerator + ("a"..).step("a").take(3).should == %w[a aa aaa] + end + end + end + + context "when range is beginless and endless" do + ruby_version_is ""..."3.4" do + it "returns an instance of Enumerator" do + Range.new(nil, nil).step.class.should == Enumerator + end + end + + ruby_version_is "3.4" do + it "raises an ArgumentError" do + -> { Range.new(nil, nil).step(1) }.should raise_error(ArgumentError) + end + end + end + + context "when begin and end are not numerics" do + ruby_version_is ""..."3.4" do + it "returns an instance of Enumerator" do + ("a".."z").step.class.should == Enumerator + ("a".."z").step(3).take(4).should == %w[a d g j] + end + end + + ruby_version_is "3.4" do + it "returns an instance of Enumerator" do + ("a".."z").step("a").class.should == Enumerator + ("a".."z").step("a").take(4).should == %w[a aa aaa aaaa] + end + end + end + end + end + end +end diff --git a/spec/ruby/core/range/to_a_spec.rb b/spec/ruby/core/range/to_a_spec.rb new file mode 100644 index 0000000000..b1d3de32db --- /dev/null +++ b/spec/ruby/core/range/to_a_spec.rb @@ -0,0 +1,39 @@ +require_relative '../../spec_helper' + +describe "Range#to_a" do + it "converts self to an array" do + (-5..5).to_a.should == [-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5] + ('A'..'D').to_a.should == ['A','B','C','D'] + ('A'...'D').to_a.should == ['A','B','C'] + (0xfffd...0xffff).to_a.should == [0xfffd,0xfffe] + -> { (0.5..2.4).to_a }.should raise_error(TypeError) + end + + it "returns empty array for descending-ordered" do + (5..-5).to_a.should == [] + ('D'..'A').to_a.should == [] + ('D'...'A').to_a.should == [] + (0xffff...0xfffd).to_a.should == [] + end + + it "works with Ranges of 64-bit integers" do + large = 1 << 40 + (large..large+1).to_a.should == [1099511627776, 1099511627777] + end + + it "works with Ranges of Symbols" do + (:A..:z).to_a.size.should == 58 + end + + it "works for non-ASCII ranges" do + ('Σ'..'Ω').to_a.should == ["Σ", "Τ", "Î¥", "Φ", "Χ", "Ψ", "Ω"] + end + + it "throws an exception for endless ranges" do + -> { eval("(1..)").to_a }.should raise_error(RangeError) + end + + it "throws an exception for beginless ranges" do + -> { (..1).to_a }.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/range/to_s_spec.rb b/spec/ruby/core/range/to_s_spec.rb new file mode 100644 index 0000000000..460c330912 --- /dev/null +++ b/spec/ruby/core/range/to_s_spec.rb @@ -0,0 +1,23 @@ +require_relative '../../spec_helper' + +describe "Range#to_s" do + it "provides a printable form of self" do + (0..21).to_s.should == "0..21" + (-8..0).to_s.should == "-8..0" + (-411..959).to_s.should == "-411..959" + ('A'..'Z').to_s.should == 'A..Z' + ('A'...'Z').to_s.should == 'A...Z' + (0xfff..0xfffff).to_s.should == "4095..1048575" + (0.5..2.4).to_s.should == "0.5..2.4" + end + + it "can show endless ranges" do + eval("(1..)").to_s.should == "1.." + eval("(1.0...)").to_s.should == "1.0..." + end + + it "can show beginless ranges" do + (..1).to_s.should == "..1" + (...1.0).to_s.should == "...1.0" + end +end diff --git a/spec/ruby/core/range/to_set_spec.rb b/spec/ruby/core/range/to_set_spec.rb new file mode 100644 index 0000000000..589c0e9aed --- /dev/null +++ b/spec/ruby/core/range/to_set_spec.rb @@ -0,0 +1,55 @@ +require_relative '../../spec_helper' +require_relative '../enumerable/fixtures/classes' + +describe "Enumerable#to_set" do + it "returns a new Set created from self" do + (1..4).to_set.should == Set[1, 2, 3, 4] + (1...4).to_set.should == Set[1, 2, 3] + end + + it "passes down passed blocks" do + (1..3).to_set { |x| x * x }.should == Set[1, 4, 9] + end + + ruby_version_is "4.0" do + it "raises a RangeError if the range is infinite" do + -> { (1..).to_set }.should raise_error(RangeError, "cannot convert endless range to a set") + -> { (1...).to_set }.should raise_error(RangeError, "cannot convert endless range to a set") + end + end + + ruby_version_is ""..."4.0" do + it "instantiates an object of provided as the first argument set class" do + set = (1..3).to_set(EnumerableSpecs::SetSubclass) + set.should be_kind_of(EnumerableSpecs::SetSubclass) + set.to_a.sort.should == [1, 2, 3] + end + end + + ruby_version_is "4.0"..."4.1" do + it "instantiates an object of provided as the first argument set class and warns" do + set = nil + proc { + set = (1..3).to_set(EnumerableSpecs::SetSubclass) + }.should complain(/Enumerable#to_set/) + set.should be_kind_of(EnumerableSpecs::SetSubclass) + set.to_a.sort.should == [1, 2, 3] + end + end + + ruby_version_is "4.1" do + it "does not accept any positional argument" do + -> { + (1..3).to_set(EnumerableSpecs::SetSubclass) + }.should raise_error(ArgumentError, 'wrong number of arguments (given 1, expected 0)') + end + end + + it "does not need explicit `require 'set'`" do + output = ruby_exe(<<~RUBY, options: '--disable-gems', args: '2>&1') + puts (1..3).to_set.to_a.inspect + RUBY + + output.chomp.should == "[1, 2, 3]" + end +end diff --git a/spec/ruby/core/rational/abs_spec.rb b/spec/ruby/core/rational/abs_spec.rb new file mode 100644 index 0000000000..54099aa14d --- /dev/null +++ b/spec/ruby/core/rational/abs_spec.rb @@ -0,0 +1,6 @@ +require_relative "../../spec_helper" +require_relative 'shared/abs' + +describe "Rational#abs" do + it_behaves_like :rational_abs, :abs +end diff --git a/spec/ruby/core/rational/ceil_spec.rb b/spec/ruby/core/rational/ceil_spec.rb new file mode 100644 index 0000000000..d5bdadf3b6 --- /dev/null +++ b/spec/ruby/core/rational/ceil_spec.rb @@ -0,0 +1,45 @@ +require_relative "../../spec_helper" + +describe "Rational#ceil" do + before do + @rational = Rational(2200, 7) + end + + describe "with no arguments (precision = 0)" do + it "returns an Integer" do + @rational.ceil.should be_kind_of(Integer) + end + + it "returns the truncated value toward positive infinity" do + @rational.ceil.should == 315 + Rational(1, 2).ceil.should == 1 + Rational(-1, 2).ceil.should == 0 + end + end + + describe "with a precision < 0" do + it "returns an Integer" do + @rational.ceil(-2).should be_kind_of(Integer) + @rational.ceil(-1).should be_kind_of(Integer) + end + + it "moves the truncation point n decimal places left" do + @rational.ceil(-3).should == 1000 + @rational.ceil(-2).should == 400 + @rational.ceil(-1).should == 320 + end + end + + describe "with precision > 0" do + it "returns a Rational" do + @rational.ceil(1).should be_kind_of(Rational) + @rational.ceil(2).should be_kind_of(Rational) + end + + it "moves the truncation point n decimal places right" do + @rational.ceil(1).should == Rational(3143, 10) + @rational.ceil(2).should == Rational(31429, 100) + @rational.ceil(3).should == Rational(157143, 500) + end + end +end diff --git a/spec/ruby/core/rational/comparison_spec.rb b/spec/ruby/core/rational/comparison_spec.rb new file mode 100644 index 0000000000..c9db60d5c7 --- /dev/null +++ b/spec/ruby/core/rational/comparison_spec.rb @@ -0,0 +1,93 @@ +require_relative "../../spec_helper" +require_relative 'fixtures/rational' + +describe "Rational#<=> when passed a Rational object" do + it "returns 1 when self is greater than the passed argument" do + (Rational(4, 4) <=> Rational(3, 4)).should equal(1) + (Rational(-3, 4) <=> Rational(-4, 4)).should equal(1) + end + + it "returns 0 when self is equal to the passed argument" do + (Rational(4, 4) <=> Rational(4, 4)).should equal(0) + (Rational(-3, 4) <=> Rational(-3, 4)).should equal(0) + end + + it "returns -1 when self is less than the passed argument" do + (Rational(3, 4) <=> Rational(4, 4)).should equal(-1) + (Rational(-4, 4) <=> Rational(-3, 4)).should equal(-1) + end +end + +describe "Rational#<=> when passed an Integer object" do + it "returns 1 when self is greater than the passed argument" do + (Rational(4, 4) <=> 0).should equal(1) + (Rational(4, 4) <=> -10).should equal(1) + (Rational(-3, 4) <=> -1).should equal(1) + end + + it "returns 0 when self is equal to the passed argument" do + (Rational(4, 4) <=> 1).should equal(0) + (Rational(-8, 4) <=> -2).should equal(0) + end + + it "returns -1 when self is less than the passed argument" do + (Rational(3, 4) <=> 1).should equal(-1) + (Rational(-4, 4) <=> 0).should equal(-1) + end +end + +describe "Rational#<=> when passed a Float object" do + it "returns 1 when self is greater than the passed argument" do + (Rational(4, 4) <=> 0.5).should equal(1) + (Rational(4, 4) <=> -1.5).should equal(1) + (Rational(-3, 4) <=> -0.8).should equal(1) + end + + it "returns 0 when self is equal to the passed argument" do + (Rational(4, 4) <=> 1.0).should equal(0) + (Rational(-6, 4) <=> -1.5).should equal(0) + end + + it "returns -1 when self is less than the passed argument" do + (Rational(3, 4) <=> 1.2).should equal(-1) + (Rational(-4, 4) <=> 0.5).should equal(-1) + end +end + +describe "Rational#<=> when passed an Object that responds to #coerce" do + it "calls #coerce on the passed argument with self" do + rational = Rational(3, 4) + + obj = mock("Object") + obj.should_receive(:coerce).with(rational).and_return([1, 2]) + + rational <=> obj + end + + it "calls #<=> on the coerced Rational with the coerced Object" do + rational = Rational(3, 4) + + coerced_rational = mock("Coerced Rational") + coerced_rational.should_receive(:<=>).and_return(:result) + + coerced_obj = mock("Coerced Object") + + obj = mock("Object") + obj.should_receive(:coerce).and_return([coerced_rational, coerced_obj]) + + (rational <=> obj).should == :result + end + + it "does not rescue exception raised in other#coerce" do + b = mock("numeric with failed #coerce") + b.should_receive(:coerce).and_raise(RationalSpecs::CoerceError) + + -> { Rational(3, 4) <=> b }.should raise_error(RationalSpecs::CoerceError) + end +end + +describe "Rational#<=> when passed a non-Numeric Object that doesn't respond to #coerce" do + it "returns nil" do + (Rational <=> mock("Object")).should be_nil + end +end diff --git a/spec/ruby/core/rational/denominator_spec.rb b/spec/ruby/core/rational/denominator_spec.rb new file mode 100644 index 0000000000..4687244893 --- /dev/null +++ b/spec/ruby/core/rational/denominator_spec.rb @@ -0,0 +1,14 @@ +require_relative "../../spec_helper" + +describe "Rational#denominator" do + it "returns the denominator" do + Rational(3, 4).denominator.should equal(4) + Rational(3, -4).denominator.should equal(4) + + Rational(1, bignum_value).denominator.should == bignum_value + end + + it "returns 1 if no denominator was given" do + Rational(80).denominator.should == 1 + end +end diff --git a/spec/ruby/core/rational/div_spec.rb b/spec/ruby/core/rational/div_spec.rb new file mode 100644 index 0000000000..d3adb9b536 --- /dev/null +++ b/spec/ruby/core/rational/div_spec.rb @@ -0,0 +1,54 @@ +require_relative "../../spec_helper" + +describe "Rational#div" do + it "returns an Integer" do + Rational(229, 21).div(82).should be_kind_of(Integer) + end + + it "raises an ArgumentError if passed more than one argument" do + -> { Rational(3, 4).div(2,3) }.should raise_error(ArgumentError) + end + + # See http://redmine.ruby-lang.org/issues/show/1648 + it "raises a TypeError if passed a non-numeric argument" do + -> { Rational(3, 4).div([]) }.should raise_error(TypeError) + end +end + +describe "Rational#div passed a Rational" do + it "performs integer division and returns the result" do + Rational(2, 3).div(Rational(2, 3)).should == 1 + Rational(-2, 9).div(Rational(-9, 2)).should == 0 + end + + it "raises a ZeroDivisionError when the argument has a numerator of 0" do + -> { Rational(3, 4).div(Rational(0, 3)) }.should raise_error(ZeroDivisionError) + end + + it "raises a ZeroDivisionError when the argument has a numerator of 0.0" do + -> { Rational(3, 4).div(Rational(0.0, 3)) }.should raise_error(ZeroDivisionError) + end +end + +describe "Rational#div passed an Integer" do + it "performs integer division and returns the result" do + Rational(2, 1).div(1).should == 2 + Rational(25, 5).div(-50).should == -1 + end + + it "raises a ZeroDivisionError when the argument is 0" do + -> { Rational(3, 4).div(0) }.should raise_error(ZeroDivisionError) + end +end + +describe "Rational#div passed a Float" do + it "performs integer division and returns the result" do + Rational(2, 3).div(30.333).should == 0 + Rational(2, 9).div(Rational(-8.6)).should == -1 + Rational(3.12).div(0.5).should == 6 + end + + it "raises a ZeroDivisionError when the argument is 0.0" do + -> { Rational(3, 4).div(0.0) }.should raise_error(ZeroDivisionError) + end +end diff --git a/spec/ruby/core/rational/divide_spec.rb b/spec/ruby/core/rational/divide_spec.rb new file mode 100644 index 0000000000..8f5ca1fdec --- /dev/null +++ b/spec/ruby/core/rational/divide_spec.rb @@ -0,0 +1,74 @@ +require_relative "../../spec_helper" +require_relative 'shared/arithmetic_exception_in_coerce' + +describe "Rational#/" do + it "calls #coerce on the passed argument with self" do + rational = Rational(3, 4) + obj = mock("Object") + obj.should_receive(:coerce).with(rational).and_return([1, 2]) + + rational / obj + end + + it "calls #/ on the coerced Rational with the coerced Object" do + rational = Rational(3, 4) + + coerced_rational = mock("Coerced Rational") + coerced_rational.should_receive(:/).and_return(:result) + + coerced_obj = mock("Coerced Object") + + obj = mock("Object") + obj.should_receive(:coerce).and_return([coerced_rational, coerced_obj]) + + (rational / obj).should == :result + end + + it_behaves_like :rational_arithmetic_exception_in_coerce, :/ +end + +describe "Rational#/ when passed an Integer" do + it "returns self divided by other as a Rational" do + (Rational(3, 4) / 2).should eql(Rational(3, 8)) + (Rational(2, 4) / 2).should eql(Rational(1, 4)) + (Rational(6, 7) / -2).should eql(Rational(-3, 7)) + end + + it "raises a ZeroDivisionError when passed 0" do + -> { Rational(3, 4) / 0 }.should raise_error(ZeroDivisionError) + end +end + +describe "Rational#/ when passed a Rational" do + it "returns self divided by other as a Rational" do + (Rational(3, 4) / Rational(3, 4)).should eql(Rational(1, 1)) + (Rational(2, 4) / Rational(1, 4)).should eql(Rational(2, 1)) + + (Rational(2, 4) / 2).should == Rational(1, 4) + (Rational(6, 7) / -2).should == Rational(-3, 7) + end + + it "raises a ZeroDivisionError when passed a Rational with a numerator of 0" do + -> { Rational(3, 4) / Rational(0, 1) }.should raise_error(ZeroDivisionError) + end +end + +describe "Rational#/ when passed a Float" do + it "returns self divided by other as a Float" do + (Rational(3, 4) / 0.75).should eql(1.0) + (Rational(3, 4) / 0.25).should eql(3.0) + (Rational(3, 4) / 0.3).should eql(2.5) + + (Rational(-3, 4) / 0.3).should eql(-2.5) + (Rational(3, -4) / 0.3).should eql(-2.5) + (Rational(3, 4) / -0.3).should eql(-2.5) + end + + it "returns infinity when passed 0" do + (Rational(3, 4) / 0.0).infinite?.should eql(1) + (Rational(-3, -4) / 0.0).infinite?.should eql(1) + + (Rational(-3, 4) / 0.0).infinite?.should eql(-1) + (Rational(3, -4) / 0.0).infinite?.should eql(-1) + end +end diff --git a/spec/ruby/core/rational/divmod_spec.rb b/spec/ruby/core/rational/divmod_spec.rb new file mode 100644 index 0000000000..f0555294a3 --- /dev/null +++ b/spec/ruby/core/rational/divmod_spec.rb @@ -0,0 +1,42 @@ +require_relative "../../spec_helper" + +describe "Rational#divmod when passed a Rational" do + it "returns the quotient as Integer and the remainder as Rational" do + Rational(7, 4).divmod(Rational(1, 2)).should eql([3, Rational(1, 4)]) + Rational(7, 4).divmod(Rational(-1, 2)).should eql([-4, Rational(-1, 4)]) + Rational(0, 4).divmod(Rational(4, 3)).should eql([0, Rational(0, 1)]) + + Rational(bignum_value, 4).divmod(Rational(4, 3)).should eql([3458764513820540928, Rational(0, 1)]) + end + + it "raises a ZeroDivisionError when passed a Rational with a numerator of 0" do + -> { Rational(7, 4).divmod(Rational(0, 3)) }.should raise_error(ZeroDivisionError) + end +end + +describe "Rational#divmod when passed an Integer" do + it "returns the quotient as Integer and the remainder as Rational" do + Rational(7, 4).divmod(2).should eql([0, Rational(7, 4)]) + Rational(7, 4).divmod(-2).should eql([-1, Rational(-1, 4)]) + + Rational(bignum_value, 4).divmod(3).should eql([1537228672809129301, Rational(1, 1)]) + end + + it "raises a ZeroDivisionError when passed 0" do + -> { Rational(7, 4).divmod(0) }.should raise_error(ZeroDivisionError) + end +end + +describe "Rational#divmod when passed a Float" do + it "returns the quotient as Integer and the remainder as Float" do + Rational(7, 4).divmod(0.5).should eql([3, 0.25]) + end + + it "returns the quotient as Integer and the remainder as Float" do + Rational(7, 4).divmod(-0.5).should eql([-4, -0.25]) + end + + it "raises a ZeroDivisionError when passed 0" do + -> { Rational(7, 4).divmod(0.0) }.should raise_error(ZeroDivisionError) + end +end diff --git a/spec/ruby/core/rational/equal_value_spec.rb b/spec/ruby/core/rational/equal_value_spec.rb new file mode 100644 index 0000000000..ba40d29c3b --- /dev/null +++ b/spec/ruby/core/rational/equal_value_spec.rb @@ -0,0 +1,39 @@ +require_relative "../../spec_helper" + +describe "Rational#==" do + it "returns the result of calling #== with self on the passed argument" do + obj = mock("Object") + obj.should_receive(:==).and_return(:result) + + (Rational(3, 4) == obj).should_not be_false + end +end + +describe "Rational#== when passed a Rational" do + it "returns true if self has the same numerator and denominator as the passed argument" do + (Rational(3, 4) == Rational(3, 4)).should be_true + (Rational(-3, -4) == Rational(3, 4)).should be_true + (Rational(-4, 5) == Rational(4, -5)).should be_true + + (Rational(bignum_value, 3) == Rational(bignum_value, 3)).should be_true + (Rational(-bignum_value, 3) == Rational(bignum_value, -3)).should be_true + end +end + +describe "Rational#== when passed a Float" do + it "converts self to a Float and compares it with the passed argument" do + (Rational(3, 4) == 0.75).should be_true + (Rational(4, 2) == 2.0).should be_true + (Rational(-4, 2) == -2.0).should be_true + (Rational(4, -2) == -2.0).should be_true + end +end + +describe "Rational#== when passed an Integer" do + it "returns true if self has the passed argument as numerator and a denominator of 1" do + # Rational(x, y) reduces x and y automatically + (Rational(4, 2) == 2).should be_true + (Rational(-4, 2) == -2).should be_true + (Rational(4, -2) == -2).should be_true + end +end diff --git a/spec/ruby/core/rational/exponent_spec.rb b/spec/ruby/core/rational/exponent_spec.rb new file mode 100644 index 0000000000..65fbf2ed1c --- /dev/null +++ b/spec/ruby/core/rational/exponent_spec.rb @@ -0,0 +1,236 @@ +require_relative "../../spec_helper" + +describe "Rational#**" do + describe "when passed Rational" do + # Guard against the Mathn library + guard -> { !defined?(Math.rsqrt) } do + it "returns Rational(1) if the exponent is Rational(0)" do + (Rational(0) ** Rational(0)).should eql(Rational(1)) + (Rational(1) ** Rational(0)).should eql(Rational(1)) + (Rational(3, 4) ** Rational(0)).should eql(Rational(1)) + (Rational(-1) ** Rational(0)).should eql(Rational(1)) + (Rational(-3, 4) ** Rational(0)).should eql(Rational(1)) + (Rational(bignum_value) ** Rational(0)).should eql(Rational(1)) + (Rational(-bignum_value) ** Rational(0)).should eql(Rational(1)) + end + + it "returns self raised to the argument as a Rational if the exponent's denominator is 1" do + (Rational(3, 4) ** Rational(1, 1)).should eql(Rational(3, 4)) + (Rational(3, 4) ** Rational(2, 1)).should eql(Rational(9, 16)) + (Rational(3, 4) ** Rational(-1, 1)).should eql(Rational(4, 3)) + (Rational(3, 4) ** Rational(-2, 1)).should eql(Rational(16, 9)) + end + + it "returns self raised to the argument as a Float if the exponent's denominator is not 1" do + (Rational(3, 4) ** Rational(4, 3)).should be_close(0.681420222312052, TOLERANCE) + (Rational(3, 4) ** Rational(-4, 3)).should be_close(1.46752322173095, TOLERANCE) + (Rational(3, 4) ** Rational(4, -3)).should be_close(1.46752322173095, TOLERANCE) + end + + it "returns a complex number when self is negative and the passed argument is not 0" do + (Rational(-3, 4) ** Rational(-4, 3)).should be_close(Complex(-0.7337616108654732, 1.2709123906625817), TOLERANCE) + end + end + end + + describe "when passed Integer" do + it "returns the Rational value of self raised to the passed argument" do + (Rational(3, 4) ** 4).should == Rational(81, 256) + (Rational(3, 4) ** -4).should == Rational(256, 81) + (Rational(-3, 4) ** -4).should == Rational(256, 81) + (Rational(3, -4) ** -4).should == Rational(256, 81) + + (Rational(bignum_value, 4) ** 4).should == Rational(452312848583266388373324160190187140051835877600158453279131187530910662656, 1) + (Rational(3, bignum_value) ** -4).should == Rational(115792089237316195423570985008687907853269984665640564039457584007913129639936, 81) + (Rational(-bignum_value, 4) ** -4).should == Rational(1, 452312848583266388373324160190187140051835877600158453279131187530910662656) + (Rational(3, -bignum_value) ** -4).should == Rational(115792089237316195423570985008687907853269984665640564039457584007913129639936, 81) + end + + # Guard against the Mathn library + guard -> { !defined?(Math.rsqrt) } do + it "returns Rational(1, 1) when the passed argument is 0" do + (Rational(3, 4) ** 0).should eql(Rational(1, 1)) + (Rational(-3, 4) ** 0).should eql(Rational(1, 1)) + (Rational(3, -4) ** 0).should eql(Rational(1, 1)) + + (Rational(bignum_value, 4) ** 0).should eql(Rational(1, 1)) + (Rational(3, -bignum_value) ** 0).should eql(Rational(1, 1)) + end + end + end + + describe "when passed Bignum" do + # #5713 + it "returns Rational(0) when self is Rational(0) and the exponent is positive" do + (Rational(0) ** bignum_value).should eql(Rational(0)) + end + + it "raises ZeroDivisionError when self is Rational(0) and the exponent is negative" do + -> { Rational(0) ** -bignum_value }.should raise_error(ZeroDivisionError) + end + + it "returns Rational(1) when self is Rational(1)" do + (Rational(1) ** bignum_value).should eql(Rational(1)) + (Rational(1) ** -bignum_value).should eql(Rational(1)) + end + + it "returns Rational(1) when self is Rational(-1) and the exponent is positive and even" do + (Rational(-1) ** bignum_value(0)).should eql(Rational(1)) + (Rational(-1) ** bignum_value(2)).should eql(Rational(1)) + end + + it "returns Rational(-1) when self is Rational(-1) and the exponent is positive and odd" do + (Rational(-1) ** bignum_value(1)).should eql(Rational(-1)) + (Rational(-1) ** bignum_value(3)).should eql(Rational(-1)) + end + + ruby_version_is ""..."3.4" do + it "returns positive Infinity when self is > 1" do + -> { + (Rational(2) ** bignum_value).infinite?.should == 1 + }.should complain(/warning: in a\*\*b, b may be too big/) + -> { + (Rational(fixnum_max) ** bignum_value).infinite?.should == 1 + }.should complain(/warning: in a\*\*b, b may be too big/) + end + + it "returns 0.0 when self is > 1 and the exponent is negative" do + -> { + (Rational(2) ** -bignum_value).should eql(0.0) + }.should complain(/warning: in a\*\*b, b may be too big/) + -> { + (Rational(fixnum_max) ** -bignum_value).should eql(0.0) + }.should complain(/warning: in a\*\*b, b may be too big/) + end + end + + ruby_version_is "3.4" do + it "raises an ArgumentError when self is > 1" do + -> { + (Rational(2) ** bignum_value) + }.should raise_error(ArgumentError) + -> { + (Rational(fixnum_max) ** bignum_value) + }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when self is > 1 and the exponent is negative" do + -> { + (Rational(2) ** -bignum_value) + }.should raise_error(ArgumentError) + -> { + (Rational(fixnum_max) ** -bignum_value) + }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when self is < -1" do + -> { + (Rational(-2) ** bignum_value) + }.should raise_error(ArgumentError) + -> { + (Rational(fixnum_min) ** bignum_value) + }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when self is < -1 and the exponent is negative" do + -> { + (Rational(-2) ** -bignum_value) + }.should raise_error(ArgumentError) + -> { + (Rational(fixnum_min) ** -bignum_value) + }.should raise_error(ArgumentError) + end + end + + # Fails on linux due to pow() bugs in glibc: http://sources.redhat.com/bugzilla/show_bug.cgi?id=3866 + platform_is_not :linux do + ruby_version_is ""..."3.4" do + it "returns positive Infinity when self < -1" do + -> { + (Rational(-2) ** bignum_value).infinite?.should == 1 + }.should complain(/warning: in a\*\*b, b may be too big/) + -> { + (Rational(-2) ** (bignum_value + 1)).infinite?.should == 1 + }.should complain(/warning: in a\*\*b, b may be too big/) + -> { + (Rational(fixnum_min) ** bignum_value).infinite?.should == 1 + }.should complain(/warning: in a\*\*b, b may be too big/) + end + + it "returns 0.0 when self is < -1 and the exponent is negative" do + -> { + (Rational(-2) ** -bignum_value).should eql(0.0) + }.should complain(/warning: in a\*\*b, b may be too big/) + -> { + (Rational(fixnum_min) ** -bignum_value).should eql(0.0) + }.should complain(/warning: in a\*\*b, b may be too big/) + end + end + end + end + + describe "when passed Float" do + it "returns self converted to Float and raised to the passed argument" do + (Rational(3, 1) ** 3.0).should eql(27.0) + (Rational(3, 1) ** 1.5).should be_close(5.19615242270663, TOLERANCE) + (Rational(3, 1) ** -1.5).should be_close(0.192450089729875, TOLERANCE) + end + + it "returns a complex number if self is negative and the passed argument is not 0" do + (Rational(-3, 2) ** 1.5).should be_close(Complex(0.0, -1.8371173070873836), TOLERANCE) + (Rational(3, -2) ** 1.5).should be_close(Complex(0.0, -1.8371173070873836), TOLERANCE) + (Rational(3, -2) ** -1.5).should be_close(Complex(0.0, 0.5443310539518174), TOLERANCE) + end + + it "returns Complex(1.0) when the passed argument is 0.0" do + (Rational(3, 4) ** 0.0).should == Complex(1.0) + (Rational(-3, 4) ** 0.0).should == Complex(1.0) + (Rational(-3, 4) ** 0.0).should == Complex(1.0) + end + end + + it "calls #coerce on the passed argument with self" do + rational = Rational(3, 4) + obj = mock("Object") + obj.should_receive(:coerce).with(rational).and_return([1, 2]) + + rational ** obj + end + + it "calls #** on the coerced Rational with the coerced Object" do + rational = Rational(3, 4) + + coerced_rational = mock("Coerced Rational") + coerced_rational.should_receive(:**).and_return(:result) + + coerced_obj = mock("Coerced Object") + + obj = mock("Object") + obj.should_receive(:coerce).and_return([coerced_rational, coerced_obj]) + + (rational ** obj).should == :result + end + + it "raises ZeroDivisionError for Rational(0, 1) passed a negative Integer" do + [-1, -4, -9999].each do |exponent| + -> { Rational(0, 1) ** exponent }.should raise_error(ZeroDivisionError, "divided by 0") + end + end + + it "raises ZeroDivisionError for Rational(0, 1) passed a negative Rational with denominator 1" do + [Rational(-1, 1), Rational(-3, 1)].each do |exponent| + -> { Rational(0, 1) ** exponent }.should raise_error(ZeroDivisionError, "divided by 0") + end + end + + # #7513 + it "raises ZeroDivisionError for Rational(0, 1) passed a negative Rational" do + -> { Rational(0, 1) ** Rational(-3, 2) }.should raise_error(ZeroDivisionError, "divided by 0") + end + + it "returns Infinity for Rational(0, 1) passed a negative Float" do + [-1.0, -3.0, -3.14].each do |exponent| + (Rational(0, 1) ** exponent).infinite?.should == 1 + end + end +end diff --git a/spec/ruby/core/rational/fdiv_spec.rb b/spec/ruby/core/rational/fdiv_spec.rb new file mode 100644 index 0000000000..118d93dbe7 --- /dev/null +++ b/spec/ruby/core/rational/fdiv_spec.rb @@ -0,0 +1,5 @@ +require_relative "../../spec_helper" + +describe "Rational#fdiv" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/rational/fixtures/rational.rb b/spec/ruby/core/rational/fixtures/rational.rb new file mode 100644 index 0000000000..844d7f9820 --- /dev/null +++ b/spec/ruby/core/rational/fixtures/rational.rb @@ -0,0 +1,14 @@ +module RationalSpecs + class SubNumeric < Numeric + def initialize(value) + @value = Rational(value) + end + + def to_r + @value + end + end + + class CoerceError < StandardError + end +end diff --git a/spec/ruby/core/rational/floor_spec.rb b/spec/ruby/core/rational/floor_spec.rb new file mode 100644 index 0000000000..8068aaf119 --- /dev/null +++ b/spec/ruby/core/rational/floor_spec.rb @@ -0,0 +1,45 @@ +require_relative "../../spec_helper" + +describe "Rational#floor" do + before do + @rational = Rational(2200, 7) + end + + describe "with no arguments (precision = 0)" do + it "returns an integer" do + @rational.floor.should be_kind_of(Integer) + end + + it "returns the truncated value toward negative infinity" do + @rational.floor.should == 314 + Rational(1, 2).floor.should == 0 + Rational(-1, 2).floor.should == -1 + end + end + + describe "with a precision < 0" do + it "returns an integer" do + @rational.floor(-2).should be_kind_of(Integer) + @rational.floor(-1).should be_kind_of(Integer) + end + + it "moves the truncation point n decimal places left" do + @rational.floor(-3).should == 0 + @rational.floor(-2).should == 300 + @rational.floor(-1).should == 310 + end + end + + describe "with a precision > 0" do + it "returns a Rational" do + @rational.floor(1).should be_kind_of(Rational) + @rational.floor(2).should be_kind_of(Rational) + end + + it "moves the truncation point n decimal places right" do + @rational.floor(1).should == Rational(1571, 5) + @rational.floor(2).should == Rational(7857, 25) + @rational.floor(3).should == Rational(62857, 200) + end + end +end diff --git a/spec/ruby/core/rational/hash_spec.rb b/spec/ruby/core/rational/hash_spec.rb new file mode 100644 index 0000000000..528638056a --- /dev/null +++ b/spec/ruby/core/rational/hash_spec.rb @@ -0,0 +1,9 @@ +require_relative "../../spec_helper" + +describe "Rational#hash" do + # BUG: Rational(2, 3).hash == Rational(3, 2).hash + it "is static" do + Rational(2, 3).hash.should == Rational(2, 3).hash + Rational(2, 4).hash.should_not == Rational(2, 3).hash + end +end diff --git a/spec/ruby/core/rational/inspect_spec.rb b/spec/ruby/core/rational/inspect_spec.rb new file mode 100644 index 0000000000..edc5cffee9 --- /dev/null +++ b/spec/ruby/core/rational/inspect_spec.rb @@ -0,0 +1,14 @@ +require_relative "../../spec_helper" + +describe "Rational#inspect" do + it "returns a string representation of self" do + Rational(3, 4).inspect.should == "(3/4)" + Rational(-5, 8).inspect.should == "(-5/8)" + Rational(-1, -2).inspect.should == "(1/2)" + + # Guard against the Mathn library + guard -> { !defined?(Math.rsqrt) } do + Rational(bignum_value, 1).inspect.should == "(#{bignum_value}/1)" + end + end +end diff --git a/spec/ruby/core/rational/integer_spec.rb b/spec/ruby/core/rational/integer_spec.rb new file mode 100644 index 0000000000..be7476a9dd --- /dev/null +++ b/spec/ruby/core/rational/integer_spec.rb @@ -0,0 +1,13 @@ +require_relative "../../spec_helper" +describe "Rational#integer?" do + # Guard against the Mathn library + guard -> { !defined?(Math.rsqrt) } do + it "returns false for a rational with a numerator and no denominator" do + Rational(20).integer?.should be_false + end + end + + it "returns false for a rational with a numerator and a denominator" do + Rational(20,3).integer?.should be_false + end +end diff --git a/spec/ruby/core/rational/magnitude_spec.rb b/spec/ruby/core/rational/magnitude_spec.rb new file mode 100644 index 0000000000..f5f667edb1 --- /dev/null +++ b/spec/ruby/core/rational/magnitude_spec.rb @@ -0,0 +1,6 @@ +require_relative "../../spec_helper" +require_relative 'shared/abs' + +describe "Rational#abs" do + it_behaves_like :rational_abs, :magnitude +end diff --git a/spec/ruby/core/rational/marshal_dump_spec.rb b/spec/ruby/core/rational/marshal_dump_spec.rb new file mode 100644 index 0000000000..17a6107cd5 --- /dev/null +++ b/spec/ruby/core/rational/marshal_dump_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' + +describe "Rational#marshal_dump" do + it "is a private method" do + Rational.should have_private_instance_method(:marshal_dump, false) + end + + it "dumps numerator and denominator" do + Rational(1, 2).send(:marshal_dump).should == [1, 2] + end +end diff --git a/spec/ruby/core/rational/minus_spec.rb b/spec/ruby/core/rational/minus_spec.rb new file mode 100644 index 0000000000..8aee85f9dd --- /dev/null +++ b/spec/ruby/core/rational/minus_spec.rb @@ -0,0 +1,51 @@ +require_relative '../../spec_helper' +require_relative 'shared/arithmetic_exception_in_coerce' + +describe "Rational#-" do + it_behaves_like :rational_arithmetic_exception_in_coerce, :- + + it "calls #coerce on the passed argument with self" do + rational = Rational(3, 4) + obj = mock("Object") + obj.should_receive(:coerce).with(rational).and_return([1, 2]) + + rational - obj + end + + it "calls #- on the coerced Rational with the coerced Object" do + rational = Rational(3, 4) + + coerced_rational = mock("Coerced Rational") + coerced_rational.should_receive(:-).and_return(:result) + + coerced_obj = mock("Coerced Object") + + obj = mock("Object") + obj.should_receive(:coerce).and_return([coerced_rational, coerced_obj]) + + (rational - obj).should == :result + end +end + +describe "Rational#- passed a Rational" do + it "returns the result of subtracting other from self as a Rational" do + (Rational(3, 4) - Rational(0, 1)).should eql(Rational(3, 4)) + (Rational(3, 4) - Rational(1, 4)).should eql(Rational(1, 2)) + + (Rational(3, 4) - Rational(2, 1)).should eql(Rational(-5, 4)) + end +end + +describe "Rational#- passed a Float" do + it "returns the result of subtracting other from self as a Float" do + (Rational(3, 4) - 0.2).should eql(0.55) + (Rational(3, 4) - 2.5).should eql(-1.75) + end +end + +describe "Rational#- passed an Integer" do + it "returns the result of subtracting other from self as a Rational" do + (Rational(3, 4) - 1).should eql(Rational(-1, 4)) + (Rational(3, 4) - 2).should eql(Rational(-5, 4)) + end +end diff --git a/spec/ruby/core/rational/modulo_spec.rb b/spec/ruby/core/rational/modulo_spec.rb new file mode 100644 index 0000000000..23ed93e118 --- /dev/null +++ b/spec/ruby/core/rational/modulo_spec.rb @@ -0,0 +1,43 @@ +require_relative "../../spec_helper" + +describe "Rational#%" do + it "returns the remainder when this value is divided by other" do + (Rational(2, 3) % Rational(2, 3)).should == Rational(0, 1) + (Rational(4, 3) % Rational(2, 3)).should == Rational(0, 1) + (Rational(2, -3) % Rational(-2, 3)).should == Rational(0, 1) + (Rational(0, -1) % -1).should == Rational(0, 1) + + (Rational(7, 4) % Rational(1, 2)).should == Rational(1, 4) + (Rational(7, 4) % 1).should == Rational(3, 4) + (Rational(7, 4) % Rational(1, 7)).should == Rational(1, 28) + + (Rational(3, 4) % -1).should == Rational(-1, 4) + (Rational(1, -5) % -1).should == Rational(-1, 5) + end + + it "returns a Float value when the argument is Float" do + (Rational(7, 4) % 1.0).should be_kind_of(Float) + (Rational(7, 4) % 1.0).should == 0.75 + (Rational(7, 4) % 0.26).should be_close(0.19, 0.0001) + end + + it "raises ZeroDivisionError on zero denominator" do + -> { + Rational(3, 5) % Rational(0, 1) + }.should raise_error(ZeroDivisionError) + + -> { + Rational(0, 1) % Rational(0, 1) + }.should raise_error(ZeroDivisionError) + + -> { + Rational(3, 5) % 0 + }.should raise_error(ZeroDivisionError) + end + + it "raises a ZeroDivisionError when the argument is 0.0" do + -> { + Rational(3, 5) % 0.0 + }.should raise_error(ZeroDivisionError) + end +end diff --git a/spec/ruby/core/rational/multiply_spec.rb b/spec/ruby/core/rational/multiply_spec.rb new file mode 100644 index 0000000000..87fb4de2b4 --- /dev/null +++ b/spec/ruby/core/rational/multiply_spec.rb @@ -0,0 +1,65 @@ +require_relative "../../spec_helper" +require_relative 'shared/arithmetic_exception_in_coerce' + +describe "Rational#*" do + it "calls #coerce on the passed argument with self" do + rational = Rational(3, 4) + obj = mock("Object") + obj.should_receive(:coerce).with(rational).and_return([1, 2]) + + rational * obj + end + + it "calls #* on the coerced Rational with the coerced Object" do + rational = Rational(3, 4) + + coerced_rational = mock("Coerced Rational") + coerced_rational.should_receive(:*).and_return(:result) + + coerced_obj = mock("Coerced Object") + + obj = mock("Object") + obj.should_receive(:coerce).and_return([coerced_rational, coerced_obj]) + + (rational * obj).should == :result + end + + it_behaves_like :rational_arithmetic_exception_in_coerce, :* +end + +describe "Rational#* passed a Rational" do + it "returns self divided by other as a Rational" do + (Rational(3, 4) * Rational(3, 4)).should eql(Rational(9, 16)) + (Rational(2, 4) * Rational(1, 4)).should eql(Rational(1, 8)) + + (Rational(3, 4) * Rational(0, 1)).should eql(Rational(0, 4)) + end +end + +describe "Rational#* passed a Float" do + it "returns self divided by other as a Float" do + (Rational(3, 4) * 0.75).should eql(0.5625) + (Rational(3, 4) * 0.25).should eql(0.1875) + (Rational(3, 4) * 0.3).should be_close(0.225, TOLERANCE) + + (Rational(-3, 4) * 0.3).should be_close(-0.225, TOLERANCE) + (Rational(3, -4) * 0.3).should be_close(-0.225, TOLERANCE) + (Rational(3, 4) * -0.3).should be_close(-0.225, TOLERANCE) + + (Rational(3, 4) * 0.0).should eql(0.0) + (Rational(-3, -4) * 0.0).should eql(0.0) + + (Rational(-3, 4) * 0.0).should eql(0.0) + (Rational(3, -4) * 0.0).should eql(0.0) + end +end + +describe "Rational#* passed an Integer" do + it "returns self divided by other as a Rational" do + (Rational(3, 4) * 2).should eql(Rational(3, 2)) + (Rational(2, 4) * 2).should eql(Rational(1, 1)) + (Rational(6, 7) * -2).should eql(Rational(-12, 7)) + + (Rational(3, 4) * 0).should eql(Rational(0, 4)) + end +end diff --git a/spec/ruby/core/rational/numerator_spec.rb b/spec/ruby/core/rational/numerator_spec.rb new file mode 100644 index 0000000000..2b9fe2ff5c --- /dev/null +++ b/spec/ruby/core/rational/numerator_spec.rb @@ -0,0 +1,10 @@ +require_relative "../../spec_helper" + +describe "Rational#numerator" do + it "returns the numerator" do + Rational(3, 4).numerator.should equal(3) + Rational(3, -4).numerator.should equal(-3) + + Rational(bignum_value, 1).numerator.should == bignum_value + end +end diff --git a/spec/ruby/core/rational/plus_spec.rb b/spec/ruby/core/rational/plus_spec.rb new file mode 100644 index 0000000000..01df5f6719 --- /dev/null +++ b/spec/ruby/core/rational/plus_spec.rb @@ -0,0 +1,50 @@ +require_relative "../../spec_helper" +require_relative 'shared/arithmetic_exception_in_coerce' + +describe "Rational#+" do + it "calls #coerce on the passed argument with self" do + rational = Rational(3, 4) + obj = mock("Object") + obj.should_receive(:coerce).with(rational).and_return([1, 2]) + + rational + obj + end + + it "calls #+ on the coerced Rational with the coerced Object" do + rational = Rational(3, 4) + + coerced_rational = mock("Coerced Rational") + coerced_rational.should_receive(:+).and_return(:result) + + coerced_obj = mock("Coerced Object") + + obj = mock("Object") + obj.should_receive(:coerce).and_return([coerced_rational, coerced_obj]) + + (rational + obj).should == :result + end + + it_behaves_like :rational_arithmetic_exception_in_coerce, :+ +end + +describe "Rational#+ with a Rational" do + it "returns the result of subtracting other from self as a Rational" do + (Rational(3, 4) + Rational(0, 1)).should eql(Rational(3, 4)) + (Rational(3, 4) + Rational(1, 4)).should eql(Rational(1, 1)) + + (Rational(3, 4) + Rational(2, 1)).should eql(Rational(11, 4)) + end +end +describe "Rational#+ with a Float" do + it "returns the result of subtracting other from self as a Float" do + (Rational(3, 4) + 0.2).should eql(0.95) + (Rational(3, 4) + 2.5).should eql(3.25) + end +end + +describe "Rational#+ with an Integer" do + it "returns the result of subtracting other from self as a Rational" do + (Rational(3, 4) + 1).should eql(Rational(7, 4)) + (Rational(3, 4) + 2).should eql(Rational(11, 4)) + end +end diff --git a/spec/ruby/core/rational/quo_spec.rb b/spec/ruby/core/rational/quo_spec.rb new file mode 100644 index 0000000000..907898ad34 --- /dev/null +++ b/spec/ruby/core/rational/quo_spec.rb @@ -0,0 +1,25 @@ +require_relative "../../spec_helper" + +describe "Rational#quo" do + it "calls #coerce on the passed argument with self" do + rational = Rational(3, 4) + obj = mock("Object") + obj.should_receive(:coerce).with(rational).and_return([1, 2]) + + rational.quo(obj) + end + + it "calls #/ on the coerced Rational with the coerced Object" do + rational = Rational(3, 4) + + coerced_rational = mock("Coerced Rational") + coerced_rational.should_receive(:/).and_return(:result) + + coerced_obj = mock("Coerced Object") + + obj = mock("Object") + obj.should_receive(:coerce).and_return([coerced_rational, coerced_obj]) + + rational.quo(obj).should == :result + end +end diff --git a/spec/ruby/core/rational/rational_spec.rb b/spec/ruby/core/rational/rational_spec.rb new file mode 100644 index 0000000000..482deab38d --- /dev/null +++ b/spec/ruby/core/rational/rational_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' + +describe "Rational" do + it "includes Comparable" do + Rational.include?(Comparable).should == true + end + + it "does not respond to new" do + -> { Rational.new(1) }.should raise_error(NoMethodError) + end +end diff --git a/spec/ruby/core/rational/rationalize_spec.rb b/spec/ruby/core/rational/rationalize_spec.rb new file mode 100644 index 0000000000..3db027d446 --- /dev/null +++ b/spec/ruby/core/rational/rationalize_spec.rb @@ -0,0 +1,36 @@ +require_relative '../../spec_helper' + +describe "Rational#rationalize" do + it "returns self with no argument" do + Rational(12,3).rationalize.should == Rational(12,3) + Rational(-45,7).rationalize.should == Rational(-45,7) + end + + # FIXME: These specs need reviewing by somebody familiar with the + # algorithm used by #rationalize + it "simplifies self to the degree specified by a Rational argument" do + r = Rational(5404319552844595,18014398509481984) + r.rationalize(Rational(1,10)).should == Rational(1,3) + r.rationalize(Rational(-1,10)).should == Rational(1,3) + + r = Rational(-5404319552844595,18014398509481984) + r.rationalize(Rational(1,10)).should == Rational(-1,3) + r.rationalize(Rational(-1,10)).should == Rational(-1,3) + + end + + it "simplifies self to the degree specified by a Float argument" do + r = Rational(5404319552844595,18014398509481984) + r.rationalize(0.05).should == Rational(1,3) + r.rationalize(0.001).should == Rational(3, 10) + + r = Rational(-5404319552844595,18014398509481984) + r.rationalize(0.05).should == Rational(-1,3) + r.rationalize(0.001).should == Rational(-3,10) + end + + it "raises ArgumentError when passed more than one argument" do + -> { Rational(1,1).rationalize(0.1, 0.1) }.should raise_error(ArgumentError) + -> { Rational(1,1).rationalize(0.1, 0.1, 2) }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/rational/remainder_spec.rb b/spec/ruby/core/rational/remainder_spec.rb new file mode 100644 index 0000000000..86ba4674e6 --- /dev/null +++ b/spec/ruby/core/rational/remainder_spec.rb @@ -0,0 +1,5 @@ +require_relative "../../spec_helper" + +describe "Rational#remainder" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/rational/round_spec.rb b/spec/ruby/core/rational/round_spec.rb new file mode 100644 index 0000000000..ac3dcafe7b --- /dev/null +++ b/spec/ruby/core/rational/round_spec.rb @@ -0,0 +1,106 @@ +require_relative '../../spec_helper' + +describe "Rational#round" do + before do + @rational = Rational(2200, 7) + end + + describe "with no arguments (precision = 0)" do + it "returns an integer" do + @rational.round.should be_kind_of(Integer) + Rational(0, 1).round(0).should be_kind_of(Integer) + Rational(124, 1).round(0).should be_kind_of(Integer) + end + + it "returns the truncated value toward the nearest integer" do + @rational.round.should == 314 + Rational(0, 1).round(0).should == 0 + Rational(2, 1).round(0).should == 2 + end + + it "returns the rounded value toward the nearest integer" do + Rational(1, 2).round.should == 1 + Rational(-1, 2).round.should == -1 + Rational(3, 2).round.should == 2 + Rational(-3, 2).round.should == -2 + Rational(5, 2).round.should == 3 + Rational(-5, 2).round.should == -3 + end + end + + describe "with a precision < 0" do + it "returns an integer" do + @rational.round(-2).should be_kind_of(Integer) + @rational.round(-1).should be_kind_of(Integer) + Rational(0, 1).round(-1).should be_kind_of(Integer) + Rational(2, 1).round(-1).should be_kind_of(Integer) + end + + it "moves the truncation point n decimal places left" do + @rational.round(-3).should == 0 + @rational.round(-2).should == 300 + @rational.round(-1).should == 310 + end + end + + describe "with a precision > 0" do + it "returns a Rational" do + @rational.round(1).should be_kind_of(Rational) + @rational.round(2).should be_kind_of(Rational) + # Guard against the Mathn library + guard -> { !defined?(Math.rsqrt) } do + Rational(0, 1).round(1).should be_kind_of(Rational) + Rational(2, 1).round(1).should be_kind_of(Rational) + end + end + + it "moves the truncation point n decimal places right" do + @rational.round(1).should == Rational(3143, 10) + @rational.round(2).should == Rational(31429, 100) + @rational.round(3).should == Rational(157143, 500) + Rational(0, 1).round(1).should == Rational(0, 1) + Rational(2, 1).round(1).should == Rational(2, 1) + end + + it "doesn't alter the value if the precision is too great" do + Rational(3, 2).round(10).should == Rational(3, 2).round(20) + end + + # #6605 + it "doesn't fail when rounding to an absurdly large positive precision" do + Rational(3, 2).round(2_097_171).should == Rational(3, 2) + end + end + + describe "with half option" do + it "returns an Integer when precision is not passed" do + Rational(10, 4).round(half: nil).should == 3 + Rational(10, 4).round(half: :up).should == 3 + Rational(10, 4).round(half: :down).should == 2 + Rational(10, 4).round(half: :even).should == 2 + Rational(-10, 4).round(half: nil).should == -3 + Rational(-10, 4).round(half: :up).should == -3 + Rational(-10, 4).round(half: :down).should == -2 + Rational(-10, 4).round(half: :even).should == -2 + end + + it "returns a Rational when the precision is greater than 0" do + Rational(25, 100).round(1, half: nil).should == Rational(3, 10) + Rational(25, 100).round(1, half: :up).should == Rational(3, 10) + Rational(25, 100).round(1, half: :down).should == Rational(1, 5) + Rational(25, 100).round(1, half: :even).should == Rational(1, 5) + Rational(35, 100).round(1, half: nil).should == Rational(2, 5) + Rational(35, 100).round(1, half: :up).should == Rational(2, 5) + Rational(35, 100).round(1, half: :down).should == Rational(3, 10) + Rational(35, 100).round(1, half: :even).should == Rational(2, 5) + Rational(-25, 100).round(1, half: nil).should == Rational(-3, 10) + Rational(-25, 100).round(1, half: :up).should == Rational(-3, 10) + Rational(-25, 100).round(1, half: :down).should == Rational(-1, 5) + Rational(-25, 100).round(1, half: :even).should == Rational(-1, 5) + end + + it "raise for a non-existent round mode" do + -> { Rational(10, 4).round(half: :nonsense) }.should raise_error(ArgumentError, "invalid rounding mode: nonsense") + end + end +end diff --git a/spec/ruby/core/rational/shared/abs.rb b/spec/ruby/core/rational/shared/abs.rb new file mode 100644 index 0000000000..3d64bcc1a0 --- /dev/null +++ b/spec/ruby/core/rational/shared/abs.rb @@ -0,0 +1,11 @@ +require_relative '../../../spec_helper' + +describe :rational_abs, shared: true do + it "returns self's absolute value" do + Rational(3, 4).send(@method).should == Rational(3, 4) + Rational(-3, 4).send(@method).should == Rational(3, 4) + Rational(3, -4).send(@method).should == Rational(3, 4) + + Rational(bignum_value, -bignum_value).send(@method).should == Rational(bignum_value, bignum_value) + end +end diff --git a/spec/ruby/core/rational/shared/arithmetic_exception_in_coerce.rb b/spec/ruby/core/rational/shared/arithmetic_exception_in_coerce.rb new file mode 100644 index 0000000000..f4cf70d147 --- /dev/null +++ b/spec/ruby/core/rational/shared/arithmetic_exception_in_coerce.rb @@ -0,0 +1,11 @@ +require_relative '../fixtures/rational' + +describe :rational_arithmetic_exception_in_coerce, shared: true do + it "does not rescue exception raised in other#coerce" do + b = mock("numeric with failed #coerce") + b.should_receive(:coerce).and_raise(RationalSpecs::CoerceError) + + # e.g. Rational(3, 4) + b + -> { Rational(3, 4).send(@method, b) }.should raise_error(RationalSpecs::CoerceError) + end +end diff --git a/spec/ruby/core/rational/to_f_spec.rb b/spec/ruby/core/rational/to_f_spec.rb new file mode 100644 index 0000000000..d0da49d377 --- /dev/null +++ b/spec/ruby/core/rational/to_f_spec.rb @@ -0,0 +1,16 @@ +require_relative "../../spec_helper" + +describe "Rational#to_f" do + it "returns self converted to a Float" do + Rational(3, 4).to_f.should eql(0.75) + Rational(3, -4).to_f.should eql(-0.75) + Rational(-1, 4).to_f.should eql(-0.25) + Rational(-1, -4).to_f.should eql(0.25) + end + + it "converts to a Float for large numerator and denominator" do + num = 1000000000000000000000000000000000048148248609680896326399448564623182963452541226153892315137780403285956264146010000000000000000000000000000000000048148248609680896326399448564623182963452541226153892315137780403285956264146010000000000000000000000000000000000048148248609680896326399448564623182963452541226153892315137780403285956264146009 + den = 2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 + Rational(num, den).to_f.should == 500.0 + end +end diff --git a/spec/ruby/core/rational/to_i_spec.rb b/spec/ruby/core/rational/to_i_spec.rb new file mode 100644 index 0000000000..520a380b2a --- /dev/null +++ b/spec/ruby/core/rational/to_i_spec.rb @@ -0,0 +1,12 @@ +require_relative "../../spec_helper" + +describe "Rational#to_i" do + it "converts self to an Integer by truncation" do + Rational(7, 4).to_i.should eql(1) + Rational(11, 4).to_i.should eql(2) + end + + it "converts self to an Integer by truncation" do + Rational(-7, 4).to_i.should eql(-1) + end +end diff --git a/spec/ruby/core/rational/to_r_spec.rb b/spec/ruby/core/rational/to_r_spec.rb new file mode 100644 index 0000000000..34f16d7890 --- /dev/null +++ b/spec/ruby/core/rational/to_r_spec.rb @@ -0,0 +1,26 @@ +require_relative "../../spec_helper" + +describe "Rational#to_r" do + it "returns self" do + a = Rational(3, 4) + a.to_r.should equal(a) + + a = Rational(bignum_value, 4) + a.to_r.should equal(a) + end + + it "raises TypeError trying to convert BasicObject" do + obj = BasicObject.new + -> { Rational(obj) }.should raise_error(TypeError) + end + + it "works when a BasicObject has to_r" do + obj = BasicObject.new; def obj.to_r; 1 / 2.to_r end + Rational(obj).should == Rational('1/2') + end + + it "fails when a BasicObject's to_r does not return a Rational" do + obj = BasicObject.new; def obj.to_r; 1 end + -> { Rational(obj) }.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/rational/to_s_spec.rb b/spec/ruby/core/rational/to_s_spec.rb new file mode 100644 index 0000000000..24e30778e5 --- /dev/null +++ b/spec/ruby/core/rational/to_s_spec.rb @@ -0,0 +1,14 @@ +require_relative "../../spec_helper" + +describe "Rational#to_s" do + it "returns a string representation of self" do + # Guard against the Mathn library + guard -> { !defined?(Math.rsqrt) } do + Rational(1, 1).to_s.should == "1/1" + Rational(2, 1).to_s.should == "2/1" + end + Rational(1, 2).to_s.should == "1/2" + Rational(-1, 3).to_s.should == "-1/3" + Rational(1, -3).to_s.should == "-1/3" + end +end diff --git a/spec/ruby/core/rational/truncate_spec.rb b/spec/ruby/core/rational/truncate_spec.rb new file mode 100644 index 0000000000..728fca34ea --- /dev/null +++ b/spec/ruby/core/rational/truncate_spec.rb @@ -0,0 +1,71 @@ +require_relative "../../spec_helper" + +describe "Rational#truncate" do + before do + @rational = Rational(2200, 7) + end + + describe "with no arguments (precision = 0)" do + it "returns an integer" do + @rational.truncate.should be_kind_of(Integer) + end + + it "returns the truncated value toward 0" do + @rational.truncate.should == 314 + Rational(1, 2).truncate.should == 0 + Rational(-1, 2).truncate.should == 0 + end + end + + describe "with an explicit precision = 0" do + it "returns an integer" do + @rational.truncate(0).should be_kind_of(Integer) + end + + it "returns the truncated value toward 0" do + @rational.truncate(0).should == 314 + Rational(1, 2).truncate(0).should == 0 + Rational(-1, 2).truncate(0).should == 0 + end + end + + describe "with a precision < 0" do + it "returns an integer" do + @rational.truncate(-2).should be_kind_of(Integer) + @rational.truncate(-1).should be_kind_of(Integer) + end + + it "moves the truncation point n decimal places left" do + @rational.truncate(-3).should == 0 + @rational.truncate(-2).should == 300 + @rational.truncate(-1).should == 310 + end + end + + describe "with a precision > 0" do + it "returns a Rational" do + @rational.truncate(1).should be_kind_of(Rational) + @rational.truncate(2).should be_kind_of(Rational) + end + + it "moves the truncation point n decimal places right" do + @rational.truncate(1).should == Rational(1571, 5) + @rational.truncate(2).should == Rational(7857, 25) + @rational.truncate(3).should == Rational(62857, 200) + end + end + + describe "with an invalid value for precision" do + it "raises a TypeError" do + -> { @rational.truncate(nil) }.should raise_error(TypeError, "not an integer") + -> { @rational.truncate(1.0) }.should raise_error(TypeError, "not an integer") + -> { @rational.truncate('') }.should raise_error(TypeError, "not an integer") + end + + it "does not call to_int on the argument" do + object = Object.new + object.should_not_receive(:to_int) + -> { @rational.truncate(object) }.should raise_error(TypeError, "not an integer") + end + end +end diff --git a/spec/ruby/core/rational/zero_spec.rb b/spec/ruby/core/rational/zero_spec.rb new file mode 100644 index 0000000000..af7fb391ac --- /dev/null +++ b/spec/ruby/core/rational/zero_spec.rb @@ -0,0 +1,14 @@ +require_relative "../../spec_helper" +describe "Rational#zero?" do + it "returns true if the numerator is 0" do + Rational(0,26).zero?.should be_true + end + + it "returns true if the numerator is 0.0" do + Rational(0.0,26).zero?.should be_true + end + + it "returns false if the numerator isn't 0" do + Rational(26).zero?.should be_false + end +end diff --git a/spec/ruby/core/refinement/append_features_spec.rb b/spec/ruby/core/refinement/append_features_spec.rb new file mode 100644 index 0000000000..f7e5f32bc1 --- /dev/null +++ b/spec/ruby/core/refinement/append_features_spec.rb @@ -0,0 +1,19 @@ +require_relative '../../spec_helper' + +describe "Refinement#append_features" do + it "is not defined" do + Refinement.should_not have_private_instance_method(:append_features) + end + + it "is not called by Module#include" do + c = Class.new + Module.new do + refine c do + called = false + define_method(:append_features){called = true} + proc{c.include(self)}.should raise_error(TypeError) + called.should == false + end + end + end +end diff --git a/spec/ruby/core/refinement/extend_object_spec.rb b/spec/ruby/core/refinement/extend_object_spec.rb new file mode 100644 index 0000000000..4da8b359cc --- /dev/null +++ b/spec/ruby/core/refinement/extend_object_spec.rb @@ -0,0 +1,21 @@ +require_relative '../../spec_helper' + +describe "Refinement#extend_object" do + it "is not defined" do + Refinement.should_not have_private_instance_method(:extend_object) + end + + it "is not called by Object#extend" do + c = Class.new + Module.new do + refine c do + called = false + define_method(:extend_object) { called = true } + -> { + c.extend(self) + }.should raise_error(TypeError) + called.should == false + end + end + end +end diff --git a/spec/ruby/core/refinement/fixtures/classes.rb b/spec/ruby/core/refinement/fixtures/classes.rb new file mode 100644 index 0000000000..94324db47c --- /dev/null +++ b/spec/ruby/core/refinement/fixtures/classes.rb @@ -0,0 +1,10 @@ +module RefinementSpec + + module ModuleWithAncestors + include Module.new do + def indent(level) + " " * level + self + end + end + end +end diff --git a/spec/ruby/core/refinement/import_methods_spec.rb b/spec/ruby/core/refinement/import_methods_spec.rb new file mode 100644 index 0000000000..13c0b1004c --- /dev/null +++ b/spec/ruby/core/refinement/import_methods_spec.rb @@ -0,0 +1,267 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Refinement#import_methods" do + context "when methods are defined in Ruby code" do + it "imports methods" do + str_utils = Module.new do + def indent(level) + " " * level + self + end + end + + Module.new do + refine String do + import_methods str_utils + "foo".indent(3).should == " foo" + end + end + end + + it "throws an exception when argument is not a module" do + Module.new do + refine String do + -> { + import_methods Integer + }.should raise_error(TypeError, "wrong argument type Class (expected Module)") + end + end + end + + it "imports methods from multiple modules" do + str_utils = Module.new do + def indent(level) + " " * level + self + end + end + + str_utils_fancy = Module.new do + def indent_star(level) + "*" * level + self + end + end + + Module.new do + refine String do + import_methods str_utils, str_utils_fancy + "foo".indent(3).should == " foo" + "foo".indent_star(3).should == "***foo" + end + end + end + + it "imports a method defined in the last module if method with same name is defined in multiple modules" do + str_utils = Module.new do + def indent(level) + " " * level + self + end + end + + str_utils_fancy = Module.new do + def indent(level) + "*" * level + self + end + end + + Module.new do + refine String do + import_methods str_utils, str_utils_fancy + "foo".indent(3).should == "***foo" + end + end + end + + it "still imports methods of modules listed before a module that contains method not defined in Ruby" do + str_utils = Module.new do + def indent(level) + " " * level + self + end + end + + string_refined = Module.new do + refine String do + -> { + import_methods str_utils, Kernel + }.should raise_error(ArgumentError) + end + end + + Module.new do + using string_refined + "foo".indent(3).should == " foo" + end + end + end + + it "warns if a module includes/prepends some other module" do + module1 = Module.new do + end + + module2 = Module.new do + include module1 + end + + Module.new do + refine String do + -> { + import_methods module2 + }.should complain(/warning: #<Module:\w*> has ancestors, but Refinement#import_methods doesn't import their methods/) + end + end + + Module.new do + refine String do + -> { + import_methods RefinementSpec::ModuleWithAncestors + }.should complain(/warning: RefinementSpec::ModuleWithAncestors has ancestors, but Refinement#import_methods doesn't import their methods/) + end + end + end + + it "doesn't import methods from included/prepended modules" do + Module.new do + refine String do + suppress_warning { import_methods RefinementSpec::ModuleWithAncestors } + end + + using self + -> { + "foo".indent(3) + }.should raise_error(NoMethodError, /undefined method [`']indent' for ("foo":String|an instance of String)/) + end + end + + it "doesn't import any methods if one of the arguments is not a module" do + str_utils = Module.new do + def indent(level) + " " * level + self + end + end + + string_refined = Module.new do + refine String do + -> { + import_methods str_utils, Integer + }.should raise_error(TypeError) + end + end + + Module.new do + using string_refined + -> { + "foo".indent(3) + }.should raise_error(NoMethodError) + end + end + + it "imports methods from multiple modules so that methods see other's module's methods" do + str_utils = Module.new do + def indent(level) + " " * level + self + end + end + + str_utils_normal = Module.new do + def indent_normal(level) + self.indent(level) + end + end + + Module.new do + refine String do + import_methods str_utils, str_utils_normal + end + + using self + "foo".indent_normal(3).should == " foo" + end + end + + it "imports methods from module so that methods can see each other" do + str_utils = Module.new do + def indent(level) + " " * level + self + end + + def indent_with_dot(level) + self.indent(level) + "." + end + end + + Module.new do + refine String do + import_methods str_utils + end + + using self + "foo".indent_with_dot(3).should == " foo." + end + end + + it "doesn't import module's class methods" do + str_utils = Module.new do + def self.indent(level) + " " * level + self + end + end + + Module.new do + refine String do + import_methods str_utils + end + + using self + -> { + String.indent(3) + }.should raise_error(NoMethodError, /undefined method [`']indent' for (String:Class|class String)/) + end + end + + it "imports module methods with super" do + class_to_refine = Class.new do + def foo(number) + 2 * number + end + end + + extension = Module.new do + def foo(number) + super * 2 + end + end + + refinement = Module.new do + refine class_to_refine do + import_methods extension + end + end + + Module.new do + using refinement + class_to_refine.new.foo(2).should == 8 + end + end + + context "when methods are not defined in Ruby code" do + it "raises ArgumentError" do + Module.new do + refine String do + -> { + import_methods Kernel + }.should raise_error(ArgumentError) + end + end + end + + it "raises ArgumentError when importing methods from C extension" do + require 'zlib' + Module.new do + refine String do + -> { + import_methods Zlib + }.should raise_error(ArgumentError, /Can't import method which is not defined with Ruby code: Zlib#*/) + end + end + end + end +end diff --git a/spec/ruby/core/refinement/include_spec.rb b/spec/ruby/core/refinement/include_spec.rb new file mode 100644 index 0000000000..57451bd9bc --- /dev/null +++ b/spec/ruby/core/refinement/include_spec.rb @@ -0,0 +1,13 @@ +require_relative '../../spec_helper' + +describe "Refinement#include" do + it "raises a TypeError" do + Module.new do + refine String do + -> { + include Module.new + }.should raise_error(TypeError, "Refinement#include has been removed") + end + end + end +end diff --git a/spec/ruby/core/refinement/prepend_features_spec.rb b/spec/ruby/core/refinement/prepend_features_spec.rb new file mode 100644 index 0000000000..fbc431bbd2 --- /dev/null +++ b/spec/ruby/core/refinement/prepend_features_spec.rb @@ -0,0 +1,19 @@ +require_relative '../../spec_helper' + +describe "Refinement#prepend_features" do + it "is not defined" do + Refinement.should_not have_private_instance_method(:prepend_features) + end + + it "is not called by Module#prepend" do + c = Class.new + Module.new do + refine c do + called = false + define_method(:prepend_features){called = true} + proc{c.prepend(self)}.should raise_error(TypeError) + called.should == false + end + end + end +end diff --git a/spec/ruby/core/refinement/prepend_spec.rb b/spec/ruby/core/refinement/prepend_spec.rb new file mode 100644 index 0000000000..64cf7cd17f --- /dev/null +++ b/spec/ruby/core/refinement/prepend_spec.rb @@ -0,0 +1,13 @@ +require_relative '../../spec_helper' + +describe "Refinement#prepend" do + it "raises a TypeError" do + Module.new do + refine String do + -> { + prepend Module.new + }.should raise_error(TypeError, "Refinement#prepend has been removed") + end + end + end +end diff --git a/spec/ruby/core/refinement/refined_class_spec.rb b/spec/ruby/core/refinement/refined_class_spec.rb new file mode 100644 index 0000000000..60a58380cc --- /dev/null +++ b/spec/ruby/core/refinement/refined_class_spec.rb @@ -0,0 +1,38 @@ +require_relative "../../spec_helper" +require_relative 'shared/target' + +describe "Refinement#refined_class" do + ruby_version_is ""..."3.3" do + it_behaves_like :refinement_target, :refined_class + end + + ruby_version_is "3.3"..."3.4" do + it "has been deprecated in favour of Refinement#target" do + refinement_int = nil + + Module.new do + refine Integer do + refinement_int = self + end + end + + -> { + refinement_int.refined_class + }.should complain(/warning: Refinement#refined_class is deprecated and will be removed in Ruby 3.4; use Refinement#target instead/) + end + end + + ruby_version_is "3.4" do + it "has been removed" do + refinement_int = nil + + Module.new do + refine Integer do + refinement_int = self + end + end + + refinement_int.should_not.respond_to?(:refined_class) + end + end +end diff --git a/spec/ruby/core/refinement/shared/target.rb b/spec/ruby/core/refinement/shared/target.rb new file mode 100644 index 0000000000..79557bea0b --- /dev/null +++ b/spec/ruby/core/refinement/shared/target.rb @@ -0,0 +1,13 @@ +describe :refinement_target, shared: true do + it "returns the class refined by the receiver" do + refinement_int = nil + + Module.new do + refine Integer do + refinement_int = self + end + end + + refinement_int.send(@method).should == Integer + end +end diff --git a/spec/ruby/core/refinement/target_spec.rb b/spec/ruby/core/refinement/target_spec.rb new file mode 100644 index 0000000000..fee9588a96 --- /dev/null +++ b/spec/ruby/core/refinement/target_spec.rb @@ -0,0 +1,8 @@ +require_relative "../../spec_helper" +require_relative 'shared/target' + +describe "Refinement#target" do + ruby_version_is "3.3" do + it_behaves_like :refinement_target, :target + end +end diff --git a/spec/ruby/core/regexp/case_compare_spec.rb b/spec/ruby/core/regexp/case_compare_spec.rb new file mode 100644 index 0000000000..5ae8b56c6a --- /dev/null +++ b/spec/ruby/core/regexp/case_compare_spec.rb @@ -0,0 +1,35 @@ +require_relative '../../spec_helper' + +describe "Regexp#===" do + it "is true if there is a match" do + (/abc/ === "aabcc").should be_true + end + + it "is false if there is no match" do + (/abc/ === "xyz").should be_false + end + + it "returns true if it matches a Symbol" do + (/a/ === :a).should be_true + end + + it "returns false if it does not match a Symbol" do + (/a/ === :b).should be_false + end + + # mirroring https://github.com/ruby/ruby/blob/master/test/ruby/test_regexp.rb + it "returns false if the other value cannot be coerced to a string" do + (/abc/ === nil).should be_false + (/abc/ === /abc/).should be_false + end + + it "uses #to_str on string-like objects" do + stringlike = Class.new do + def to_str + "abc" + end + end.new + + (/abc/ === stringlike).should be_true + end +end diff --git a/spec/ruby/core/regexp/casefold_spec.rb b/spec/ruby/core/regexp/casefold_spec.rb new file mode 100644 index 0000000000..d36467a989 --- /dev/null +++ b/spec/ruby/core/regexp/casefold_spec.rb @@ -0,0 +1,8 @@ +require_relative '../../spec_helper' + +describe "Regexp#casefold?" do + it "returns the value of the case-insensitive flag" do + /abc/i.should.casefold? + /xyz/.should_not.casefold? + end +end diff --git a/spec/ruby/core/regexp/compile_spec.rb b/spec/ruby/core/regexp/compile_spec.rb new file mode 100644 index 0000000000..887c8d77dc --- /dev/null +++ b/spec/ruby/core/regexp/compile_spec.rb @@ -0,0 +1,19 @@ +require_relative '../../spec_helper' +require_relative 'shared/new' + +describe "Regexp.compile" do + it_behaves_like :regexp_new, :compile +end + +describe "Regexp.compile given a String" do + it_behaves_like :regexp_new_string, :compile + it_behaves_like :regexp_new_string_binary, :compile +end + +describe "Regexp.compile given a Regexp" do + it_behaves_like :regexp_new_regexp, :compile +end + +describe "Regexp.compile given a non-String/Regexp" do + it_behaves_like :regexp_new_non_string_or_regexp, :compile +end diff --git a/spec/ruby/core/regexp/encoding_spec.rb b/spec/ruby/core/regexp/encoding_spec.rb new file mode 100644 index 0000000000..dfc835b4e4 --- /dev/null +++ b/spec/ruby/core/regexp/encoding_spec.rb @@ -0,0 +1,62 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' + +describe "Regexp#encoding" do + it "returns an Encoding object" do + /glar/.encoding.should be_an_instance_of(Encoding) + end + + it "defaults to US-ASCII if the Regexp contains only US-ASCII character" do + /ASCII/.encoding.should == Encoding::US_ASCII + end + + it "returns US_ASCII if the 'n' modifier is supplied and only US-ASCII characters are present" do + /ASCII/n.encoding.should == Encoding::US_ASCII + end + + it "returns BINARY if the 'n' modifier is supplied and non-US-ASCII characters are present" do + /\xc2\xa1/n.encoding.should == Encoding::BINARY + end + + it "defaults to UTF-8 if \\u escapes appear" do + /\u{9879}/.encoding.should == Encoding::UTF_8 + end + + it "defaults to UTF-8 if a literal UTF-8 character appears" do + /Â¥/.encoding.should == Encoding::UTF_8 + end + + it "returns UTF-8 if the 'u' modifier is supplied" do + /ASCII/u.encoding.should == Encoding::UTF_8 + end + + it "returns Windows-31J if the 's' modifier is supplied" do + /ASCII/s.encoding.should == Encoding::Windows_31J + end + + it "returns EUC_JP if the 'e' modifier is supplied" do + /ASCII/e.encoding.should == Encoding::EUC_JP + end + + it "upgrades the encoding to that of an embedded String" do + str = "æ–‡å—化ã‘".encode('euc-jp') + /#{str}/.encoding.should == Encoding::EUC_JP + end + + it "ignores the encoding and uses US-ASCII if the string has only ASCII characters" do + str = "abc".encode('euc-jp') + str.encoding.should == Encoding::EUC_JP + /#{str}/.encoding.should == Encoding::US_ASCII + end + + it "ignores the default_internal encoding" do + old_internal = Encoding.default_internal + Encoding.default_internal = Encoding::EUC_JP + /foo/.encoding.should_not == Encoding::EUC_JP + Encoding.default_internal = old_internal + end + + it "allows otherwise invalid characters if NOENCODING is specified" do + Regexp.new('([\x00-\xFF])', Regexp::IGNORECASE | Regexp::NOENCODING).encoding.should == Encoding::BINARY + end +end diff --git a/spec/ruby/core/regexp/eql_spec.rb b/spec/ruby/core/regexp/eql_spec.rb new file mode 100644 index 0000000000..bd5ae43eb2 --- /dev/null +++ b/spec/ruby/core/regexp/eql_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/equal_value' + +describe "Regexp#eql?" do + it_behaves_like :regexp_eql, :eql? +end diff --git a/spec/ruby/core/regexp/equal_value_spec.rb b/spec/ruby/core/regexp/equal_value_spec.rb new file mode 100644 index 0000000000..5455a30598 --- /dev/null +++ b/spec/ruby/core/regexp/equal_value_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/equal_value' + +describe "Regexp#==" do + it_behaves_like :regexp_eql, :== +end diff --git a/spec/ruby/core/regexp/escape_spec.rb b/spec/ruby/core/regexp/escape_spec.rb new file mode 100644 index 0000000000..6b06ab1cbc --- /dev/null +++ b/spec/ruby/core/regexp/escape_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/quote' + +describe "Regexp.escape" do + it_behaves_like :regexp_quote, :escape +end diff --git a/spec/ruby/core/regexp/fixed_encoding_spec.rb b/spec/ruby/core/regexp/fixed_encoding_spec.rb new file mode 100644 index 0000000000..29d0a22c53 --- /dev/null +++ b/spec/ruby/core/regexp/fixed_encoding_spec.rb @@ -0,0 +1,36 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' + +describe "Regexp#fixed_encoding?" do + it "returns false by default" do + /needle/.fixed_encoding?.should be_false + end + + it "returns false if the 'n' modifier was supplied to the Regexp" do + /needle/n.fixed_encoding?.should be_false + end + + it "returns true if the 'u' modifier was supplied to the Regexp" do + /needle/u.fixed_encoding?.should be_true + end + + it "returns true if the 's' modifier was supplied to the Regexp" do + /needle/s.fixed_encoding?.should be_true + end + + it "returns true if the 'e' modifier was supplied to the Regexp" do + /needle/e.fixed_encoding?.should be_true + end + + it "returns true if the Regexp contains a \\u escape" do + /needle \u{8768}/.fixed_encoding?.should be_true + end + + it "returns true if the Regexp contains a UTF-8 literal" do + /æ–‡å—化ã‘/.fixed_encoding?.should be_true + end + + it "returns true if the Regexp was created with the Regexp::FIXEDENCODING option" do + Regexp.new("", Regexp::FIXEDENCODING).fixed_encoding?.should be_true + end +end diff --git a/spec/ruby/core/regexp/hash_spec.rb b/spec/ruby/core/regexp/hash_spec.rb new file mode 100644 index 0000000000..2d42e288e6 --- /dev/null +++ b/spec/ruby/core/regexp/hash_spec.rb @@ -0,0 +1,20 @@ +require_relative '../../spec_helper' + +describe "Regexp#hash" do + it "is provided" do + Regexp.new('').respond_to?(:hash).should == true + end + + it "is based on the text and options of Regexp" do + (/cat/.hash == /dog/.hash).should == false + (/dog/m.hash == /dog/m.hash).should == true + not_supported_on :opal do + (/cat/ix.hash == /cat/ixn.hash).should == true + (/cat/.hash == /cat/ix.hash).should == false + end + end + + it "returns the same value for two Regexps differing only in the /n option" do + (//.hash == //n.hash).should == true + end +end diff --git a/spec/ruby/core/regexp/initialize_spec.rb b/spec/ruby/core/regexp/initialize_spec.rb new file mode 100644 index 0000000000..dd57292242 --- /dev/null +++ b/spec/ruby/core/regexp/initialize_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../spec_helper' + +describe "Regexp#initialize" do + it "is a private method" do + Regexp.should have_private_instance_method(:initialize) + end + + it "raises a FrozenError on a Regexp literal" do + -> { //.send(:initialize, "") }.should raise_error(FrozenError) + end + + it "raises a TypeError on an initialized non-literal Regexp" do + -> { Regexp.new("").send(:initialize, "") }.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/regexp/inspect_spec.rb b/spec/ruby/core/regexp/inspect_spec.rb new file mode 100644 index 0000000000..f4e39234f5 --- /dev/null +++ b/spec/ruby/core/regexp/inspect_spec.rb @@ -0,0 +1,44 @@ +require_relative '../../spec_helper' + +describe "Regexp#inspect" do + it "returns a formatted string that would eval to the same regexp" do + not_supported_on :opal do + /ab+c/ix.inspect.should == "/ab+c/ix" + /a(.)+s/n.inspect.should =~ %r|/a(.)+s/n?| # Default 'n' may not appear + end + # 1.9 doesn't round-trip the encoding flags, such as 'u'. This is + # seemingly by design. + /a(.)+s/m.inspect.should == "/a(.)+s/m" # But a specified one does + end + + it "returns options in the order 'mixn'" do + //nixm.inspect.should == "//mixn" + end + + it "does not include the 'o' option" do + //o.inspect.should == "//" + end + + it "does not include a character set code" do + //u.inspect.should == "//" + //s.inspect.should == "//" + //e.inspect.should == "//" + end + + it "correctly escapes forward slashes /" do + Regexp.new("/foo/bar").inspect.should == "/\\/foo\\/bar/" + Regexp.new("/foo/bar[/]").inspect.should == "/\\/foo\\/bar[\\/]/" + end + + it "doesn't over escape forward slashes" do + /\/foo\/bar/.inspect.should == '/\/foo\/bar/' + end + + it "escapes 2 slashes in a row properly" do + Regexp.new("//").inspect.should == '/\/\//' + end + + it "does not over escape" do + Regexp.new('\\\/').inspect.should == "/\\\\\\//" + end +end diff --git a/spec/ruby/core/regexp/last_match_spec.rb b/spec/ruby/core/regexp/last_match_spec.rb new file mode 100644 index 0000000000..0bfed32051 --- /dev/null +++ b/spec/ruby/core/regexp/last_match_spec.rb @@ -0,0 +1,56 @@ +require_relative '../../spec_helper' + +describe "Regexp.last_match" do + it "returns MatchData instance when not passed arguments" do + /c(.)t/ =~ 'cat' + + Regexp.last_match.should be_kind_of(MatchData) + end + + it "returns the nth field in this MatchData when passed an Integer" do + /c(.)t/ =~ 'cat' + Regexp.last_match(1).should == 'a' + end + + it "returns nil when there is no match" do + /foo/ =~ "TEST123" + Regexp.last_match(:test).should == nil + Regexp.last_match(1).should == nil + Regexp.last_match(Object.new).should == nil + Regexp.last_match("test").should == nil + end + + describe "when given a Symbol" do + it "returns a named capture" do + /(?<test>[A-Z]+.*)/ =~ "TEST123" + Regexp.last_match(:test).should == "TEST123" + end + + it "raises an IndexError when given a missing name" do + /(?<test>[A-Z]+.*)/ =~ "TEST123" + -> { Regexp.last_match(:missing) }.should raise_error(IndexError) + end + end + + describe "when given a String" do + it "returns a named capture" do + /(?<test>[A-Z]+.*)/ =~ "TEST123" + Regexp.last_match("test").should == "TEST123" + end + end + + describe "when given an Object" do + it "coerces argument to an index using #to_int" do + obj = mock("converted to int") + obj.should_receive(:to_int).and_return(1) + /(?<test>[A-Z]+.*)/ =~ "TEST123" + Regexp.last_match(obj).should == "TEST123" + end + + it "raises a TypeError when unable to coerce" do + obj = Object.new + /(?<test>[A-Z]+.*)/ =~ "TEST123" + -> { Regexp.last_match(obj) }.should raise_error(TypeError) + end + end +end diff --git a/spec/ruby/core/regexp/linear_time_spec.rb b/spec/ruby/core/regexp/linear_time_spec.rb new file mode 100644 index 0000000000..cf9e73c37c --- /dev/null +++ b/spec/ruby/core/regexp/linear_time_spec.rb @@ -0,0 +1,33 @@ +require_relative '../../spec_helper' + +describe "Regexp.linear_time?" do + it "returns true if matching can be done in linear time" do + Regexp.linear_time?(/a/).should == true + Regexp.linear_time?('a').should == true + end + + it "returns true if matching can be done in linear time for a binary Regexp" do + Regexp.linear_time?(/[\x80-\xff]/n).should == true + end + + it "return false if matching can't be done in linear time" do + Regexp.linear_time?(/(a)\1/).should == false + Regexp.linear_time?("(a)\\1").should == false + end + + it "accepts flags for string argument" do + Regexp.linear_time?('a', Regexp::IGNORECASE).should == true + end + + it "warns about flags being ignored for regexp arguments" do + -> { + Regexp.linear_time?(/a/, Regexp::IGNORECASE) + }.should complain(/warning: flags ignored/) + end + + ruby_version_is "3.3" do + it "returns true for positive lookarounds" do + Regexp.linear_time?(/(?:(?=a*)a)*/).should == true + end + end +end diff --git a/spec/ruby/core/regexp/match_spec.rb b/spec/ruby/core/regexp/match_spec.rb new file mode 100644 index 0000000000..80dbfb4c10 --- /dev/null +++ b/spec/ruby/core/regexp/match_spec.rb @@ -0,0 +1,146 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' + +describe :regexp_match, shared: true do + it "returns nil if there is no match" do + /xyz/.send(@method,"abxyc").should be_nil + end + + it "returns nil if the object is nil" do + /\w+/.send(@method, nil).should be_nil + end +end + +describe "Regexp#=~" do + it_behaves_like :regexp_match, :=~ + + it "returns the index of the first character of the matching region" do + (/(.)(.)(.)/ =~ "abc").should == 0 + end + + it "returns the index too, when argument is a Symbol" do + (/(.)(.)(.)/ =~ :abc).should == 0 + end +end + +describe "Regexp#match" do + it_behaves_like :regexp_match, :match + + it "returns a MatchData object" do + /(.)(.)(.)/.match("abc").should be_kind_of(MatchData) + end + + it "returns a MatchData object, when argument is a Symbol" do + /(.)(.)(.)/.match(:abc).should be_kind_of(MatchData) + end + + it "raises a TypeError on an uninitialized Regexp" do + -> { Regexp.allocate.match('foo') }.should raise_error(TypeError) + end + + it "raises TypeError on an uninitialized Regexp" do + -> { Regexp.allocate.match('foo'.encode("UTF-16LE")) }.should raise_error(TypeError) + end + + describe "with [string, position]" do + describe "when given a positive position" do + it "matches the input at a given position" do + /(.).(.)/.match("01234", 1).captures.should == ["1", "3"] + end + + it "uses the start as a character offset" do + /(.).(.)/.match("零一二三四", 1).captures.should == ["一", "三"] + end + + it "raises an ArgumentError for an invalid encoding" do + x96 = ([150].pack('C')).force_encoding('utf-8') + -> { /(.).(.)/.match("Hello, #{x96} world!", 1) }.should raise_error(ArgumentError) + end + end + + describe "when given a negative position" do + it "matches the input at a given position" do + /(.).(.)/.match("01234", -4).captures.should == ["1", "3"] + end + + it "uses the start as a character offset" do + /(.).(.)/.match("零一二三四", -4).captures.should == ["一", "三"] + end + + it "raises an ArgumentError for an invalid encoding" do + x96 = ([150].pack('C')).force_encoding('utf-8') + -> { /(.).(.)/.match("Hello, #{x96} world!", -1) }.should raise_error(ArgumentError) + end + end + + describe "when passed a block" do + it "yields the MatchData" do + /./.match("abc") {|m| ScratchPad.record m } + ScratchPad.recorded.should be_kind_of(MatchData) + end + + it "returns the block result" do + /./.match("abc") { :result }.should == :result + end + + it "does not yield if there is no match" do + ScratchPad.record [] + /a/.match("b") {|m| ScratchPad << m } + ScratchPad.recorded.should == [] + end + end + end + + it "resets $~ if passed nil" do + # set $~ + /./.match("a") + $~.should be_kind_of(MatchData) + + /1/.match(nil) + $~.should be_nil + end + + it "raises TypeError when the given argument cannot be coerced to String" do + f = 1 + -> { /foo/.match(f)[0] }.should raise_error(TypeError) + end + + it "raises TypeError when the given argument is an Exception" do + f = Exception.new("foo") + -> { /foo/.match(f)[0] }.should raise_error(TypeError) + end +end + +describe "Regexp#match?" do + before :each do + # Resetting Regexp.last_match + /DONTMATCH/.match '' + end + + context "when matches the given value" do + it "returns true but does not set Regexp.last_match" do + /string/i.match?('string').should be_true + Regexp.last_match.should be_nil + end + end + + it "returns false when does not match the given value" do + /STRING/.match?('string').should be_false + end + + it "takes matching position as the 2nd argument" do + /str/i.match?('string', 0).should be_true + /str/i.match?('string', 1).should be_false + end + + it "returns false when given nil" do + /./.match?(nil).should be_false + end +end + +describe "Regexp#~" do + it "matches against the contents of $_" do + $_ = "input data" + (~ /at/).should == 7 + end +end diff --git a/spec/ruby/core/regexp/named_captures_spec.rb b/spec/ruby/core/regexp/named_captures_spec.rb new file mode 100644 index 0000000000..1a68d7877b --- /dev/null +++ b/spec/ruby/core/regexp/named_captures_spec.rb @@ -0,0 +1,35 @@ +require_relative '../../spec_helper' + +describe "Regexp#named_captures" do + it "returns a Hash" do + /foo/.named_captures.should be_an_instance_of(Hash) + end + + it "returns an empty Hash when there are no capture groups" do + /foo/.named_captures.should == {} + end + + it "sets the keys of the Hash to the names of the capture groups" do + rex = /this (?<is>is) [aA] (?<pat>pate?rn)/ + rex.named_captures.keys.should == ['is','pat'] + end + + it "sets the values of the Hash to Arrays" do + rex = /this (?<is>is) [aA] (?<pat>pate?rn)/ + rex.named_captures.values.each do |value| + value.should be_an_instance_of(Array) + end + end + + it "sets each element of the Array to the corresponding group's index" do + rex = /this (?<is>is) [aA] (?<pat>pate?rn)/ + rex.named_captures['is'].should == [1] + rex.named_captures['pat'].should == [2] + end + + it "works with duplicate capture group names" do + rex = /this (?<is>is) [aA] (?<pat>pate?(?<is>rn))/ + rex.named_captures['is'].should == [1,3] + rex.named_captures['pat'].should == [2] + end +end diff --git a/spec/ruby/core/regexp/names_spec.rb b/spec/ruby/core/regexp/names_spec.rb new file mode 100644 index 0000000000..099768fd26 --- /dev/null +++ b/spec/ruby/core/regexp/names_spec.rb @@ -0,0 +1,29 @@ +require_relative '../../spec_helper' + +describe "Regexp#names" do + it "returns an Array" do + /foo/.names.should be_an_instance_of(Array) + end + + it "returns an empty Array if there are no named captures" do + /needle/.names.should == [] + end + + it "returns each named capture as a String" do + /n(?<cap>ee)d(?<ture>le)/.names.each do |name| + name.should be_an_instance_of(String) + end + end + + it "returns all of the named captures" do + /n(?<cap>ee)d(?<ture>le)/.names.should == ['cap', 'ture'] + end + + it "works with nested named captures" do + /n(?<cap>eed(?<ture>le))/.names.should == ['cap', 'ture'] + end + + it "returns each capture name only once" do + /n(?<cap>ee)d(?<cap>le)/.names.should == ['cap'] + end +end diff --git a/spec/ruby/core/regexp/new_spec.rb b/spec/ruby/core/regexp/new_spec.rb new file mode 100644 index 0000000000..79210e9a23 --- /dev/null +++ b/spec/ruby/core/regexp/new_spec.rb @@ -0,0 +1,19 @@ +require_relative '../../spec_helper' +require_relative 'shared/new' + +describe "Regexp.new" do + it_behaves_like :regexp_new, :new +end + +describe "Regexp.new given a String" do + it_behaves_like :regexp_new_string, :new + it_behaves_like :regexp_new_string_binary, :new +end + +describe "Regexp.new given a Regexp" do + it_behaves_like :regexp_new_regexp, :new +end + +describe "Regexp.new given a non-String/Regexp" do + it_behaves_like :regexp_new_non_string_or_regexp, :new +end diff --git a/spec/ruby/core/regexp/options_spec.rb b/spec/ruby/core/regexp/options_spec.rb new file mode 100644 index 0000000000..527b51a3b2 --- /dev/null +++ b/spec/ruby/core/regexp/options_spec.rb @@ -0,0 +1,54 @@ +require_relative '../../spec_helper' + +describe "Regexp#options" do + it "returns an Integer bitvector of regexp options for the Regexp object" do + /cat/.options.should be_kind_of(Integer) + not_supported_on :opal do + /cat/ix.options.should be_kind_of(Integer) + end + end + + it "allows checking for presence of a certain option with bitwise &" do + (/cat/.options & Regexp::IGNORECASE).should == 0 + (/cat/i.options & Regexp::IGNORECASE).should_not == 0 + (/cat/.options & Regexp::MULTILINE).should == 0 + (/cat/m.options & Regexp::MULTILINE).should_not == 0 + not_supported_on :opal do + (/cat/.options & Regexp::EXTENDED).should == 0 + (/cat/x.options & Regexp::EXTENDED).should_not == 0 + (/cat/mx.options & Regexp::MULTILINE).should_not == 0 + (/cat/mx.options & Regexp::EXTENDED).should_not == 0 + (/cat/xi.options & Regexp::IGNORECASE).should_not == 0 + (/cat/xi.options & Regexp::EXTENDED).should_not == 0 + end + end + + it "returns 0 for a Regexp literal without options" do + //.options.should == 0 + /abc/.options.should == 0 + end + + it "raises a TypeError on an uninitialized Regexp" do + -> { Regexp.allocate.options }.should raise_error(TypeError) + end + + it "includes Regexp::FIXEDENCODING for a Regexp literal with the 'u' option" do + (//u.options & Regexp::FIXEDENCODING).should_not == 0 + end + + it "includes Regexp::FIXEDENCODING for a Regexp literal with the 'e' option" do + (//e.options & Regexp::FIXEDENCODING).should_not == 0 + end + + it "includes Regexp::FIXEDENCODING for a Regexp literal with the 's' option" do + (//s.options & Regexp::FIXEDENCODING).should_not == 0 + end + + it "does not include Regexp::FIXEDENCODING for a Regexp literal with the 'n' option" do + (//n.options & Regexp::FIXEDENCODING).should == 0 + end + + it "includes Regexp::NOENCODING for a Regexp literal with the 'n' option" do + (//n.options & Regexp::NOENCODING).should_not == 0 + end +end diff --git a/spec/ruby/core/regexp/quote_spec.rb b/spec/ruby/core/regexp/quote_spec.rb new file mode 100644 index 0000000000..370ab13e30 --- /dev/null +++ b/spec/ruby/core/regexp/quote_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/quote' + +describe "Regexp.quote" do + it_behaves_like :regexp_quote, :quote +end diff --git a/spec/ruby/core/regexp/shared/equal_value.rb b/spec/ruby/core/regexp/shared/equal_value.rb new file mode 100644 index 0000000000..803988de9e --- /dev/null +++ b/spec/ruby/core/regexp/shared/equal_value.rb @@ -0,0 +1,31 @@ +describe :regexp_eql, shared: true do + it "is true if self and other have the same pattern" do + /abc/.send(@method, /abc/).should == true + /abc/.send(@method, /abd/).should == false + end + + not_supported_on :opal do + it "is true if self and other have the same character set code" do + /abc/.send(@method, /abc/x).should == false + /abc/x.send(@method, /abc/x).should == true + /abc/u.send(@method, /abc/n).should == false + /abc/u.send(@method, /abc/u).should == true + /abc/n.send(@method, /abc/n).should == true + end + end + + it "is true if other has the same #casefold? values" do + /abc/.send(@method, /abc/i).should == false + /abc/i.send(@method, /abc/i).should == true + end + + not_supported_on :opal do + it "is true if self does not specify /n option and other does" do + //.send(@method, //n).should == true + end + + it "is true if self specifies /n option and other does not" do + //n.send(@method, //).should == true + end + end +end diff --git a/spec/ruby/core/regexp/shared/new.rb b/spec/ruby/core/regexp/shared/new.rb new file mode 100644 index 0000000000..12c3d7c9c2 --- /dev/null +++ b/spec/ruby/core/regexp/shared/new.rb @@ -0,0 +1,315 @@ +# encoding: binary + +describe :regexp_new, shared: true do + it "requires one argument and creates a new regular expression object" do + Regexp.send(@method, '').is_a?(Regexp).should == true + end + + it "works by default for subclasses with overridden #initialize" do + class RegexpSpecsSubclass < Regexp + def initialize(*args) + super + @args = args + end + + attr_accessor :args + end + + class RegexpSpecsSubclassTwo < Regexp; end + + RegexpSpecsSubclass.send(@method, "hi").should be_kind_of(RegexpSpecsSubclass) + RegexpSpecsSubclass.send(@method, "hi").args.first.should == "hi" + + RegexpSpecsSubclassTwo.send(@method, "hi").should be_kind_of(RegexpSpecsSubclassTwo) + end +end + +describe :regexp_new_non_string_or_regexp, shared: true do + it "calls #to_str method for non-String/Regexp argument" do + obj = Object.new + def obj.to_str() "a" end + + Regexp.send(@method, obj).should == /a/ + end + + it "raises TypeError if there is no #to_str method for non-String/Regexp argument" do + obj = Object.new + -> { Regexp.send(@method, obj) }.should raise_error(TypeError, "no implicit conversion of Object into String") + + -> { Regexp.send(@method, 1) }.should raise_error(TypeError, "no implicit conversion of Integer into String") + -> { Regexp.send(@method, 1.0) }.should raise_error(TypeError, "no implicit conversion of Float into String") + -> { Regexp.send(@method, :symbol) }.should raise_error(TypeError, "no implicit conversion of Symbol into String") + -> { Regexp.send(@method, []) }.should raise_error(TypeError, "no implicit conversion of Array into String") + end + + it "raises TypeError if #to_str returns non-String value" do + obj = Object.new + def obj.to_str() [] end + + -> { Regexp.send(@method, obj) }.should raise_error(TypeError, /can't convert Object to String/) + end +end + +describe :regexp_new_string, shared: true do + it "uses the String argument as an unescaped literal to construct a Regexp object" do + Regexp.send(@method, "^hi{2,3}fo.o$").should == /^hi{2,3}fo.o$/ + end + + it "raises a RegexpError when passed an incorrect regexp" do + -> { Regexp.send(@method, "^[$", 0) }.should raise_error(RegexpError, Regexp.new(Regexp.escape("premature end of char-class: /^[$/"))) + end + + it "does not set Regexp options if only given one argument" do + r = Regexp.send(@method, 'Hi') + (r.options & Regexp::IGNORECASE).should == 0 + (r.options & Regexp::MULTILINE).should == 0 + not_supported_on :opal do + (r.options & Regexp::EXTENDED).should == 0 + end + end + + it "does not set Regexp options if second argument is nil or false" do + r = Regexp.send(@method, 'Hi', nil) + (r.options & Regexp::IGNORECASE).should == 0 + (r.options & Regexp::MULTILINE).should == 0 + not_supported_on :opal do + (r.options & Regexp::EXTENDED).should == 0 + end + + r = Regexp.send(@method, 'Hi', false) + (r.options & Regexp::IGNORECASE).should == 0 + (r.options & Regexp::MULTILINE).should == 0 + not_supported_on :opal do + (r.options & Regexp::EXTENDED).should == 0 + end + end + + it "sets options from second argument if it is true" do + r = Regexp.send(@method, 'Hi', true) + (r.options & Regexp::IGNORECASE).should_not == 0 + (r.options & Regexp::MULTILINE).should == 0 + not_supported_on :opal do + (r.options & Regexp::EXTENDED).should == 0 + end + end + + it "sets options from second argument if it is one of the Integer option constants" do + r = Regexp.send(@method, 'Hi', Regexp::IGNORECASE) + (r.options & Regexp::IGNORECASE).should_not == 0 + (r.options & Regexp::MULTILINE).should == 0 + not_supported_on :opal do + (r.options & Regexp::EXTENDED).should == 0 + end + + r = Regexp.send(@method, 'Hi', Regexp::MULTILINE) + (r.options & Regexp::IGNORECASE).should == 0 + (r.options & Regexp::MULTILINE).should_not == 0 + not_supported_on :opal do + (r.options & Regexp::EXTENDED).should == 0 + end + + not_supported_on :opal do + r = Regexp.send(@method, 'Hi', Regexp::EXTENDED) + (r.options & Regexp::IGNORECASE).should == 0 + (r.options & Regexp::MULTILINE).should == 0 + (r.options & Regexp::EXTENDED).should_not == 1 + end + end + + it "accepts an Integer of two or more options ORed together as the second argument" do + r = Regexp.send(@method, 'Hi', Regexp::IGNORECASE | Regexp::EXTENDED) + (r.options & Regexp::IGNORECASE).should_not == 0 + (r.options & Regexp::MULTILINE).should == 0 + (r.options & Regexp::EXTENDED).should_not == 0 + end + + it "does not try to convert the second argument to Integer with #to_int method call" do + ScratchPad.clear + obj = Object.new + def obj.to_int() ScratchPad.record(:called) end + + -> { + Regexp.send(@method, "Hi", obj) + }.should complain(/expected true or false as ignorecase/, {verbose: true}) + + ScratchPad.recorded.should == nil + end + + it "warns any non-Integer, non-nil, non-false second argument" do + r = nil + -> { + r = Regexp.send(@method, 'Hi', Object.new) + }.should complain(/expected true or false as ignorecase/, {verbose: true}) + (r.options & Regexp::IGNORECASE).should_not == 0 + (r.options & Regexp::MULTILINE).should == 0 + not_supported_on :opal do + (r.options & Regexp::EXTENDED).should == 0 + end + end + + it "accepts a String of supported flags as the second argument" do + r = Regexp.send(@method, 'Hi', 'i') + (r.options & Regexp::IGNORECASE).should_not == 0 + (r.options & Regexp::MULTILINE).should == 0 + not_supported_on :opal do + (r.options & Regexp::EXTENDED).should == 0 + end + + r = Regexp.send(@method, 'Hi', 'imx') + (r.options & Regexp::IGNORECASE).should_not == 0 + (r.options & Regexp::MULTILINE).should_not == 0 + not_supported_on :opal do + (r.options & Regexp::EXTENDED).should_not == 0 + end + + r = Regexp.send(@method, 'Hi', 'mimi') + (r.options & Regexp::IGNORECASE).should_not == 0 + (r.options & Regexp::MULTILINE).should_not == 0 + not_supported_on :opal do + (r.options & Regexp::EXTENDED).should == 0 + end + + r = Regexp.send(@method, 'Hi', '') + (r.options & Regexp::IGNORECASE).should == 0 + (r.options & Regexp::MULTILINE).should == 0 + not_supported_on :opal do + (r.options & Regexp::EXTENDED).should == 0 + end + end + + it "raises an Argument error if the second argument contains unsupported chars" do + -> { Regexp.send(@method, 'Hi', 'e') }.should raise_error(ArgumentError, "unknown regexp option: e") + -> { Regexp.send(@method, 'Hi', 'n') }.should raise_error(ArgumentError, "unknown regexp option: n") + -> { Regexp.send(@method, 'Hi', 's') }.should raise_error(ArgumentError, "unknown regexp option: s") + -> { Regexp.send(@method, 'Hi', 'u') }.should raise_error(ArgumentError, "unknown regexp option: u") + -> { Regexp.send(@method, 'Hi', 'j') }.should raise_error(ArgumentError, "unknown regexp option: j") + -> { Regexp.send(@method, 'Hi', 'mjx') }.should raise_error(ArgumentError, /unknown regexp option: mjx\b/) + end + + describe "with escaped characters" do + it "raises a Regexp error if there is a trailing backslash" do + -> { Regexp.send(@method, "\\") }.should raise_error(RegexpError, Regexp.new(Regexp.escape("too short escape sequence: /\\/"))) + end + + it "does not raise a Regexp error if there is an escaped trailing backslash" do + -> { Regexp.send(@method, "\\\\") }.should_not raise_error(RegexpError) + end + + it "accepts a backspace followed by a non-special character" do + Regexp.send(@method, "\\N").should == /#{"\x5c"+"N"}/ + end + + it "raises a RegexpError if \\x is not followed by any hexadecimal digits" do + -> { Regexp.send(@method, "\\" + "xn") }.should raise_error(RegexpError, Regexp.new(Regexp.escape("invalid hex escape: /\\xn/"))) + end + + it "raises a RegexpError if less than four digits are given for \\uHHHH" do + -> { Regexp.send(@method, "\\" + "u304") }.should raise_error(RegexpError, Regexp.new(Regexp.escape("invalid Unicode escape: /\\u304/"))) + end + + it "raises a RegexpError if the \\u{} escape is empty" do + -> { Regexp.send(@method, "\\" + "u{}") }.should raise_error(RegexpError, Regexp.new(Regexp.escape("invalid Unicode list: /\\u{}/"))) + end + + it "raises a RegexpError if the \\u{} escape contains non hexadecimal digits" do + -> { Regexp.send(@method, "\\" + "u{abcX}") }.should raise_error(RegexpError, Regexp.new(Regexp.escape("invalid Unicode list: /\\u{abcX}/"))) + end + + it "raises a RegexpError if more than six hexadecimal digits are given" do + -> { Regexp.send(@method, "\\" + "u{0ffffff}") }.should raise_error(RegexpError, Regexp.new(Regexp.escape("invalid Unicode range: /\\u{0ffffff}/"))) + end + + it "returns a Regexp with US-ASCII encoding if only 7-bit ASCII characters are present regardless of the input String's encoding" do + Regexp.send(@method, "abc").encoding.should == Encoding::US_ASCII + end + + it "returns a Regexp with source String having US-ASCII encoding if only 7-bit ASCII characters are present regardless of the input String's encoding" do + Regexp.send(@method, "abc").source.encoding.should == Encoding::US_ASCII + end + + it "returns a Regexp with US-ASCII encoding if UTF-8 escape sequences using only 7-bit ASCII are present" do + Regexp.send(@method, "\u{61}").encoding.should == Encoding::US_ASCII + end + + it "returns a Regexp with source String having US-ASCII encoding if UTF-8 escape sequences using only 7-bit ASCII are present" do + Regexp.send(@method, "\u{61}").source.encoding.should == Encoding::US_ASCII + end + + it "returns a Regexp with UTF-8 encoding if any UTF-8 escape sequences outside 7-bit ASCII are present" do + Regexp.send(@method, "\u{ff}").encoding.should == Encoding::UTF_8 + end + + it "returns a Regexp with source String having UTF-8 encoding if any UTF-8 escape sequences outside 7-bit ASCII are present" do + Regexp.send(@method, "\u{ff}").source.encoding.should == Encoding::UTF_8 + end + + it "returns a Regexp with the input String's encoding" do + str = "\x82\xa0".dup.force_encoding(Encoding::Shift_JIS) + Regexp.send(@method, str).encoding.should == Encoding::Shift_JIS + end + + it "returns a Regexp with source String having the input String's encoding" do + str = "\x82\xa0".dup.force_encoding(Encoding::Shift_JIS) + Regexp.send(@method, str).source.encoding.should == Encoding::Shift_JIS + end + end +end + +describe :regexp_new_string_binary, shared: true do + describe "with escaped characters" do + end +end + +describe :regexp_new_regexp, shared: true do + it "uses the argument as a literal to construct a Regexp object" do + Regexp.send(@method, /^hi{2,3}fo.o$/).should == /^hi{2,3}fo.o$/ + end + + it "preserves any options given in the Regexp literal" do + (Regexp.send(@method, /Hi/i).options & Regexp::IGNORECASE).should_not == 0 + (Regexp.send(@method, /Hi/m).options & Regexp::MULTILINE).should_not == 0 + not_supported_on :opal do + (Regexp.send(@method, /Hi/x).options & Regexp::EXTENDED).should_not == 0 + end + + not_supported_on :opal do + r = Regexp.send @method, /Hi/imx + (r.options & Regexp::IGNORECASE).should_not == 0 + (r.options & Regexp::MULTILINE).should_not == 0 + (r.options & Regexp::EXTENDED).should_not == 0 + end + + r = Regexp.send @method, /Hi/ + (r.options & Regexp::IGNORECASE).should == 0 + (r.options & Regexp::MULTILINE).should == 0 + not_supported_on :opal do + (r.options & Regexp::EXTENDED).should == 0 + end + end + + it "does not honour options given as additional arguments" do + r = nil + -> { + r = Regexp.send @method, /hi/, Regexp::IGNORECASE + }.should complain(/flags ignored/) + (r.options & Regexp::IGNORECASE).should == 0 + end + + not_supported_on :opal do + it "sets the encoding to UTF-8 if the Regexp literal has the 'u' option" do + Regexp.send(@method, /Hi/u).encoding.should == Encoding::UTF_8 + end + + it "sets the encoding to EUC-JP if the Regexp literal has the 'e' option" do + Regexp.send(@method, /Hi/e).encoding.should == Encoding::EUC_JP + end + + it "sets the encoding to Windows-31J if the Regexp literal has the 's' option" do + Regexp.send(@method, /Hi/s).encoding.should == Encoding::Windows_31J + end + + it "sets the encoding to US-ASCII if the Regexp literal has the 'n' option and the source String is ASCII only" do + Regexp.send(@method, /Hi/n).encoding.should == Encoding::US_ASCII + end + end +end diff --git a/spec/ruby/core/regexp/shared/quote.rb b/spec/ruby/core/regexp/shared/quote.rb new file mode 100644 index 0000000000..3f46a18b5b --- /dev/null +++ b/spec/ruby/core/regexp/shared/quote.rb @@ -0,0 +1,41 @@ +# encoding: binary + +describe :regexp_quote, shared: true do + it "escapes any characters with special meaning in a regular expression" do + Regexp.send(@method, '\*?{}.+^$[]()- ').should == '\\\\\*\?\{\}\.\+\^\$\[\]\(\)\-\\ ' + Regexp.send(@method, "\*?{}.+^$[]()- ").should == '\\*\\?\\{\\}\\.\\+\\^\\$\\[\\]\\(\\)\\-\\ ' + Regexp.send(@method, '\n\r\f\t').should == '\\\\n\\\\r\\\\f\\\\t' + Regexp.send(@method, "\n\r\f\t").should == '\\n\\r\\f\\t' + end + + it "works with symbols" do + Regexp.send(@method, :symbol).should == 'symbol' + end + + it "works with substrings" do + str = ".+[]()"[1...-1] + Regexp.send(@method, str).should == '\+\[\]\(' + end + + it "works for broken strings" do + Regexp.send(@method, "a.\x85b.".dup.force_encoding("US-ASCII")).should =="a\\.\x85b\\.".dup.force_encoding("US-ASCII") + Regexp.send(@method, "a.\x80".dup.force_encoding("UTF-8")).should == "a\\.\x80".dup.force_encoding("UTF-8") + end + + it "sets the encoding of the result to US-ASCII if there are only US-ASCII characters present in the input String" do + str = "abc".dup.force_encoding("euc-jp") + Regexp.send(@method, str).encoding.should == Encoding::US_ASCII + end + + it "sets the encoding of the result to the encoding of the String if any non-US-ASCII characters are present in an input String with valid encoding" do + str = "ã‚りãŒã¨ã†".dup.force_encoding("utf-8") + str.valid_encoding?.should be_true + Regexp.send(@method, str).encoding.should == Encoding::UTF_8 + end + + it "sets the encoding of the result to BINARY if any non-US-ASCII characters are present in an input String with invalid encoding" do + str = "\xff".dup.force_encoding "us-ascii" + str.valid_encoding?.should be_false + Regexp.send(@method, "\xff").encoding.should == Encoding::BINARY + end +end diff --git a/spec/ruby/core/regexp/source_spec.rb b/spec/ruby/core/regexp/source_spec.rb new file mode 100644 index 0000000000..5f253da9ea --- /dev/null +++ b/spec/ruby/core/regexp/source_spec.rb @@ -0,0 +1,47 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' + +describe "Regexp#source" do + it "returns the original string of the pattern" do + not_supported_on :opal do + /ab+c/ix.source.should == "ab+c" + end + /x(.)xz/.source.should == "x(.)xz" + end + + it "keeps escape sequences as is" do + /\x20\+/.source.should == '\x20\+' + end + + describe "escaping" do + it "keeps escaping of metacharacter" do + /\$/.source.should == "\\$" + end + + it "keeps escaping of metacharacter used as a terminator" do + %r+\++.source.should == "\\+" + end + + it "removes escaping of non-metacharacter used as a terminator" do + %r@\@@.source.should == "@" + end + + it "keeps escaping of non-metacharacter not used as a terminator" do + /\@/.source.should == "\\@" + end + end + + not_supported_on :opal do + it "has US-ASCII encoding when created from an ASCII-only \\u{} literal" do + re = /[\u{20}-\u{7E}]/ + re.source.encoding.should equal(Encoding::US_ASCII) + end + end + + not_supported_on :opal do + it "has UTF-8 encoding when created from a non-ASCII-only \\u{} literal" do + re = /[\u{20}-\u{7EE}]/ + re.source.encoding.should equal(Encoding::UTF_8) + end + end +end diff --git a/spec/ruby/core/regexp/timeout_spec.rb b/spec/ruby/core/regexp/timeout_spec.rb new file mode 100644 index 0000000000..c64103c82c --- /dev/null +++ b/spec/ruby/core/regexp/timeout_spec.rb @@ -0,0 +1,33 @@ +require_relative '../../spec_helper' + +describe "Regexp.timeout" do + after :each do + Regexp.timeout = nil + end + + it "returns global timeout" do + Regexp.timeout = 3 + Regexp.timeout.should == 3 + end + + it "raises Regexp::TimeoutError after global timeout elapsed" do + Regexp.timeout = 0.001 + Regexp.timeout.should == 0.001 + + -> { + # A typical ReDoS case + /^(a*)*$/ =~ "a" * 1000000 + "x" + }.should raise_error(Regexp::TimeoutError, "regexp match timeout") + end + + it "raises Regexp::TimeoutError after timeout keyword value elapsed" do + Regexp.timeout = 3 # This should be ignored + Regexp.timeout.should == 3 + + re = Regexp.new("^a*b?a*$", timeout: 0.001) + + -> { + re =~ "a" * 1000000 + "x" + }.should raise_error(Regexp::TimeoutError, "regexp match timeout") + end +end diff --git a/spec/ruby/core/regexp/to_s_spec.rb b/spec/ruby/core/regexp/to_s_spec.rb new file mode 100644 index 0000000000..798eaee6c2 --- /dev/null +++ b/spec/ruby/core/regexp/to_s_spec.rb @@ -0,0 +1,62 @@ +require_relative '../../spec_helper' + +describe "Regexp#to_s" do + not_supported_on :opal do + it "displays options if included" do + /abc/mxi.to_s.should == "(?mix:abc)" + end + end + + it "shows non-included options after a - sign" do + /abc/i.to_s.should == "(?i-mx:abc)" + end + + it "shows all options as excluded if none are selected" do + /abc/.to_s.should == "(?-mix:abc)" + end + + it "shows the pattern after the options" do + not_supported_on :opal do + /ab+c/mix.to_s.should == "(?mix:ab+c)" + end + /xyz/.to_s.should == "(?-mix:xyz)" + end + + not_supported_on :opal do + it "displays groups with options" do + /(?ix:foo)(?m:bar)/.to_s.should == "(?-mix:(?ix:foo)(?m:bar))" + /(?ix:foo)bar/m.to_s.should == "(?m-ix:(?ix:foo)bar)" + end + + it "displays single group with same options as main regex as the main regex" do + /(?i:nothing outside this group)/.to_s.should == "(?i-mx:nothing outside this group)" + end + end + + not_supported_on :opal do + it "deals properly with uncaptured groups" do + /whatever(?:0d)/ix.to_s.should == "(?ix-m:whatever(?:0d))" + end + end + + it "deals properly with the two types of lookahead groups" do + /(?=5)/.to_s.should == "(?-mix:(?=5))" + /(?!5)/.to_s.should == "(?-mix:(?!5))" + end + + it "returns a string in (?xxx:yyy) notation" do + not_supported_on :opal do + /ab+c/ix.to_s.should == "(?ix-m:ab+c)" + /jis/s.to_s.should == "(?-mix:jis)" + /(?i:.)/.to_s.should == "(?i-mx:.)" + end + /(?:.)/.to_s.should == "(?-mix:.)" + end + + not_supported_on :opal do + it "handles abusive option groups" do + /(?mmmmix-miiiix:)/.to_s.should == '(?-mix:)' + end + end + +end diff --git a/spec/ruby/core/regexp/try_convert_spec.rb b/spec/ruby/core/regexp/try_convert_spec.rb new file mode 100644 index 0000000000..e775dbe971 --- /dev/null +++ b/spec/ruby/core/regexp/try_convert_spec.rb @@ -0,0 +1,27 @@ +require_relative '../../spec_helper' + +describe "Regexp.try_convert" do + not_supported_on :opal do + it "returns the argument if given a Regexp" do + Regexp.try_convert(/foo/s).should == /foo/s + end + end + + it "returns nil if given an argument that can't be converted to a Regexp" do + ['', 'glark', [], Object.new, :pat].each do |arg| + Regexp.try_convert(arg).should be_nil + end + end + + it "tries to coerce the argument by calling #to_regexp" do + rex = mock('regexp') + rex.should_receive(:to_regexp).and_return(/(p(a)t[e]rn)/) + Regexp.try_convert(rex).should == /(p(a)t[e]rn)/ + end + + it "raises a TypeError if the object does not return an Regexp from #to_regexp" do + obj = mock("regexp") + obj.should_receive(:to_regexp).and_return("string") + -> { Regexp.try_convert(obj) }.should raise_error(TypeError, "can't convert MockObject to Regexp (MockObject#to_regexp gives String)") + end +end diff --git a/spec/ruby/core/regexp/union_spec.rb b/spec/ruby/core/regexp/union_spec.rb new file mode 100644 index 0000000000..ea5a5053f7 --- /dev/null +++ b/spec/ruby/core/regexp/union_spec.rb @@ -0,0 +1,182 @@ +# encoding: utf-8 + +require_relative '../../spec_helper' + +describe "Regexp.union" do + it "returns /(?!)/ when passed no arguments" do + Regexp.union.should == /(?!)/ + end + + it "returns a regular expression that will match passed arguments" do + Regexp.union("penzance").should == /penzance/ + Regexp.union("skiing", "sledding").should == /skiing|sledding/ + not_supported_on :opal do + Regexp.union(/dogs/, /cats/i).should == /(?-mix:dogs)|(?i-mx:cats)/ + end + end + + it "quotes any string arguments" do + Regexp.union("n", ".").should == /n|\./ + end + + it "returns a Regexp with the encoding of an ASCII-incompatible String argument" do + Regexp.union("a".encode("UTF-16LE")).encoding.should == Encoding::UTF_16LE + end + + it "returns a Regexp with the encoding of a String containing non-ASCII-compatible characters" do + Regexp.union("\u00A9".encode("ISO-8859-1")).encoding.should == Encoding::ISO_8859_1 + end + + it "returns a Regexp with US-ASCII encoding if all arguments are ASCII-only" do + Regexp.union("a".encode("UTF-8"), "b".encode("SJIS")).encoding.should == Encoding::US_ASCII + end + + it "returns a Regexp with the encoding of multiple non-conflicting ASCII-incompatible String arguments" do + Regexp.union("a".encode("UTF-16LE"), "b".encode("UTF-16LE")).encoding.should == Encoding::UTF_16LE + end + + it "returns a Regexp with the encoding of multiple non-conflicting Strings containing non-ASCII-compatible characters" do + Regexp.union("\u00A9".encode("ISO-8859-1"), "\u00B0".encode("ISO-8859-1")).encoding.should == Encoding::ISO_8859_1 + end + + it "returns a Regexp with the encoding of a String containing non-ASCII-compatible characters and another ASCII-only String" do + Regexp.union("\u00A9".encode("ISO-8859-1"), "a".encode("UTF-8")).encoding.should == Encoding::ISO_8859_1 + end + + it "returns ASCII-8BIT if the regexp encodings are ASCII-8BIT and at least one has non-ASCII characters" do + us_ascii_implicit, us_ascii_explicit, binary = /abc/, /[\x00-\x7f]/n, /[\x80-\xBF]/n + us_ascii_implicit.encoding.should == Encoding::US_ASCII + us_ascii_explicit.encoding.should == Encoding::US_ASCII + binary.encoding.should == Encoding::BINARY + + Regexp.union(us_ascii_implicit, us_ascii_explicit, binary).encoding.should == Encoding::BINARY + Regexp.union(us_ascii_implicit, binary, us_ascii_explicit).encoding.should == Encoding::BINARY + Regexp.union(us_ascii_explicit, us_ascii_implicit, binary).encoding.should == Encoding::BINARY + Regexp.union(us_ascii_explicit, binary, us_ascii_implicit).encoding.should == Encoding::BINARY + Regexp.union(binary, us_ascii_implicit, us_ascii_explicit).encoding.should == Encoding::BINARY + Regexp.union(binary, us_ascii_explicit, us_ascii_implicit).encoding.should == Encoding::BINARY + end + + it "return US-ASCII if all patterns are ASCII-only" do + Regexp.union(/abc/e, /def/e).encoding.should == Encoding::US_ASCII + Regexp.union(/abc/n, /def/n).encoding.should == Encoding::US_ASCII + Regexp.union(/abc/s, /def/s).encoding.should == Encoding::US_ASCII + Regexp.union(/abc/u, /def/u).encoding.should == Encoding::US_ASCII + end + + it "returns a Regexp with UTF-8 if one part is UTF-8" do + Regexp.union(/probl[éeè]me/i, /help/i).encoding.should == Encoding::UTF_8 + end + + it "returns a Regexp if an array of string with special characters is passed" do + Regexp.union(["+","-"]).should == /\+|\-/ + end + + it "raises ArgumentError if the arguments include conflicting ASCII-incompatible Strings" do + -> { + Regexp.union("a".encode("UTF-16LE"), "b".encode("UTF-16BE")) + }.should raise_error(ArgumentError, 'incompatible encodings: UTF-16LE and UTF-16BE') + end + + it "raises ArgumentError if the arguments include conflicting ASCII-incompatible Regexps" do + -> { + Regexp.union(Regexp.new("a".encode("UTF-16LE")), + Regexp.new("b".encode("UTF-16BE"))) + }.should raise_error(ArgumentError, 'incompatible encodings: UTF-16LE and UTF-16BE') + end + + it "raises ArgumentError if the arguments include conflicting fixed encoding Regexps" do + -> { + Regexp.union(Regexp.new("a".encode("UTF-8"), Regexp::FIXEDENCODING), + Regexp.new("b".encode("US-ASCII"), Regexp::FIXEDENCODING)) + }.should raise_error(ArgumentError, 'incompatible encodings: UTF-8 and US-ASCII') + end + + it "raises ArgumentError if the arguments include a fixed encoding Regexp and a String containing non-ASCII-compatible characters in a different encoding" do + -> { + Regexp.union(Regexp.new("a".encode("UTF-8"), Regexp::FIXEDENCODING), + "\u00A9".encode("ISO-8859-1")) + }.should raise_error(ArgumentError, 'incompatible encodings: UTF-8 and ISO-8859-1') + end + + it "raises ArgumentError if the arguments include a String containing non-ASCII-compatible characters and a fixed encoding Regexp in a different encoding" do + -> { + Regexp.union("\u00A9".encode("ISO-8859-1"), + Regexp.new("a".encode("UTF-8"), Regexp::FIXEDENCODING)) + }.should raise_error(ArgumentError, 'incompatible encodings: ISO-8859-1 and UTF-8') + end + + it "raises ArgumentError if the arguments include an ASCII-incompatible String and an ASCII-only String" do + -> { + Regexp.union("a".encode("UTF-16LE"), "b".encode("UTF-8")) + }.should raise_error(ArgumentError, /ASCII incompatible encoding: UTF-16LE|incompatible encodings: UTF-16LE and US-ASCII/) + end + + it "raises ArgumentError if the arguments include an ASCII-incompatible Regexp and an ASCII-only String" do + -> { + Regexp.union(Regexp.new("a".encode("UTF-16LE")), "b".encode("UTF-8")) + }.should raise_error(ArgumentError, /ASCII incompatible encoding: UTF-16LE|incompatible encodings: UTF-16LE and US-ASCII/) + end + + it "raises ArgumentError if the arguments include an ASCII-incompatible String and an ASCII-only Regexp" do + -> { + Regexp.union("a".encode("UTF-16LE"), Regexp.new("b".encode("UTF-8"))) + }.should raise_error(ArgumentError, /ASCII incompatible encoding: UTF-16LE|incompatible encodings: UTF-16LE and US-ASCII/) + end + + it "raises ArgumentError if the arguments include an ASCII-incompatible Regexp and an ASCII-only Regexp" do + -> { + Regexp.union(Regexp.new("a".encode("UTF-16LE")), Regexp.new("b".encode("UTF-8"))) + }.should raise_error(ArgumentError, /ASCII incompatible encoding: UTF-16LE|incompatible encodings: UTF-16LE and US-ASCII/) + end + + it "raises ArgumentError if the arguments include an ASCII-incompatible String and a String containing non-ASCII-compatible characters in a different encoding" do + -> { + Regexp.union("a".encode("UTF-16LE"), "\u00A9".encode("ISO-8859-1")) + }.should raise_error(ArgumentError, 'incompatible encodings: UTF-16LE and ISO-8859-1') + end + + it "raises ArgumentError if the arguments include an ASCII-incompatible Regexp and a String containing non-ASCII-compatible characters in a different encoding" do + -> { + Regexp.union(Regexp.new("a".encode("UTF-16LE")), "\u00A9".encode("ISO-8859-1")) + }.should raise_error(ArgumentError, 'incompatible encodings: UTF-16LE and ISO-8859-1') + end + + it "raises ArgumentError if the arguments include an ASCII-incompatible String and a Regexp containing non-ASCII-compatible characters in a different encoding" do + -> { + Regexp.union("a".encode("UTF-16LE"), Regexp.new("\u00A9".encode("ISO-8859-1"))) + }.should raise_error(ArgumentError, 'incompatible encodings: UTF-16LE and ISO-8859-1') + end + + it "raises ArgumentError if the arguments include an ASCII-incompatible Regexp and a Regexp containing non-ASCII-compatible characters in a different encoding" do + -> { + Regexp.union(Regexp.new("a".encode("UTF-16LE")), Regexp.new("\u00A9".encode("ISO-8859-1"))) + }.should raise_error(ArgumentError, 'incompatible encodings: UTF-16LE and ISO-8859-1') + end + + it "uses to_str to convert arguments (if not Regexp)" do + obj = mock('pattern') + obj.should_receive(:to_str).and_return('foo') + Regexp.union(obj, "bar").should == /foo|bar/ + end + + it "uses to_regexp to convert argument" do + obj = mock('pattern') + obj.should_receive(:to_regexp).and_return(/foo/) + Regexp.union(obj).should == /foo/ + end + + it "accepts a Symbol as argument" do + Regexp.union(:foo).should == /foo/ + end + + it "accepts a single array of patterns as arguments" do + Regexp.union(["skiing", "sledding"]).should == /skiing|sledding/ + not_supported_on :opal do + Regexp.union([/dogs/, /cats/i]).should == /(?-mix:dogs)|(?i-mx:cats)/ + end + -> { + Regexp.union(["skiing", "sledding"], [/dogs/, /cats/i]) + }.should raise_error(TypeError, 'no implicit conversion of Array into String') + end +end diff --git a/spec/ruby/core/set/add_spec.rb b/spec/ruby/core/set/add_spec.rb new file mode 100644 index 0000000000..0fe1a0926c --- /dev/null +++ b/spec/ruby/core/set/add_spec.rb @@ -0,0 +1,34 @@ +require_relative '../../spec_helper' +require_relative 'shared/add' + +describe "Set#add" do + it_behaves_like :set_add, :add +end + +describe "Set#add?" do + before :each do + @set = Set.new + end + + it "adds the passed Object to self" do + @set.add?("cat") + @set.should include("cat") + end + + it "returns self when the Object has not yet been added to self" do + @set.add?("cat").should equal(@set) + end + + it "returns nil when the Object has already been added to self" do + @set.add?("cat") + @set.add?("cat").should be_nil + end + + it "raises RuntimeError when called during iteration" do + set = Set[:a, :b, :c, :d, :e, :f] + set.each do |_m| + -> { set << 1 }.should raise_error(RuntimeError, /iteration/) + end + set.should == Set[:a, :b, :c, :d, :e, :f] + end +end diff --git a/spec/ruby/core/set/append_spec.rb b/spec/ruby/core/set/append_spec.rb new file mode 100644 index 0000000000..82d34d9130 --- /dev/null +++ b/spec/ruby/core/set/append_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/add' + +describe "Set#<<" do + it_behaves_like :set_add, :<< +end diff --git a/spec/ruby/core/set/case_compare_spec.rb b/spec/ruby/core/set/case_compare_spec.rb new file mode 100644 index 0000000000..3781b1b963 --- /dev/null +++ b/spec/ruby/core/set/case_compare_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' +require_relative 'shared/include' + +describe "Set#===" do + it_behaves_like :set_include, :=== + + it "is an alias for include?" do + set = Set.new + set.method(:===).should == set.method(:include?) + end +end diff --git a/spec/ruby/core/set/case_equality_spec.rb b/spec/ruby/core/set/case_equality_spec.rb new file mode 100644 index 0000000000..19c1fb6b9c --- /dev/null +++ b/spec/ruby/core/set/case_equality_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/include' + +describe "Set#===" do + it_behaves_like :set_include, :=== +end diff --git a/spec/ruby/core/set/classify_spec.rb b/spec/ruby/core/set/classify_spec.rb new file mode 100644 index 0000000000..d86ea2722d --- /dev/null +++ b/spec/ruby/core/set/classify_spec.rb @@ -0,0 +1,26 @@ +require_relative '../../spec_helper' + +describe "Set#classify" do + before :each do + @set = Set["one", "two", "three", "four"] + end + + it "yields each Object in self" do + res = [] + @set.classify { |x| res << x } + res.sort.should == ["one", "two", "three", "four"].sort + end + + it "returns an Enumerator when passed no block" do + enum = @set.classify + enum.should be_an_instance_of(Enumerator) + + classified = enum.each { |x| x.length } + classified.should == { 3 => Set["one", "two"], 4 => Set["four"], 5 => Set["three"] } + end + + it "classifies the Objects in self based on the block's return value" do + classified = @set.classify { |x| x.length } + classified.should == { 3 => Set["one", "two"], 4 => Set["four"], 5 => Set["three"] } + end +end diff --git a/spec/ruby/core/set/clear_spec.rb b/spec/ruby/core/set/clear_spec.rb new file mode 100644 index 0000000000..ebeac211d3 --- /dev/null +++ b/spec/ruby/core/set/clear_spec.rb @@ -0,0 +1,16 @@ +require_relative '../../spec_helper' + +describe "Set#clear" do + before :each do + @set = Set["one", "two", "three", "four"] + end + + it "removes all elements from self" do + @set.clear + @set.should be_empty + end + + it "returns self" do + @set.clear.should equal(@set) + end +end diff --git a/spec/ruby/core/set/collect_spec.rb b/spec/ruby/core/set/collect_spec.rb new file mode 100644 index 0000000000..d186f1a0d9 --- /dev/null +++ b/spec/ruby/core/set/collect_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/collect' + +describe "Set#collect!" do + it_behaves_like :set_collect_bang, :collect! +end diff --git a/spec/ruby/core/set/compare_by_identity_spec.rb b/spec/ruby/core/set/compare_by_identity_spec.rb new file mode 100644 index 0000000000..238dc117a6 --- /dev/null +++ b/spec/ruby/core/set/compare_by_identity_spec.rb @@ -0,0 +1,153 @@ +require_relative '../../spec_helper' + +describe "Set#compare_by_identity" do + it "compares its members by identity" do + a = "a" + b1 = "b" + b2 = b1.dup + + set = Set.new + set.compare_by_identity + set.merge([a, a, b1, b2]) + set.to_a.sort.should == [a, b1, b2].sort + end + + it "causes future comparisons on the receiver to be made by identity" do + elt = [1] + set = Set.new + set << elt + set.member?(elt.dup).should be_true + set.compare_by_identity + set.member?(elt.dup).should be_false + end + + it "rehashes internally so that old members can be looked up" do + set = Set.new + (1..10).each { |k| set << k } + o = Object.new + def o.hash; 123; end + set << o + set.compare_by_identity + set.member?(o).should be_true + end + + it "returns self" do + set = Set.new + result = set.compare_by_identity + result.should equal(set) + end + + it "is idempotent and has no effect on an already compare_by_identity set" do + set = Set.new.compare_by_identity + set << :foo + set.compare_by_identity.should equal(set) + set.should.compare_by_identity? + set.to_a.should == [:foo] + end + + it "uses the semantics of BasicObject#equal? to determine members identity" do + :a.equal?(:a).should == true + Set.new.compare_by_identity.merge([:a, :a]).to_a.should == [:a] + + ary1 = [1] + ary2 = [1] + ary1.equal?(ary2).should == false + Set.new.compare_by_identity.merge([ary1, ary2]).to_a.sort.should == [ary1, ary2].sort + end + + it "uses #equal? semantics, but doesn't actually call #equal? to determine identity" do + set = Set.new.compare_by_identity + obj = mock("equal") + obj.should_not_receive(:equal?) + set << :foo + set << obj + set.to_a.should == [:foo, obj] + end + + it "does not call #hash on members" do + elt = mock("element") + elt.should_not_receive(:hash) + set = Set.new.compare_by_identity + set << elt + set.member?(elt).should be_true + end + + it "regards #dup'd objects as having different identities" do + a1 = "a" + a2 = a1.dup + + set = Set.new.compare_by_identity + set.merge([a1, a2]) + set.to_a.sort.should == [a1, a2].sort + end + + it "regards #clone'd objects as having different identities" do + a1 = "a" + a2 = a1.clone + + set = Set.new.compare_by_identity + set.merge([a1, a2]) + set.to_a.sort.should == [a1, a2].sort + end + + ruby_version_is "4.0" do + it "raises a FrozenError on frozen sets" do + set = Set.new.freeze + -> { + set.compare_by_identity + }.should raise_error(FrozenError, /can't modify frozen Set: (#<)?Set(\[|: {)[\]}]>?/) + end + end + + ruby_version_is ""..."4.0" do + it "raises a FrozenError on frozen sets" do + set = Set.new.freeze + -> { + set.compare_by_identity + }.should raise_error(FrozenError, /frozen Hash/) + end + end + + it "persists over #dups" do + set = Set.new.compare_by_identity + set << :a + set_dup = set.dup + set_dup.should == set + set_dup << :a + set_dup.to_a.should == [:a] + end + + it "persists over #clones" do + set = Set.new.compare_by_identity + set << :a + set_clone = set.clone + set_clone.should == set + set_clone << :a + set_clone.to_a.should == [:a] + end + + it "is not equal to set what does not compare by identity" do + Set.new([1, 2]).should == Set.new([1, 2]) + Set.new([1, 2]).should_not == Set.new([1, 2]).compare_by_identity + end +end + +describe "Set#compare_by_identity?" do + it "returns false by default" do + Set.new.should_not.compare_by_identity? + end + + it "returns true once #compare_by_identity has been invoked on self" do + set = Set.new + set.compare_by_identity + set.should.compare_by_identity? + end + + it "returns true when called multiple times on the same set" do + set = Set.new + set.compare_by_identity + set.should.compare_by_identity? + set.should.compare_by_identity? + set.should.compare_by_identity? + end +end diff --git a/spec/ruby/core/set/comparison_spec.rb b/spec/ruby/core/set/comparison_spec.rb new file mode 100644 index 0000000000..62059b70b3 --- /dev/null +++ b/spec/ruby/core/set/comparison_spec.rb @@ -0,0 +1,26 @@ +require_relative '../../spec_helper' + +describe "Set#<=>" do + it "returns 0 if the sets are equal" do + (Set[] <=> Set[]).should == 0 + (Set[:a, :b, :c] <=> Set[:a, :b, :c]).should == 0 + end + + it "returns -1 if the set is a proper subset of the other set" do + (Set[] <=> Set[1]).should == -1 + (Set[1, 2] <=> Set[1, 2, 3]).should == -1 + end + + it "returns +1 if the set is a proper superset of other set" do + (Set[1] <=> Set[]).should == +1 + (Set[1, 2, 3] <=> Set[1, 2]).should == +1 + end + + it "returns nil if the set has unique elements" do + (Set[1, 2, 3] <=> Set[:a, :b, :c]).should be_nil + end + + it "returns nil when the argument is not set-like" do + (Set[] <=> false).should be_nil + end +end diff --git a/spec/ruby/core/set/constructor_spec.rb b/spec/ruby/core/set/constructor_spec.rb new file mode 100644 index 0000000000..365081ad39 --- /dev/null +++ b/spec/ruby/core/set/constructor_spec.rb @@ -0,0 +1,14 @@ +require_relative '../../spec_helper' + +describe "Set[]" do + it "returns a new Set populated with the passed Objects" do + set = Set[1, 2, 3] + + set.instance_of?(Set).should be_true + set.size.should eql(3) + + set.should include(1) + set.should include(2) + set.should include(3) + end +end diff --git a/spec/ruby/core/set/delete_if_spec.rb b/spec/ruby/core/set/delete_if_spec.rb new file mode 100644 index 0000000000..beda73a5e5 --- /dev/null +++ b/spec/ruby/core/set/delete_if_spec.rb @@ -0,0 +1,37 @@ +require_relative '../../spec_helper' + +describe "Set#delete_if" do + before :each do + @set = Set["one", "two", "three"] + end + + it "yields every element of self" do + ret = [] + @set.delete_if { |x| ret << x } + ret.sort.should == ["one", "two", "three"].sort + end + + it "deletes every element from self for which the passed block returns true" do + @set.delete_if { |x| x.size == 3 } + @set.size.should eql(1) + + @set.should_not include("one") + @set.should_not include("two") + @set.should include("three") + end + + it "returns self" do + @set.delete_if { |x| x }.should equal(@set) + end + + it "returns an Enumerator when passed no block" do + enum = @set.delete_if + enum.should be_an_instance_of(Enumerator) + + enum.each { |x| x.size == 3 } + + @set.should_not include("one") + @set.should_not include("two") + @set.should include("three") + end +end diff --git a/spec/ruby/core/set/delete_spec.rb b/spec/ruby/core/set/delete_spec.rb new file mode 100644 index 0000000000..a2543ecbee --- /dev/null +++ b/spec/ruby/core/set/delete_spec.rb @@ -0,0 +1,36 @@ +require_relative '../../spec_helper' + +describe "Set#delete" do + before :each do + @set = Set["a", "b", "c"] + end + + it "deletes the passed Object from self" do + @set.delete("a") + @set.should_not include("a") + end + + it "returns self" do + @set.delete("a").should equal(@set) + @set.delete("x").should equal(@set) + end +end + +describe "Set#delete?" do + before :each do + @set = Set["a", "b", "c"] + end + + it "deletes the passed Object from self" do + @set.delete?("a") + @set.should_not include("a") + end + + it "returns self when the passed Object is in self" do + @set.delete?("a").should equal(@set) + end + + it "returns nil when the passed Object is not in self" do + @set.delete?("x").should be_nil + end +end diff --git a/spec/ruby/core/set/difference_spec.rb b/spec/ruby/core/set/difference_spec.rb new file mode 100644 index 0000000000..149f946592 --- /dev/null +++ b/spec/ruby/core/set/difference_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/difference' + +describe "Set#difference" do + it_behaves_like :set_difference, :difference +end diff --git a/spec/ruby/core/set/disjoint_spec.rb b/spec/ruby/core/set/disjoint_spec.rb new file mode 100644 index 0000000000..89a3c4b157 --- /dev/null +++ b/spec/ruby/core/set/disjoint_spec.rb @@ -0,0 +1,22 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/set_like' + +describe "Set#disjoint?" do + it "returns false when two Sets have at least one element in common" do + Set[1, 2].disjoint?(Set[2, 3]).should == false + end + + it "returns true when two Sets have no element in common" do + Set[1, 2].disjoint?(Set[3, 4]).should == true + end + + context "when comparing to a Set-like object" do + it "returns false when a Set has at least one element in common with a Set-like object" do + Set[1, 2].disjoint?(SetSpecs::SetLike.new([2, 3])).should be_false + end + + it "returns true when a Set has no element in common with a Set-like object" do + Set[1, 2].disjoint?(SetSpecs::SetLike.new([3, 4])).should be_true + end + end +end diff --git a/spec/ruby/core/set/divide_spec.rb b/spec/ruby/core/set/divide_spec.rb new file mode 100644 index 0000000000..c6c6003e99 --- /dev/null +++ b/spec/ruby/core/set/divide_spec.rb @@ -0,0 +1,68 @@ +require_relative '../../spec_helper' + +describe "Set#divide" do + it "divides self into a set of subsets based on the blocks return values" do + set = Set["one", "two", "three", "four", "five"].divide { |x| x.length } + set.map { |x| x.to_a.sort }.sort.should == [["five", "four"], ["one", "two"], ["three"]] + end + + it "yields each Object to the block" do + ret = [] + Set["one", "two", "three", "four", "five"].divide { |x| ret << x } + ret.sort.should == ["five", "four", "one", "three", "two"] + end + + it "returns an enumerator when not passed a block" do + ret = Set[1, 2, 3, 4].divide + ret.should be_kind_of(Enumerator) + ret.each(&:even?).should == Set[Set[1, 3], Set[2, 4]] + end +end + +describe "Set#divide when passed a block with an arity of 2" do + it "divides self into a set of subsets based on the blocks return values" do + set = Set[1, 3, 4, 6, 9, 10, 11].divide { |x, y| (x - y).abs == 1 } + set.map{ |x| x.to_a.sort }.sort.should == [[1], [3, 4], [6], [9, 10, 11]] + end + + ruby_version_is "4.0" do + it "yields each two Object to the block" do + ret = [] + Set[1, 2].divide { |x, y| ret << [x, y] } + ret.sort.should == [[1, 2], [2, 1]] + end + end + + ruby_version_is ""..."4.0" do + it "yields each two Object to the block" do + ret = [] + Set[1, 2].divide { |x, y| ret << [x, y] } + ret.sort.should == [[1, 1], [1, 2], [2, 1], [2, 2]] + end + end + + it "returns an enumerator when not passed a block" do + ret = Set[1, 2, 3, 4].divide + ret.should be_kind_of(Enumerator) + ret.each { |a, b| (a + b).even? }.should == Set[Set[1, 3], Set[2, 4]] + end +end + +describe "Set#divide when passed a block with an arity of > 2" do + it "only uses the first element if the arity > 2" do + set = Set["one", "two", "three", "four", "five"].divide do |x, y, z| + y.should be_nil + z.should be_nil + x.length + end + set.map { |x| x.to_a.sort }.sort.should == [["five", "four"], ["one", "two"], ["three"]] + end + + it "only uses the first element if the arity = -1" do + set = Set["one", "two", "three", "four", "five"].divide do |*xs| + xs.size.should == 1 + xs.first.length + end + set.map { |x| x.to_a.sort }.sort.should == [["five", "four"], ["one", "two"], ["three"]] + end +end diff --git a/spec/ruby/core/set/each_spec.rb b/spec/ruby/core/set/each_spec.rb new file mode 100644 index 0000000000..3d9cdc2d46 --- /dev/null +++ b/spec/ruby/core/set/each_spec.rb @@ -0,0 +1,26 @@ +require_relative '../../spec_helper' + +describe "Set#each" do + before :each do + @set = Set[1, 2, 3] + end + + it "yields each Object in self" do + ret = [] + @set.each { |x| ret << x } + ret.sort.should == [1, 2, 3] + end + + it "returns self" do + @set.each { |x| x }.should equal(@set) + end + + it "returns an Enumerator when not passed a block" do + enum = @set.each + enum.should be_an_instance_of(Enumerator) + + ret = [] + enum.each { |x| ret << x } + ret.sort.should == [1, 2, 3] + end +end diff --git a/spec/ruby/core/set/empty_spec.rb b/spec/ruby/core/set/empty_spec.rb new file mode 100644 index 0000000000..4b55658e20 --- /dev/null +++ b/spec/ruby/core/set/empty_spec.rb @@ -0,0 +1,9 @@ +require_relative '../../spec_helper' + +describe "Set#empty?" do + it "returns true if self is empty" do + Set[].empty?.should be_true + Set[1].empty?.should be_false + Set[1,2,3].empty?.should be_false + end +end diff --git a/spec/ruby/core/set/enumerable/to_set_spec.rb b/spec/ruby/core/set/enumerable/to_set_spec.rb new file mode 100644 index 0000000000..f139e1c025 --- /dev/null +++ b/spec/ruby/core/set/enumerable/to_set_spec.rb @@ -0,0 +1,12 @@ +require_relative '../../../spec_helper' + +describe "Enumerable#to_set" do + it "returns a new Set created from self" do + [1, 2, 3].to_set.should == Set[1, 2, 3] + {a: 1, b: 2}.to_set.should == Set[[:b, 2], [:a, 1]] + end + + it "passes down passed blocks" do + [1, 2, 3].to_set { |x| x * x }.should == Set[1, 4, 9] + end +end diff --git a/spec/ruby/core/set/eql_spec.rb b/spec/ruby/core/set/eql_spec.rb new file mode 100644 index 0000000000..4ad5c3aa5a --- /dev/null +++ b/spec/ruby/core/set/eql_spec.rb @@ -0,0 +1,14 @@ +require_relative '../../spec_helper' + +describe "Set#eql?" do + it "returns true when the passed argument is a Set and contains the same elements" do + Set[].should eql(Set[]) + Set[1, 2, 3].should eql(Set[1, 2, 3]) + Set[1, 2, 3].should eql(Set[3, 2, 1]) + Set["a", :b, ?c].should eql(Set[?c, :b, "a"]) + + Set[1, 2, 3].should_not eql(Set[1.0, 2, 3]) + Set[1, 2, 3].should_not eql(Set[2, 3]) + Set[1, 2, 3].should_not eql(Set[]) + end +end diff --git a/spec/ruby/core/set/equal_value_spec.rb b/spec/ruby/core/set/equal_value_spec.rb new file mode 100644 index 0000000000..721a79a3f1 --- /dev/null +++ b/spec/ruby/core/set/equal_value_spec.rb @@ -0,0 +1,34 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/set_like' + +describe "Set#==" do + it "returns true when the passed Object is a Set and self and the Object contain the same elements" do + Set[].should == Set[] + Set[1, 2, 3].should == Set[1, 2, 3] + Set["1", "2", "3"].should == Set["1", "2", "3"] + + Set[1, 2, 3].should_not == Set[1.0, 2, 3] + Set[1, 2, 3].should_not == [1, 2, 3] + end + + it "does not depend on the order of the elements" do + Set[1, 2, 3].should == Set[3, 2, 1] + Set[:a, "b", ?c].should == Set[?c, "b", :a] + end + + it "does not depend on the order of nested Sets" do + Set[Set[1], Set[2], Set[3]].should == Set[Set[3], Set[2], Set[1]] + + set1 = Set[Set["a", "b"], Set["c", "d"], Set["e", "f"]] + set2 = Set[Set["c", "d"], Set["a", "b"], Set["e", "f"]] + set1.should == set2 + end + + ruby_version_is ""..."4.0" do + context "when comparing to a Set-like object" do + it "returns true when a Set and a Set-like object contain the same elements" do + Set[1, 2, 3].should == SetSpecs::SetLike.new([1, 2, 3]) + end + end + end +end diff --git a/spec/ruby/core/set/exclusion_spec.rb b/spec/ruby/core/set/exclusion_spec.rb new file mode 100644 index 0000000000..bbc29afa95 --- /dev/null +++ b/spec/ruby/core/set/exclusion_spec.rb @@ -0,0 +1,17 @@ +require_relative '../../spec_helper' + +describe "Set#^" do + before :each do + @set = Set[1, 2, 3, 4] + end + + it "returns a new Set containing elements that are not in both self and the passed Enumerable" do + (@set ^ Set[3, 4, 5]).should == Set[1, 2, 5] + (@set ^ [3, 4, 5]).should == Set[1, 2, 5] + end + + it "raises an ArgumentError when passed a non-Enumerable" do + -> { @set ^ 3 }.should raise_error(ArgumentError) + -> { @set ^ Object.new }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/set/filter_spec.rb b/spec/ruby/core/set/filter_spec.rb new file mode 100644 index 0000000000..779254ad68 --- /dev/null +++ b/spec/ruby/core/set/filter_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/select' + +describe "Set#filter!" do + it_behaves_like :set_select_bang, :filter! +end diff --git a/spec/ruby/core/set/fixtures/set_like.rb b/spec/ruby/core/set/fixtures/set_like.rb new file mode 100644 index 0000000000..86dec2ed52 --- /dev/null +++ b/spec/ruby/core/set/fixtures/set_like.rb @@ -0,0 +1,30 @@ + +module SetSpecs + # This class is used to test the interaction of "Set-like" objects with real Sets + # + # These "Set-like" objects reply to is_a?(Set) with true and thus real Set objects are able to transparently + # interoperate with them in a duck-typing manner. + class SetLike + include Enumerable + + def is_a?(klass) + super || klass == ::Set + end + + def initialize(entries) + @entries = entries + end + + def each(&block) + @entries.each(&block) + end + + def inspect + "#<#{self.class}: {#{map(&:inspect).join(", ")}}>" + end + + def size + @entries.size + end + end +end diff --git a/spec/ruby/core/set/flatten_merge_spec.rb b/spec/ruby/core/set/flatten_merge_spec.rb new file mode 100644 index 0000000000..13cedeead9 --- /dev/null +++ b/spec/ruby/core/set/flatten_merge_spec.rb @@ -0,0 +1,24 @@ +require_relative '../../spec_helper' + +describe "Set#flatten_merge" do + ruby_version_is ""..."4.0" do + it "is protected" do + Set.should have_protected_instance_method("flatten_merge") + end + + it "flattens the passed Set and merges it into self" do + set1 = Set[1, 2] + set2 = Set[3, 4, Set[5, 6]] + + set1.send(:flatten_merge, set2).should == Set[1, 2, 3, 4, 5, 6] + end + + it "raises an ArgumentError when trying to flatten a recursive Set" do + set1 = Set[1, 2, 3] + set2 = Set[5, 6, 7] + set2 << set2 + + -> { set1.send(:flatten_merge, set2) }.should raise_error(ArgumentError) + end + end +end diff --git a/spec/ruby/core/set/flatten_spec.rb b/spec/ruby/core/set/flatten_spec.rb new file mode 100644 index 0000000000..f2cb3dfa52 --- /dev/null +++ b/spec/ruby/core/set/flatten_spec.rb @@ -0,0 +1,59 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/set_like' +set_version = defined?(Set::VERSION) ? Set::VERSION : '1.0.0' + +describe "Set#flatten" do + it "returns a copy of self with each included Set flattened" do + set = Set[1, 2, Set[3, 4, Set[5, 6, Set[7, 8]]], 9, 10] + flattened_set = set.flatten + + flattened_set.should_not equal(set) + flattened_set.should == Set[1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + end + + it "raises an ArgumentError when self is recursive" do + (set = Set[]) << set + -> { set.flatten }.should raise_error(ArgumentError) + end + + ruby_version_is ""..."4.0" do + context "when Set contains a Set-like object" do + it "returns a copy of self with each included Set-like object flattened" do + Set[SetSpecs::SetLike.new([1])].flatten.should == Set[1] + end + end + end +end + +describe "Set#flatten!" do + it "flattens self" do + set = Set[1, 2, Set[3, 4, Set[5, 6, Set[7, 8]]], 9, 10] + set.flatten! + set.should == Set[1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + end + + it "returns self when self was modified" do + set = Set[1, 2, Set[3, 4]] + set.flatten!.should equal(set) + end + + it "returns nil when self was not modified" do + set = Set[1, 2, 3, 4] + set.flatten!.should be_nil + end + + it "raises an ArgumentError when self is recursive" do + (set = Set[]) << set + -> { set.flatten! }.should raise_error(ArgumentError) + end + + version_is(set_version, ""..."1.1.0") do #ruby_version_is ""..."3.3" do + ruby_version_is ""..."4.0" do + context "when Set contains a Set-like object" do + it "flattens self, including Set-like objects" do + Set[SetSpecs::SetLike.new([1])].flatten!.should == Set[1] + end + end + end + end +end diff --git a/spec/ruby/core/set/hash_spec.rb b/spec/ruby/core/set/hash_spec.rb new file mode 100644 index 0000000000..63a0aa66a5 --- /dev/null +++ b/spec/ruby/core/set/hash_spec.rb @@ -0,0 +1,19 @@ +require_relative '../../spec_helper' + +describe "Set#hash" do + it "is static" do + Set[].hash.should == Set[].hash + Set[1, 2, 3].hash.should == Set[1, 2, 3].hash + Set[:a, "b", ?c].hash.should == Set[?c, "b", :a].hash + + Set[].hash.should_not == Set[1, 2, 3].hash + Set[1, 2, 3].hash.should_not == Set[:a, "b", ?c].hash + end + + ruby_version_is ""..."4.0" do + # see https://github.com/jruby/jruby/issues/8393 + it "is equal to nil.hash for an uninitialized Set" do + Set.allocate.hash.should == nil.hash + end + end +end diff --git a/spec/ruby/core/set/include_spec.rb b/spec/ruby/core/set/include_spec.rb new file mode 100644 index 0000000000..dd33bbc3bd --- /dev/null +++ b/spec/ruby/core/set/include_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/include' + +describe "Set#include?" do + it_behaves_like :set_include, :include? +end diff --git a/spec/ruby/core/set/initialize_clone_spec.rb b/spec/ruby/core/set/initialize_clone_spec.rb new file mode 100644 index 0000000000..13abb7ee4e --- /dev/null +++ b/spec/ruby/core/set/initialize_clone_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../spec_helper' + +describe "Set#initialize_clone" do + # See https://bugs.ruby-lang.org/issues/14266 + it "does not freeze the new Set when called from clone(freeze: false)" do + set1 = Set[1, 2] + set1.freeze + set2 = set1.clone(freeze: false) + set1.frozen?.should == true + set2.frozen?.should == false + set2.add 3 + set1.should == Set[1, 2] + set2.should == Set[1, 2, 3] + end +end diff --git a/spec/ruby/core/set/initialize_spec.rb b/spec/ruby/core/set/initialize_spec.rb new file mode 100644 index 0000000000..ad9e1bd8c9 --- /dev/null +++ b/spec/ruby/core/set/initialize_spec.rb @@ -0,0 +1,72 @@ +require_relative '../../spec_helper' + +describe "Set#initialize" do + it "is private" do + Set.should have_private_instance_method(:initialize) + end + + it "adds all elements of the passed Enumerable to self" do + s = Set.new([1, 2, 3]) + s.size.should eql(3) + s.should include(1) + s.should include(2) + s.should include(3) + end + + it "uses #each_entry on the provided Enumerable" do + enumerable = MockObject.new('mock-enumerable') + enumerable.should_receive(:each_entry).and_yield(1).and_yield(2).and_yield(3) + s = Set.new(enumerable) + s.size.should eql(3) + s.should include(1) + s.should include(2) + s.should include(3) + end + + it "uses #each on the provided Enumerable if it does not respond to #each_entry" do + enumerable = MockObject.new('mock-enumerable') + enumerable.should_receive(:each).and_yield(1).and_yield(2).and_yield(3) + s = Set.new(enumerable) + s.size.should eql(3) + s.should include(1) + s.should include(2) + s.should include(3) + end + + it "raises if the provided Enumerable does not respond to #each_entry or #each" do + enumerable = MockObject.new('mock-enumerable') + -> { Set.new(enumerable) }.should raise_error(ArgumentError, "value must be enumerable") + end + + it "should initialize with empty array and set" do + s = Set.new([]) + s.size.should eql(0) + + s = Set.new({}) + s.size.should eql(0) + end + + it "preprocesses all elements by a passed block before adding to self" do + s = Set.new([1, 2, 3]) { |x| x * x } + s.size.should eql(3) + s.should include(1) + s.should include(4) + s.should include(9) + end + + it "should initialize with empty array and block" do + s = Set.new([]) { |x| x * x } + s.size.should eql(0) + end + + it "should initialize with empty set and block" do + s = Set.new(Set.new) { |x| x * x } + s.size.should eql(0) + end + + it "should initialize with just block" do + s = Set.new { |x| x * x } + s.size.should eql(0) + s.should eql(Set.new) + end +end diff --git a/spec/ruby/core/set/inspect_spec.rb b/spec/ruby/core/set/inspect_spec.rb new file mode 100644 index 0000000000..0dcce83eb6 --- /dev/null +++ b/spec/ruby/core/set/inspect_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/inspect' + +describe "Set#inspect" do + it_behaves_like :set_inspect, :inspect +end diff --git a/spec/ruby/core/set/intersect_spec.rb b/spec/ruby/core/set/intersect_spec.rb new file mode 100644 index 0000000000..0736dea5fd --- /dev/null +++ b/spec/ruby/core/set/intersect_spec.rb @@ -0,0 +1,22 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/set_like' + +describe "Set#intersect?" do + it "returns true when two Sets have at least one element in common" do + Set[1, 2].intersect?(Set[2, 3]).should == true + end + + it "returns false when two Sets have no element in common" do + Set[1, 2].intersect?(Set[3, 4]).should == false + end + + context "when comparing to a Set-like object" do + it "returns true when a Set has at least one element in common with a Set-like object" do + Set[1, 2].intersect?(SetSpecs::SetLike.new([2, 3])).should be_true + end + + it "returns false when a Set has no element in common with a Set-like object" do + Set[1, 2].intersect?(SetSpecs::SetLike.new([3, 4])).should be_false + end + end +end diff --git a/spec/ruby/core/set/intersection_spec.rb b/spec/ruby/core/set/intersection_spec.rb new file mode 100644 index 0000000000..136b886775 --- /dev/null +++ b/spec/ruby/core/set/intersection_spec.rb @@ -0,0 +1,10 @@ +require_relative '../../spec_helper' +require_relative 'shared/intersection' + +describe "Set#intersection" do + it_behaves_like :set_intersection, :intersection +end + +describe "Set#&" do + it_behaves_like :set_intersection, :& +end diff --git a/spec/ruby/core/set/join_spec.rb b/spec/ruby/core/set/join_spec.rb new file mode 100644 index 0000000000..1c1e8a8af8 --- /dev/null +++ b/spec/ruby/core/set/join_spec.rb @@ -0,0 +1,30 @@ +require_relative '../../spec_helper' + +describe "Set#join" do + it "returns an empty string if the Set is empty" do + Set[].join.should == '' + end + + it "returns a new string formed by joining elements after conversion" do + set = Set[:a, :b, :c] + set.join.should == "abc" + end + + it "does not separate elements when the passed separator is nil" do + set = Set[:a, :b, :c] + set.join(nil).should == "abc" + end + + it "returns a string formed by concatenating each element separated by the separator" do + set = Set[:a, :b, :c] + set.join(' | ').should == "a | b | c" + end + + ruby_version_is ""..."4.0" do + it "calls #to_a to convert the Set in to an Array" do + set = Set[:a, :b, :c] + set.should_receive(:to_a).and_return([:a, :b, :c]) + set.join.should == "abc" + end + end +end diff --git a/spec/ruby/core/set/keep_if_spec.rb b/spec/ruby/core/set/keep_if_spec.rb new file mode 100644 index 0000000000..d6abdd6adc --- /dev/null +++ b/spec/ruby/core/set/keep_if_spec.rb @@ -0,0 +1,37 @@ +require_relative '../../spec_helper' + +describe "Set#keep_if" do + before :each do + @set = Set["one", "two", "three"] + end + + it "yields every element of self" do + ret = [] + @set.keep_if { |x| ret << x } + ret.sort.should == ["one", "two", "three"].sort + end + + it "keeps every element from self for which the passed block returns true" do + @set.keep_if { |x| x.size != 3 } + @set.size.should eql(1) + + @set.should_not include("one") + @set.should_not include("two") + @set.should include("three") + end + + it "returns self" do + @set.keep_if {}.should equal(@set) + end + + it "returns an Enumerator when passed no block" do + enum = @set.keep_if + enum.should be_an_instance_of(Enumerator) + + enum.each { |x| x.size != 3 } + + @set.should_not include("one") + @set.should_not include("two") + @set.should include("three") + end +end diff --git a/spec/ruby/core/set/length_spec.rb b/spec/ruby/core/set/length_spec.rb new file mode 100644 index 0000000000..6bb697b4ca --- /dev/null +++ b/spec/ruby/core/set/length_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/length' + +describe "Set#length" do + it_behaves_like :set_length, :length +end diff --git a/spec/ruby/core/set/map_spec.rb b/spec/ruby/core/set/map_spec.rb new file mode 100644 index 0000000000..996191b0a8 --- /dev/null +++ b/spec/ruby/core/set/map_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/collect' + +describe "Set#map!" do + it_behaves_like :set_collect_bang, :map! +end diff --git a/spec/ruby/core/set/member_spec.rb b/spec/ruby/core/set/member_spec.rb new file mode 100644 index 0000000000..5c82e8f826 --- /dev/null +++ b/spec/ruby/core/set/member_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/include' + +describe "Set#member?" do + it_behaves_like :set_include, :member? +end diff --git a/spec/ruby/core/set/merge_spec.rb b/spec/ruby/core/set/merge_spec.rb new file mode 100644 index 0000000000..0c6ed27670 --- /dev/null +++ b/spec/ruby/core/set/merge_spec.rb @@ -0,0 +1,37 @@ +require_relative '../../spec_helper' + +describe "Set#merge" do + it "adds the elements of the passed Enumerable to self" do + Set[:a, :b].merge(Set[:b, :c, :d]).should == Set[:a, :b, :c, :d] + Set[1, 2].merge([3, 4]).should == Set[1, 2, 3, 4] + end + + it "returns self" do + set = Set[1, 2] + set.merge([3, 4]).should equal(set) + end + + it "raises an ArgumentError when passed a non-Enumerable" do + -> { Set[1, 2].merge(1) }.should raise_error(ArgumentError) + -> { Set[1, 2].merge(Object.new) }.should raise_error(ArgumentError) + end + + it "raises RuntimeError when called during iteration" do + set = Set[:a, :b] + set.each do |_m| + -> { set.merge([1, 2]) }.should raise_error(RuntimeError, /iteration/) + end + end + + ruby_version_is ""..."3.3" do + it "accepts only a single argument" do + -> { Set[].merge([], []) }.should raise_error(ArgumentError, "wrong number of arguments (given 2, expected 1)") + end + end + + ruby_version_is "3.3" do + it "accepts multiple arguments" do + Set[:a, :b].merge(Set[:b, :c], [:d]).should == Set[:a, :b, :c, :d] + end + end +end diff --git a/spec/ruby/core/set/minus_spec.rb b/spec/ruby/core/set/minus_spec.rb new file mode 100644 index 0000000000..72f98f985e --- /dev/null +++ b/spec/ruby/core/set/minus_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/difference' + +describe "Set#-" do + it_behaves_like :set_difference, :- +end diff --git a/spec/ruby/core/set/plus_spec.rb b/spec/ruby/core/set/plus_spec.rb new file mode 100644 index 0000000000..7e44ff0b7e --- /dev/null +++ b/spec/ruby/core/set/plus_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/union' + +describe "Set#+" do + it_behaves_like :set_union, :+ +end diff --git a/spec/ruby/core/set/pretty_print_cycle_spec.rb b/spec/ruby/core/set/pretty_print_cycle_spec.rb new file mode 100644 index 0000000000..7e6017c112 --- /dev/null +++ b/spec/ruby/core/set/pretty_print_cycle_spec.rb @@ -0,0 +1,14 @@ +require_relative '../../spec_helper' + +describe "Set#pretty_print_cycle" do + it "passes the 'pretty print' representation of a self-referencing Set to the pretty print writer" do + pp = mock("PrettyPrint") + ruby_version_is(""..."4.0") do + pp.should_receive(:text).with("#<Set: {...}>") + end + ruby_version_is("4.0") do + pp.should_receive(:text).with("Set[...]") + end + Set[1, 2, 3].pretty_print_cycle(pp) + end +end diff --git a/spec/ruby/core/set/proper_subset_spec.rb b/spec/ruby/core/set/proper_subset_spec.rb new file mode 100644 index 0000000000..fb7848c001 --- /dev/null +++ b/spec/ruby/core/set/proper_subset_spec.rb @@ -0,0 +1,45 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/set_like' +set_version = defined?(Set::VERSION) ? Set::VERSION : '1.0.0' + +describe "Set#proper_subset?" do + before :each do + @set = Set[1, 2, 3, 4] + end + + it "returns true if passed a Set that self is a proper subset of" do + Set[].proper_subset?(@set).should be_true + Set[].proper_subset?(Set[1, 2, 3]).should be_true + Set[].proper_subset?(Set["a", :b, ?c]).should be_true + + Set[1, 2, 3].proper_subset?(@set).should be_true + Set[1, 3].proper_subset?(@set).should be_true + Set[1, 2].proper_subset?(@set).should be_true + Set[1].proper_subset?(@set).should be_true + + Set[5].proper_subset?(@set).should be_false + Set[1, 5].proper_subset?(@set).should be_false + Set[nil].proper_subset?(@set).should be_false + Set["test"].proper_subset?(@set).should be_false + + @set.proper_subset?(@set).should be_false + Set[].proper_subset?(Set[]).should be_false + end + + it "raises an ArgumentError when passed a non-Set" do + -> { Set[].proper_subset?([]) }.should raise_error(ArgumentError) + -> { Set[].proper_subset?(1) }.should raise_error(ArgumentError) + -> { Set[].proper_subset?("test") }.should raise_error(ArgumentError) + -> { Set[].proper_subset?(Object.new) }.should raise_error(ArgumentError) + end + + version_is(set_version, ""..."1.1.0") do #ruby_version_is ""..."3.3" do + ruby_version_is ""..."4.0" do + context "when comparing to a Set-like object" do + it "returns true if passed a Set-like object that self is a proper subset of" do + Set[1, 2, 3].proper_subset?(SetSpecs::SetLike.new([1, 2, 3, 4])).should be_true + end + end + end + end +end diff --git a/spec/ruby/core/set/proper_superset_spec.rb b/spec/ruby/core/set/proper_superset_spec.rb new file mode 100644 index 0000000000..dc1e87e230 --- /dev/null +++ b/spec/ruby/core/set/proper_superset_spec.rb @@ -0,0 +1,42 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/set_like' + +describe "Set#proper_superset?" do + before :each do + @set = Set[1, 2, 3, 4] + end + + it "returns true if passed a Set that self is a proper superset of" do + @set.proper_superset?(Set[]).should be_true + Set[1, 2, 3].proper_superset?(Set[]).should be_true + Set["a", :b, ?c].proper_superset?(Set[]).should be_true + + @set.proper_superset?(Set[1, 2, 3]).should be_true + @set.proper_superset?(Set[1, 3]).should be_true + @set.proper_superset?(Set[1, 2]).should be_true + @set.proper_superset?(Set[1]).should be_true + + @set.proper_superset?(Set[5]).should be_false + @set.proper_superset?(Set[1, 5]).should be_false + @set.proper_superset?(Set[nil]).should be_false + @set.proper_superset?(Set["test"]).should be_false + + @set.proper_superset?(@set).should be_false + Set[].proper_superset?(Set[]).should be_false + end + + it "raises an ArgumentError when passed a non-Set" do + -> { Set[].proper_superset?([]) }.should raise_error(ArgumentError) + -> { Set[].proper_superset?(1) }.should raise_error(ArgumentError) + -> { Set[].proper_superset?("test") }.should raise_error(ArgumentError) + -> { Set[].proper_superset?(Object.new) }.should raise_error(ArgumentError) + end + + ruby_version_is ""..."4.0" do + context "when comparing to a Set-like object" do + it "returns true if passed a Set-like object that self is a proper superset of" do + Set[1, 2, 3, 4].proper_superset?(SetSpecs::SetLike.new([1, 2, 3])).should be_true + end + end + end +end diff --git a/spec/ruby/core/set/reject_spec.rb b/spec/ruby/core/set/reject_spec.rb new file mode 100644 index 0000000000..91d0293415 --- /dev/null +++ b/spec/ruby/core/set/reject_spec.rb @@ -0,0 +1,41 @@ +require_relative '../../spec_helper' + +describe "Set#reject!" do + before :each do + @set = Set["one", "two", "three"] + end + + it "yields every element of self" do + ret = [] + @set.reject! { |x| ret << x } + ret.sort.should == ["one", "two", "three"].sort + end + + it "deletes every element from self for which the passed block returns true" do + @set.reject! { |x| x.size == 3 } + @set.size.should eql(1) + + @set.should_not include("one") + @set.should_not include("two") + @set.should include("three") + end + + it "returns self when self was modified" do + @set.reject! { |x| true }.should equal(@set) + end + + it "returns nil when self was not modified" do + @set.reject! { |x| false }.should be_nil + end + + it "returns an Enumerator when passed no block" do + enum = @set.reject! + enum.should be_an_instance_of(Enumerator) + + enum.each { |x| x.size == 3 } + + @set.should_not include("one") + @set.should_not include("two") + @set.should include("three") + end +end diff --git a/spec/ruby/core/set/replace_spec.rb b/spec/ruby/core/set/replace_spec.rb new file mode 100644 index 0000000000..c66a2d0ec3 --- /dev/null +++ b/spec/ruby/core/set/replace_spec.rb @@ -0,0 +1,24 @@ +require_relative '../../spec_helper' + +describe "Set#replace" do + before :each do + @set = Set[:a, :b, :c] + end + + it "replaces the contents with other and returns self" do + @set.replace(Set[1, 2, 3]).should == @set + @set.should == Set[1, 2, 3] + end + + it "raises RuntimeError when called during iteration" do + set = Set[:a, :b, :c, :d, :e, :f] + set.each do |_m| + -> { set.replace(Set[1, 2, 3]) }.should raise_error(RuntimeError, /iteration/) + end + set.should == Set[:a, :b, :c, :d, :e, :f] + end + + it "accepts any enumerable as other" do + @set.replace([1, 2, 3]).should == Set[1, 2, 3] + end +end diff --git a/spec/ruby/core/set/select_spec.rb b/spec/ruby/core/set/select_spec.rb new file mode 100644 index 0000000000..b458ffacaa --- /dev/null +++ b/spec/ruby/core/set/select_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/select' + +describe "Set#select!" do + it_behaves_like :set_select_bang, :select! +end diff --git a/spec/ruby/core/set/set_spec.rb b/spec/ruby/core/set/set_spec.rb new file mode 100644 index 0000000000..fd1d2072e3 --- /dev/null +++ b/spec/ruby/core/set/set_spec.rb @@ -0,0 +1,10 @@ +require_relative '../../spec_helper' + +describe 'Set' do + it 'is available without explicit requiring' do + output = ruby_exe(<<~RUBY, options: '--disable-gems', args: '2>&1') + puts Set.new([1, 2, 3]).to_a.inspect + RUBY + output.chomp.should == "[1, 2, 3]" + end +end diff --git a/spec/ruby/core/set/shared/add.rb b/spec/ruby/core/set/shared/add.rb new file mode 100644 index 0000000000..9e797f5df9 --- /dev/null +++ b/spec/ruby/core/set/shared/add.rb @@ -0,0 +1,14 @@ +describe :set_add, shared: true do + before :each do + @set = Set.new + end + + it "adds the passed Object to self" do + @set.send(@method, "dog") + @set.should include("dog") + end + + it "returns self" do + @set.send(@method, "dog").should equal(@set) + end +end diff --git a/spec/ruby/core/set/shared/collect.rb b/spec/ruby/core/set/shared/collect.rb new file mode 100644 index 0000000000..bc58c231be --- /dev/null +++ b/spec/ruby/core/set/shared/collect.rb @@ -0,0 +1,20 @@ +describe :set_collect_bang, shared: true do + before :each do + @set = Set[1, 2, 3, 4, 5] + end + + it "yields each Object in self" do + res = [] + @set.send(@method) { |x| res << x } + res.sort.should == [1, 2, 3, 4, 5].sort + end + + it "returns self" do + @set.send(@method) { |x| x }.should equal(@set) + end + + it "replaces self with the return values of the block" do + @set.send(@method) { |x| x * 2 } + @set.should == Set[2, 4, 6, 8, 10] + end +end diff --git a/spec/ruby/core/set/shared/difference.rb b/spec/ruby/core/set/shared/difference.rb new file mode 100644 index 0000000000..f88987ed2a --- /dev/null +++ b/spec/ruby/core/set/shared/difference.rb @@ -0,0 +1,15 @@ +describe :set_difference, shared: true do + before :each do + @set = Set[:a, :b, :c] + end + + it "returns a new Set containing self's elements excluding the elements in the passed Enumerable" do + @set.send(@method, Set[:a, :b]).should == Set[:c] + @set.send(@method, [:b, :c]).should == Set[:a] + end + + it "raises an ArgumentError when passed a non-Enumerable" do + -> { @set.send(@method, 1) }.should raise_error(ArgumentError) + -> { @set.send(@method, Object.new) }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/set/shared/include.rb b/spec/ruby/core/set/shared/include.rb new file mode 100644 index 0000000000..b4d95cde24 --- /dev/null +++ b/spec/ruby/core/set/shared/include.rb @@ -0,0 +1,29 @@ +describe :set_include, shared: true do + it "returns true when self contains the passed Object" do + set = Set[:a, :b, :c] + set.send(@method, :a).should be_true + set.send(@method, :e).should be_false + end + + describe "member equality" do + it "is checked using both #hash and #eql?" do + obj = Object.new + obj_another = Object.new + + def obj.hash; 42 end + def obj_another.hash; 42 end + def obj_another.eql?(o) hash == o.hash end + + set = Set["a", "b", "c", obj] + set.send(@method, obj_another).should == true + end + + it "is not checked using #==" do + obj = Object.new + set = Set["a", "b", "c"] + + obj.should_not_receive(:==) + set.send(@method, obj) + end + end +end diff --git a/spec/ruby/core/set/shared/inspect.rb b/spec/ruby/core/set/shared/inspect.rb new file mode 100644 index 0000000000..a90af66c98 --- /dev/null +++ b/spec/ruby/core/set/shared/inspect.rb @@ -0,0 +1,45 @@ +describe :set_inspect, shared: true do + it "returns a String representation of self" do + Set[].send(@method).should be_kind_of(String) + Set[nil, false, true].send(@method).should be_kind_of(String) + Set[1, 2, 3].send(@method).should be_kind_of(String) + Set["1", "2", "3"].send(@method).should be_kind_of(String) + Set[:a, "b", Set[?c]].send(@method).should be_kind_of(String) + end + + ruby_version_is "4.0" do + it "does include the elements of the set" do + Set["1"].send(@method).should == 'Set["1"]' + end + end + + ruby_version_is ""..."4.0" do + it "does include the elements of the set" do + Set["1"].send(@method).should == '#<Set: {"1"}>' + end + end + + it "puts spaces between the elements" do + Set["1", "2"].send(@method).should include('", "') + end + + ruby_version_is "4.0" do + it "correctly handles cyclic-references" do + set1 = Set[] + set2 = Set[set1] + set1 << set2 + set1.send(@method).should be_kind_of(String) + set1.send(@method).should include("Set[...]") + end + end + + ruby_version_is ""..."4.0" do + it "correctly handles cyclic-references" do + set1 = Set[] + set2 = Set[set1] + set1 << set2 + set1.send(@method).should be_kind_of(String) + set1.send(@method).should include("#<Set: {...}>") + end + end +end diff --git a/spec/ruby/core/set/shared/intersection.rb b/spec/ruby/core/set/shared/intersection.rb new file mode 100644 index 0000000000..5ae4199c94 --- /dev/null +++ b/spec/ruby/core/set/shared/intersection.rb @@ -0,0 +1,15 @@ +describe :set_intersection, shared: true do + before :each do + @set = Set[:a, :b, :c] + end + + it "returns a new Set containing only elements shared by self and the passed Enumerable" do + @set.send(@method, Set[:b, :c, :d, :e]).should == Set[:b, :c] + @set.send(@method, [:b, :c, :d]).should == Set[:b, :c] + end + + it "raises an ArgumentError when passed a non-Enumerable" do + -> { @set.send(@method, 1) }.should raise_error(ArgumentError) + -> { @set.send(@method, Object.new) }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/set/shared/length.rb b/spec/ruby/core/set/shared/length.rb new file mode 100644 index 0000000000..a8fcee9f39 --- /dev/null +++ b/spec/ruby/core/set/shared/length.rb @@ -0,0 +1,6 @@ +describe :set_length, shared: true do + it "returns the number of elements in the set" do + set = Set[:a, :b, :c] + set.send(@method).should == 3 + end +end diff --git a/spec/ruby/core/set/shared/select.rb b/spec/ruby/core/set/shared/select.rb new file mode 100644 index 0000000000..467b236ed3 --- /dev/null +++ b/spec/ruby/core/set/shared/select.rb @@ -0,0 +1,41 @@ +require_relative '../../../spec_helper' + +describe :set_select_bang, shared: true do + before :each do + @set = Set["one", "two", "three"] + end + + it "yields every element of self" do + ret = [] + @set.send(@method) { |x| ret << x } + ret.sort.should == ["one", "two", "three"].sort + end + + it "keeps every element from self for which the passed block returns true" do + @set.send(@method) { |x| x.size != 3 } + @set.size.should eql(1) + + @set.should_not include("one") + @set.should_not include("two") + @set.should include("three") + end + + it "returns self when self was modified" do + @set.send(@method) { false }.should equal(@set) + end + + it "returns nil when self was not modified" do + @set.send(@method) { true }.should be_nil + end + + it "returns an Enumerator when passed no block" do + enum = @set.send(@method) + enum.should be_an_instance_of(Enumerator) + + enum.each { |x| x.size != 3 } + + @set.should_not include("one") + @set.should_not include("two") + @set.should include("three") + end +end diff --git a/spec/ruby/core/set/shared/union.rb b/spec/ruby/core/set/shared/union.rb new file mode 100644 index 0000000000..314f0e852d --- /dev/null +++ b/spec/ruby/core/set/shared/union.rb @@ -0,0 +1,15 @@ +describe :set_union, shared: true do + before :each do + @set = Set[:a, :b, :c] + end + + it "returns a new Set containing all elements of self and the passed Enumerable" do + @set.send(@method, Set[:b, :d, :e]).should == Set[:a, :b, :c, :d, :e] + @set.send(@method, [:b, :e]).should == Set[:a, :b, :c, :e] + end + + it "raises an ArgumentError when passed a non-Enumerable" do + -> { @set.send(@method, 1) }.should raise_error(ArgumentError) + -> { @set.send(@method, Object.new) }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/set/size_spec.rb b/spec/ruby/core/set/size_spec.rb new file mode 100644 index 0000000000..4ae22c5f0a --- /dev/null +++ b/spec/ruby/core/set/size_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/length' + +describe "Set#size" do + it_behaves_like :set_length, :size +end diff --git a/spec/ruby/core/set/sortedset/sortedset_spec.rb b/spec/ruby/core/set/sortedset/sortedset_spec.rb new file mode 100644 index 0000000000..f3c1ec058d --- /dev/null +++ b/spec/ruby/core/set/sortedset/sortedset_spec.rb @@ -0,0 +1,13 @@ +require_relative '../../../spec_helper' + +describe "SortedSet" do + ruby_version_is ""..."4.0" do + it "raises error including message that it has been extracted from the set stdlib" do + -> { + SortedSet + }.should raise_error(RuntimeError) { |e| + e.message.should.include?("The `SortedSet` class has been extracted from the `set` library") + } + end + end +end diff --git a/spec/ruby/core/set/subset_spec.rb b/spec/ruby/core/set/subset_spec.rb new file mode 100644 index 0000000000..112bd9b38a --- /dev/null +++ b/spec/ruby/core/set/subset_spec.rb @@ -0,0 +1,45 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/set_like' +set_version = defined?(Set::VERSION) ? Set::VERSION : '1.0.0' + +describe "Set#subset?" do + before :each do + @set = Set[1, 2, 3, 4] + end + + it "returns true if passed a Set that is equal to self or self is a subset of" do + @set.subset?(@set).should be_true + Set[].subset?(Set[]).should be_true + + Set[].subset?(@set).should be_true + Set[].subset?(Set[1, 2, 3]).should be_true + Set[].subset?(Set["a", :b, ?c]).should be_true + + Set[1, 2, 3].subset?(@set).should be_true + Set[1, 3].subset?(@set).should be_true + Set[1, 2].subset?(@set).should be_true + Set[1].subset?(@set).should be_true + + Set[5].subset?(@set).should be_false + Set[1, 5].subset?(@set).should be_false + Set[nil].subset?(@set).should be_false + Set["test"].subset?(@set).should be_false + end + + it "raises an ArgumentError when passed a non-Set" do + -> { Set[].subset?([]) }.should raise_error(ArgumentError) + -> { Set[].subset?(1) }.should raise_error(ArgumentError) + -> { Set[].subset?("test") }.should raise_error(ArgumentError) + -> { Set[].subset?(Object.new) }.should raise_error(ArgumentError) + end + + version_is(set_version, ""..."1.1.0") do #ruby_version_is ""..."3.3" do + ruby_version_is ""..."4.0" do + context "when comparing to a Set-like object" do + it "returns true if passed a Set-like object that self is a subset of" do + Set[1, 2, 3].subset?(SetSpecs::SetLike.new([1, 2, 3, 4])).should be_true + end + end + end + end +end diff --git a/spec/ruby/core/set/subtract_spec.rb b/spec/ruby/core/set/subtract_spec.rb new file mode 100644 index 0000000000..ae4bc73d41 --- /dev/null +++ b/spec/ruby/core/set/subtract_spec.rb @@ -0,0 +1,16 @@ +require_relative '../../spec_helper' + +describe "Set#subtract" do + before :each do + @set = Set[:a, :b, :c] + end + + it "deletes any elements contained in other and returns self" do + @set.subtract(Set[:b, :c]).should == @set + @set.should == Set[:a] + end + + it "accepts any enumerable as other" do + @set.subtract([:c]).should == Set[:a, :b] + end +end diff --git a/spec/ruby/core/set/superset_spec.rb b/spec/ruby/core/set/superset_spec.rb new file mode 100644 index 0000000000..9b3df2d047 --- /dev/null +++ b/spec/ruby/core/set/superset_spec.rb @@ -0,0 +1,42 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/set_like' + +describe "Set#superset?" do + before :each do + @set = Set[1, 2, 3, 4] + end + + it "returns true if passed a Set that equals self or self is a proper superset of" do + @set.superset?(@set).should be_true + Set[].superset?(Set[]).should be_true + + @set.superset?(Set[]).should be_true + Set[1, 2, 3].superset?(Set[]).should be_true + Set["a", :b, ?c].superset?(Set[]).should be_true + + @set.superset?(Set[1, 2, 3]).should be_true + @set.superset?(Set[1, 3]).should be_true + @set.superset?(Set[1, 2]).should be_true + @set.superset?(Set[1]).should be_true + + @set.superset?(Set[5]).should be_false + @set.superset?(Set[1, 5]).should be_false + @set.superset?(Set[nil]).should be_false + @set.superset?(Set["test"]).should be_false + end + + it "raises an ArgumentError when passed a non-Set" do + -> { Set[].superset?([]) }.should raise_error(ArgumentError) + -> { Set[].superset?(1) }.should raise_error(ArgumentError) + -> { Set[].superset?("test") }.should raise_error(ArgumentError) + -> { Set[].superset?(Object.new) }.should raise_error(ArgumentError) + end + + ruby_version_is ""..."4.0" do + context "when comparing to a Set-like object" do + it "returns true if passed a Set-like object that self is a superset of" do + Set[1, 2, 3, 4].superset?(SetSpecs::SetLike.new([1, 2, 3])).should be_true + end + end + end +end diff --git a/spec/ruby/core/set/to_a_spec.rb b/spec/ruby/core/set/to_a_spec.rb new file mode 100644 index 0000000000..1e9800167a --- /dev/null +++ b/spec/ruby/core/set/to_a_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' + +describe "Set#to_a" do + it "returns an array containing elements of self" do + Set[1, 2, 3].to_a.sort.should == [1, 2, 3] + end +end diff --git a/spec/ruby/core/set/to_s_spec.rb b/spec/ruby/core/set/to_s_spec.rb new file mode 100644 index 0000000000..55b8bfd9b2 --- /dev/null +++ b/spec/ruby/core/set/to_s_spec.rb @@ -0,0 +1,11 @@ +require_relative "../../spec_helper" +require_relative 'shared/inspect' + +describe "Set#to_s" do + it_behaves_like :set_inspect, :to_s + + it "is an alias of inspect" do + set = Set.new + set.method(:to_s).should == set.method(:inspect) + end +end diff --git a/spec/ruby/core/set/union_spec.rb b/spec/ruby/core/set/union_spec.rb new file mode 100644 index 0000000000..3e77022d4b --- /dev/null +++ b/spec/ruby/core/set/union_spec.rb @@ -0,0 +1,10 @@ +require_relative '../../spec_helper' +require_relative 'shared/union' + +describe "Set#union" do + it_behaves_like :set_union, :union +end + +describe "Set#|" do + it_behaves_like :set_union, :| +end diff --git a/spec/ruby/core/signal/fixtures/trap_all.rb b/spec/ruby/core/signal/fixtures/trap_all.rb new file mode 100644 index 0000000000..afa00b498d --- /dev/null +++ b/spec/ruby/core/signal/fixtures/trap_all.rb @@ -0,0 +1,15 @@ +cannot_be_trapped = %w[KILL STOP] # See man 2 signal + +(Signal.list.keys - cannot_be_trapped).each do |signal| + begin + Signal.trap(signal, -> {}) + rescue ArgumentError => e + unless /can't trap reserved signal|Signal already used by VM or OS/ =~ e.message + raise e + end + else + Signal.trap(signal, "DEFAULT") + end +end + +puts "OK" diff --git a/spec/ruby/core/signal/list_spec.rb b/spec/ruby/core/signal/list_spec.rb new file mode 100644 index 0000000000..56ad6828fe --- /dev/null +++ b/spec/ruby/core/signal/list_spec.rb @@ -0,0 +1,68 @@ +require_relative '../../spec_helper' + +describe "Signal.list" do + RUBY_SIGNALS = %w{ + EXIT + HUP + INT + QUIT + ILL + TRAP + IOT + ABRT + EMT + FPE + KILL + BUS + SEGV + SYS + PIPE + ALRM + TERM + URG + STOP + TSTP + CONT + CHLD + CLD + TTIN + TTOU + IO + XCPU + XFSZ + VTALRM + PROF + WINCH + USR1 + USR2 + LOST + MSG + PWR + POLL + DANGER + MIGRATE + PRE + GRANT + RETRACT + SOUND + INFO + } + + it "doesn't contain other signals than the known list" do + (Signal.list.keys - RUBY_SIGNALS).should == [] + end + + if Signal.list["CHLD"] + it "redefines CLD with CHLD if defined" do + Signal.list["CLD"].should == Signal.list["CHLD"] + end + end + + it "includes the EXIT key with a value of zero" do + Signal.list["EXIT"].should == 0 + end + + it "includes the KILL key with a value of nine" do + Signal.list["KILL"].should == 9 + end +end diff --git a/spec/ruby/core/signal/signame_spec.rb b/spec/ruby/core/signal/signame_spec.rb new file mode 100644 index 0000000000..adfe895d97 --- /dev/null +++ b/spec/ruby/core/signal/signame_spec.rb @@ -0,0 +1,34 @@ +require_relative '../../spec_helper' + +describe "Signal.signame" do + it "takes a signal name with a well known signal number" do + Signal.signame(0).should == "EXIT" + end + + it "returns nil if the argument is an invalid signal number" do + Signal.signame(-1).should == nil + end + + it "calls #to_int on an object to convert to an Integer" do + obj = mock('signal') + obj.should_receive(:to_int).and_return(0) + Signal.signame(obj).should == "EXIT" + end + + it "raises a TypeError when the passed argument can't be coerced to Integer" do + -> { Signal.signame("hello") }.should raise_error(TypeError) + end + + it "raises a TypeError when the passed argument responds to #to_int but does not return an Integer" do + obj = mock('signal') + obj.should_receive(:to_int).and_return('not an int') + -> { Signal.signame(obj) }.should raise_error(TypeError) + end + + platform_is_not :windows do + it "the original should take precedence over alias when looked up by number" do + Signal.signame(Signal.list["ABRT"]).should == "ABRT" + Signal.signame(Signal.list["CHLD"]).should == "CHLD" + end + end +end diff --git a/spec/ruby/core/signal/trap_spec.rb b/spec/ruby/core/signal/trap_spec.rb new file mode 100644 index 0000000000..6d654a99be --- /dev/null +++ b/spec/ruby/core/signal/trap_spec.rb @@ -0,0 +1,320 @@ +require_relative '../../spec_helper' + +describe "Signal.trap" do + platform_is_not :windows do + before :each do + ScratchPad.clear + @proc = -> {} + @saved_trap = Signal.trap(:HUP, @proc) + @hup_number = Signal.list["HUP"] + end + + after :each do + Signal.trap(:HUP, @saved_trap) if @saved_trap + end + + it "returns the previous handler" do + Signal.trap(:HUP, @saved_trap).should equal(@proc) + end + + it "accepts a block" do + done = false + + Signal.trap(:HUP) do |signo| + signo.should == @hup_number + ScratchPad.record :block_trap + done = true + end + + Process.kill :HUP, Process.pid + Thread.pass until done + + ScratchPad.recorded.should == :block_trap + end + + it "accepts a proc" do + done = false + + handler = -> signo { + signo.should == @hup_number + ScratchPad.record :proc_trap + done = true + } + + Signal.trap(:HUP, handler) + + Process.kill :HUP, Process.pid + Thread.pass until done + + ScratchPad.recorded.should == :proc_trap + end + + it "accepts a method" do + done = false + + handler_class = Class.new + hup_number = @hup_number + + handler_class.define_method :handler_method do |signo| + signo.should == hup_number + ScratchPad.record :method_trap + done = true + end + + handler_method = handler_class.new.method(:handler_method) + + Signal.trap(:HUP, handler_method) + + Process.kill :HUP, Process.pid + Thread.pass until done + + ScratchPad.recorded.should == :method_trap + end + + it "accepts anything you can call" do + done = false + + callable = Object.new + hup_number = @hup_number + + callable.singleton_class.define_method :call do |signo| + signo.should == hup_number + ScratchPad.record :callable_trap + done = true + end + + Signal.trap(:HUP, callable) + + Process.kill :HUP, Process.pid + Thread.pass until done + + ScratchPad.recorded.should == :callable_trap + end + + it "raises an exception for a non-callable at the point of use" do + not_callable = Object.new + Signal.trap(:HUP, not_callable) + -> { + Process.kill :HUP, Process.pid + loop { Thread.pass } + }.should raise_error(NoMethodError) + end + + it "accepts a non-callable that becomes callable when used" do + done = false + + late_callable = Object.new + hup_number = @hup_number + + Signal.trap(:HUP, late_callable) + + late_callable.singleton_class.define_method :call do |signo| + signo.should == hup_number + ScratchPad.record :late_callable_trap + done = true + end + + Process.kill :HUP, Process.pid + Thread.pass until done + + ScratchPad.recorded.should == :late_callable_trap + end + + it "is possible to create a new Thread when the handler runs" do + done = false + + Signal.trap(:HUP) do + thr = Thread.new { } + thr.join + ScratchPad.record(thr.group == Thread.main.group) + + done = true + end + + Process.kill :HUP, Process.pid + Thread.pass until done + + ScratchPad.recorded.should be_true + end + + it "registers an handler doing nothing with :IGNORE" do + Signal.trap :HUP, :IGNORE + Signal.trap(:HUP, @saved_trap).should == "IGNORE" + end + + it "can register a new handler after :IGNORE" do + Signal.trap :HUP, :IGNORE + + done = false + Signal.trap(:HUP) do + ScratchPad.record :block_trap + done = true + end + + Process.kill(:HUP, Process.pid).should == 1 + Thread.pass until done + ScratchPad.recorded.should == :block_trap + end + + it "ignores the signal when passed nil" do + Signal.trap :HUP, nil + Signal.trap(:HUP, @saved_trap).should be_nil + end + + it "accepts :DEFAULT in place of a proc" do + Signal.trap :HUP, :DEFAULT + Signal.trap(:HUP, @saved_trap).should == "DEFAULT" + end + + it "accepts :SIG_DFL in place of a proc" do + Signal.trap :HUP, :SIG_DFL + Signal.trap(:HUP, @saved_trap).should == "DEFAULT" + end + + it "accepts :SIG_IGN in place of a proc" do + Signal.trap :HUP, :SIG_IGN + Signal.trap(:HUP, @saved_trap).should == "IGNORE" + end + + it "accepts :IGNORE in place of a proc" do + Signal.trap :HUP, :IGNORE + Signal.trap(:HUP, @saved_trap).should == "IGNORE" + end + + it "accepts 'SIG_DFL' in place of a proc" do + Signal.trap :HUP, "SIG_DFL" + Signal.trap(:HUP, @saved_trap).should == "DEFAULT" + end + + it "accepts 'DEFAULT' in place of a proc" do + Signal.trap :HUP, "DEFAULT" + Signal.trap(:HUP, @saved_trap).should == "DEFAULT" + end + + it "accepts 'SIG_IGN' in place of a proc" do + Signal.trap :HUP, "SIG_IGN" + Signal.trap(:HUP, @saved_trap).should == "IGNORE" + end + + it "accepts 'IGNORE' in place of a proc" do + Signal.trap :HUP, "IGNORE" + Signal.trap(:HUP, @saved_trap).should == "IGNORE" + end + + it "accepts long names as Strings" do + Signal.trap "SIGHUP", @proc + Signal.trap("SIGHUP", @saved_trap).should equal(@proc) + end + + it "accepts short names as Strings" do + Signal.trap "HUP", @proc + Signal.trap("HUP", @saved_trap).should equal(@proc) + end + + it "accepts long names as Symbols" do + Signal.trap :SIGHUP, @proc + Signal.trap(:SIGHUP, @saved_trap).should equal(@proc) + end + + it "accepts short names as Symbols" do + Signal.trap :HUP, @proc + Signal.trap(:HUP, @saved_trap).should equal(@proc) + end + + it "calls #to_str on an object to convert to a String" do + obj = mock("signal") + obj.should_receive(:to_str).exactly(2).times.and_return("HUP") + Signal.trap obj, @proc + Signal.trap(obj, @saved_trap).should equal(@proc) + end + + it "accepts Integer values" do + hup = Signal.list["HUP"] + Signal.trap hup, @proc + Signal.trap(hup, @saved_trap).should equal(@proc) + end + + it "does not call #to_int on an object to convert to an Integer" do + obj = mock("signal") + obj.should_not_receive(:to_int) + -> { Signal.trap obj, @proc }.should raise_error(ArgumentError, /bad signal type/) + end + + it "raises ArgumentError when passed unknown signal" do + -> { Signal.trap(300) { } }.should raise_error(ArgumentError, "invalid signal number (300)") + -> { Signal.trap("USR10") { } }.should raise_error(ArgumentError, /\Aunsupported signal [`']SIGUSR10'\z/) + -> { Signal.trap("SIGUSR10") { } }.should raise_error(ArgumentError, /\Aunsupported signal [`']SIGUSR10'\z/) + end + + it "raises ArgumentError when passed signal is not Integer, String or Symbol" do + -> { Signal.trap(nil) { } }.should raise_error(ArgumentError, "bad signal type NilClass") + -> { Signal.trap(100.0) { } }.should raise_error(ArgumentError, "bad signal type Float") + -> { Signal.trap(Rational(100)) { } }.should raise_error(ArgumentError, "bad signal type Rational") + end + + # See man 2 signal + %w[KILL STOP].each do |signal| + it "raises ArgumentError or Errno::EINVAL for SIG#{signal}" do + -> { + Signal.trap(signal, -> {}) + }.should raise_error(StandardError) { |e| + [ArgumentError, Errno::EINVAL].should include(e.class) + e.message.should =~ /Invalid argument|Signal already used by VM or OS/ + } + end + end + + %w[SEGV BUS ILL FPE VTALRM].each do |signal| + it "raises ArgumentError for SIG#{signal} which is reserved by Ruby" do + -> { + Signal.trap(signal, -> {}) + }.should raise_error(ArgumentError, "can't trap reserved signal: SIG#{signal}") + end + end + + it "allows to register a handler for all known signals, except reserved signals for which it raises ArgumentError" do + out = ruby_exe(fixture(__FILE__, "trap_all.rb"), args: "2>&1") + out.should == "OK\n" + $?.exitstatus.should == 0 + end + + it "returns 'DEFAULT' for the initial SIGINT handler" do + ruby_exe("print Signal.trap(:INT) { abort }").should == 'DEFAULT' + end + + it "returns SYSTEM_DEFAULT if passed DEFAULT and no handler was ever set" do + Signal.trap("PROF", "DEFAULT").should == "SYSTEM_DEFAULT" + end + + it "accepts 'SYSTEM_DEFAULT' and uses the OS handler for SIGPIPE" do + code = <<-RUBY + p Signal.trap('PIPE', 'SYSTEM_DEFAULT') + r, w = IO.pipe + r.close + loop { w.write("a"*1024) } + RUBY + out = ruby_exe(code, exit_status: :SIGPIPE) + status = $? + out.should == "nil\n" + status.should.signaled? + end + end + + describe "the special EXIT signal code" do + it "accepts the EXIT code" do + code = "Signal.trap(:EXIT, proc { print 1 })" + ruby_exe(code).should == "1" + end + + it "runs the proc before at_exit handlers" do + code = "at_exit {print 1}; Signal.trap(:EXIT, proc {print 2}); at_exit {print 3}" + ruby_exe(code).should == "231" + end + + it "can unset the handler" do + code = "Signal.trap(:EXIT, proc { print 1 }); Signal.trap(:EXIT, 'DEFAULT')" + ruby_exe(code).should == "" + end + end + +end diff --git a/spec/ruby/core/sizedqueue/append_spec.rb b/spec/ruby/core/sizedqueue/append_spec.rb new file mode 100644 index 0000000000..c52baa3802 --- /dev/null +++ b/spec/ruby/core/sizedqueue/append_spec.rb @@ -0,0 +1,16 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/enque' +require_relative '../../shared/sizedqueue/enque' +require_relative '../../shared/types/rb_num2dbl_fails' + +describe "SizedQueue#<<" do + it_behaves_like :queue_enq, :<<, -> { SizedQueue.new(10) } +end + +describe "SizedQueue#<<" do + it_behaves_like :sizedqueue_enq, :<<, -> n { SizedQueue.new(n) } +end + +describe "SizedQueue operations with timeout" do + it_behaves_like :rb_num2dbl_fails, nil, -> v { q = SizedQueue.new(1); q.send(:<<, 1, timeout: v) } +end diff --git a/spec/ruby/core/sizedqueue/clear_spec.rb b/spec/ruby/core/sizedqueue/clear_spec.rb new file mode 100644 index 0000000000..abae01c6c0 --- /dev/null +++ b/spec/ruby/core/sizedqueue/clear_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/clear' + +describe "SizedQueue#clear" do + it_behaves_like :queue_clear, :clear, -> { SizedQueue.new(10) } +end diff --git a/spec/ruby/core/sizedqueue/close_spec.rb b/spec/ruby/core/sizedqueue/close_spec.rb new file mode 100644 index 0000000000..0e0af851cb --- /dev/null +++ b/spec/ruby/core/sizedqueue/close_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/close' + +describe "SizedQueue#close" do + it_behaves_like :queue_close, :close, -> { SizedQueue.new(10) } +end diff --git a/spec/ruby/core/sizedqueue/closed_spec.rb b/spec/ruby/core/sizedqueue/closed_spec.rb new file mode 100644 index 0000000000..4b90da1faa --- /dev/null +++ b/spec/ruby/core/sizedqueue/closed_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/closed' + +describe "SizedQueue#closed?" do + it_behaves_like :queue_closed?, :closed?, -> { SizedQueue.new(10) } +end diff --git a/spec/ruby/core/sizedqueue/deq_spec.rb b/spec/ruby/core/sizedqueue/deq_spec.rb new file mode 100644 index 0000000000..2aeb52f8a6 --- /dev/null +++ b/spec/ruby/core/sizedqueue/deq_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/deque' +require_relative '../../shared/types/rb_num2dbl_fails' + +describe "SizedQueue#deq" do + it_behaves_like :queue_deq, :deq, -> { SizedQueue.new(10) } +end + +describe "SizedQueue operations with timeout" do + it_behaves_like :rb_num2dbl_fails, nil, -> v { q = SizedQueue.new(10); q.push(1); q.deq(timeout: v) } +end diff --git a/spec/ruby/core/sizedqueue/empty_spec.rb b/spec/ruby/core/sizedqueue/empty_spec.rb new file mode 100644 index 0000000000..9b0d4ff013 --- /dev/null +++ b/spec/ruby/core/sizedqueue/empty_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/empty' + +describe "SizedQueue#empty?" do + it_behaves_like :queue_empty?, :empty?, -> { SizedQueue.new(10) } +end diff --git a/spec/ruby/core/sizedqueue/enq_spec.rb b/spec/ruby/core/sizedqueue/enq_spec.rb new file mode 100644 index 0000000000..b955909475 --- /dev/null +++ b/spec/ruby/core/sizedqueue/enq_spec.rb @@ -0,0 +1,16 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/enque' +require_relative '../../shared/sizedqueue/enque' +require_relative '../../shared/types/rb_num2dbl_fails' + +describe "SizedQueue#enq" do + it_behaves_like :queue_enq, :enq, -> { SizedQueue.new(10) } +end + +describe "SizedQueue#enq" do + it_behaves_like :sizedqueue_enq, :enq, -> n { SizedQueue.new(n) } +end + +describe "SizedQueue operations with timeout" do + it_behaves_like :rb_num2dbl_fails, nil, -> v { q = SizedQueue.new(1); q.enq(1, timeout: v) } +end diff --git a/spec/ruby/core/sizedqueue/freeze_spec.rb b/spec/ruby/core/sizedqueue/freeze_spec.rb new file mode 100644 index 0000000000..98f01cae2f --- /dev/null +++ b/spec/ruby/core/sizedqueue/freeze_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/freeze' + +describe "SizedQueue#freeze" do + it_behaves_like :queue_freeze, :freeze, -> { SizedQueue.new(1) } +end diff --git a/spec/ruby/core/sizedqueue/length_spec.rb b/spec/ruby/core/sizedqueue/length_spec.rb new file mode 100644 index 0000000000..b93e7f8997 --- /dev/null +++ b/spec/ruby/core/sizedqueue/length_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/length' + +describe "SizedQueue#length" do + it_behaves_like :queue_length, :length, -> { SizedQueue.new(10) } +end diff --git a/spec/ruby/core/sizedqueue/max_spec.rb b/spec/ruby/core/sizedqueue/max_spec.rb new file mode 100644 index 0000000000..cbabb5dc10 --- /dev/null +++ b/spec/ruby/core/sizedqueue/max_spec.rb @@ -0,0 +1,10 @@ +require_relative '../../spec_helper' +require_relative '../../shared/sizedqueue/max' + +describe "SizedQueue#max" do + it_behaves_like :sizedqueue_max, :max, -> n { SizedQueue.new(n) } +end + +describe "SizedQueue#max=" do + it_behaves_like :sizedqueue_max=, :max=, -> n { SizedQueue.new(n) } +end diff --git a/spec/ruby/core/sizedqueue/new_spec.rb b/spec/ruby/core/sizedqueue/new_spec.rb new file mode 100644 index 0000000000..d5704732c3 --- /dev/null +++ b/spec/ruby/core/sizedqueue/new_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/sizedqueue/new' + +describe "SizedQueue.new" do + it_behaves_like :sizedqueue_new, :new, -> *n { SizedQueue.new(*n) } +end diff --git a/spec/ruby/core/sizedqueue/num_waiting_spec.rb b/spec/ruby/core/sizedqueue/num_waiting_spec.rb new file mode 100644 index 0000000000..9aef25e33d --- /dev/null +++ b/spec/ruby/core/sizedqueue/num_waiting_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/sizedqueue/num_waiting' + +describe "SizedQueue#num_waiting" do + it_behaves_like :sizedqueue_num_waiting, :new, -> n { SizedQueue.new(n) } +end diff --git a/spec/ruby/core/sizedqueue/pop_spec.rb b/spec/ruby/core/sizedqueue/pop_spec.rb new file mode 100644 index 0000000000..6338ddbaa0 --- /dev/null +++ b/spec/ruby/core/sizedqueue/pop_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/deque' +require_relative '../../shared/types/rb_num2dbl_fails' + +describe "SizedQueue#pop" do + it_behaves_like :queue_deq, :pop, -> { SizedQueue.new(10) } +end + +describe "SizedQueue operations with timeout" do + it_behaves_like :rb_num2dbl_fails, nil, -> v { q = SizedQueue.new(10); q.push(1); q.pop(timeout: v) } +end diff --git a/spec/ruby/core/sizedqueue/push_spec.rb b/spec/ruby/core/sizedqueue/push_spec.rb new file mode 100644 index 0000000000..9eaa6beca0 --- /dev/null +++ b/spec/ruby/core/sizedqueue/push_spec.rb @@ -0,0 +1,16 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/enque' +require_relative '../../shared/sizedqueue/enque' +require_relative '../../shared/types/rb_num2dbl_fails' + +describe "SizedQueue#push" do + it_behaves_like :queue_enq, :push, -> { SizedQueue.new(10) } +end + +describe "SizedQueue#push" do + it_behaves_like :sizedqueue_enq, :push, -> n { SizedQueue.new(n) } +end + +describe "SizedQueue operations with timeout" do + it_behaves_like :rb_num2dbl_fails, nil, -> v { q = SizedQueue.new(1); q.push(1, timeout: v) } +end diff --git a/spec/ruby/core/sizedqueue/shift_spec.rb b/spec/ruby/core/sizedqueue/shift_spec.rb new file mode 100644 index 0000000000..52974c1d99 --- /dev/null +++ b/spec/ruby/core/sizedqueue/shift_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/deque' +require_relative '../../shared/types/rb_num2dbl_fails' + +describe "SizedQueue#shift" do + it_behaves_like :queue_deq, :shift, -> { SizedQueue.new(10) } +end + +describe "SizedQueue operations with timeout" do + it_behaves_like :rb_num2dbl_fails, nil, -> v { q = SizedQueue.new(10); q.push(1); q.shift(timeout: v) } +end diff --git a/spec/ruby/core/sizedqueue/size_spec.rb b/spec/ruby/core/sizedqueue/size_spec.rb new file mode 100644 index 0000000000..dfa76faabe --- /dev/null +++ b/spec/ruby/core/sizedqueue/size_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative '../../shared/queue/length' + +describe "SizedQueue#size" do + it_behaves_like :queue_length, :size, -> { SizedQueue.new(10) } +end diff --git a/spec/ruby/core/string/allocate_spec.rb b/spec/ruby/core/string/allocate_spec.rb new file mode 100644 index 0000000000..30d5f60594 --- /dev/null +++ b/spec/ruby/core/string/allocate_spec.rb @@ -0,0 +1,19 @@ +require_relative '../../spec_helper' + +describe "String.allocate" do + it "returns an instance of String" do + str = String.allocate + str.should be_an_instance_of(String) + end + + it "returns a fully-formed String" do + str = String.allocate + str.size.should == 0 + str << "more" + str.should == "more" + end + + it "returns a binary String" do + String.allocate.encoding.should == Encoding::BINARY + end +end diff --git a/spec/ruby/core/string/append_as_bytes_spec.rb b/spec/ruby/core/string/append_as_bytes_spec.rb new file mode 100644 index 0000000000..b1703e5f89 --- /dev/null +++ b/spec/ruby/core/string/append_as_bytes_spec.rb @@ -0,0 +1,58 @@ +require_relative '../../spec_helper' + +describe "String#append_bytes" do + ruby_version_is "3.4" do + it "doesn't allow to mutate frozen strings" do + str = "hello".freeze + -> { str.append_as_bytes("\xE2\x82") }.should raise_error(FrozenError) + end + + it "allows creating broken strings" do + str = +"hello" + str.append_as_bytes("\xE2\x82") + str.valid_encoding?.should == false + + str.append_as_bytes("\xAC") + str.valid_encoding?.should == true + + str = "abc".encode(Encoding::UTF_32LE) + str.append_as_bytes("def") + str.encoding.should == Encoding::UTF_32LE + str.valid_encoding?.should == false + end + + it "never changes the receiver encoding" do + str = "".b + str.append_as_bytes("€") + str.encoding.should == Encoding::BINARY + end + + it "accepts variadic String or Integer arguments" do + str = "hello".b + str.append_as_bytes("\xE2\x82", 12, 43, "\xAC") + str.encoding.should == Encoding::BINARY + str.should == "hello\xE2\x82\f+\xAC".b + end + + it "truncates integers to the least significant byte" do + str = +"" + str.append_as_bytes(0x131, 0x232, 0x333, bignum_value, bignum_value(1)) + str.bytes.should == [0x31, 0x32, 0x33, 0, 1] + end + + it "wraps negative integers" do + str = "".b + str.append_as_bytes(-1, -bignum_value, -bignum_value(1)) + str.bytes.should == [0xFF, 0, 0xFF] + end + + it "only accepts strings or integers, and doesn't attempt to cast with #to_str or #to_int" do + to_str = mock("to_str") + to_str.should_not_receive(:to_str) + to_str.should_not_receive(:to_int) + + str = +"hello" + -> { str.append_as_bytes(to_str) }.should raise_error(TypeError, "wrong argument type MockObject (expected String or Integer)") + end + end +end diff --git a/spec/ruby/core/string/append_spec.rb b/spec/ruby/core/string/append_spec.rb new file mode 100644 index 0000000000..8497ce8262 --- /dev/null +++ b/spec/ruby/core/string/append_spec.rb @@ -0,0 +1,14 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/concat' + +describe "String#<<" do + it_behaves_like :string_concat, :<< + it_behaves_like :string_concat_encoding, :<< + it_behaves_like :string_concat_type_coercion, :<< + + it "raises an ArgumentError when given the incorrect number of arguments" do + -> { "hello".send(:<<) }.should raise_error(ArgumentError) + -> { "hello".send(:<<, "one", "two") }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/string/ascii_only_spec.rb b/spec/ruby/core/string/ascii_only_spec.rb new file mode 100644 index 0000000000..88a0559cfd --- /dev/null +++ b/spec/ruby/core/string/ascii_only_spec.rb @@ -0,0 +1,82 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "String#ascii_only?" do + describe "with ASCII only characters" do + it "returns true if the encoding is UTF-8" do + [ ["hello", true], + ["hello".encode('UTF-8'), true], + ["hello".dup.force_encoding('UTF-8'), true], + ].should be_computed_by(:ascii_only?) + end + + it "returns true if the encoding is US-ASCII" do + "hello".dup.force_encoding(Encoding::US_ASCII).ascii_only?.should be_true + "hello".encode(Encoding::US_ASCII).ascii_only?.should be_true + end + + it "returns true for all single-character UTF-8 Strings" do + 0.upto(127) do |n| + n.chr.ascii_only?.should be_true + end + end + end + + describe "with non-ASCII only characters" do + it "returns false if the encoding is BINARY" do + chr = 128.chr + chr.encoding.should == Encoding::BINARY + chr.ascii_only?.should be_false + end + + it "returns false if the String contains any non-ASCII characters" do + [ ["\u{6666}", false], + ["hello, \u{6666}", false], + ["\u{6666}".encode('UTF-8'), false], + ["\u{6666}".dup.force_encoding('UTF-8'), false], + ].should be_computed_by(:ascii_only?) + end + + it "returns false if the encoding is US-ASCII" do + [ ["\u{6666}".dup.force_encoding(Encoding::US_ASCII), false], + ["hello, \u{6666}".dup.force_encoding(Encoding::US_ASCII), false], + ].should be_computed_by(:ascii_only?) + end + end + + it "returns true for the empty String with an ASCII-compatible encoding" do + "".ascii_only?.should be_true + "".encode('UTF-8').ascii_only?.should be_true + end + + it "returns false for the empty String with a non-ASCII-compatible encoding" do + "".dup.force_encoding('UTF-16LE').ascii_only?.should be_false + "".encode('UTF-16BE').ascii_only?.should be_false + end + + it "returns false for a non-empty String with non-ASCII-compatible encoding" do + "\x78\x00".dup.force_encoding("UTF-16LE").ascii_only?.should be_false + end + + it "returns false when interpolating non ascii strings" do + base = "EU currency is".dup.force_encoding(Encoding::US_ASCII) + euro = "\u20AC" + interp = "#{base} #{euro}" + euro.ascii_only?.should be_false + base.ascii_only?.should be_true + interp.ascii_only?.should be_false + end + + it "returns false after appending non ASCII characters to an empty String" do + ("".dup << "λ").ascii_only?.should be_false + end + + it "returns false when concatenating an ASCII and non-ASCII String" do + "".dup.concat("λ").ascii_only?.should be_false + end + + it "returns false when replacing an ASCII String with a non-ASCII String" do + "".dup.replace("λ").ascii_only?.should be_false + end +end diff --git a/spec/ruby/core/string/b_spec.rb b/spec/ruby/core/string/b_spec.rb new file mode 100644 index 0000000000..4b1fafff11 --- /dev/null +++ b/spec/ruby/core/string/b_spec.rb @@ -0,0 +1,16 @@ +# -*- encoding: utf-8 -*- +# frozen_string_literal: false +require_relative '../../spec_helper' + +describe "String#b" do + it "returns a binary encoded string" do + "Hello".b.should == "Hello".force_encoding(Encoding::BINARY) + "ã“ã‚“ã¡ã«ã¯".b.should == "ã“ã‚“ã¡ã«ã¯".force_encoding(Encoding::BINARY) + end + + it "returns new string without modifying self" do + str = "ã“ã‚“ã¡ã«ã¯" + str.b.should_not equal(str) + str.should == "ã“ã‚“ã¡ã«ã¯" + end +end diff --git a/spec/ruby/core/string/byteindex_spec.rb b/spec/ruby/core/string/byteindex_spec.rb new file mode 100644 index 0000000000..d420f3f683 --- /dev/null +++ b/spec/ruby/core/string/byteindex_spec.rb @@ -0,0 +1,298 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/byte_index_common.rb' + +describe "String#byteindex" do + it "calls #to_str to convert the first argument" do + char = mock("string index char") + char.should_receive(:to_str).and_return("b") + "abc".byteindex(char).should == 1 + end + + it "calls #to_int to convert the second argument" do + offset = mock("string index offset") + offset.should_receive(:to_int).and_return(1) + "abc".byteindex("c", offset).should == 2 + end + + it "does not raise IndexError when byte offset is correct or on string boundary" do + "ã‚".byteindex("").should == 0 + "ã‚".byteindex("", 0).should == 0 + "ã‚".byteindex("", 3).should == 3 + end + + it_behaves_like :byte_index_common, :byteindex +end + +describe "String#byteindex with String" do + it "behaves the same as String#byteindex(char) for one-character strings" do + "blablabla hello cruel world...!".split("").uniq.each do |str| + chr = str[0] + str.byteindex(str).should == str.byteindex(chr) + + 0.upto(str.size + 1) do |start| + str.byteindex(str, start).should == str.byteindex(chr, start) + end + + (-str.size - 1).upto(-1) do |start| + str.byteindex(str, start).should == str.byteindex(chr, start) + end + end + end + + it "returns the byteindex of the first occurrence of the given substring" do + "blablabla".byteindex("").should == 0 + "blablabla".byteindex("b").should == 0 + "blablabla".byteindex("bla").should == 0 + "blablabla".byteindex("blabla").should == 0 + "blablabla".byteindex("blablabla").should == 0 + + "blablabla".byteindex("l").should == 1 + "blablabla".byteindex("la").should == 1 + "blablabla".byteindex("labla").should == 1 + "blablabla".byteindex("lablabla").should == 1 + + "blablabla".byteindex("a").should == 2 + "blablabla".byteindex("abla").should == 2 + "blablabla".byteindex("ablabla").should == 2 + end + + it "treats the offset as a byteindex" do + "aaaaa".byteindex("a", 0).should == 0 + "aaaaa".byteindex("a", 2).should == 2 + "aaaaa".byteindex("a", 4).should == 4 + end + + it "ignores string subclasses" do + "blablabla".byteindex(StringSpecs::MyString.new("bla")).should == 0 + StringSpecs::MyString.new("blablabla").byteindex("bla").should == 0 + StringSpecs::MyString.new("blablabla").byteindex(StringSpecs::MyString.new("bla")).should == 0 + end + + it "starts the search at the given offset" do + "blablabla".byteindex("bl", 0).should == 0 + "blablabla".byteindex("bl", 1).should == 3 + "blablabla".byteindex("bl", 2).should == 3 + "blablabla".byteindex("bl", 3).should == 3 + + "blablabla".byteindex("bla", 0).should == 0 + "blablabla".byteindex("bla", 1).should == 3 + "blablabla".byteindex("bla", 2).should == 3 + "blablabla".byteindex("bla", 3).should == 3 + + "blablabla".byteindex("blab", 0).should == 0 + "blablabla".byteindex("blab", 1).should == 3 + "blablabla".byteindex("blab", 2).should == 3 + "blablabla".byteindex("blab", 3).should == 3 + + "blablabla".byteindex("la", 1).should == 1 + "blablabla".byteindex("la", 2).should == 4 + "blablabla".byteindex("la", 3).should == 4 + "blablabla".byteindex("la", 4).should == 4 + + "blablabla".byteindex("lab", 1).should == 1 + "blablabla".byteindex("lab", 2).should == 4 + "blablabla".byteindex("lab", 3).should == 4 + "blablabla".byteindex("lab", 4).should == 4 + + "blablabla".byteindex("ab", 2).should == 2 + "blablabla".byteindex("ab", 3).should == 5 + "blablabla".byteindex("ab", 4).should == 5 + "blablabla".byteindex("ab", 5).should == 5 + + "blablabla".byteindex("", 0).should == 0 + "blablabla".byteindex("", 1).should == 1 + "blablabla".byteindex("", 2).should == 2 + "blablabla".byteindex("", 7).should == 7 + "blablabla".byteindex("", 8).should == 8 + "blablabla".byteindex("", 9).should == 9 + end + + it "starts the search at offset + self.length if offset is negative" do + str = "blablabla" + + ["bl", "bla", "blab", "la", "lab", "ab", ""].each do |needle| + (-str.length .. -1).each do |offset| + str.byteindex(needle, offset).should == + str.byteindex(needle, offset + str.length) + end + end + end + + it "returns nil if the substring isn't found" do + "blablabla".byteindex("B").should == nil + "blablabla".byteindex("z").should == nil + "blablabla".byteindex("BLA").should == nil + "blablabla".byteindex("blablablabla").should == nil + "blablabla".byteindex("", 10).should == nil + + "hello".byteindex("he", 1).should == nil + "hello".byteindex("he", 2).should == nil + "I’ve got a multibyte character.\n".byteindex("\n\n").should == nil + end + + it "returns the character byteindex of a multibyte character" do + "ã‚りãŒã¨ã†".byteindex("ãŒ").should == 6 + end + + it "returns the character byteindex after offset" do + "ã‚れã‚れ".byteindex("ã‚", 3).should == 6 + "ã‚りãŒã¨ã†ã‚りãŒã¨ã†".byteindex("ãŒ", 9).should == 21 + end + + it "returns the character byteindex after a partial first match" do + "</</h".byteindex("</h").should == 2 + end + + it "raises an Encoding::CompatibilityError if the encodings are incompatible" do + char = "れ".encode Encoding::EUC_JP + -> do + "ã‚れ".byteindex(char) + end.should raise_error(Encoding::CompatibilityError) + end + + it "handles a substring in a superset encoding" do + 'abc'.dup.force_encoding(Encoding::US_ASCII).byteindex('é').should == nil + end + + it "handles a substring in a subset encoding" do + 'été'.byteindex('t'.dup.force_encoding(Encoding::US_ASCII)).should == 2 + end +end + +describe "String#byteindex with Regexp" do + it "behaves the same as String#byteindex(string) for escaped string regexps" do + ["blablabla", "hello cruel world...!"].each do |str| + ["", "b", "bla", "lab", "o c", "d."].each do |needle| + regexp = Regexp.new(Regexp.escape(needle)) + str.byteindex(regexp).should == str.byteindex(needle) + + 0.upto(str.size + 1) do |start| + str.byteindex(regexp, start).should == str.byteindex(needle, start) + end + + (-str.size - 1).upto(-1) do |start| + str.byteindex(regexp, start).should == str.byteindex(needle, start) + end + end + end + end + + it "returns the byteindex of the first match of regexp" do + "blablabla".byteindex(/bla/).should == 0 + "blablabla".byteindex(/BLA/i).should == 0 + + "blablabla".byteindex(/.{0}/).should == 0 + "blablabla".byteindex(/.{6}/).should == 0 + "blablabla".byteindex(/.{9}/).should == 0 + + "blablabla".byteindex(/.*/).should == 0 + "blablabla".byteindex(/.+/).should == 0 + + "blablabla".byteindex(/lab|b/).should == 0 + + not_supported_on :opal do + "blablabla".byteindex(/\A/).should == 0 + "blablabla".byteindex(/\Z/).should == 9 + "blablabla".byteindex(/\z/).should == 9 + "blablabla\n".byteindex(/\Z/).should == 9 + "blablabla\n".byteindex(/\z/).should == 10 + end + + "blablabla".byteindex(/^/).should == 0 + "\nblablabla".byteindex(/^/).should == 0 + "b\nablabla".byteindex(/$/).should == 1 + "bl\nablabla".byteindex(/$/).should == 2 + + "blablabla".byteindex(/.l./).should == 0 + end + + it "starts the search at the given offset" do + "blablabla".byteindex(/.{0}/, 5).should == 5 + "blablabla".byteindex(/.{1}/, 5).should == 5 + "blablabla".byteindex(/.{2}/, 5).should == 5 + "blablabla".byteindex(/.{3}/, 5).should == 5 + "blablabla".byteindex(/.{4}/, 5).should == 5 + + "blablabla".byteindex(/.{0}/, 3).should == 3 + "blablabla".byteindex(/.{1}/, 3).should == 3 + "blablabla".byteindex(/.{2}/, 3).should == 3 + "blablabla".byteindex(/.{5}/, 3).should == 3 + "blablabla".byteindex(/.{6}/, 3).should == 3 + + "blablabla".byteindex(/.l./, 0).should == 0 + "blablabla".byteindex(/.l./, 1).should == 3 + "blablabla".byteindex(/.l./, 2).should == 3 + "blablabla".byteindex(/.l./, 3).should == 3 + + "xblaxbla".byteindex(/x./, 0).should == 0 + "xblaxbla".byteindex(/x./, 1).should == 4 + "xblaxbla".byteindex(/x./, 2).should == 4 + + not_supported_on :opal do + "blablabla\n".byteindex(/\Z/, 9).should == 9 + end + end + + it "starts the search at offset + self.length if offset is negative" do + str = "blablabla" + + ["bl", "bla", "blab", "la", "lab", "ab", ""].each do |needle| + (-str.length .. -1).each do |offset| + str.byteindex(needle, offset).should == + str.byteindex(needle, offset + str.length) + end + end + end + + it "returns nil if the substring isn't found" do + "blablabla".byteindex(/BLA/).should == nil + + "blablabla".byteindex(/.{10}/).should == nil + "blaxbla".byteindex(/.x/, 3).should == nil + "blaxbla".byteindex(/..x/, 2).should == nil + end + + it "returns nil if the Regexp matches the empty string and the offset is out of range" do + "ruby".byteindex(//, 12).should be_nil + end + + it "supports \\G which matches at the given start offset" do + "helloYOU.".byteindex(/\GYOU/, 5).should == 5 + "helloYOU.".byteindex(/\GYOU/).should == nil + + re = /\G.+YOU/ + # The # marks where \G will match. + [ + ["#hi!YOUall.", 0], + ["h#i!YOUall.", 1], + ["hi#!YOUall.", 2], + ["hi!#YOUall.", nil] + ].each do |spec| + + start = spec[0].byteindex("#") + str = spec[0].delete("#") + + str.byteindex(re, start).should == spec[1] + end + end + + it "converts start_offset to an integer via to_int" do + obj = mock('1') + obj.should_receive(:to_int).and_return(1) + "RWOARW".byteindex(/R./, obj).should == 4 + end + + it "returns the character byteindex of a multibyte character" do + "ã‚りãŒã¨ã†".byteindex(/ãŒ/).should == 6 + end + + it "returns the character byteindex after offset" do + "ã‚れã‚れ".byteindex(/ã‚/, 3).should == 6 + end + + it "treats the offset as a byteindex" do + "ã‚れã‚ã‚れ".byteindex(/ã‚/, 6).should == 6 + end +end diff --git a/spec/ruby/core/string/byterindex_spec.rb b/spec/ruby/core/string/byterindex_spec.rb new file mode 100644 index 0000000000..983222e35d --- /dev/null +++ b/spec/ruby/core/string/byterindex_spec.rb @@ -0,0 +1,353 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/byte_index_common.rb' + +describe "String#byterindex with object" do + it "tries to convert obj to a string via to_str" do + obj = mock('lo') + def obj.to_str() "lo" end + "hello".byterindex(obj).should == "hello".byterindex("lo") + + obj = mock('o') + def obj.respond_to?(arg, *) true end + def obj.method_missing(*args) "o" end + "hello".byterindex(obj).should == "hello".byterindex("o") + end + + it "calls #to_int to convert the second argument" do + offset = mock("string index offset") + offset.should_receive(:to_int).and_return(3) + "abc".byterindex("c", offset).should == 2 + end + + it "does not raise IndexError when byte offset is correct or on string boundary" do + "ã‚".byterindex("", 0).should == 0 + "ã‚".byterindex("", 3).should == 3 + "ã‚".byterindex("").should == 3 + end + + it_behaves_like :byte_index_common, :byterindex +end + +describe "String#byterindex with String" do + it "behaves the same as String#byterindex(char) for one-character strings" do + "blablabla hello cruel world...!".split("").uniq.each do |str| + chr = str[0] + str.byterindex(str).should == str.byterindex(chr) + + 0.upto(str.size + 1) do |start| + str.byterindex(str, start).should == str.byterindex(chr, start) + end + + (-str.size - 1).upto(-1) do |start| + str.byterindex(str, start).should == str.byterindex(chr, start) + end + end + end + + it "behaves the same as String#byterindex(?char) for one-character strings" do + "blablabla hello cruel world...!".split("").uniq.each do |str| + chr = str[0] =~ / / ? str[0] : eval("?#{str[0]}") + str.byterindex(str).should == str.byterindex(chr) + + 0.upto(str.size + 1) do |start| + str.byterindex(str, start).should == str.byterindex(chr, start) + end + + (-str.size - 1).upto(-1) do |start| + str.byterindex(str, start).should == str.byterindex(chr, start) + end + end + end + + it "returns the index of the last occurrence of the given substring" do + "blablabla".byterindex("").should == 9 + "blablabla".byterindex("a").should == 8 + "blablabla".byterindex("la").should == 7 + "blablabla".byterindex("bla").should == 6 + "blablabla".byterindex("abla").should == 5 + "blablabla".byterindex("labla").should == 4 + "blablabla".byterindex("blabla").should == 3 + "blablabla".byterindex("ablabla").should == 2 + "blablabla".byterindex("lablabla").should == 1 + "blablabla".byterindex("blablabla").should == 0 + + "blablabla".byterindex("l").should == 7 + "blablabla".byterindex("bl").should == 6 + "blablabla".byterindex("abl").should == 5 + "blablabla".byterindex("labl").should == 4 + "blablabla".byterindex("blabl").should == 3 + "blablabla".byterindex("ablabl").should == 2 + "blablabla".byterindex("lablabl").should == 1 + "blablabla".byterindex("blablabl").should == 0 + + "blablabla".byterindex("b").should == 6 + "blablabla".byterindex("ab").should == 5 + "blablabla".byterindex("lab").should == 4 + "blablabla".byterindex("blab").should == 3 + "blablabla".byterindex("ablab").should == 2 + "blablabla".byterindex("lablab").should == 1 + "blablabla".byterindex("blablab").should == 0 + end + + it "ignores string subclasses" do + "blablabla".byterindex(StringSpecs::MyString.new("bla")).should == 6 + StringSpecs::MyString.new("blablabla").byterindex("bla").should == 6 + StringSpecs::MyString.new("blablabla").byterindex(StringSpecs::MyString.new("bla")).should == 6 + end + + it "starts the search at the given offset" do + "blablabla".byterindex("bl", 0).should == 0 + "blablabla".byterindex("bl", 1).should == 0 + "blablabla".byterindex("bl", 2).should == 0 + "blablabla".byterindex("bl", 3).should == 3 + + "blablabla".byterindex("bla", 0).should == 0 + "blablabla".byterindex("bla", 1).should == 0 + "blablabla".byterindex("bla", 2).should == 0 + "blablabla".byterindex("bla", 3).should == 3 + + "blablabla".byterindex("blab", 0).should == 0 + "blablabla".byterindex("blab", 1).should == 0 + "blablabla".byterindex("blab", 2).should == 0 + "blablabla".byterindex("blab", 3).should == 3 + "blablabla".byterindex("blab", 6).should == 3 + "blablablax".byterindex("blab", 6).should == 3 + + "blablabla".byterindex("la", 1).should == 1 + "blablabla".byterindex("la", 2).should == 1 + "blablabla".byterindex("la", 3).should == 1 + "blablabla".byterindex("la", 4).should == 4 + + "blablabla".byterindex("lab", 1).should == 1 + "blablabla".byterindex("lab", 2).should == 1 + "blablabla".byterindex("lab", 3).should == 1 + "blablabla".byterindex("lab", 4).should == 4 + + "blablabla".byterindex("ab", 2).should == 2 + "blablabla".byterindex("ab", 3).should == 2 + "blablabla".byterindex("ab", 4).should == 2 + "blablabla".byterindex("ab", 5).should == 5 + + "blablabla".byterindex("", 0).should == 0 + "blablabla".byterindex("", 1).should == 1 + "blablabla".byterindex("", 2).should == 2 + "blablabla".byterindex("", 7).should == 7 + "blablabla".byterindex("", 8).should == 8 + "blablabla".byterindex("", 9).should == 9 + "blablabla".byterindex("", 10).should == 9 + end + + it "starts the search at offset + self.length if offset is negative" do + str = "blablabla" + + ["bl", "bla", "blab", "la", "lab", "ab", ""].each do |needle| + (-str.length .. -1).each do |offset| + str.byterindex(needle, offset).should == + str.byterindex(needle, offset + str.length) + end + end + end + + it "returns nil if the substring isn't found" do + "blablabla".byterindex("B").should == nil + "blablabla".byterindex("z").should == nil + "blablabla".byterindex("BLA").should == nil + "blablabla".byterindex("blablablabla").should == nil + + "hello".byterindex("lo", 0).should == nil + "hello".byterindex("lo", 1).should == nil + "hello".byterindex("lo", 2).should == nil + + "hello".byterindex("llo", 0).should == nil + "hello".byterindex("llo", 1).should == nil + + "hello".byterindex("el", 0).should == nil + "hello".byterindex("ello", 0).should == nil + + "hello".byterindex("", -6).should == nil + "hello".byterindex("", -7).should == nil + + "hello".byterindex("h", -6).should == nil + end + + it "tries to convert start_offset to an integer via to_int" do + obj = mock('5') + def obj.to_int() 5 end + "str".byterindex("st", obj).should == 0 + + obj = mock('5') + def obj.respond_to?(arg, *) true end + def obj.method_missing(*args) 5 end + "str".byterindex("st", obj).should == 0 + end + + it "raises a TypeError when given offset is nil" do + -> { "str".byterindex("st", nil) }.should raise_error(TypeError) + end + + it "handles a substring in a superset encoding" do + 'abc'.dup.force_encoding(Encoding::US_ASCII).byterindex('é').should == nil + end + + it "handles a substring in a subset encoding" do + 'été'.byterindex('t'.dup.force_encoding(Encoding::US_ASCII)).should == 2 + end +end + +describe "String#byterindex with Regexp" do + it "behaves the same as String#byterindex(string) for escaped string regexps" do + ["blablabla", "hello cruel world...!"].each do |str| + ["", "b", "bla", "lab", "o c", "d."].each do |needle| + regexp = Regexp.new(Regexp.escape(needle)) + str.byterindex(regexp).should == str.byterindex(needle) + + 0.upto(str.size + 1) do |start| + str.byterindex(regexp, start).should == str.byterindex(needle, start) + end + + (-str.size - 1).upto(-1) do |start| + str.byterindex(regexp, start).should == str.byterindex(needle, start) + end + end + end + end + + it "returns the index of the first match from the end of string of regexp" do + "blablabla".byterindex(/bla/).should == 6 + "blablabla".byterindex(/BLA/i).should == 6 + + "blablabla".byterindex(/.{0}/).should == 9 + "blablabla".byterindex(/.{1}/).should == 8 + "blablabla".byterindex(/.{2}/).should == 7 + "blablabla".byterindex(/.{6}/).should == 3 + "blablabla".byterindex(/.{9}/).should == 0 + + "blablabla".byterindex(/.*/).should == 9 + "blablabla".byterindex(/.+/).should == 8 + + "blablabla".byterindex(/bla|a/).should == 8 + + not_supported_on :opal do + "blablabla".byterindex(/\A/).should == 0 + "blablabla".byterindex(/\Z/).should == 9 + "blablabla".byterindex(/\z/).should == 9 + "blablabla\n".byterindex(/\Z/).should == 10 + "blablabla\n".byterindex(/\z/).should == 10 + end + + "blablabla".byterindex(/^/).should == 0 + not_supported_on :opal do + "\nblablabla".byterindex(/^/).should == 1 + "b\nlablabla".byterindex(/^/).should == 2 + end + "blablabla".byterindex(/$/).should == 9 + + "blablabla".byterindex(/.l./).should == 6 + end + + it "starts the search at the given offset" do + "blablabla".byterindex(/.{0}/, 5).should == 5 + "blablabla".byterindex(/.{1}/, 5).should == 5 + "blablabla".byterindex(/.{2}/, 5).should == 5 + "blablabla".byterindex(/.{3}/, 5).should == 5 + "blablabla".byterindex(/.{4}/, 5).should == 5 + + "blablabla".byterindex(/.{0}/, 3).should == 3 + "blablabla".byterindex(/.{1}/, 3).should == 3 + "blablabla".byterindex(/.{2}/, 3).should == 3 + "blablabla".byterindex(/.{5}/, 3).should == 3 + "blablabla".byterindex(/.{6}/, 3).should == 3 + + "blablabla".byterindex(/.l./, 0).should == 0 + "blablabla".byterindex(/.l./, 1).should == 0 + "blablabla".byterindex(/.l./, 2).should == 0 + "blablabla".byterindex(/.l./, 3).should == 3 + + "blablablax".byterindex(/.x/, 10).should == 8 + "blablablax".byterindex(/.x/, 9).should == 8 + "blablablax".byterindex(/.x/, 8).should == 8 + + "blablablax".byterindex(/..x/, 10).should == 7 + "blablablax".byterindex(/..x/, 9).should == 7 + "blablablax".byterindex(/..x/, 8).should == 7 + "blablablax".byterindex(/..x/, 7).should == 7 + + not_supported_on :opal do + "blablabla\n".byterindex(/\Z/, 9).should == 9 + end + end + + it "starts the search at offset + self.length if offset is negative" do + str = "blablabla" + + ["bl", "bla", "blab", "la", "lab", "ab", ""].each do |needle| + (-str.length .. -1).each do |offset| + str.byterindex(needle, offset).should == + str.byterindex(needle, offset + str.length) + end + end + end + + it "returns nil if the substring isn't found" do + "blablabla".byterindex(/BLA/).should == nil + "blablabla".byterindex(/.{10}/).should == nil + "blablablax".byterindex(/.x/, 7).should == nil + "blablablax".byterindex(/..x/, 6).should == nil + + not_supported_on :opal do + "blablabla".byterindex(/\Z/, 5).should == nil + "blablabla".byterindex(/\z/, 5).should == nil + "blablabla\n".byterindex(/\z/, 9).should == nil + end + end + + not_supported_on :opal do + it "supports \\G which matches at the given start offset" do + "helloYOU.".byterindex(/YOU\G/, 8).should == 5 + "helloYOU.".byterindex(/YOU\G/).should == nil + + idx = "helloYOUall!".index("YOU") + re = /YOU.+\G.+/ + # The # marks where \G will match. + [ + ["helloYOU#all.", nil], + ["helloYOUa#ll.", idx], + ["helloYOUal#l.", idx], + ["helloYOUall#.", idx], + ["helloYOUall.#", nil] + ].each do |i| + start = i[0].index("#") + str = i[0].delete("#") + + str.byterindex(re, start).should == i[1] + end + end + end + + it "tries to convert start_offset to an integer" do + obj = mock('5') + def obj.to_int() 5 end + "str".byterindex(/../, obj).should == 1 + + obj = mock('5') + def obj.respond_to?(arg, *) true end + def obj.method_missing(*args); 5; end + "str".byterindex(/../, obj).should == 1 + end + + it "raises a TypeError when given offset is nil" do + -> { "str".byterindex(/../, nil) }.should raise_error(TypeError) + end + + it "returns the reverse byte index of a multibyte character" do + "ã‚りãŒã‚ŠãŒã¨ã†".byterindex("ãŒ").should == 12 + "ã‚りãŒã‚ŠãŒã¨ã†".byterindex(/ãŒ/).should == 12 + end + + it "returns the character index before the finish" do + "ã‚りãŒã‚ŠãŒã¨ã†".byterindex("ãŒ", 9).should == 6 + "ã‚りãŒã‚ŠãŒã¨ã†".byterindex(/ãŒ/, 9).should == 6 + end +end diff --git a/spec/ruby/core/string/bytes_spec.rb b/spec/ruby/core/string/bytes_spec.rb new file mode 100644 index 0000000000..02151eebbc --- /dev/null +++ b/spec/ruby/core/string/bytes_spec.rb @@ -0,0 +1,55 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' + +describe "String#bytes" do + before :each do + @utf8 = "æ±äº¬" + @ascii = 'Tokyo' + @utf8_ascii = @utf8 + @ascii + end + + it "returns an Array when no block is given" do + @utf8.bytes.should be_an_instance_of(Array) + end + + it "yields each byte to a block if one is given, returning self" do + bytes = [] + @utf8.bytes {|b| bytes << b}.should == @utf8 + bytes.should == @utf8.bytes.to_a + end + + it "returns #bytesize bytes" do + @utf8_ascii.bytes.to_a.size.should == @utf8_ascii.bytesize + end + + it "returns bytes as Integers" do + @ascii.bytes.to_a.each {|b| b.should be_an_instance_of(Integer)} + @utf8_ascii.bytes { |b| b.should be_an_instance_of(Integer) } + end + + it "agrees with #unpack('C*')" do + @utf8_ascii.bytes.to_a.should == @utf8_ascii.unpack("C*") + end + + it "yields/returns no bytes for the empty string" do + ''.bytes.to_a.should == [] + end +end + +describe "String#bytes" do + before :each do + @utf8 = "æ±äº¬" + @ascii = 'Tokyo' + @utf8_ascii = @utf8 + @ascii + end + + it "agrees with #getbyte" do + @utf8_ascii.bytes.to_a.each_with_index do |byte,index| + byte.should == @utf8_ascii.getbyte(index) + end + end + + it "is unaffected by #force_encoding" do + @utf8.dup.force_encoding('ASCII').bytes.to_a.should == @utf8.bytes.to_a + end +end diff --git a/spec/ruby/core/string/bytesize_spec.rb b/spec/ruby/core/string/bytesize_spec.rb new file mode 100644 index 0000000000..2bbefc0820 --- /dev/null +++ b/spec/ruby/core/string/bytesize_spec.rb @@ -0,0 +1,33 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "String#bytesize" do + it "returns the length of self in bytes" do + "hello".bytesize.should == 5 + " ".bytesize.should == 1 + end + + it "works with strings containing single UTF-8 characters" do + "\u{6666}".bytesize.should == 3 + end + + it "works with pseudo-ASCII strings containing single UTF-8 characters" do + "\u{6666}".dup.force_encoding('ASCII').bytesize.should == 3 + end + + it "works with strings containing UTF-8 characters" do + "c \u{6666}".dup.force_encoding('UTF-8').bytesize.should == 5 + "c \u{6666}".bytesize.should == 5 + end + + it "works with pseudo-ASCII strings containing UTF-8 characters" do + "c \u{6666}".dup.force_encoding('ASCII').bytesize.should == 5 + end + + it "returns 0 for the empty string" do + "".bytesize.should == 0 + "".dup.force_encoding('ASCII').bytesize.should == 0 + "".dup.force_encoding('UTF-8').bytesize.should == 0 + end +end diff --git a/spec/ruby/core/string/byteslice_spec.rb b/spec/ruby/core/string/byteslice_spec.rb new file mode 100644 index 0000000000..4ad9e8d8f1 --- /dev/null +++ b/spec/ruby/core/string/byteslice_spec.rb @@ -0,0 +1,33 @@ +# encoding: binary +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/slice' + +describe "String#byteslice" do + it "needs to reviewed for spec completeness" + + it_behaves_like :string_slice, :byteslice +end + +describe "String#byteslice with index, length" do + it_behaves_like :string_slice_index_length, :byteslice +end + +describe "String#byteslice with Range" do + it_behaves_like :string_slice_range, :byteslice +end + +describe "String#byteslice on non ASCII strings" do + it "returns byteslice of unicode strings" do + "\u3042".byteslice(1).should == "\x81".dup.force_encoding("UTF-8") + "\u3042".byteslice(1, 2).should == "\x81\x82".dup.force_encoding("UTF-8") + "\u3042".byteslice(1..2).should == "\x81\x82".dup.force_encoding("UTF-8") + "\u3042".byteslice(-1).should == "\x82".dup.force_encoding("UTF-8") + end + + it "returns a String in the same encoding as self" do + "ruby".encode("UTF-8").slice(0).encoding.should == Encoding::UTF_8 + "ruby".encode("US-ASCII").slice(0).encoding.should == Encoding::US_ASCII + "ruby".encode("Windows-1251").slice(0).encoding.should == Encoding::Windows_1251 + end +end diff --git a/spec/ruby/core/string/bytesplice_spec.rb b/spec/ruby/core/string/bytesplice_spec.rb new file mode 100644 index 0000000000..2c770e340a --- /dev/null +++ b/spec/ruby/core/string/bytesplice_spec.rb @@ -0,0 +1,294 @@ +# -*- encoding: utf-8 -*- +# frozen_string_literal: false +require_relative '../../spec_helper' + +describe "String#bytesplice" do + it "raises IndexError when index is less than -bytesize" do + -> { "hello".bytesplice(-6, 0, "xxx") }.should raise_error(IndexError, "index -6 out of string") + end + + it "raises IndexError when index is greater than bytesize" do + -> { "hello".bytesplice(6, 0, "xxx") }.should raise_error(IndexError, "index 6 out of string") + end + + it "raises IndexError for negative length" do + -> { "abc".bytesplice(0, -2, "") }.should raise_error(IndexError, "negative length -2") + end + + it "replaces with integer indices" do + "hello".bytesplice(-5, 0, "xxx").should == "xxxhello" + "hello".bytesplice(0, 0, "xxx").should == "xxxhello" + "hello".bytesplice(0, 1, "xxx").should == "xxxello" + "hello".bytesplice(0, 5, "xxx").should == "xxx" + "hello".bytesplice(0, 6, "xxx").should == "xxx" + end + + it "raises RangeError when range left boundary is less than -bytesize" do + -> { "hello".bytesplice(-6...-6, "xxx") }.should raise_error(RangeError, "-6...-6 out of range") + end + + it "replaces with ranges" do + "hello".bytesplice(-5...-5, "xxx").should == "xxxhello" + "hello".bytesplice(0...0, "xxx").should == "xxxhello" + "hello".bytesplice(0..0, "xxx").should == "xxxello" + "hello".bytesplice(0...1, "xxx").should == "xxxello" + "hello".bytesplice(0..1, "xxx").should == "xxxllo" + "hello".bytesplice(0..-1, "xxx").should == "xxx" + "hello".bytesplice(0...5, "xxx").should == "xxx" + "hello".bytesplice(0...6, "xxx").should == "xxx" + end + + it "raises TypeError when integer index is provided without length argument" do + -> { "hello".bytesplice(0, "xxx") }.should raise_error(TypeError, "wrong argument type Integer (expected Range)") + end + + it "replaces on an empty string" do + "".bytesplice(0, 0, "").should == "" + "".bytesplice(0, 0, "xxx").should == "xxx" + end + + it "mutates self" do + s = "hello" + s.bytesplice(2, 1, "xxx").should.equal?(s) + end + + it "raises when string is frozen" do + s = "hello".freeze + -> { s.bytesplice(2, 1, "xxx") }.should raise_error(FrozenError, "can't modify frozen String: \"hello\"") + end + + ruby_version_is "3.3" do + it "raises IndexError when str_index is less than -bytesize" do + -> { "hello".bytesplice(2, 1, "HELLO", -6, 0) }.should raise_error(IndexError, "index -6 out of string") + end + + it "raises IndexError when str_index is greater than bytesize" do + -> { "hello".bytesplice(2, 1, "HELLO", 6, 0) }.should raise_error(IndexError, "index 6 out of string") + end + + it "raises IndexError for negative str length" do + -> { "abc".bytesplice(0, 1, "", 0, -2) }.should raise_error(IndexError, "negative length -2") + end + + it "replaces with integer str indices" do + "hello".bytesplice(1, 2, "HELLO", -5, 0).should == "hlo" + "hello".bytesplice(1, 2, "HELLO", 0, 0).should == "hlo" + "hello".bytesplice(1, 2, "HELLO", 0, 1).should == "hHlo" + "hello".bytesplice(1, 2, "HELLO", 0, 5).should == "hHELLOlo" + "hello".bytesplice(1, 2, "HELLO", 0, 6).should == "hHELLOlo" + end + + it "raises RangeError when str range left boundary is less than -bytesize" do + -> { "hello".bytesplice(0..1, "HELLO", -6...-6) }.should raise_error(RangeError, "-6...-6 out of range") + end + + it "replaces with str ranges" do + "hello".bytesplice(1..2, "HELLO", -5...-5).should == "hlo" + "hello".bytesplice(1..2, "HELLO", 0...0).should == "hlo" + "hello".bytesplice(1..2, "HELLO", 0..0).should == "hHlo" + "hello".bytesplice(1..2, "HELLO", 0...1).should == "hHlo" + "hello".bytesplice(1..2, "HELLO", 0..1).should == "hHElo" + "hello".bytesplice(1..2, "HELLO", 0..-1).should == "hHELLOlo" + "hello".bytesplice(1..2, "HELLO", 0...5).should == "hHELLOlo" + "hello".bytesplice(1..2, "HELLO", 0...6).should == "hHELLOlo" + end + + it "raises ArgumentError when integer str index is provided without str length argument" do + -> { "hello".bytesplice(0, 1, "xxx", 0) }.should raise_error(ArgumentError, "wrong number of arguments (given 4, expected 2, 3, or 5)") + end + + it "replaces on an empty string with str index/length" do + "".bytesplice(0, 0, "", 0, 0).should == "" + "".bytesplice(0, 0, "xxx", 0, 1).should == "x" + end + + it "mutates self with substring and str index/length" do + s = "hello" + s.bytesplice(2, 1, "xxx", 1, 2).should.equal?(s) + s.should.eql?("hexxlo") + end + + it "raises when string is frozen and str index/length" do + s = "hello".freeze + -> { s.bytesplice(2, 1, "xxx", 0, 1) }.should raise_error(FrozenError, "can't modify frozen String: \"hello\"") + end + + it "replaces on an empty string with str range" do + "".bytesplice(0..0, "", 0..0).should == "" + "".bytesplice(0..0, "xyz", 0..1).should == "xy" + end + + it "mutates self with substring and str range" do + s = "hello" + s.bytesplice(2..2, "xyz", 1..2).should.equal?(s) + s.should.eql?("heyzlo") + end + + it "raises when string is frozen and str range" do + s = "hello".freeze + -> { s.bytesplice(2..2, "yzx", 0..1) }.should raise_error(FrozenError, "can't modify frozen String: \"hello\"") + end + end +end + +describe "String#bytesplice with multibyte characters" do + it "raises IndexError when index is out of byte size boundary" do + -> { "ã“ã‚“ã«ã¡ã¯".bytesplice(-16, 0, "xxx") }.should raise_error(IndexError, "index -16 out of string") + end + + it "raises IndexError when index is not on a codepoint boundary" do + -> { "ã“ã‚“ã«ã¡ã¯".bytesplice(1, 0, "xxx") }.should raise_error(IndexError, "offset 1 does not land on character boundary") + end + + it "raises IndexError when length is not matching the codepoint boundary" do + -> { "ã“ã‚“ã«ã¡ã¯".bytesplice(0, 1, "xxx") }.should raise_error(IndexError, "offset 1 does not land on character boundary") + -> { "ã“ã‚“ã«ã¡ã¯".bytesplice(0, 2, "xxx") }.should raise_error(IndexError, "offset 2 does not land on character boundary") + end + + it "replaces with integer indices" do + "ã“ã‚“ã«ã¡ã¯".bytesplice(-15, 0, "xxx").should == "xxxã“ã‚“ã«ã¡ã¯" + "ã“ã‚“ã«ã¡ã¯".bytesplice(0, 0, "xxx").should == "xxxã“ã‚“ã«ã¡ã¯" + "ã“ã‚“ã«ã¡ã¯".bytesplice(0, 3, "xxx").should == "xxxã‚“ã«ã¡ã¯" + "ã“ã‚“ã«ã¡ã¯".bytesplice(3, 3, "ã¯ã¯").should == "ã“ã¯ã¯ã«ã¡ã¯" + "ã“ã‚“ã«ã¡ã¯".bytesplice(15, 0, "xxx").should == "ã“ã‚“ã«ã¡ã¯xxx" + end + + it "replaces with range" do + "ã“ã‚“ã«ã¡ã¯".bytesplice(-15...-16, "xxx").should == "xxxã“ã‚“ã«ã¡ã¯" + "ã“ã‚“ã«ã¡ã¯".bytesplice(0...0, "xxx").should == "xxxã“ã‚“ã«ã¡ã¯" + "ã“ã‚“ã«ã¡ã¯".bytesplice(0..2, "xxx").should == "xxxã‚“ã«ã¡ã¯" + "ã“ã‚“ã«ã¡ã¯".bytesplice(0...3, "xxx").should == "xxxã‚“ã«ã¡ã¯" + "ã“ã‚“ã«ã¡ã¯".bytesplice(0..5, "xxx").should == "xxxã«ã¡ã¯" + "ã“ã‚“ã«ã¡ã¯".bytesplice(0..-1, "xxx").should == "xxx" + "ã“ã‚“ã«ã¡ã¯".bytesplice(0...15, "xxx").should == "xxx" + "ã“ã‚“ã«ã¡ã¯".bytesplice(0...18, "xxx").should == "xxx" + end + + it "treats negative length for range as 0" do + "ã“ã‚“ã«ã¡ã¯".bytesplice(0...-100, "xxx").should == "xxxã“ã‚“ã«ã¡ã¯" + "ã“ã‚“ã«ã¡ã¯".bytesplice(3...-100, "xxx").should == "ã“xxxã‚“ã«ã¡ã¯" + "ã“ã‚“ã«ã¡ã¯".bytesplice(-15...-100, "xxx").should == "xxxã“ã‚“ã«ã¡ã¯" + end + + it "raises when ranges not match codepoint boundaries" do + -> { "ã“ã‚“ã«ã¡ã¯".bytesplice(0..0, "x") }.should raise_error(IndexError, "offset 1 does not land on character boundary") + -> { "ã“ã‚“ã«ã¡ã¯".bytesplice(0..1, "x") }.should raise_error(IndexError, "offset 2 does not land on character boundary") + # Begin is incorrect + -> { "ã“ã‚“ã«ã¡ã¯".bytesplice(-4..-1, "x") }.should raise_error(IndexError, "offset 11 does not land on character boundary") + -> { "ã“ã‚“ã«ã¡ã¯".bytesplice(-5..-1, "x") }.should raise_error(IndexError, "offset 10 does not land on character boundary") + # End is incorrect + -> { "ã“ã‚“ã«ã¡ã¯".bytesplice(-3..-2, "x") }.should raise_error(IndexError, "offset 14 does not land on character boundary") + -> { "ã“ã‚“ã«ã¡ã¯".bytesplice(-3..-3, "x") }.should raise_error(IndexError, "offset 13 does not land on character boundary") + end + + it "deals with a different encoded argument" do + s = "ã“ã‚“ã«ã¡ã¯" + s.encoding.should == Encoding::UTF_8 + sub = "xxxxxx" + sub.force_encoding(Encoding::US_ASCII) + + result = s.bytesplice(0, 3, sub) + result.should == "xxxxxxã‚“ã«ã¡ã¯" + result.encoding.should == Encoding::UTF_8 + + s = "xxxxxx" + s.force_encoding(Encoding::US_ASCII) + sub = "ã“ã‚“ã«ã¡ã¯" + sub.encoding.should == Encoding::UTF_8 + + result = s.bytesplice(0, 3, sub) + result.should == "ã“ã‚“ã«ã¡ã¯xxx" + result.encoding.should == Encoding::UTF_8 + end + + ruby_version_is "3.3" do + it "raises IndexError when str_index is out of byte size boundary" do + -> { "ã“ã‚“ã«ã¡ã¯".bytesplice(3, 3, "ã“ã‚“ã«ã¡ã¯", -16, 0) }.should raise_error(IndexError, "index -16 out of string") + end + + it "raises IndexError when str_index is not on a codepoint boundary" do + -> { "ã“ã‚“ã«ã¡ã¯".bytesplice(3, 3, "ã“ã‚“ã«ã¡ã¯", 1, 0) }.should raise_error(IndexError, "offset 1 does not land on character boundary") + end + + it "raises IndexError when str_length is not matching the codepoint boundary" do + -> { "ã“ã‚“ã«ã¡ã¯".bytesplice(3, 3, "ã“ã‚“ã«ã¡ã¯", 0, 1) }.should raise_error(IndexError, "offset 1 does not land on character boundary") + -> { "ã“ã‚“ã«ã¡ã¯".bytesplice(3, 3, "ã“ã‚“ã«ã¡ã¯", 0, 2) }.should raise_error(IndexError, "offset 2 does not land on character boundary") + end + + it "replaces with integer str indices" do + "ã“ã‚“ã«ã¡ã¯".bytesplice(3, 3, "ã“ã‚“ã«ã¡ã¯", -15, 0).should == "ã“ã«ã¡ã¯" + "ã“ã‚“ã«ã¡ã¯".bytesplice(3, 3, "ã“ã‚“ã«ã¡ã¯", 0, 0).should == "ã“ã«ã¡ã¯" + "ã“ã‚“ã«ã¡ã¯".bytesplice(3, 3, "ã“ã‚“ã«ã¡ã¯", 0, 3).should == "ã“ã“ã«ã¡ã¯" + "ã“ã‚“ã«ã¡ã¯".bytesplice(3, 3, "ã¯ã¯", 3, 3).should == "ã“ã¯ã«ã¡ã¯" + "ã“ã‚“ã«ã¡ã¯".bytesplice(3, 3, "ã“ã‚“ã«ã¡ã¯", 15, 0).should == "ã“ã«ã¡ã¯" + end + + it "replaces with str range" do + "ã“ã‚“ã«ã¡ã¯".bytesplice(0..2, "ã“ã‚“ã«ã¡ã¯", -15...-16).should == "ã‚“ã«ã¡ã¯" + "ã“ã‚“ã«ã¡ã¯".bytesplice(0..2, "ã“ã‚“ã«ã¡ã¯", 0...0).should == "ã‚“ã«ã¡ã¯" + "ã“ã‚“ã«ã¡ã¯".bytesplice(0..2, "ã“ã‚“ã«ã¡ã¯", 3..5).should == "ã‚“ã‚“ã«ã¡ã¯" + "ã“ã‚“ã«ã¡ã¯".bytesplice(0..2, "ã“ã‚“ã«ã¡ã¯", 3...6).should == "ã‚“ã‚“ã«ã¡ã¯" + "ã“ã‚“ã«ã¡ã¯".bytesplice(0..2, "ã“ã‚“ã«ã¡ã¯", 3..8).should == "ã‚“ã«ã‚“ã«ã¡ã¯" + "ã“ã‚“ã«ã¡ã¯".bytesplice(0..2, "ã“ã‚“ã«ã¡ã¯", 0..-1).should == "ã“ã‚“ã«ã¡ã¯ã‚“ã«ã¡ã¯" + "ã“ã‚“ã«ã¡ã¯".bytesplice(0..2, "ã“ã‚“ã«ã¡ã¯", 0...15).should == "ã“ã‚“ã«ã¡ã¯ã‚“ã«ã¡ã¯" + "ã“ã‚“ã«ã¡ã¯".bytesplice(0..2, "ã“ã‚“ã«ã¡ã¯", 0...18).should == "ã“ã‚“ã«ã¡ã¯ã‚“ã«ã¡ã¯" + end + + it "treats negative length for str range as 0" do + "ã“ã‚“ã«ã¡ã¯".bytesplice(0..2, "ã“ã‚“ã«ã¡ã¯", 0...-100).should == "ã‚“ã«ã¡ã¯" + "ã“ã‚“ã«ã¡ã¯".bytesplice(0..2, "ã“ã‚“ã«ã¡ã¯", 3...-100).should == "ã‚“ã«ã¡ã¯" + "ã“ã‚“ã«ã¡ã¯".bytesplice(0..2, "ã“ã‚“ã«ã¡ã¯", -15...-100).should == "ã‚“ã«ã¡ã¯" + end + + it "raises when ranges not match codepoint boundaries in str" do + -> { "ã“ã‚“ã«ã¡ã¯".bytesplice(3...3, "ã“", 0..0) }.should raise_error(IndexError, "offset 1 does not land on character boundary") + -> { "ã“ã‚“ã«ã¡ã¯".bytesplice(3...3, "ã“", 0..1) }.should raise_error(IndexError, "offset 2 does not land on character boundary") + # Begin is incorrect + -> { "ã“ã‚“ã«ã¡ã¯".bytesplice(3...3, "ã“ã‚“ã«ã¡ã¯", -4..-1) }.should raise_error(IndexError, "offset 11 does not land on character boundary") + -> { "ã“ã‚“ã«ã¡ã¯".bytesplice(3...3, "ã“ã‚“ã«ã¡ã¯", -5..-1) }.should raise_error(IndexError, "offset 10 does not land on character boundary") + # End is incorrect + -> { "ã“ã‚“ã«ã¡ã¯".bytesplice(3...3, "ã“ã‚“ã«ã¡ã¯", -3..-2) }.should raise_error(IndexError, "offset 14 does not land on character boundary") + -> { "ã“ã‚“ã«ã¡ã¯".bytesplice(3...3, "ã“ã‚“ã«ã¡ã¯", -3..-3) }.should raise_error(IndexError, "offset 13 does not land on character boundary") + end + + it "deals with a different encoded argument with str index/length" do + s = "ã“ã‚“ã«ã¡ã¯" + s.encoding.should == Encoding::UTF_8 + sub = "goodbye" + sub.force_encoding(Encoding::US_ASCII) + + result = s.bytesplice(3, 3, sub, 0, 3) + result.should == "ã“gooã«ã¡ã¯" + result.encoding.should == Encoding::UTF_8 + + s = "hello" + s.force_encoding(Encoding::US_ASCII) + sub = "ã“ã‚“ã«ã¡ã¯" + sub.encoding.should == Encoding::UTF_8 + + result = s.bytesplice(1, 2, sub, 3, 3) + result.should == "hã‚“lo" + result.encoding.should == Encoding::UTF_8 + end + + it "deals with a different encoded argument with str range" do + s = "ã“ã‚“ã«ã¡ã¯" + s.encoding.should == Encoding::UTF_8 + sub = "goodbye" + sub.force_encoding(Encoding::US_ASCII) + + result = s.bytesplice(3..5, sub, 0..2) + result.should == "ã“gooã«ã¡ã¯" + result.encoding.should == Encoding::UTF_8 + + s = "hello" + s.force_encoding(Encoding::US_ASCII) + sub = "ã“ã‚“ã«ã¡ã¯" + sub.encoding.should == Encoding::UTF_8 + + result = s.bytesplice(1..2, sub, 3..5) + result.should == "hã‚“lo" + result.encoding.should == Encoding::UTF_8 + end + end +end diff --git a/spec/ruby/core/string/capitalize_spec.rb b/spec/ruby/core/string/capitalize_spec.rb new file mode 100644 index 0000000000..5e59b656c5 --- /dev/null +++ b/spec/ruby/core/string/capitalize_spec.rb @@ -0,0 +1,207 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "String#capitalize" do + it "returns a copy of self with the first character converted to uppercase and the remainder to lowercase" do + "".capitalize.should == "" + "h".capitalize.should == "H" + "H".capitalize.should == "H" + "hello".capitalize.should == "Hello" + "HELLO".capitalize.should == "Hello" + "123ABC".capitalize.should == "123abc" + "abcdef"[1...-1].capitalize.should == "Bcde" + end + + describe "full Unicode case mapping" do + it "works for all of Unicode with no option" do + "äöÜ".capitalize.should == "Äöü" + end + + it "only capitalizes the first resulting character when upcasing a character produces a multi-character sequence" do + "ß".capitalize.should == "Ss" + end + + it "updates string metadata" do + capitalized = "ßeT".capitalize + + capitalized.should == "Sset" + capitalized.size.should == 4 + capitalized.bytesize.should == 4 + capitalized.ascii_only?.should be_true + end + end + + describe "ASCII-only case mapping" do + it "does not capitalize non-ASCII characters" do + "ßet".capitalize(:ascii).should == "ßet" + end + + it "handles non-ASCII substrings properly" do + "garçon"[1...-1].capitalize(:ascii).should == "Arço" + end + end + + describe "full Unicode case mapping adapted for Turkic languages" do + it "capitalizes ASCII characters according to Turkic semantics" do + "iSa".capitalize(:turkic).should == "İsa" + end + + it "allows Lithuanian as an extra option" do + "iSa".capitalize(:turkic, :lithuanian).should == "İsa" + end + + it "does not allow any other additional option" do + -> { "iSa".capitalize(:turkic, :ascii) }.should raise_error(ArgumentError) + end + end + + describe "full Unicode case mapping adapted for Lithuanian" do + it "currently works the same as full Unicode case mapping" do + "iß".capitalize(:lithuanian).should == "Iß" + end + + it "allows Turkic as an extra option (and applies Turkic semantics)" do + "iß".capitalize(:lithuanian, :turkic).should == "İß" + end + + it "does not allow any other additional option" do + -> { "iß".capitalize(:lithuanian, :ascii) }.should raise_error(ArgumentError) + end + end + + it "does not allow the :fold option for upcasing" do + -> { "abc".capitalize(:fold) }.should raise_error(ArgumentError) + end + + it "does not allow invalid options" do + -> { "abc".capitalize(:invalid_option) }.should raise_error(ArgumentError) + end + + it "returns String instances when called on a subclass" do + StringSpecs::MyString.new("hello").capitalize.should be_an_instance_of(String) + StringSpecs::MyString.new("Hello").capitalize.should be_an_instance_of(String) + end + + it "returns a String in the same encoding as self" do + "h".encode("US-ASCII").capitalize.encoding.should == Encoding::US_ASCII + end +end + +describe "String#capitalize!" do + it "capitalizes self in place" do + a = +"hello" + a.capitalize!.should equal(a) + a.should == "Hello" + end + + it "modifies self in place for non-ascii-compatible encodings" do + a = "heLLo".encode("utf-16le") + a.capitalize! + a.should == "Hello".encode("utf-16le") + end + + describe "full Unicode case mapping" do + it "modifies self in place for all of Unicode with no option" do + a = +"äöÜ" + a.capitalize! + a.should == "Äöü" + end + + it "only capitalizes the first resulting character when upcasing a character produces a multi-character sequence" do + a = +"ß" + a.capitalize! + a.should == "Ss" + end + + it "works for non-ascii-compatible encodings" do + a = "äöü".encode("utf-16le") + a.capitalize! + a.should == "Äöü".encode("utf-16le") + end + + it "updates string metadata" do + capitalized = +"ßeT" + capitalized.capitalize! + + capitalized.should == "Sset" + capitalized.size.should == 4 + capitalized.bytesize.should == 4 + capitalized.ascii_only?.should be_true + end + end + + describe "modifies self in place for ASCII-only case mapping" do + it "does not capitalize non-ASCII characters" do + a = +"ßet" + a.capitalize!(:ascii) + a.should == "ßet" + end + + it "works for non-ascii-compatible encodings" do + a = "aBc".encode("utf-16le") + a.capitalize!(:ascii) + a.should == "Abc".encode("utf-16le") + end + end + + describe "modifies self in place for full Unicode case mapping adapted for Turkic languages" do + it "capitalizes ASCII characters according to Turkic semantics" do + a = +"iSa" + a.capitalize!(:turkic) + a.should == "İsa" + end + + it "allows Lithuanian as an extra option" do + a = +"iSa" + a.capitalize!(:turkic, :lithuanian) + a.should == "İsa" + end + + it "does not allow any other additional option" do + -> { a = "iSa"; a.capitalize!(:turkic, :ascii) }.should raise_error(ArgumentError) + end + end + + describe "modifies self in place for full Unicode case mapping adapted for Lithuanian" do + it "currently works the same as full Unicode case mapping" do + a = +"iß" + a.capitalize!(:lithuanian) + a.should == "Iß" + end + + it "allows Turkic as an extra option (and applies Turkic semantics)" do + a = +"iß" + a.capitalize!(:lithuanian, :turkic) + a.should == "İß" + end + + it "does not allow any other additional option" do + -> { a = "iß"; a.capitalize!(:lithuanian, :ascii) }.should raise_error(ArgumentError) + end + end + + it "does not allow the :fold option for upcasing" do + -> { a = "abc"; a.capitalize!(:fold) }.should raise_error(ArgumentError) + end + + it "does not allow invalid options" do + -> { a = "abc"; a.capitalize!(:invalid_option) }.should raise_error(ArgumentError) + end + + it "returns nil when no changes are made" do + a = +"Hello" + a.capitalize!.should == nil + a.should == "Hello" + + (+"").capitalize!.should == nil + (+"H").capitalize!.should == nil + end + + it "raises a FrozenError when self is frozen" do + ["", "Hello", "hello"].each do |a| + a.freeze + -> { a.capitalize! }.should raise_error(FrozenError) + end + end +end diff --git a/spec/ruby/core/string/case_compare_spec.rb b/spec/ruby/core/string/case_compare_spec.rb new file mode 100644 index 0000000000..b83d1adb91 --- /dev/null +++ b/spec/ruby/core/string/case_compare_spec.rb @@ -0,0 +1,8 @@ +require_relative '../../spec_helper' +require_relative 'shared/eql' +require_relative 'shared/equal_value' + +describe "String#===" do + it_behaves_like :string_eql_value, :=== + it_behaves_like :string_equal_value, :=== +end diff --git a/spec/ruby/core/string/casecmp_spec.rb b/spec/ruby/core/string/casecmp_spec.rb new file mode 100644 index 0000000000..81ebea557c --- /dev/null +++ b/spec/ruby/core/string/casecmp_spec.rb @@ -0,0 +1,204 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "String#casecmp independent of case" do + it "returns -1 when less than other" do + "a".casecmp("b").should == -1 + "A".casecmp("b").should == -1 + end + + it "returns 0 when equal to other" do + "a".casecmp("a").should == 0 + "A".casecmp("a").should == 0 + end + + it "returns 1 when greater than other" do + "b".casecmp("a").should == 1 + "B".casecmp("a").should == 1 + end + + it "tries to convert other to string using to_str" do + other = mock('x') + other.should_receive(:to_str).and_return("abc") + + "abc".casecmp(other).should == 0 + end + + it "returns nil if other can't be converted to a string" do + "abc".casecmp(mock('abc')).should be_nil + end + + it "returns nil if incompatible encodings" do + "ã‚れ".casecmp("れ".encode(Encoding::EUC_JP)).should be_nil + end + + describe "in UTF-8 mode" do + describe "for non-ASCII characters" do + before :each do + @upper_a_tilde = "Ã" + @lower_a_tilde = "ã" + @upper_a_umlaut = "Ä" + @lower_a_umlaut = "ä" + end + + it "returns -1 when numerically less than other" do + @upper_a_tilde.casecmp(@lower_a_tilde).should == -1 + @upper_a_tilde.casecmp(@upper_a_umlaut).should == -1 + end + + it "returns 0 when numerically equal to other" do + @upper_a_tilde.casecmp(@upper_a_tilde).should == 0 + end + + it "returns 1 when numerically greater than other" do + @lower_a_umlaut.casecmp(@upper_a_umlaut).should == 1 + @lower_a_umlaut.casecmp(@lower_a_tilde).should == 1 + end + end + + describe "for ASCII characters" do + it "returns -1 when less than other" do + "a".casecmp("b").should == -1 + "A".casecmp("b").should == -1 + end + + it "returns 0 when equal to other" do + "a".casecmp("a").should == 0 + "A".casecmp("a").should == 0 + end + + it "returns 1 when greater than other" do + "b".casecmp("a").should == 1 + "B".casecmp("a").should == 1 + end + end + end + + describe "for non-ASCII characters" do + before :each do + @upper_a_tilde = "\xc3" + @lower_a_tilde = "\xe3" + end + + it "returns -1 when numerically less than other" do + @upper_a_tilde.casecmp(@lower_a_tilde).should == -1 + end + + it "returns 0 when equal to other" do + @upper_a_tilde.casecmp("\xc3").should == 0 + end + + it "returns 1 when numerically greater than other" do + @lower_a_tilde.casecmp(@upper_a_tilde).should == 1 + end + + it "does not case fold" do + "ß".casecmp("ss").should == 1 + end + end + + describe "when comparing a subclass instance" do + it "returns -1 when less than other" do + b = StringSpecs::MyString.new "b" + "a".casecmp(b).should == -1 + "A".casecmp(b).should == -1 + end + + it "returns 0 when equal to other" do + a = StringSpecs::MyString.new "a" + "a".casecmp(a).should == 0 + "A".casecmp(a).should == 0 + end + + it "returns 1 when greater than other" do + a = StringSpecs::MyString.new "a" + "b".casecmp(a).should == 1 + "B".casecmp(a).should == 1 + end + end + + it "returns 0 for empty strings in different encodings" do + ''.b.casecmp('').should == 0 + ''.b.casecmp(''.encode("UTF-32LE")).should == 0 + end +end + +describe 'String#casecmp? independent of case' do + it 'returns true when equal to other' do + 'abc'.casecmp?('abc').should == true + 'abc'.casecmp?('ABC').should == true + end + + it 'returns false when not equal to other' do + 'abc'.casecmp?('DEF').should == false + 'abc'.casecmp?('def').should == false + end + + it "tries to convert other to string using to_str" do + other = mock('x') + other.should_receive(:to_str).and_return("abc") + + "abc".casecmp?(other).should == true + end + + it "returns nil if incompatible encodings" do + "ã‚れ".casecmp?("れ".encode(Encoding::EUC_JP)).should be_nil + end + + describe 'for UNICODE characters' do + it 'returns true when downcase(:fold) on unicode' do + 'äöü'.casecmp?('ÄÖÜ').should == true + end + end + + describe "when comparing a subclass instance" do + it 'returns true when equal to other' do + a = StringSpecs::MyString.new "a" + 'a'.casecmp?(a).should == true + 'A'.casecmp?(a).should == true + end + + it 'returns false when not equal to other' do + b = StringSpecs::MyString.new "a" + 'b'.casecmp?(b).should == false + 'B'.casecmp?(b).should == false + end + end + + describe "in UTF-8 mode" do + describe "for non-ASCII characters" do + before :each do + @upper_a_tilde = "Ã" + @lower_a_tilde = "ã" + @upper_a_umlaut = "Ä" + @lower_a_umlaut = "ä" + end + + it "returns true when they are the same with normalized case" do + @upper_a_tilde.casecmp?(@lower_a_tilde).should == true + end + + it "returns false when they are unrelated" do + @upper_a_tilde.casecmp?(@upper_a_umlaut).should == false + end + + it "returns true when they have the same bytes" do + @upper_a_tilde.casecmp?(@upper_a_tilde).should == true + end + end + end + + it "case folds" do + "ß".casecmp?("ss").should be_true + end + + it "returns nil if other can't be converted to a string" do + "abc".casecmp?(mock('abc')).should be_nil + end + + it "returns true for empty strings in different encodings" do + ''.b.should.casecmp?('') + ''.b.should.casecmp?(''.encode("UTF-32LE")) + end +end diff --git a/spec/ruby/core/string/center_spec.rb b/spec/ruby/core/string/center_spec.rb new file mode 100644 index 0000000000..1667b59327 --- /dev/null +++ b/spec/ruby/core/string/center_spec.rb @@ -0,0 +1,117 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "String#center with length, padding" do + it "returns a new string of specified length with self centered and padded with padstr" do + "one".center(9, '.').should == "...one..." + "hello".center(20, '123').should == "1231231hello12312312" + "middle".center(13, '-').should == "---middle----" + + "".center(1, "abcd").should == "a" + "".center(2, "abcd").should == "aa" + "".center(3, "abcd").should == "aab" + "".center(4, "abcd").should == "abab" + "".center(6, "xy").should == "xyxxyx" + "".center(11, "12345").should == "12345123451" + + "|".center(2, "abcd").should == "|a" + "|".center(3, "abcd").should == "a|a" + "|".center(4, "abcd").should == "a|ab" + "|".center(5, "abcd").should == "ab|ab" + "|".center(6, "xy").should == "xy|xyx" + "|".center(7, "xy").should == "xyx|xyx" + "|".center(11, "12345").should == "12345|12345" + "|".center(12, "12345").should == "12345|123451" + + "||".center(3, "abcd").should == "||a" + "||".center(4, "abcd").should == "a||a" + "||".center(5, "abcd").should == "a||ab" + "||".center(6, "abcd").should == "ab||ab" + "||".center(8, "xy").should == "xyx||xyx" + "||".center(12, "12345").should == "12345||12345" + "||".center(13, "12345").should == "12345||123451" + end + + it "pads with whitespace if no padstr is given" do + "two".center(5).should == " two " + "hello".center(20).should == " hello " + end + + it "returns self if it's longer than or as long as the specified length" do + "".center(0).should == "" + "".center(-1).should == "" + "hello".center(4).should == "hello" + "hello".center(-1).should == "hello" + "this".center(3).should == "this" + "radiology".center(8, '-').should == "radiology" + end + + it "calls #to_int to convert length to an integer" do + "_".center(3.8, "^").should == "^_^" + + obj = mock('3') + obj.should_receive(:to_int).and_return(3) + + "_".center(obj, "o").should == "o_o" + end + + it "raises a TypeError when length can't be converted to an integer" do + -> { "hello".center("x") }.should raise_error(TypeError) + -> { "hello".center("x", "y") }.should raise_error(TypeError) + -> { "hello".center([]) }.should raise_error(TypeError) + -> { "hello".center(mock('x')) }.should raise_error(TypeError) + end + + it "calls #to_str to convert padstr to a String" do + padstr = mock('123') + padstr.should_receive(:to_str).and_return("123") + + "hello".center(20, padstr).should == "1231231hello12312312" + end + + it "raises a TypeError when padstr can't be converted to a string" do + -> { "hello".center(20, 100) }.should raise_error(TypeError) + -> { "hello".center(20, []) }.should raise_error(TypeError) + -> { "hello".center(20, mock('x')) }.should raise_error(TypeError) + end + + it "raises an ArgumentError if padstr is empty" do + -> { "hello".center(10, "") }.should raise_error(ArgumentError) + -> { "hello".center(0, "") }.should raise_error(ArgumentError) + end + + it "returns String instances when called on subclasses" do + StringSpecs::MyString.new("").center(10).should be_an_instance_of(String) + StringSpecs::MyString.new("foo").center(10).should be_an_instance_of(String) + StringSpecs::MyString.new("foo").center(10, StringSpecs::MyString.new("x")).should be_an_instance_of(String) + + "".center(10, StringSpecs::MyString.new("x")).should be_an_instance_of(String) + "foo".center(10, StringSpecs::MyString.new("x")).should be_an_instance_of(String) + end + + describe "with width" do + it "returns a String in the same encoding as the original" do + str = "abc".dup.force_encoding Encoding::IBM437 + result = str.center 6 + result.should == " abc " + result.encoding.should equal(Encoding::IBM437) + end + end + + describe "with width, pattern" do + it "returns a String in the compatible encoding" do + str = "abc".dup.force_encoding Encoding::IBM437 + result = str.center 6, "ã‚" + result.should == "ã‚abcã‚ã‚" + result.encoding.should equal(Encoding::UTF_8) + end + + it "raises an Encoding::CompatibilityError if the encodings are incompatible" do + pat = "ã‚¢".encode Encoding::EUC_JP + -> do + "ã‚れ".center 5, pat + end.should raise_error(Encoding::CompatibilityError) + end + end +end diff --git a/spec/ruby/core/string/chars_spec.rb b/spec/ruby/core/string/chars_spec.rb new file mode 100644 index 0000000000..ee85430574 --- /dev/null +++ b/spec/ruby/core/string/chars_spec.rb @@ -0,0 +1,16 @@ +require_relative "../../spec_helper" +require_relative 'shared/chars' + +describe "String#chars" do + it_behaves_like :string_chars, :chars + + it "returns an array when no block given" do + "hello".chars.should == ['h', 'e', 'l', 'l', 'o'] + end + + it "returns Strings in the same encoding as self" do + "hello".encode("US-ASCII").chars.each do |c| + c.encoding.should == Encoding::US_ASCII + end + end +end diff --git a/spec/ruby/core/string/chilled_string_spec.rb b/spec/ruby/core/string/chilled_string_spec.rb new file mode 100644 index 0000000000..73d055cbdf --- /dev/null +++ b/spec/ruby/core/string/chilled_string_spec.rb @@ -0,0 +1,151 @@ +require_relative '../../spec_helper' + +describe "chilled String" do + guard -> { ruby_version_is "3.4" and !"test".equal?("test") } do + describe "chilled string literals" do + + describe "#frozen?" do + it "returns false" do + "chilled".frozen?.should == false + end + end + + describe "#-@" do + it "returns a different instance" do + input = "chilled" + interned = (-input) + interned.frozen?.should == true + interned.object_id.should_not == input.object_id + end + end + + describe "#+@" do + it "returns a different instance" do + input = "chilled" + duped = (+input) + duped.frozen?.should == false + duped.object_id.should_not == input.object_id + end + end + + describe "#clone" do + it "preserves chilled status" do + input = "chilled".clone + -> { + input << "-mutated" + }.should complain(/literal string will be frozen in the future/) + input.should == "chilled-mutated" + end + end + + describe "mutation" do + it "emits a warning" do + input = "chilled" + -> { + input << "-mutated" + }.should complain(/literal string will be frozen in the future/) + input.should == "chilled-mutated" + end + + it "emits a warning for concatenated strings" do + input = "still" "+chilled" + -> { + input << "-mutated" + }.should complain(/literal string will be frozen in the future/) + input.should == "still+chilled-mutated" + end + + it "emits a warning on singleton_class creation" do + -> { + "chilled".singleton_class + }.should complain(/literal string will be frozen in the future/) + end + + it "emits a warning on instance variable assignment" do + -> { + "chilled".instance_variable_set(:@ivar, 42) + }.should complain(/literal string will be frozen in the future/) + end + + it "raises FrozenError after the string was explicitly frozen" do + input = "chilled" + input.freeze + -> { + -> { + input << "mutated" + }.should raise_error(FrozenError) + }.should_not complain(/literal string will be frozen in the future/) + end + end + end + + describe "chilled strings returned by Symbol#to_s" do + + describe "#frozen?" do + it "returns false" do + :chilled.to_s.frozen?.should == false + end + end + + describe "#-@" do + it "returns a different instance" do + input = :chilled.to_s + interned = (-input) + interned.frozen?.should == true + interned.object_id.should_not == input.object_id + end + end + + describe "#+@" do + it "returns a different instance" do + input = :chilled.to_s + duped = (+input) + duped.frozen?.should == false + duped.object_id.should_not == input.object_id + end + end + + describe "#clone" do + it "preserves chilled status" do + input = :chilled.to_s.clone + -> { + input << "-mutated" + }.should complain(/string returned by :chilled\.to_s will be frozen in the future/) + input.should == "chilled-mutated" + end + end + + describe "mutation" do + it "emits a warning" do + input = :chilled.to_s + -> { + input << "-mutated" + }.should complain(/string returned by :chilled\.to_s will be frozen in the future/) + input.should == "chilled-mutated" + end + + it "emits a warning on singleton_class creation" do + -> { + :chilled.to_s.singleton_class + }.should complain(/string returned by :chilled\.to_s will be frozen in the future/) + end + + it "emits a warning on instance variable assignment" do + -> { + :chilled.to_s.instance_variable_set(:@ivar, 42) + }.should complain(/string returned by :chilled\.to_s will be frozen in the future/) + end + + it "raises FrozenError after the string was explicitly frozen" do + input = :chilled.to_s + input.freeze + -> { + -> { + input << "mutated" + }.should raise_error(FrozenError) + }.should_not complain(/string returned by :chilled\.to_s will be frozen in the future/) + end + end + end + end +end diff --git a/spec/ruby/core/string/chomp_spec.rb b/spec/ruby/core/string/chomp_spec.rb new file mode 100644 index 0000000000..d27c84c6f6 --- /dev/null +++ b/spec/ruby/core/string/chomp_spec.rb @@ -0,0 +1,366 @@ +# -*- encoding: utf-8 -*- +# frozen_string_literal: false +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "String#chomp" do + describe "when passed no argument" do + before do + # Ensure that $/ is set to the default value + @verbose, $VERBOSE = $VERBOSE, nil + @dollar_slash, $/ = $/, "\n" + end + + after do + $/ = @dollar_slash + $VERBOSE = @verbose + end + + it "does not modify a String with no trailing carriage return or newline" do + "abc".chomp.should == "abc" + end + + it "returns a copy of the String when it is not modified" do + str = "abc" + str.chomp.should_not equal(str) + end + + it "removes one trailing newline" do + "abc\n\n".chomp.should == "abc\n" + end + + it "removes one trailing carriage return" do + "abc\r\r".chomp.should == "abc\r" + end + + it "removes one trailing carriage return, newline pair" do + "abc\r\n\r\n".chomp.should == "abc\r\n" + end + + it "returns an empty String when self is empty" do + "".chomp.should == "" + end + + it "returns a String in the same encoding as self" do + "abc\n\n".encode("US-ASCII").chomp.encoding.should == Encoding::US_ASCII + end + + it "returns String instances when called on a subclass" do + str = StringSpecs::MyString.new("hello\n").chomp + str.should be_an_instance_of(String) + end + + it "removes trailing characters that match $/ when it has been assigned a value" do + $/ = "cdef" + "abcdef".chomp.should == "ab" + end + + it "removes one trailing newline for string with invalid encoding" do + "\xa0\xa1\n".chomp.should == "\xa0\xa1" + end + end + + describe "when passed nil" do + it "does not modify the String" do + "abc\r\n".chomp(nil).should == "abc\r\n" + end + + it "returns a copy of the String" do + str = "abc" + str.chomp(nil).should_not equal(str) + end + + it "returns an empty String when self is empty" do + "".chomp(nil).should == "" + end + end + + describe "when passed ''" do + it "removes a final newline" do + "abc\n".chomp("").should == "abc" + end + + it "removes a final carriage return, newline" do + "abc\r\n".chomp("").should == "abc" + end + + it "does not remove a final carriage return" do + "abc\r".chomp("").should == "abc\r" + end + + it "removes more than one trailing newlines" do + "abc\n\n\n".chomp("").should == "abc" + end + + it "removes more than one trailing carriage return, newline pairs" do + "abc\r\n\r\n\r\n".chomp("").should == "abc" + end + + it "returns an empty String when self is empty" do + "".chomp("").should == "" + end + + it "removes one trailing newline for string with invalid encoding" do + "\xa0\xa1\n".chomp("").should == "\xa0\xa1" + end + end + + describe "when passed '\\n'" do + it "removes one trailing newline" do + "abc\n\n".chomp("\n").should == "abc\n" + end + + it "removes one trailing carriage return" do + "abc\r\r".chomp("\n").should == "abc\r" + end + + it "removes one trailing carriage return, newline pair" do + "abc\r\n\r\n".chomp("\n").should == "abc\r\n" + end + + it "returns an empty String when self is empty" do + "".chomp("\n").should == "" + end + end + + describe "when passed an Object" do + it "calls #to_str to convert to a String" do + arg = mock("string chomp") + arg.should_receive(:to_str).and_return("bc") + "abc".chomp(arg).should == "a" + end + + it "raises a TypeError if #to_str does not return a String" do + arg = mock("string chomp") + arg.should_receive(:to_str).and_return(1) + -> { "abc".chomp(arg) }.should raise_error(TypeError) + end + end + + describe "when passed a String" do + it "removes the trailing characters if they match the argument" do + "abcabc".chomp("abc").should == "abc" + end + + it "does not modify the String if the argument does not match the trailing characters" do + "abc".chomp("def").should == "abc" + end + + it "returns an empty String when self is empty" do + "".chomp("abc").should == "" + end + + it "returns an empty String when the argument equals self" do + "abc".chomp("abc").should == "" + end + end +end + +describe "String#chomp!" do + describe "when passed no argument" do + before do + # Ensure that $/ is set to the default value + @verbose, $VERBOSE = $VERBOSE, nil + @dollar_slash, $/ = $/, "\n" + end + + after do + $/ = @dollar_slash + $VERBOSE = @verbose + end + + it "modifies self" do + str = "abc\n" + str.chomp!.should equal(str) + end + + it "returns nil if self is not modified" do + "abc".chomp!.should be_nil + end + + it "removes one trailing newline" do + "abc\n\n".chomp!.should == "abc\n" + end + + it "removes one trailing carriage return" do + "abc\r\r".chomp!.should == "abc\r" + end + + it "removes one trailing carriage return, newline pair" do + "abc\r\n\r\n".chomp!.should == "abc\r\n" + end + + it "returns nil when self is empty" do + "".chomp!.should be_nil + end + + it "returns subclass instances when called on a subclass" do + str = StringSpecs::MyString.new("hello\n").chomp! + str.should be_an_instance_of(StringSpecs::MyString) + end + + it "removes trailing characters that match $/ when it has been assigned a value" do + $/ = "cdef" + "abcdef".chomp!.should == "ab" + end + end + + describe "when passed nil" do + it "returns nil" do + "abc\r\n".chomp!(nil).should be_nil + end + + it "returns nil when self is empty" do + "".chomp!(nil).should be_nil + end + end + + describe "when passed ''" do + it "removes a final newline" do + "abc\n".chomp!("").should == "abc" + end + + it "removes a final carriage return, newline" do + "abc\r\n".chomp!("").should == "abc" + end + + it "does not remove a final carriage return" do + "abc\r".chomp!("").should be_nil + end + + it "removes more than one trailing newlines" do + "abc\n\n\n".chomp!("").should == "abc" + end + + it "removes more than one trailing carriage return, newline pairs" do + "abc\r\n\r\n\r\n".chomp!("").should == "abc" + end + + it "returns nil when self is empty" do + "".chomp!("").should be_nil + end + end + + describe "when passed '\\n'" do + it "removes one trailing newline" do + "abc\n\n".chomp!("\n").should == "abc\n" + end + + it "removes one trailing carriage return" do + "abc\r\r".chomp!("\n").should == "abc\r" + end + + it "removes one trailing carriage return, newline pair" do + "abc\r\n\r\n".chomp!("\n").should == "abc\r\n" + end + + it "returns nil when self is empty" do + "".chomp!("\n").should be_nil + end + end + + describe "when passed an Object" do + it "calls #to_str to convert to a String" do + arg = mock("string chomp") + arg.should_receive(:to_str).and_return("bc") + "abc".chomp!(arg).should == "a" + end + + it "raises a TypeError if #to_str does not return a String" do + arg = mock("string chomp") + arg.should_receive(:to_str).and_return(1) + -> { "abc".chomp!(arg) }.should raise_error(TypeError) + end + end + + describe "when passed a String" do + it "removes the trailing characters if they match the argument" do + "abcabc".chomp!("abc").should == "abc" + end + + it "returns nil if the argument does not match the trailing characters" do + "abc".chomp!("def").should be_nil + end + + it "returns nil when self is empty" do + "".chomp!("abc").should be_nil + end + end + + it "raises a FrozenError on a frozen instance when it is modified" do + a = "string\n\r" + a.freeze + + -> { a.chomp! }.should raise_error(FrozenError) + end + + # see [ruby-core:23666] + it "raises a FrozenError on a frozen instance when it would not be modified" do + a = "string\n\r" + a.freeze + -> { a.chomp!(nil) }.should raise_error(FrozenError) + -> { a.chomp!("x") }.should raise_error(FrozenError) + end +end + +describe "String#chomp" do + before :each do + @verbose, $VERBOSE = $VERBOSE, nil + @before_separator = $/ + end + + after :each do + $/ = @before_separator + $VERBOSE = @verbose + end + + it "does not modify a multi-byte character" do + "ã‚れ".chomp.should == "ã‚れ" + end + + it "removes the final carriage return, newline from a multibyte String" do + "ã‚れ\r\n".chomp.should == "ã‚れ" + end + + it "removes the final carriage return, newline from a non-ASCII String" do + str = "abc\r\n".encode "utf-32be" + str.chomp.should == "abc".encode("utf-32be") + end + + it "removes the final carriage return, newline from a non-ASCII String when the record separator is changed" do + $/ = "\n".encode("utf-8") + str = "abc\r\n".encode "utf-32be" + str.chomp.should == "abc".encode("utf-32be") + end +end + +describe "String#chomp!" do + before :each do + @verbose, $VERBOSE = $VERBOSE, nil + @before_separator = $/ + end + + after :each do + $/ = @before_separator + $VERBOSE = @verbose + end + + it "returns nil when the String is not modified" do + "ã‚れ".chomp!.should be_nil + end + + it "removes the final carriage return, newline from a multibyte String" do + "ã‚れ\r\n".chomp!.should == "ã‚れ" + end + + it "removes the final carriage return, newline from a non-ASCII String" do + str = "abc\r\n".encode "utf-32be" + str.chomp!.should == "abc".encode("utf-32be") + end + + it "removes the final carriage return, newline from a non-ASCII String when the record separator is changed" do + $/ = "\n".encode("utf-8") + str = "abc\r\n".encode "utf-32be" + str.chomp!.should == "abc".encode("utf-32be") + end +end diff --git a/spec/ruby/core/string/chop_spec.rb b/spec/ruby/core/string/chop_spec.rb new file mode 100644 index 0000000000..99c2c82190 --- /dev/null +++ b/spec/ruby/core/string/chop_spec.rb @@ -0,0 +1,119 @@ +# -*- encoding: utf-8 -*- +# frozen_string_literal: false +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "String#chop" do + it "removes the final character" do + "abc".chop.should == "ab" + end + + it "removes the final carriage return" do + "abc\r".chop.should == "abc" + end + + it "removes the final newline" do + "abc\n".chop.should == "abc" + end + + it "removes the final carriage return, newline" do + "abc\r\n".chop.should == "abc" + end + + it "removes the carriage return, newline if they are the only characters" do + "\r\n".chop.should == "" + end + + it "does not remove more than the final carriage return, newline" do + "abc\r\n\r\n".chop.should == "abc\r\n" + end + + it "removes a multi-byte character" do + "ã‚れ".chop.should == "ã‚" + end + + it "removes the final carriage return, newline from a multibyte String" do + "ã‚れ\r\n".chop.should == "ã‚れ" + end + + it "removes the final carriage return, newline from a non-ASCII String" do + str = "abc\r\n".encode "utf-32be" + str.chop.should == "abc".encode("utf-32be") + end + + it "returns an empty string when applied to an empty string" do + "".chop.should == "" + end + + it "returns a new string when applied to an empty string" do + s = "" + s.chop.should_not equal(s) + end + + it "returns String instances when called on a subclass" do + StringSpecs::MyString.new("hello\n").chop.should be_an_instance_of(String) + end + + it "returns a String in the same encoding as self" do + "abc\n\n".encode("US-ASCII").chop.encoding.should == Encoding::US_ASCII + end +end + +describe "String#chop!" do + it "removes the final character" do + "abc".chop!.should == "ab" + end + + it "removes the final carriage return" do + "abc\r".chop!.should == "abc" + end + + it "removes the final newline" do + "abc\n".chop!.should == "abc" + end + + it "removes the final carriage return, newline" do + "abc\r\n".chop!.should == "abc" + end + + it "removes the carriage return, newline if they are the only characters" do + "\r\n".chop!.should == "" + end + + it "does not remove more than the final carriage return, newline" do + "abc\r\n\r\n".chop!.should == "abc\r\n" + end + + it "removes a multi-byte character" do + "ã‚れ".chop!.should == "ã‚" + end + + it "removes the final carriage return, newline from a multibyte String" do + "ã‚れ\r\n".chop!.should == "ã‚れ" + end + + it "removes the final carriage return, newline from a non-ASCII String" do + str = "abc\r\n".encode "utf-32be" + str.chop!.should == "abc".encode("utf-32be") + end + + it "returns self if modifications were made" do + str = "hello" + str.chop!.should equal(str) + end + + it "returns nil when called on an empty string" do + "".chop!.should be_nil + end + + it "raises a FrozenError on a frozen instance that is modified" do + -> { "string\n\r".freeze.chop! }.should raise_error(FrozenError) + end + + # see [ruby-core:23666] + it "raises a FrozenError on a frozen instance that would not be modified" do + a = "" + a.freeze + -> { a.chop! }.should raise_error(FrozenError) + end +end diff --git a/spec/ruby/core/string/chr_spec.rb b/spec/ruby/core/string/chr_spec.rb new file mode 100644 index 0000000000..9ed29542e6 --- /dev/null +++ b/spec/ruby/core/string/chr_spec.rb @@ -0,0 +1,42 @@ +require_relative '../../spec_helper' + +describe "String#chr" do + it "returns a copy of self" do + s = 'e' + s.should_not equal s.chr + end + + it "returns a String" do + 'glark'.chr.should be_an_instance_of(String) + end + + it "returns an empty String if self is an empty String" do + "".chr.should == "" + end + + it "returns a 1-character String" do + "glark".chr.size.should == 1 + end + + it "returns the character at the start of the String" do + "Goodbye, world".chr.should == "G" + end + + it "returns a String in the same encoding as self" do + "\x24".encode(Encoding::US_ASCII).chr.encoding.should == Encoding::US_ASCII + end + + it "understands multi-byte characters" do + s = "\u{9879}" + s.bytesize.should == 3 + s.chr.should == s + end + + it "understands Strings that contain a mixture of character widths" do + three = "\u{8082}" + three.bytesize.should == 3 + four = "\u{77082}" + four.bytesize.should == 4 + "#{three}#{four}".chr.should == three + end +end diff --git a/spec/ruby/core/string/clear_spec.rb b/spec/ruby/core/string/clear_spec.rb new file mode 100644 index 0000000000..152986fd0f --- /dev/null +++ b/spec/ruby/core/string/clear_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: false +require_relative '../../spec_helper' + +describe "String#clear" do + before :each do + @s = "Jolene" + end + + it "sets self equal to the empty String" do + @s.clear + @s.should == "" + end + + it "returns self after emptying it" do + cleared = @s.clear + cleared.should == "" + cleared.should equal @s + end + + it "preserves its encoding" do + @s.encode!(Encoding::SHIFT_JIS) + @s.encoding.should == Encoding::SHIFT_JIS + @s.clear.encoding.should == Encoding::SHIFT_JIS + @s.encoding.should == Encoding::SHIFT_JIS + end + + it "works with multibyte Strings" do + s = "\u{9765}\u{876}" + s.clear + s.should == "" + end + + it "raises a FrozenError if self is frozen" do + @s.freeze + -> { @s.clear }.should raise_error(FrozenError) + -> { "".freeze.clear }.should raise_error(FrozenError) + end +end diff --git a/spec/ruby/core/string/clone_spec.rb b/spec/ruby/core/string/clone_spec.rb new file mode 100644 index 0000000000..a2ba2f9877 --- /dev/null +++ b/spec/ruby/core/string/clone_spec.rb @@ -0,0 +1,61 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "String#clone" do + before :each do + ScratchPad.clear + @obj = StringSpecs::InitializeString.new "string" + end + + it "calls #initialize_copy on the new instance" do + clone = @obj.clone + ScratchPad.recorded.should_not == @obj.object_id + ScratchPad.recorded.should == clone.object_id + end + + it "copies instance variables" do + clone = @obj.clone + clone.ivar.should == 1 + end + + it "copies singleton methods" do + def @obj.special() :the_one end + clone = @obj.clone + clone.special.should == :the_one + end + + it "copies modules included in the singleton class" do + class << @obj + include StringSpecs::StringModule + end + + clone = @obj.clone + clone.repr.should == 1 + end + + it "copies constants defined in the singleton class" do + class << @obj + CLONE = :clone + end + + clone = @obj.clone + (class << clone; CLONE; end).should == :clone + end + + it "copies frozen state" do + @obj.freeze.clone.frozen?.should be_true + "".freeze.clone.frozen?.should be_true + end + + it "does not modify the original string when changing cloned string" do + orig = "string"[0..100] + clone = orig.clone + orig[0] = 'x' + orig.should == "xtring" + clone.should == "string" + end + + it "returns a String in the same encoding as self" do + "a".encode("US-ASCII").clone.encoding.should == Encoding::US_ASCII + end +end diff --git a/spec/ruby/core/string/codepoints_spec.rb b/spec/ruby/core/string/codepoints_spec.rb new file mode 100644 index 0000000000..12a5bf5892 --- /dev/null +++ b/spec/ruby/core/string/codepoints_spec.rb @@ -0,0 +1,18 @@ +# encoding: binary +require_relative '../../spec_helper' +require_relative 'shared/codepoints' +require_relative 'shared/each_codepoint_without_block' + +describe "String#codepoints" do + it_behaves_like :string_codepoints, :codepoints + + it "returns an Array when no block is given" do + "abc".codepoints.should == [?a.ord, ?b.ord, ?c.ord] + end + + it "raises an ArgumentError when no block is given if self has an invalid encoding" do + s = "\xDF".dup.force_encoding(Encoding::UTF_8) + s.valid_encoding?.should be_false + -> { s.codepoints }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/string/comparison_spec.rb b/spec/ruby/core/string/comparison_spec.rb new file mode 100644 index 0000000000..9db0cff5ee --- /dev/null +++ b/spec/ruby/core/string/comparison_spec.rb @@ -0,0 +1,112 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "String#<=> with String" do + it "compares individual characters based on their ascii value" do + ascii_order = Array.new(256) { |x| x.chr } + sort_order = ascii_order.sort + sort_order.should == ascii_order + end + + it "returns -1 when self is less than other" do + ("this" <=> "those").should == -1 + end + + it "returns 0 when self is equal to other" do + ("yep" <=> "yep").should == 0 + end + + it "returns 1 when self is greater than other" do + ("yoddle" <=> "griddle").should == 1 + end + + it "considers string that comes lexicographically first to be less if strings have same size" do + ("aba" <=> "abc").should == -1 + ("abc" <=> "aba").should == 1 + end + + it "doesn't consider shorter string to be less if longer string starts with shorter one" do + ("abc" <=> "abcd").should == -1 + ("abcd" <=> "abc").should == 1 + end + + it "compares shorter string with corresponding number of first chars of longer string" do + ("abx" <=> "abcd").should == 1 + ("abcd" <=> "abx").should == -1 + end + + it "ignores subclass differences" do + a = "hello" + b = StringSpecs::MyString.new("hello") + + (a <=> b).should == 0 + (b <=> a).should == 0 + end + + it "returns 0 if self and other are bytewise identical and have the same encoding" do + ("ÄÖÜ" <=> "ÄÖÜ").should == 0 + end + + it "returns 0 if self and other are bytewise identical and have the same encoding" do + ("ÄÖÜ" <=> "ÄÖÜ").should == 0 + end + + it "returns -1 if self is bytewise less than other" do + ("ÄÖÛ" <=> "ÄÖÜ").should == -1 + end + + it "returns 1 if self is bytewise greater than other" do + ("ÄÖÜ" <=> "ÄÖÛ").should == 1 + end + + it "ignores encoding difference" do + ("ÄÖÛ".dup.force_encoding("utf-8") <=> "ÄÖÜ".dup.force_encoding("iso-8859-1")).should == -1 + ("ÄÖÜ".dup.force_encoding("utf-8") <=> "ÄÖÛ".dup.force_encoding("iso-8859-1")).should == 1 + end + + it "returns 0 with identical ASCII-compatible bytes of different encodings" do + ("abc".dup.force_encoding("utf-8") <=> "abc".dup.force_encoding("iso-8859-1")).should == 0 + end + + it "compares the indices of the encodings when the strings have identical non-ASCII-compatible bytes" do + xff_1 = [0xFF].pack('C').force_encoding("utf-8") + xff_2 = [0xFF].pack('C').force_encoding("iso-8859-1") + (xff_1 <=> xff_2).should == -1 + (xff_2 <=> xff_1).should == 1 + end + + it "returns 0 when comparing 2 empty strings but one is not ASCII-compatible" do + ("" <=> "".dup.force_encoding('iso-2022-jp')).should == 0 + end +end + +# Note: This is inconsistent with Array#<=> which calls #to_ary instead of +# just using it as an indicator. +describe "String#<=>" do + it "returns nil if its argument provides neither #to_str nor #<=>" do + ("abc" <=> mock('x')).should be_nil + end + + it "uses the result of calling #to_str for comparison when #to_str is defined" do + obj = mock('x') + obj.should_receive(:to_str).and_return("aaa") + + ("abc" <=> obj).should == 1 + end + + it "uses the result of calling #<=> on its argument when #<=> is defined but #to_str is not" do + obj = mock('x') + obj.should_receive(:<=>).and_return(-1) + + ("abc" <=> obj).should == 1 + end + + it "returns nil if argument also uses an inverse comparison for <=>" do + obj = mock('x') + def obj.<=>(other); other <=> self; end + obj.should_receive(:<=>).once + + ("abc" <=> obj).should be_nil + end +end diff --git a/spec/ruby/core/string/concat_spec.rb b/spec/ruby/core/string/concat_spec.rb new file mode 100644 index 0000000000..cbd7df54e2 --- /dev/null +++ b/spec/ruby/core/string/concat_spec.rb @@ -0,0 +1,27 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/concat' + +describe "String#concat" do + it_behaves_like :string_concat, :concat + it_behaves_like :string_concat_encoding, :concat + it_behaves_like :string_concat_type_coercion, :concat + + it "takes multiple arguments" do + str = +"hello " + str.concat "wo", "", "rld" + str.should == "hello world" + end + + it "concatenates the initial value when given arguments contain 2 self" do + str = +"hello" + str.concat str, str + str.should == "hellohellohello" + end + + it "returns self when given no arguments" do + str = +"hello" + str.concat.should equal(str) + str.should == "hello" + end +end diff --git a/spec/ruby/core/string/count_spec.rb b/spec/ruby/core/string/count_spec.rb new file mode 100644 index 0000000000..e614e901dd --- /dev/null +++ b/spec/ruby/core/string/count_spec.rb @@ -0,0 +1,105 @@ +# encoding: binary +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "String#count" do + it "counts occurrences of chars from the intersection of the specified sets" do + s = "hello\nworld\x00\x00" + + s.count(s).should == s.size + s.count("lo").should == 5 + s.count("eo").should == 3 + s.count("l").should == 3 + s.count("\n").should == 1 + s.count("\x00").should == 2 + + s.count("").should == 0 + "".count("").should == 0 + + s.count("l", "lo").should == s.count("l") + s.count("l", "lo", "o").should == s.count("") + s.count("helo", "hel", "h").should == s.count("h") + s.count("helo", "", "x").should == 0 + end + + it "raises an ArgumentError when given no arguments" do + -> { "hell yeah".count }.should raise_error(ArgumentError) + end + + it "negates sets starting with ^" do + s = "^hello\nworld\x00\x00" + + s.count("^").should == 1 # no negation, counts ^ + + s.count("^leh").should == 9 + s.count("^o").should == 12 + + s.count("helo", "^el").should == s.count("ho") + s.count("aeiou", "^e").should == s.count("aiou") + + "^_^".count("^^").should == 1 + "oa^_^o".count("a^").should == 3 + end + + it "counts all chars in a sequence" do + s = "hel-[()]-lo012^" + + s.count("\x00-\xFF").should == s.size + s.count("ej-m").should == 3 + s.count("e-h").should == 2 + + # no sequences + s.count("-").should == 2 + s.count("e-").should == s.count("e") + s.count("-") + s.count("-h").should == s.count("h") + s.count("-") + + s.count("---").should == s.count("-") + + # see an ASCII table for reference + s.count("--2").should == s.count("-./012") + s.count("(--").should == s.count("()*+,-") + s.count("A-a").should == s.count("A-Z[\\]^_`a") + + # negated sequences + s.count("^e-h").should == s.size - s.count("e-h") + s.count("^^-^").should == s.size - s.count("^") + s.count("^---").should == s.size - s.count("-") + + "abcdefgh".count("a-ce-fh").should == 6 + "abcdefgh".count("he-fa-c").should == 6 + "abcdefgh".count("e-fha-c").should == 6 + + "abcde".count("ac-e").should == 4 + "abcde".count("^ac-e").should == 1 + end + + it "raises if the given sequences are invalid" do + s = "hel-[()]-lo012^" + + -> { s.count("h-e") }.should raise_error(ArgumentError) + -> { s.count("^h-e") }.should raise_error(ArgumentError) + end + + it 'returns the number of occurrences of a multi-byte character' do + str = "\u{2605}" + str.count(str).should == 1 + "asd#{str}zzz#{str}ggg".count(str).should == 2 + end + + it "calls #to_str to convert each set arg to a String" do + other_string = mock('lo') + other_string.should_receive(:to_str).and_return("lo") + + other_string2 = mock('o') + other_string2.should_receive(:to_str).and_return("o") + + s = "hello world" + s.count(other_string, other_string2).should == s.count("o") + end + + it "raises a TypeError when a set arg can't be converted to a string" do + -> { "hello world".count(100) }.should raise_error(TypeError) + -> { "hello world".count([]) }.should raise_error(TypeError) + -> { "hello world".count(mock('x')) }.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/string/crypt_spec.rb b/spec/ruby/core/string/crypt_spec.rb new file mode 100644 index 0000000000..06f84c70a4 --- /dev/null +++ b/spec/ruby/core/string/crypt_spec.rb @@ -0,0 +1,92 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "String#crypt" do + platform_is :openbsd do + it "returns a cryptographic hash of self by applying the bcrypt algorithm with the specified salt" do + "mypassword".crypt("$2a$04$0WVaz0pV3jzfZ5G5tpmHWu").should == "$2a$04$0WVaz0pV3jzfZ5G5tpmHWuBQGbkjzgtSc3gJbmdy0GAGMa45MFM2." + + # Only uses first 72 characters of string + ("12345678"*9).crypt("$2a$04$0WVaz0pV3jzfZ5G5tpmHWu").should == "$2a$04$0WVaz0pV3jzfZ5G5tpmHWukj/ORBnsMjCGpST/zCJnAypc7eAbutK" + ("12345678"*10).crypt("$2a$04$0WVaz0pV3jzfZ5G5tpmHWu").should == "$2a$04$0WVaz0pV3jzfZ5G5tpmHWukj/ORBnsMjCGpST/zCJnAypc7eAbutK" + + # Only uses first 29 characters of salt + "mypassword".crypt("$2a$04$0WVaz0pV3jzfZ5G5tpmHWuB").should == "$2a$04$0WVaz0pV3jzfZ5G5tpmHWuBQGbkjzgtSc3gJbmdy0GAGMa45MFM2." + end + + it "raises Errno::EINVAL when the salt is shorter than 29 characters" do + -> { "mypassword".crypt("$2a$04$0WVaz0pV3jzfZ5G5tpmHW") }.should raise_error(Errno::EINVAL) + end + + it "calls #to_str to converts the salt arg to a String" do + obj = mock('$2a$04$0WVaz0pV3jzfZ5G5tpmHWu') + obj.should_receive(:to_str).and_return("$2a$04$0WVaz0pV3jzfZ5G5tpmHWu") + + "mypassword".crypt(obj).should == "$2a$04$0WVaz0pV3jzfZ5G5tpmHWuBQGbkjzgtSc3gJbmdy0GAGMa45MFM2." + end + + it "doesn't return subclass instances" do + StringSpecs::MyString.new("mypassword").crypt("$2a$04$0WVaz0pV3jzfZ5G5tpmHWu").should be_an_instance_of(String) + "mypassword".crypt(StringSpecs::MyString.new("$2a$04$0WVaz0pV3jzfZ5G5tpmHWu")).should be_an_instance_of(String) + StringSpecs::MyString.new("mypassword").crypt(StringSpecs::MyString.new("$2a$04$0WVaz0pV3jzfZ5G5tpmHWu")).should be_an_instance_of(String) + end + end + + platform_is_not :openbsd do + # Note: MRI's documentation just says that the C stdlib function crypt() is + # called. + # + # I'm not sure if crypt() is guaranteed to produce the same result across + # different platforms. It seems that there is one standard UNIX implementation + # of crypt(), but that alternative implementations are possible. See + # http://www.unix.org.ua/orelly/networking/puis/ch08_06.htm + it "returns a cryptographic hash of self by applying the UNIX crypt algorithm with the specified salt" do + "".crypt("aa").should == "aaQSqAReePlq6" + "nutmeg".crypt("Mi").should == "MiqkFWCm1fNJI" + "ellen1".crypt("ri").should == "ri79kNd7V6.Sk" + "Sharon".crypt("./").should == "./UY9Q7TvYJDg" + "norahs".crypt("am").should == "amfIADT2iqjA." + "norahs".crypt("7a").should == "7azfT5tIdyh0I" + + # Only uses first 8 chars of string + "01234567".crypt("aa").should == "aa4c4gpuvCkSE" + "012345678".crypt("aa").should == "aa4c4gpuvCkSE" + "0123456789".crypt("aa").should == "aa4c4gpuvCkSE" + + # Only uses first 2 chars of salt + "hello world".crypt("aa").should == "aayPz4hyPS1wI" + "hello world".crypt("aab").should == "aayPz4hyPS1wI" + "hello world".crypt("aabc").should == "aayPz4hyPS1wI" + end + + it "raises an ArgumentError when the string contains NUL character" do + -> { "poison\0null".crypt("aa") }.should raise_error(ArgumentError) + end + + it "calls #to_str to converts the salt arg to a String" do + obj = mock('aa') + obj.should_receive(:to_str).and_return("aa") + + "".crypt(obj).should == "aaQSqAReePlq6" + end + + it "doesn't return subclass instances" do + StringSpecs::MyString.new("hello").crypt("aa").should be_an_instance_of(String) + "hello".crypt(StringSpecs::MyString.new("aa")).should be_an_instance_of(String) + StringSpecs::MyString.new("hello").crypt(StringSpecs::MyString.new("aa")).should be_an_instance_of(String) + end + + it "raises an ArgumentError when the salt is shorter than two characters" do + -> { "hello".crypt("") }.should raise_error(ArgumentError) + -> { "hello".crypt("f") }.should raise_error(ArgumentError) + -> { "hello".crypt("\x00\x00") }.should raise_error(ArgumentError) + -> { "hello".crypt("\x00a") }.should raise_error(ArgumentError) + -> { "hello".crypt("a\x00") }.should raise_error(ArgumentError) + end + end + + it "raises a type error when the salt arg can't be converted to a string" do + -> { "".crypt(5) }.should raise_error(TypeError) + -> { "".crypt(mock('x')) }.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/string/dedup_spec.rb b/spec/ruby/core/string/dedup_spec.rb new file mode 100644 index 0000000000..2b31d80708 --- /dev/null +++ b/spec/ruby/core/string/dedup_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/dedup' + +describe 'String#dedup' do + it_behaves_like :string_dedup, :dedup +end diff --git a/spec/ruby/core/string/delete_prefix_spec.rb b/spec/ruby/core/string/delete_prefix_spec.rb new file mode 100644 index 0000000000..ee7f044905 --- /dev/null +++ b/spec/ruby/core/string/delete_prefix_spec.rb @@ -0,0 +1,83 @@ +# -*- encoding: utf-8 -*- +# frozen_string_literal: false +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "String#delete_prefix" do + it "returns a copy of the string, with the given prefix removed" do + 'hello'.delete_prefix('hell').should == 'o' + 'hello'.delete_prefix('hello').should == '' + end + + it "returns a copy of the string, when the prefix isn't found" do + s = 'hello' + r = s.delete_prefix('hello!') + r.should_not equal s + r.should == s + r = s.delete_prefix('ell') + r.should_not equal s + r.should == s + r = s.delete_prefix('') + r.should_not equal s + r.should == s + end + + it "does not remove partial bytes, only full characters" do + "\xe3\x81\x82".delete_prefix("\xe3").should == "\xe3\x81\x82" + end + + it "doesn't set $~" do + $~ = nil + + 'hello'.delete_prefix('hell') + $~.should == nil + end + + it "calls to_str on its argument" do + o = mock('x') + o.should_receive(:to_str).and_return 'hell' + 'hello'.delete_prefix(o).should == 'o' + end + + it "returns a String instance when called on a subclass instance" do + s = StringSpecs::MyString.new('hello') + s.delete_prefix('hell').should be_an_instance_of(String) + end + + it "returns a String in the same encoding as self" do + 'hello'.encode("US-ASCII").delete_prefix('hell').encoding.should == Encoding::US_ASCII + end +end + +describe "String#delete_prefix!" do + it "removes the found prefix" do + s = 'hello' + s.delete_prefix!('hell').should equal(s) + s.should == 'o' + end + + it "returns nil if no change is made" do + s = 'hello' + s.delete_prefix!('ell').should == nil + s.delete_prefix!('').should == nil + end + + it "doesn't set $~" do + $~ = nil + + 'hello'.delete_prefix!('hell') + $~.should == nil + end + + it "calls to_str on its argument" do + o = mock('x') + o.should_receive(:to_str).and_return 'hell' + 'hello'.delete_prefix!(o).should == 'o' + end + + it "raises a FrozenError when self is frozen" do + -> { 'hello'.freeze.delete_prefix!('hell') }.should raise_error(FrozenError) + -> { 'hello'.freeze.delete_prefix!('') }.should raise_error(FrozenError) + -> { ''.freeze.delete_prefix!('') }.should raise_error(FrozenError) + end +end diff --git a/spec/ruby/core/string/delete_spec.rb b/spec/ruby/core/string/delete_spec.rb new file mode 100644 index 0000000000..6d359776e4 --- /dev/null +++ b/spec/ruby/core/string/delete_spec.rb @@ -0,0 +1,117 @@ +# -*- encoding: utf-8 -*- +# frozen_string_literal: false +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "String#delete" do + it "returns a new string with the chars from the intersection of sets removed" do + s = "hello" + s.delete("lo").should == "he" + s.should == "hello" + + "hello".delete("l", "lo").should == "heo" + + "hell yeah".delete("").should == "hell yeah" + end + + it "raises an ArgumentError when given no arguments" do + -> { "hell yeah".delete }.should raise_error(ArgumentError) + end + + it "negates sets starting with ^" do + "hello".delete("aeiou", "^e").should == "hell" + "hello".delete("^leh").should == "hell" + "hello".delete("^o").should == "o" + "hello".delete("^").should == "hello" + "^_^".delete("^^").should == "^^" + "oa^_^o".delete("a^").should == "o_o" + end + + it "deletes all chars in a sequence" do + "hello".delete("ej-m").should == "ho" + "hello".delete("e-h").should == "llo" + "hel-lo".delete("e-").should == "hllo" + "hel-lo".delete("-h").should == "ello" + "hel-lo".delete("---").should == "hello" + "hel-012".delete("--2").should == "hel" + "hel-()".delete("(--").should == "hel" + "hello".delete("^e-h").should == "he" + "hello^".delete("^^-^").should == "^" + "hel--lo".delete("^---").should == "--" + + "abcdefgh".delete("a-ce-fh").should == "dg" + "abcdefgh".delete("he-fa-c").should == "dg" + "abcdefgh".delete("e-fha-c").should == "dg" + + "abcde".delete("ac-e").should == "b" + "abcde".delete("^ac-e").should == "acde" + + "ABCabc[]".delete("A-a").should == "bc" + end + + it "deletes multibyte characters" do + "四月".delete("月").should == "å››" + '哥哥我倒'.delete('å“¥').should == "我倒" + end + + it "respects backslash for escaping a -" do + 'Non-Authoritative Information'.delete(' \-\'').should == + 'NonAuthoritativeInformation' + end + + it "raises if the given ranges are invalid" do + not_supported_on :opal do + xFF = [0xFF].pack('C') + range = "\x00 - #{xFF}".force_encoding('utf-8') + -> { "hello".delete(range).should == "" }.should raise_error(ArgumentError) + end + -> { "hello".delete("h-e") }.should raise_error(ArgumentError) + -> { "hello".delete("^h-e") }.should raise_error(ArgumentError) + end + + it "tries to convert each set arg to a string using to_str" do + other_string = mock('lo') + other_string.should_receive(:to_str).and_return("lo") + + other_string2 = mock('o') + other_string2.should_receive(:to_str).and_return("o") + + "hello world".delete(other_string, other_string2).should == "hell wrld" + end + + it "raises a TypeError when one set arg can't be converted to a string" do + -> { "hello world".delete(100) }.should raise_error(TypeError) + -> { "hello world".delete([]) }.should raise_error(TypeError) + -> { "hello world".delete(mock('x')) }.should raise_error(TypeError) + end + + it "returns String instances when called on a subclass" do + StringSpecs::MyString.new("oh no!!!").delete("!").should be_an_instance_of(String) + end + + it "returns a String in the same encoding as self" do + "hello".encode("US-ASCII").delete("lo").encoding.should == Encoding::US_ASCII + end +end + +describe "String#delete!" do + it "modifies self in place and returns self" do + a = "hello" + a.delete!("aeiou", "^e").should equal(a) + a.should == "hell" + end + + it "returns nil if no modifications were made" do + a = "hello" + a.delete!("z").should == nil + a.should == "hello" + end + + it "raises a FrozenError when self is frozen" do + a = "hello" + a.freeze + + -> { a.delete!("") }.should raise_error(FrozenError) + -> { a.delete!("aeiou", "^e") }.should raise_error(FrozenError) + end +end diff --git a/spec/ruby/core/string/delete_suffix_spec.rb b/spec/ruby/core/string/delete_suffix_spec.rb new file mode 100644 index 0000000000..1842d75aa5 --- /dev/null +++ b/spec/ruby/core/string/delete_suffix_spec.rb @@ -0,0 +1,83 @@ +# -*- encoding: utf-8 -*- +# frozen_string_literal: false +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "String#delete_suffix" do + it "returns a copy of the string, with the given suffix removed" do + 'hello'.delete_suffix('ello').should == 'h' + 'hello'.delete_suffix('hello').should == '' + end + + it "returns a copy of the string, when the suffix isn't found" do + s = 'hello' + r = s.delete_suffix('!hello') + r.should_not equal s + r.should == s + r = s.delete_suffix('ell') + r.should_not equal s + r.should == s + r = s.delete_suffix('') + r.should_not equal s + r.should == s + end + + it "does not remove partial bytes, only full characters" do + "\xe3\x81\x82".delete_suffix("\x82").should == "\xe3\x81\x82" + end + + it "doesn't set $~" do + $~ = nil + + 'hello'.delete_suffix('ello') + $~.should == nil + end + + it "calls to_str on its argument" do + o = mock('x') + o.should_receive(:to_str).and_return 'ello' + 'hello'.delete_suffix(o).should == 'h' + end + + it "returns a String instance when called on a subclass instance" do + s = StringSpecs::MyString.new('hello') + s.delete_suffix('ello').should be_an_instance_of(String) + end + + it "returns a String in the same encoding as self" do + "hello".encode("US-ASCII").delete_suffix("ello").encoding.should == Encoding::US_ASCII + end +end + +describe "String#delete_suffix!" do + it "removes the found prefix" do + s = 'hello' + s.delete_suffix!('ello').should equal(s) + s.should == 'h' + end + + it "returns nil if no change is made" do + s = 'hello' + s.delete_suffix!('ell').should == nil + s.delete_suffix!('').should == nil + end + + it "doesn't set $~" do + $~ = nil + + 'hello'.delete_suffix!('ello') + $~.should == nil + end + + it "calls to_str on its argument" do + o = mock('x') + o.should_receive(:to_str).and_return 'ello' + 'hello'.delete_suffix!(o).should == 'h' + end + + it "raises a FrozenError when self is frozen" do + -> { 'hello'.freeze.delete_suffix!('ello') }.should raise_error(FrozenError) + -> { 'hello'.freeze.delete_suffix!('') }.should raise_error(FrozenError) + -> { ''.freeze.delete_suffix!('') }.should raise_error(FrozenError) + end +end diff --git a/spec/ruby/core/string/downcase_spec.rb b/spec/ruby/core/string/downcase_spec.rb new file mode 100644 index 0000000000..2d260f23f1 --- /dev/null +++ b/spec/ruby/core/string/downcase_spec.rb @@ -0,0 +1,195 @@ +# -*- encoding: utf-8 -*- +# frozen_string_literal: false +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "String#downcase" do + it "returns a copy of self with all uppercase letters downcased" do + "hELLO".downcase.should == "hello" + "hello".downcase.should == "hello" + end + + it "returns a String in the same encoding as self" do + "hELLO".encode("US-ASCII").downcase.encoding.should == Encoding::US_ASCII + end + + describe "full Unicode case mapping" do + it "works for all of Unicode with no option" do + "ÄÖÜ".downcase.should == "äöü" + end + + it "updates string metadata" do + downcased = "\u{212A}ING".downcase + + downcased.should == "king" + downcased.size.should == 4 + downcased.bytesize.should == 4 + downcased.ascii_only?.should be_true + end + end + + describe "ASCII-only case mapping" do + it "does not downcase non-ASCII characters" do + "Câ„«R".downcase(:ascii).should == "câ„«r" + end + + it "works with substrings" do + "prefix TÉ"[-2..-1].downcase(:ascii).should == "tÉ" + end + end + + describe "full Unicode case mapping adapted for Turkic languages" do + it "downcases characters according to Turkic semantics" do + "İ".downcase(:turkic).should == "i" + end + + it "allows Lithuanian as an extra option" do + "İ".downcase(:turkic, :lithuanian).should == "i" + end + + it "does not allow any other additional option" do + -> { "İ".downcase(:turkic, :ascii) }.should raise_error(ArgumentError) + end + end + + describe "full Unicode case mapping adapted for Lithuanian" do + it "currently works the same as full Unicode case mapping" do + "İS".downcase(:lithuanian).should == "i\u{307}s" + end + + it "allows Turkic as an extra option (and applies Turkic semantics)" do + "İS".downcase(:lithuanian, :turkic).should == "is" + end + + it "does not allow any other additional option" do + -> { "İS".downcase(:lithuanian, :ascii) }.should raise_error(ArgumentError) + end + end + + describe "case folding" do + it "case folds special characters" do + "ß".downcase.should == "ß" + "ß".downcase(:fold).should == "ss" + end + end + + it "does not allow invalid options" do + -> { "ABC".downcase(:invalid_option) }.should raise_error(ArgumentError) + end + + it "returns a String instance for subclasses" do + StringSpecs::MyString.new("FOObar").downcase.should be_an_instance_of(String) + end +end + +describe "String#downcase!" do + it "modifies self in place" do + a = "HeLlO" + a.downcase!.should equal(a) + a.should == "hello" + end + + it "modifies self in place for non-ascii-compatible encodings" do + a = "HeLlO".encode("utf-16le") + a.downcase! + a.should == "hello".encode("utf-16le") + end + + describe "full Unicode case mapping" do + it "modifies self in place for all of Unicode with no option" do + a = "ÄÖÜ" + a.downcase! + a.should == "äöü" + end + + it "updates string metadata" do + downcased = "\u{212A}ING" + downcased.downcase! + + downcased.should == "king" + downcased.size.should == 4 + downcased.bytesize.should == 4 + downcased.ascii_only?.should be_true + end + end + + describe "ASCII-only case mapping" do + it "does not downcase non-ASCII characters" do + a = "Câ„«R" + a.downcase!(:ascii) + a.should == "câ„«r" + end + + it "works for non-ascii-compatible encodings" do + a = "ABC".encode("utf-16le") + a.downcase!(:ascii) + a.should == "abc".encode("utf-16le") + end + end + + describe "full Unicode case mapping adapted for Turkic languages" do + it "downcases characters according to Turkic semantics" do + a = "İ" + a.downcase!(:turkic) + a.should == "i" + end + + it "allows Lithuanian as an extra option" do + a = "İ" + a.downcase!(:turkic, :lithuanian) + a.should == "i" + end + + it "does not allow any other additional option" do + -> { a = "İ"; a.downcase!(:turkic, :ascii) }.should raise_error(ArgumentError) + end + end + + describe "full Unicode case mapping adapted for Lithuanian" do + it "currently works the same as full Unicode case mapping" do + a = "İS" + a.downcase!(:lithuanian) + a.should == "i\u{307}s" + end + + it "allows Turkic as an extra option (and applies Turkic semantics)" do + a = "İS" + a.downcase!(:lithuanian, :turkic) + a.should == "is" + end + + it "does not allow any other additional option" do + -> { a = "İS"; a.downcase!(:lithuanian, :ascii) }.should raise_error(ArgumentError) + end + end + + describe "case folding" do + it "case folds special characters" do + a = "ß" + a.downcase! + a.should == "ß" + + a.downcase!(:fold) + a.should == "ss" + end + end + + it "does not allow invalid options" do + -> { a = "ABC"; a.downcase!(:invalid_option) }.should raise_error(ArgumentError) + end + + it "returns nil if no modifications were made" do + a = "hello" + a.downcase!.should == nil + a.should == "hello" + end + + it "raises a FrozenError when self is frozen" do + -> { "HeLlo".freeze.downcase! }.should raise_error(FrozenError) + -> { "hello".freeze.downcase! }.should raise_error(FrozenError) + end + + it "sets the result String encoding to the source String encoding" do + "ABC".downcase.encoding.should equal(Encoding::UTF_8) + end +end diff --git a/spec/ruby/core/string/dump_spec.rb b/spec/ruby/core/string/dump_spec.rb new file mode 100644 index 0000000000..cab8beff5a --- /dev/null +++ b/spec/ruby/core/string/dump_spec.rb @@ -0,0 +1,396 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "String#dump" do + it "does not take into account if a string is frozen" do + "foo".freeze.dump.should_not.frozen? + end + + it "returns a String instance" do + StringSpecs::MyString.new.dump.should be_an_instance_of(String) + end + + it "wraps string with \"" do + "foo".dump.should == '"foo"' + end + + it "returns a string with special characters replaced with \\<char> notation" do + [ ["\a", '"\\a"'], + ["\b", '"\\b"'], + ["\t", '"\\t"'], + ["\n", '"\\n"'], + ["\v", '"\\v"'], + ["\f", '"\\f"'], + ["\r", '"\\r"'], + ["\e", '"\\e"'] + ].should be_computed_by(:dump) + end + + it "returns a string with \" and \\ escaped with a backslash" do + [ ["\"", '"\\""'], + ["\\", '"\\\\"'] + ].should be_computed_by(:dump) + end + + it "returns a string with \\#<char> when # is followed by $, @, @@, {" do + [ ["\#$PATH", '"\\#$PATH"'], + ["\#@a", '"\\#@a"'], + ["\#@@a", '"\\#@@a"'], + ["\#{a}", '"\\#{a}"'] + ].should be_computed_by(:dump) + end + + it "returns a string with # not escaped when followed by any other character" do + [ ["#", '"#"'], + ["#1", '"#1"'] + ].should be_computed_by(:dump) + end + + it "returns a string with printable non-alphanumeric characters unescaped" do + [ [" ", '" "'], + ["!", '"!"'], + ["$", '"$"'], + ["%", '"%"'], + ["&", '"&"'], + ["'", '"\'"'], + ["(", '"("'], + [")", '")"'], + ["*", '"*"'], + ["+", '"+"'], + [",", '","'], + ["-", '"-"'], + [".", '"."'], + ["/", '"/"'], + [":", '":"'], + [";", '";"'], + ["<", '"<"'], + ["=", '"="'], + [">", '">"'], + ["?", '"?"'], + ["@", '"@"'], + ["[", '"["'], + ["]", '"]"'], + ["^", '"^"'], + ["_", '"_"'], + ["`", '"`"'], + ["{", '"{"'], + ["|", '"|"'], + ["}", '"}"'], + ["~", '"~"'] + ].should be_computed_by(:dump) + end + + it "returns a string with numeric characters unescaped" do + [ ["0", '"0"'], + ["1", '"1"'], + ["2", '"2"'], + ["3", '"3"'], + ["4", '"4"'], + ["5", '"5"'], + ["6", '"6"'], + ["7", '"7"'], + ["8", '"8"'], + ["9", '"9"'], + ].should be_computed_by(:dump) + end + + it "returns a string with upper-case alpha characters unescaped" do + [ ["A", '"A"'], + ["B", '"B"'], + ["C", '"C"'], + ["D", '"D"'], + ["E", '"E"'], + ["F", '"F"'], + ["G", '"G"'], + ["H", '"H"'], + ["I", '"I"'], + ["J", '"J"'], + ["K", '"K"'], + ["L", '"L"'], + ["M", '"M"'], + ["N", '"N"'], + ["O", '"O"'], + ["P", '"P"'], + ["Q", '"Q"'], + ["R", '"R"'], + ["S", '"S"'], + ["T", '"T"'], + ["U", '"U"'], + ["V", '"V"'], + ["W", '"W"'], + ["X", '"X"'], + ["Y", '"Y"'], + ["Z", '"Z"'] + ].should be_computed_by(:dump) + end + + it "returns a string with lower-case alpha characters unescaped" do + [ ["a", '"a"'], + ["b", '"b"'], + ["c", '"c"'], + ["d", '"d"'], + ["e", '"e"'], + ["f", '"f"'], + ["g", '"g"'], + ["h", '"h"'], + ["i", '"i"'], + ["j", '"j"'], + ["k", '"k"'], + ["l", '"l"'], + ["m", '"m"'], + ["n", '"n"'], + ["o", '"o"'], + ["p", '"p"'], + ["q", '"q"'], + ["r", '"r"'], + ["s", '"s"'], + ["t", '"t"'], + ["u", '"u"'], + ["v", '"v"'], + ["w", '"w"'], + ["x", '"x"'], + ["y", '"y"'], + ["z", '"z"'] + ].should be_computed_by(:dump) + end + + it "returns a string with non-printing ASCII characters replaced by \\x notation" do + # Avoid the file encoding by computing the string with #chr. + [ [0000.chr, '"\\x00"'], + [0001.chr, '"\\x01"'], + [0002.chr, '"\\x02"'], + [0003.chr, '"\\x03"'], + [0004.chr, '"\\x04"'], + [0005.chr, '"\\x05"'], + [0006.chr, '"\\x06"'], + [0016.chr, '"\\x0E"'], + [0017.chr, '"\\x0F"'], + [0020.chr, '"\\x10"'], + [0021.chr, '"\\x11"'], + [0022.chr, '"\\x12"'], + [0023.chr, '"\\x13"'], + [0024.chr, '"\\x14"'], + [0025.chr, '"\\x15"'], + [0026.chr, '"\\x16"'], + [0027.chr, '"\\x17"'], + [0030.chr, '"\\x18"'], + [0031.chr, '"\\x19"'], + [0032.chr, '"\\x1A"'], + [0034.chr, '"\\x1C"'], + [0035.chr, '"\\x1D"'], + [0036.chr, '"\\x1E"'], + [0037.chr, '"\\x1F"'], + [0177.chr, '"\\x7F"'], + [0200.chr, '"\\x80"'], + [0201.chr, '"\\x81"'], + [0202.chr, '"\\x82"'], + [0203.chr, '"\\x83"'], + [0204.chr, '"\\x84"'], + [0205.chr, '"\\x85"'], + [0206.chr, '"\\x86"'], + [0207.chr, '"\\x87"'], + [0210.chr, '"\\x88"'], + [0211.chr, '"\\x89"'], + [0212.chr, '"\\x8A"'], + [0213.chr, '"\\x8B"'], + [0214.chr, '"\\x8C"'], + [0215.chr, '"\\x8D"'], + [0216.chr, '"\\x8E"'], + [0217.chr, '"\\x8F"'], + [0220.chr, '"\\x90"'], + [0221.chr, '"\\x91"'], + [0222.chr, '"\\x92"'], + [0223.chr, '"\\x93"'], + [0224.chr, '"\\x94"'], + [0225.chr, '"\\x95"'], + [0226.chr, '"\\x96"'], + [0227.chr, '"\\x97"'], + [0230.chr, '"\\x98"'], + [0231.chr, '"\\x99"'], + [0232.chr, '"\\x9A"'], + [0233.chr, '"\\x9B"'], + [0234.chr, '"\\x9C"'], + [0235.chr, '"\\x9D"'], + [0236.chr, '"\\x9E"'], + [0237.chr, '"\\x9F"'], + [0240.chr, '"\\xA0"'], + [0241.chr, '"\\xA1"'], + [0242.chr, '"\\xA2"'], + [0243.chr, '"\\xA3"'], + [0244.chr, '"\\xA4"'], + [0245.chr, '"\\xA5"'], + [0246.chr, '"\\xA6"'], + [0247.chr, '"\\xA7"'], + [0250.chr, '"\\xA8"'], + [0251.chr, '"\\xA9"'], + [0252.chr, '"\\xAA"'], + [0253.chr, '"\\xAB"'], + [0254.chr, '"\\xAC"'], + [0255.chr, '"\\xAD"'], + [0256.chr, '"\\xAE"'], + [0257.chr, '"\\xAF"'], + [0260.chr, '"\\xB0"'], + [0261.chr, '"\\xB1"'], + [0262.chr, '"\\xB2"'], + [0263.chr, '"\\xB3"'], + [0264.chr, '"\\xB4"'], + [0265.chr, '"\\xB5"'], + [0266.chr, '"\\xB6"'], + [0267.chr, '"\\xB7"'], + [0270.chr, '"\\xB8"'], + [0271.chr, '"\\xB9"'], + [0272.chr, '"\\xBA"'], + [0273.chr, '"\\xBB"'], + [0274.chr, '"\\xBC"'], + [0275.chr, '"\\xBD"'], + [0276.chr, '"\\xBE"'], + [0277.chr, '"\\xBF"'], + [0300.chr, '"\\xC0"'], + [0301.chr, '"\\xC1"'], + [0302.chr, '"\\xC2"'], + [0303.chr, '"\\xC3"'], + [0304.chr, '"\\xC4"'], + [0305.chr, '"\\xC5"'], + [0306.chr, '"\\xC6"'], + [0307.chr, '"\\xC7"'], + [0310.chr, '"\\xC8"'], + [0311.chr, '"\\xC9"'], + [0312.chr, '"\\xCA"'], + [0313.chr, '"\\xCB"'], + [0314.chr, '"\\xCC"'], + [0315.chr, '"\\xCD"'], + [0316.chr, '"\\xCE"'], + [0317.chr, '"\\xCF"'], + [0320.chr, '"\\xD0"'], + [0321.chr, '"\\xD1"'], + [0322.chr, '"\\xD2"'], + [0323.chr, '"\\xD3"'], + [0324.chr, '"\\xD4"'], + [0325.chr, '"\\xD5"'], + [0326.chr, '"\\xD6"'], + [0327.chr, '"\\xD7"'], + [0330.chr, '"\\xD8"'], + [0331.chr, '"\\xD9"'], + [0332.chr, '"\\xDA"'], + [0333.chr, '"\\xDB"'], + [0334.chr, '"\\xDC"'], + [0335.chr, '"\\xDD"'], + [0336.chr, '"\\xDE"'], + [0337.chr, '"\\xDF"'], + [0340.chr, '"\\xE0"'], + [0341.chr, '"\\xE1"'], + [0342.chr, '"\\xE2"'], + [0343.chr, '"\\xE3"'], + [0344.chr, '"\\xE4"'], + [0345.chr, '"\\xE5"'], + [0346.chr, '"\\xE6"'], + [0347.chr, '"\\xE7"'], + [0350.chr, '"\\xE8"'], + [0351.chr, '"\\xE9"'], + [0352.chr, '"\\xEA"'], + [0353.chr, '"\\xEB"'], + [0354.chr, '"\\xEC"'], + [0355.chr, '"\\xED"'], + [0356.chr, '"\\xEE"'], + [0357.chr, '"\\xEF"'], + [0360.chr, '"\\xF0"'], + [0361.chr, '"\\xF1"'], + [0362.chr, '"\\xF2"'], + [0363.chr, '"\\xF3"'], + [0364.chr, '"\\xF4"'], + [0365.chr, '"\\xF5"'], + [0366.chr, '"\\xF6"'], + [0367.chr, '"\\xF7"'], + [0370.chr, '"\\xF8"'], + [0371.chr, '"\\xF9"'], + [0372.chr, '"\\xFA"'], + [0373.chr, '"\\xFB"'], + [0374.chr, '"\\xFC"'], + [0375.chr, '"\\xFD"'], + [0376.chr, '"\\xFE"'], + [0377.chr, '"\\xFF"'] + ].should be_computed_by(:dump) + end + + it "returns a string with non-printing single-byte UTF-8 characters replaced by \\x notation" do + [ [0000.chr('utf-8'), '"\x00"'], + [0001.chr('utf-8'), '"\x01"'], + [0002.chr('utf-8'), '"\x02"'], + [0003.chr('utf-8'), '"\x03"'], + [0004.chr('utf-8'), '"\x04"'], + [0005.chr('utf-8'), '"\x05"'], + [0006.chr('utf-8'), '"\x06"'], + [0016.chr('utf-8'), '"\x0E"'], + [0017.chr('utf-8'), '"\x0F"'], + [0020.chr('utf-8'), '"\x10"'], + [0021.chr('utf-8'), '"\x11"'], + [0022.chr('utf-8'), '"\x12"'], + [0023.chr('utf-8'), '"\x13"'], + [0024.chr('utf-8'), '"\x14"'], + [0025.chr('utf-8'), '"\x15"'], + [0026.chr('utf-8'), '"\x16"'], + [0027.chr('utf-8'), '"\x17"'], + [0030.chr('utf-8'), '"\x18"'], + [0031.chr('utf-8'), '"\x19"'], + [0032.chr('utf-8'), '"\x1A"'], + [0034.chr('utf-8'), '"\x1C"'], + [0035.chr('utf-8'), '"\x1D"'], + [0036.chr('utf-8'), '"\x1E"'], + [0037.chr('utf-8'), '"\x1F"'], + [0177.chr('utf-8'), '"\x7F"'] + ].should be_computed_by(:dump) + end + + it "returns a string with multi-byte UTF-8 characters less than or equal 0xFFFF replaced by \\uXXXX notation with upper-case hex digits" do + [ [0200.chr('utf-8'), '"\u0080"'], + [0201.chr('utf-8'), '"\u0081"'], + [0202.chr('utf-8'), '"\u0082"'], + [0203.chr('utf-8'), '"\u0083"'], + [0204.chr('utf-8'), '"\u0084"'], + [0206.chr('utf-8'), '"\u0086"'], + [0207.chr('utf-8'), '"\u0087"'], + [0210.chr('utf-8'), '"\u0088"'], + [0211.chr('utf-8'), '"\u0089"'], + [0212.chr('utf-8'), '"\u008A"'], + [0213.chr('utf-8'), '"\u008B"'], + [0214.chr('utf-8'), '"\u008C"'], + [0215.chr('utf-8'), '"\u008D"'], + [0216.chr('utf-8'), '"\u008E"'], + [0217.chr('utf-8'), '"\u008F"'], + [0220.chr('utf-8'), '"\u0090"'], + [0221.chr('utf-8'), '"\u0091"'], + [0222.chr('utf-8'), '"\u0092"'], + [0223.chr('utf-8'), '"\u0093"'], + [0224.chr('utf-8'), '"\u0094"'], + [0225.chr('utf-8'), '"\u0095"'], + [0226.chr('utf-8'), '"\u0096"'], + [0227.chr('utf-8'), '"\u0097"'], + [0230.chr('utf-8'), '"\u0098"'], + [0231.chr('utf-8'), '"\u0099"'], + [0232.chr('utf-8'), '"\u009A"'], + [0233.chr('utf-8'), '"\u009B"'], + [0234.chr('utf-8'), '"\u009C"'], + [0235.chr('utf-8'), '"\u009D"'], + [0236.chr('utf-8'), '"\u009E"'], + [0237.chr('utf-8'), '"\u009F"'], + [0177777.chr('utf-8'), '"\uFFFF"'], + ].should be_computed_by(:dump) + end + + it "returns a string with multi-byte UTF-8 characters greater than 0xFFFF replaced by \\u{XXXXXX} notation with upper-case hex digits" do + 0x10000.chr('utf-8').dump.should == '"\u{10000}"' + 0x10FFFF.chr('utf-8').dump.should == '"\u{10FFFF}"' + end + + it "includes .force_encoding(name) if the encoding isn't ASCII compatible" do + "\u{876}".encode('utf-16be').dump.should.end_with?(".force_encoding(\"UTF-16BE\")") + "\u{876}".encode('utf-16le').dump.should.end_with?(".force_encoding(\"UTF-16LE\")") + end + + it "returns a String in the same encoding as self" do + "foo".encode("ISO-8859-1").dump.encoding.should == Encoding::ISO_8859_1 + "foo".encode('windows-1251').dump.encoding.should == Encoding::Windows_1251 + 1.chr.dump.encoding.should == Encoding::US_ASCII + end +end diff --git a/spec/ruby/core/string/dup_spec.rb b/spec/ruby/core/string/dup_spec.rb new file mode 100644 index 0000000000..073802d84b --- /dev/null +++ b/spec/ruby/core/string/dup_spec.rb @@ -0,0 +1,65 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "String#dup" do + before :each do + ScratchPad.clear + @obj = StringSpecs::InitializeString.new "string" + end + + it "calls #initialize_copy on the new instance" do + dup = @obj.dup + ScratchPad.recorded.should_not == @obj.object_id + ScratchPad.recorded.should == dup.object_id + end + + it "copies instance variables" do + dup = @obj.dup + dup.ivar.should == 1 + end + + it "does not copy singleton methods" do + def @obj.special() :the_one end + dup = @obj.dup + -> { dup.special }.should raise_error(NameError) + end + + it "does not copy modules included in the singleton class" do + class << @obj + include StringSpecs::StringModule + end + + dup = @obj.dup + -> { dup.repr }.should raise_error(NameError) + end + + it "does not copy constants defined in the singleton class" do + class << @obj + CLONE = :clone + end + + dup = @obj.dup + -> { class << dup; CLONE; end }.should raise_error(NameError) + end + + it "does not modify the original string when changing dupped string" do + orig = "string"[0..100] + dup = orig.dup + orig[0] = 'x' + orig.should == "xtring" + dup.should == "string" + end + + it "does not modify the original setbyte-mutated string when changing dupped string" do + orig = +"a" + orig.setbyte 0, "b".ord + copy = orig.dup + orig.setbyte 0, "c".ord + orig.should == "c" + copy.should == "b" + end + + it "returns a String in the same encoding as self" do + "hello".encode("US-ASCII").dup.encoding.should == Encoding::US_ASCII + end +end diff --git a/spec/ruby/core/string/each_byte_spec.rb b/spec/ruby/core/string/each_byte_spec.rb new file mode 100644 index 0000000000..7b3db265ac --- /dev/null +++ b/spec/ruby/core/string/each_byte_spec.rb @@ -0,0 +1,61 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "String#each_byte" do + it "passes each byte in self to the given block" do + a = [] + "hello\x00".each_byte { |c| a << c } + a.should == [104, 101, 108, 108, 111, 0] + end + + it "keeps iterating from the old position (to new string end) when self changes" do + r = +"" + s = +"hello world" + s.each_byte do |c| + r << c + s.insert(0, "<>") if r.size < 3 + end + r.should == "h><>hello world" + + r = +"" + s = +"hello world" + s.each_byte { |c| s.slice!(-1); r << c } + r.should == "hello " + + r = +"" + s = +"hello world" + s.each_byte { |c| s.slice!(0); r << c } + r.should == "hlowrd" + + r = +"" + s = +"hello world" + s.each_byte { |c| s.slice!(0..-1); r << c } + r.should == "h" + end + + it "returns self" do + s = "hello" + (s.each_byte {}).should equal(s) + end + + describe "when no block is given" do + it "returns an enumerator" do + enum = "hello".each_byte + enum.should be_an_instance_of(Enumerator) + enum.to_a.should == [104, 101, 108, 108, 111] + end + + describe "returned enumerator" do + describe "size" do + it "should return the bytesize of the string" do + str = "hello" + str.each_byte.size.should == str.bytesize + str = "ola" + str.each_byte.size.should == str.bytesize + str = "\303\207\342\210\202\303\251\306\222g" + str.each_byte.size.should == str.bytesize + end + end + end + end +end diff --git a/spec/ruby/core/string/each_char_spec.rb b/spec/ruby/core/string/each_char_spec.rb new file mode 100644 index 0000000000..36219f79db --- /dev/null +++ b/spec/ruby/core/string/each_char_spec.rb @@ -0,0 +1,8 @@ +require_relative "../../spec_helper" +require_relative 'shared/chars' +require_relative 'shared/each_char_without_block' + +describe "String#each_char" do + it_behaves_like :string_chars, :each_char + it_behaves_like :string_each_char_without_block, :each_char +end diff --git a/spec/ruby/core/string/each_codepoint_spec.rb b/spec/ruby/core/string/each_codepoint_spec.rb new file mode 100644 index 0000000000..c11cb1beae --- /dev/null +++ b/spec/ruby/core/string/each_codepoint_spec.rb @@ -0,0 +1,8 @@ +require_relative '../../spec_helper' +require_relative 'shared/codepoints' +require_relative 'shared/each_codepoint_without_block' + +describe "String#each_codepoint" do + it_behaves_like :string_codepoints, :each_codepoint + it_behaves_like :string_each_codepoint_without_block, :each_codepoint +end diff --git a/spec/ruby/core/string/each_grapheme_cluster_spec.rb b/spec/ruby/core/string/each_grapheme_cluster_spec.rb new file mode 100644 index 0000000000..e1fa4ae67b --- /dev/null +++ b/spec/ruby/core/string/each_grapheme_cluster_spec.rb @@ -0,0 +1,16 @@ +require_relative "../../spec_helper" +require_relative 'shared/chars' +require_relative 'shared/grapheme_clusters' +require_relative 'shared/each_char_without_block' + +describe "String#each_grapheme_cluster" do + it_behaves_like :string_chars, :each_grapheme_cluster + it_behaves_like :string_grapheme_clusters, :each_grapheme_cluster + it_behaves_like :string_each_char_without_block, :each_grapheme_cluster + + it "yields String instances for subclasses" do + a = [] + StringSpecs::MyString.new("abc").each_grapheme_cluster { |s| a << s.class } + a.should == [String, String, String] + end +end diff --git a/spec/ruby/core/string/each_line_spec.rb b/spec/ruby/core/string/each_line_spec.rb new file mode 100644 index 0000000000..90fc920bf1 --- /dev/null +++ b/spec/ruby/core/string/each_line_spec.rb @@ -0,0 +1,9 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/each_line' +require_relative 'shared/each_line_without_block' + +describe "String#each_line" do + it_behaves_like :string_each_line, :each_line + it_behaves_like :string_each_line_without_block, :each_line +end diff --git a/spec/ruby/core/string/element_reference_spec.rb b/spec/ruby/core/string/element_reference_spec.rb new file mode 100644 index 0000000000..f6e1750c93 --- /dev/null +++ b/spec/ruby/core/string/element_reference_spec.rb @@ -0,0 +1,35 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/slice' + +describe "String#[]" do + it_behaves_like :string_slice, :[] +end + +describe "String#[] with index, length" do + it_behaves_like :string_slice_index_length, :[] +end + +describe "String#[] with Range" do + it_behaves_like :string_slice_range, :[] +end + +describe "String#[] with Regexp" do + it_behaves_like :string_slice_regexp, :[] +end + +describe "String#[] with Regexp, index" do + it_behaves_like :string_slice_regexp_index, :[] +end + +describe "String#[] with Regexp, group" do + it_behaves_like :string_slice_regexp_group, :[] +end + +describe "String#[] with String" do + it_behaves_like :string_slice_string, :[] +end + +describe "String#[] with Symbol" do + it_behaves_like :string_slice_symbol, :[] +end diff --git a/spec/ruby/core/string/element_set_spec.rb b/spec/ruby/core/string/element_set_spec.rb new file mode 100644 index 0000000000..e7599f832c --- /dev/null +++ b/spec/ruby/core/string/element_set_spec.rb @@ -0,0 +1,589 @@ +# -*- encoding: utf-8 -*- +# frozen_string_literal: false +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +# TODO: Add missing String#[]= specs: +# String#[re, idx] = obj + +describe "String#[]= with Integer index" do + it "replaces the char at idx with other_str" do + a = "hello" + a[0] = "bam" + a.should == "bamello" + a[-2] = "" + a.should == "bamelo" + end + + it "raises an IndexError without changing self if idx is outside of self" do + str = "hello" + + -> { str[20] = "bam" }.should raise_error(IndexError) + str.should == "hello" + + -> { str[-20] = "bam" }.should raise_error(IndexError) + str.should == "hello" + + -> { ""[-1] = "bam" }.should raise_error(IndexError) + end + + # Behaviour is verified by matz in + # http://redmine.ruby-lang.org/issues/show/1750 + it "allows assignment to the zero'th element of an empty String" do + str = "" + str[0] = "bam" + str.should == "bam" + end + + it "raises IndexError if the string index doesn't match a position in the string" do + str = "hello" + -> { str['y'] = "bam" }.should raise_error(IndexError) + str.should == "hello" + end + + it "raises a FrozenError when self is frozen" do + a = "hello" + a.freeze + + -> { a[0] = "bam" }.should raise_error(FrozenError) + end + + it "calls to_int on index" do + str = "hello" + str[0.5] = "hi " + str.should == "hi ello" + + obj = mock('-1') + obj.should_receive(:to_int).and_return(-1) + str[obj] = "!" + str.should == "hi ell!" + end + + it "calls #to_str to convert other to a String" do + other_str = mock('-test-') + other_str.should_receive(:to_str).and_return("-test-") + + a = "abc" + a[1] = other_str + a.should == "a-test-c" + end + + it "raises a TypeError if other_str can't be converted to a String" do + -> { "test"[1] = [] }.should raise_error(TypeError) + -> { "test"[1] = mock('x') }.should raise_error(TypeError) + -> { "test"[1] = nil }.should raise_error(TypeError) + end + + it "raises a TypeError if passed an Integer replacement" do + -> { "abc"[1] = 65 }.should raise_error(TypeError) + end + + it "raises an IndexError if the index is greater than character size" do + -> { "ã‚れ"[4] = "a" }.should raise_error(IndexError) + end + + it "calls #to_int to convert the index" do + index = mock("string element set") + index.should_receive(:to_int).and_return(1) + + str = "ã‚れ" + str[index] = "a" + str.should == "ã‚a" + end + + it "raises a TypeError if #to_int does not return an Integer" do + index = mock("string element set") + index.should_receive(:to_int).and_return('1') + + -> { "abc"[index] = "d" }.should raise_error(TypeError) + end + + it "raises an IndexError if #to_int returns a value out of range" do + index = mock("string element set") + index.should_receive(:to_int).and_return(4) + + -> { "ab"[index] = "c" }.should raise_error(IndexError) + end + + it "replaces a character with a multibyte character" do + str = "ã‚りãŒã¨u" + str[4] = "ã†" + str.should == "ã‚りãŒã¨ã†" + end + + it "replaces a multibyte character with a character" do + str = "ã‚りãŒã¨ã†" + str[4] = "u" + str.should == "ã‚りãŒã¨u" + end + + it "replaces a multibyte character with a multibyte character" do + str = "ã‚りãŒã¨ãŠ" + str[4] = "ã†" + str.should == "ã‚りãŒã¨ã†" + end + + it "encodes the String in an encoding compatible with the replacement" do + str = " ".force_encoding Encoding::US_ASCII + rep = [160].pack('C').force_encoding Encoding::BINARY + str[0] = rep + str.encoding.should equal(Encoding::BINARY) + end + + it "updates the string to a compatible encoding" do + str = " " + str[1] = [0xB9].pack("C*") + str.encoding.should == Encoding::ASCII_8BIT + end + + it "raises an Encoding::CompatibilityError if the replacement encoding is incompatible" do + str = "ã‚れ" + rep = "ãŒ".encode Encoding::EUC_JP + -> { str[0] = rep }.should raise_error(Encoding::CompatibilityError) + end +end + +describe "String#[]= with String index" do + it "replaces fewer characters with more characters" do + str = "abcde" + str["cd"] = "ghi" + str.should == "abghie" + end + + it "replaces more characters with fewer characters" do + str = "abcde" + str["bcd"] = "f" + str.should == "afe" + end + + it "replaces characters with no characters" do + str = "abcde" + str["cd"] = "" + str.should == "abe" + end + + it "raises an IndexError if the search String is not found" do + str = "abcde" + -> { str["g"] = "h" }.should raise_error(IndexError) + end + + it "replaces characters with a multibyte character" do + str = "ã‚りgaã¨ã†" + str["ga"] = "ãŒ" + str.should == "ã‚りãŒã¨ã†" + end + + it "replaces multibyte characters with characters" do + str = "ã‚りãŒã¨ã†" + str["ãŒ"] = "ga" + str.should == "ã‚りgaã¨ã†" + end + + it "replaces multibyte characters with multibyte characters" do + str = "ã‚りãŒã¨ã†" + str["ãŒ"] = "ã‹" + str.should == "ã‚りã‹ã¨ã†" + end + + it "encodes the String in an encoding compatible with the replacement" do + str = " ".force_encoding Encoding::US_ASCII + rep = [160].pack('C').force_encoding Encoding::BINARY + str[" "] = rep + str.encoding.should equal(Encoding::BINARY) + end + + it "raises an Encoding::CompatibilityError if the replacement encoding is incompatible" do + str = "ã‚れ" + rep = "ãŒ".encode Encoding::EUC_JP + -> { str["れ"] = rep }.should raise_error(Encoding::CompatibilityError) + end +end + +describe "String#[]= with a Regexp index" do + it "replaces the matched text with the rhs" do + str = "hello" + str[/lo/] = "x" + str.should == "helx" + end + + it "raises IndexError if the regexp index doesn't match a position in the string" do + str = "hello" + -> { str[/y/] = "bam" }.should raise_error(IndexError) + str.should == "hello" + end + + it "calls #to_str to convert the replacement" do + rep = mock("string element set regexp") + rep.should_receive(:to_str).and_return("b") + + str = "abc" + str[/ab/] = rep + str.should == "bc" + end + + it "checks the match before calling #to_str to convert the replacement" do + rep = mock("string element set regexp") + rep.should_not_receive(:to_str) + + -> { "abc"[/def/] = rep }.should raise_error(IndexError) + end + + describe "with 3 arguments" do + it "calls #to_int to convert the second object" do + ref = mock("string element set regexp ref") + ref.should_receive(:to_int).and_return(1) + + str = "abc" + str[/a(b)/, ref] = "x" + str.should == "axc" + end + + it "raises a TypeError if #to_int does not return an Integer" do + ref = mock("string element set regexp ref") + ref.should_receive(:to_int).and_return(nil) + + -> { "abc"[/a(b)/, ref] = "x" }.should raise_error(TypeError) + end + + it "uses the 2nd of 3 arguments as which capture should be replaced" do + str = "aaa bbb ccc" + str[/a (bbb) c/, 1] = "ddd" + str.should == "aaa ddd ccc" + end + + it "allows the specified capture to be negative and count from the end" do + str = "abcd" + str[/(a)(b)(c)(d)/, -2] = "e" + str.should == "abed" + end + + it "checks the match index before calling #to_str to convert the replacement" do + rep = mock("string element set regexp") + rep.should_not_receive(:to_str) + + -> { "abc"[/a(b)/, 2] = rep }.should raise_error(IndexError) + end + + it "raises IndexError if the specified capture isn't available" do + str = "aaa bbb ccc" + -> { str[/a (bbb) c/, 2] = "ddd" }.should raise_error(IndexError) + -> { str[/a (bbb) c/, -2] = "ddd" }.should raise_error(IndexError) + end + + describe "when the optional capture does not match" do + it "raises an IndexError before setting the replacement" do + str1 = "a b c" + str2 = str1.dup + -> { str2[/a (b) (Z)?/, 2] = "d" }.should raise_error(IndexError) + str2.should == str1 + end + end + end + + it "replaces characters with a multibyte character" do + str = "ã‚りgaã¨ã†" + str[/ga/] = "ãŒ" + str.should == "ã‚りãŒã¨ã†" + end + + it "replaces multibyte characters with characters" do + str = "ã‚りãŒã¨ã†" + str[/ãŒ/] = "ga" + str.should == "ã‚りgaã¨ã†" + end + + it "replaces multibyte characters with multibyte characters" do + str = "ã‚りãŒã¨ã†" + str[/ãŒ/] = "ã‹" + str.should == "ã‚りã‹ã¨ã†" + end + + it "encodes the String in an encoding compatible with the replacement" do + str = " ".force_encoding Encoding::US_ASCII + rep = [160].pack('C').force_encoding Encoding::BINARY + str[/ /] = rep + str.encoding.should equal(Encoding::BINARY) + end + + it "raises an Encoding::CompatibilityError if the replacement encoding is incompatible" do + str = "ã‚れ" + rep = "ãŒ".encode Encoding::EUC_JP + -> { str[/れ/] = rep }.should raise_error(Encoding::CompatibilityError) + end +end + +describe "String#[]= with a Range index" do + describe "with an empty replacement" do + it "does not replace a character with a zero-index, zero exclude-end range" do + str = "abc" + str[0...0] = "" + str.should == "abc" + end + + it "does not replace a character with a zero exclude-end range" do + str = "abc" + str[1...1] = "" + str.should == "abc" + end + + it "replaces a character with zero-index, zero non-exclude-end range" do + str = "abc" + str[0..0] = "" + str.should == "bc" + end + + it "replaces a character with a zero non-exclude-end range" do + str = "abc" + str[1..1] = "" + str.should == "ac" + end + end + + it "replaces the contents with a shorter String" do + str = "abcde" + str[0..-1] = "hg" + str.should == "hg" + end + + it "replaces the contents with a longer String" do + str = "abc" + str[0...4] = "uvwxyz" + str.should == "uvwxyz" + end + + it "replaces a partial string" do + str = "abcde" + str[1..3] = "B" + str.should == "aBe" + end + + it "raises a RangeError if negative Range begin is out of range" do + -> { "abc"[-4..-2] = "x" }.should raise_error(RangeError, "-4..-2 out of range") + end + + it "raises a RangeError if positive Range begin is greater than String size" do + -> { "abc"[4..2] = "x" }.should raise_error(RangeError, "4..2 out of range") + end + + it "uses the Range end as an index rather than a count" do + str = "abcdefg" + str[-5..3] = "xyz" + str.should == "abxyzefg" + end + + it "treats a negative out-of-range Range end with a positive Range begin as a zero count" do + str = "abc" + str[1..-4] = "x" + str.should == "axbc" + end + + it "treats a negative out-of-range Range end with a negative Range begin as a zero count" do + str = "abcd" + str[-1..-4] = "x" + str.should == "abcxd" + end + + it "replaces characters with a multibyte character" do + str = "ã‚りgaã¨ã†" + str[2..3] = "ãŒ" + str.should == "ã‚りãŒã¨ã†" + end + + it "replaces multibyte characters with characters" do + str = "ã‚りãŒã¨ã†" + str[2...3] = "ga" + str.should == "ã‚りgaã¨ã†" + end + + it "replaces multibyte characters by negative indexes" do + str = "ã‚りãŒã¨ã†" + str[-3...-2] = "ga" + str.should == "ã‚りgaã¨ã†" + end + + it "replaces multibyte characters with multibyte characters" do + str = "ã‚りãŒã¨ã†" + str[2..2] = "ã‹" + str.should == "ã‚りã‹ã¨ã†" + end + + it "deletes a multibyte character" do + str = "ã‚りã¨ã†" + str[2..3] = "" + str.should == "ã‚り" + end + + it "inserts a multibyte character" do + str = "ã‚りã¨ã†" + str[2...2] = "ãŒ" + str.should == "ã‚りãŒã¨ã†" + end + + it "encodes the String in an encoding compatible with the replacement" do + str = " ".force_encoding Encoding::US_ASCII + rep = [160].pack('C').force_encoding Encoding::BINARY + str[0..1] = rep + str.encoding.should equal(Encoding::BINARY) + end + + it "raises an Encoding::CompatibilityError if the replacement encoding is incompatible" do + str = "ã‚れ" + rep = "ãŒ".encode Encoding::EUC_JP + -> { str[0..1] = rep }.should raise_error(Encoding::CompatibilityError) + end +end + +describe "String#[]= with Integer index, count" do + it "starts at idx and overwrites count characters before inserting the rest of other_str" do + a = "hello" + a[0, 2] = "xx" + a.should == "xxllo" + a = "hello" + a[0, 2] = "jello" + a.should == "jellollo" + end + + it "counts negative idx values from end of the string" do + a = "hello" + a[-1, 0] = "bob" + a.should == "hellbobo" + a = "hello" + a[-5, 0] = "bob" + a.should == "bobhello" + end + + it "overwrites and deletes characters if count is more than the length of other_str" do + a = "hello" + a[0, 4] = "x" + a.should == "xo" + a = "hello" + a[0, 5] = "x" + a.should == "x" + end + + it "deletes characters if other_str is an empty string" do + a = "hello" + a[0, 2] = "" + a.should == "llo" + end + + it "deletes characters up to the maximum length of the existing string" do + a = "hello" + a[0, 6] = "x" + a.should == "x" + a = "hello" + a[0, 100] = "" + a.should == "" + end + + it "appends other_str to the end of the string if idx == the length of the string" do + a = "hello" + a[5, 0] = "bob" + a.should == "hellobob" + end + + it "calls #to_int to convert the index and count objects" do + index = mock("string element set index") + index.should_receive(:to_int).and_return(-4) + + count = mock("string element set count") + count.should_receive(:to_int).and_return(2) + + str = "abcde" + str[index, count] = "xyz" + str.should == "axyzde" + end + + it "raises a TypeError if #to_int for index does not return an Integer" do + index = mock("string element set index") + index.should_receive(:to_int).and_return("1") + + -> { "abc"[index, 2] = "xyz" }.should raise_error(TypeError) + end + + it "raises a TypeError if #to_int for count does not return an Integer" do + count = mock("string element set count") + count.should_receive(:to_int).and_return("1") + + -> { "abc"[1, count] = "xyz" }.should raise_error(TypeError) + end + + it "calls #to_str to convert the replacement object" do + r = mock("string element set replacement") + r.should_receive(:to_str).and_return("xyz") + + str = "abcde" + str[2, 2] = r + str.should == "abxyze" + end + + it "raises a TypeError of #to_str does not return a String" do + r = mock("string element set replacement") + r.should_receive(:to_str).and_return(nil) + + -> { "abc"[1, 1] = r }.should raise_error(TypeError) + end + + it "raises an IndexError if |idx| is greater than the length of the string" do + -> { "hello"[6, 0] = "bob" }.should raise_error(IndexError) + -> { "hello"[-6, 0] = "bob" }.should raise_error(IndexError) + end + + it "raises an IndexError if count < 0" do + -> { "hello"[0, -1] = "bob" }.should raise_error(IndexError) + -> { "hello"[1, -1] = "bob" }.should raise_error(IndexError) + end + + it "raises a TypeError if other_str is a type other than String" do + -> { "hello"[0, 2] = nil }.should raise_error(TypeError) + -> { "hello"[0, 2] = [] }.should raise_error(TypeError) + -> { "hello"[0, 2] = 33 }.should raise_error(TypeError) + end + + it "replaces characters with a multibyte character" do + str = "ã‚りgaã¨ã†" + str[2, 2] = "ãŒ" + str.should == "ã‚りãŒã¨ã†" + end + + it "replaces multibyte characters with characters" do + str = "ã‚りãŒã¨ã†" + str[2, 1] = "ga" + str.should == "ã‚りgaã¨ã†" + end + + it "replaces multibyte characters with multibyte characters" do + str = "ã‚りãŒã¨ã†" + str[2, 1] = "ã‹" + str.should == "ã‚りã‹ã¨ã†" + end + + it "deletes a multibyte character" do + str = "ã‚りã¨ã†" + str[2, 2] = "" + str.should == "ã‚り" + end + + it "inserts a multibyte character" do + str = "ã‚りã¨ã†" + str[2, 0] = "ãŒ" + str.should == "ã‚りãŒã¨ã†" + end + + it "raises an IndexError if the character index is out of range of a multibyte String" do + -> { "ã‚れ"[3, 0] = "り" }.should raise_error(IndexError) + end + + it "encodes the String in an encoding compatible with the replacement" do + str = " ".force_encoding Encoding::US_ASCII + rep = [160].pack('C').force_encoding Encoding::BINARY + str[0, 1] = rep + str.encoding.should equal(Encoding::BINARY) + end + + it "raises an Encoding::CompatibilityError if the replacement encoding is incompatible" do + str = "ã‚れ" + rep = "ãŒ".encode Encoding::EUC_JP + -> { str[0, 1] = rep }.should raise_error(Encoding::CompatibilityError) + end +end diff --git a/spec/ruby/core/string/empty_spec.rb b/spec/ruby/core/string/empty_spec.rb new file mode 100644 index 0000000000..8e53a16afc --- /dev/null +++ b/spec/ruby/core/string/empty_spec.rb @@ -0,0 +1,12 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "String#empty?" do + it "returns true if the string has a length of zero" do + "hello".should_not.empty? + " ".should_not.empty? + "\x00".should_not.empty? + "".should.empty? + StringSpecs::MyString.new("").should.empty? + end +end diff --git a/spec/ruby/core/string/encode_spec.rb b/spec/ruby/core/string/encode_spec.rb new file mode 100644 index 0000000000..cd449498a3 --- /dev/null +++ b/spec/ruby/core/string/encode_spec.rb @@ -0,0 +1,240 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'shared/encode' + +describe "String#encode" do + before :each do + @external = Encoding.default_external + @internal = Encoding.default_internal + end + + after :each do + Encoding.default_external = @external + Encoding.default_internal = @internal + end + + it_behaves_like :string_encode, :encode + + describe "when passed no options" do + it "returns a copy when Encoding.default_internal is nil" do + Encoding.default_internal = nil + str = "ã‚" + encoded = str.encode + encoded.should_not equal(str) + encoded.should == str + end + + it "returns a copy for a ASCII-only String when Encoding.default_internal is nil" do + Encoding.default_internal = nil + str = "abc" + encoded = str.encode + encoded.should_not equal(str) + encoded.should == str + end + + it "encodes an ascii substring of a binary string to UTF-8" do + x82 = [0x82].pack('C') + str = "#{x82}foo".dup.force_encoding("binary")[1..-1].encode("utf-8") + str.should == "foo".dup.force_encoding("utf-8") + str.encoding.should equal(Encoding::UTF_8) + end + end + + describe "when passed to encoding" do + it "returns a copy when passed the same encoding as the String" do + str = "ã‚" + encoded = str.encode(Encoding::UTF_8) + encoded.should_not equal(str) + encoded.should == str + end + + it "round trips a String" do + str = "abc def".dup.force_encoding Encoding::US_ASCII + str.encode("utf-32be").encode("ascii").should == "abc def" + end + end + + describe "when passed options" do + it "returns a copy when Encoding.default_internal is nil" do + Encoding.default_internal = nil + str = "ã‚" + str.encode(invalid: :replace).should_not equal(str) + end + + it "normalizes newlines with cr_newline option" do + "\r\nfoo".encode(cr_newline: true).should == "\r\rfoo" + "\rfoo".encode(cr_newline: true).should == "\rfoo" + "\nfoo".encode(cr_newline: true).should == "\rfoo" + end + + it "normalizes newlines with crlf_newline option" do + "\r\nfoo".encode(crlf_newline: true).should == "\r\r\nfoo" + "\rfoo".encode(crlf_newline: true).should == "\rfoo" + "\nfoo".encode(crlf_newline: true).should == "\r\nfoo" + end + + it "normalizes newlines with universal_newline option" do + "\r\nfoo".encode(universal_newline: true).should == "\nfoo" + "\rfoo".encode(universal_newline: true).should == "\nfoo" + "\nfoo".encode(universal_newline: true).should == "\nfoo" + end + + it "replaces invalid encoding in source with default replacement" do + encoded = "ã¡\xE3\x81\xFF".encode("UTF-16LE", invalid: :replace) + encoded.should == "\u3061\ufffd\ufffd".encode("UTF-16LE") + encoded.encode("UTF-8").should == "ã¡\ufffd\ufffd" + end + + it "replaces invalid encoding in source with a specified replacement" do + encoded = "ã¡\xE3\x81\xFF".encode("UTF-16LE", invalid: :replace, replace: "foo") + encoded.should == "\u3061foofoo".encode("UTF-16LE") + encoded.encode("UTF-8").should == "ã¡foofoo" + end + + it "replace multiple invalid bytes at the end with a single replacement character" do + "\xE3\x81\x93\xE3\x81".encode("UTF-8", invalid: :replace).should == "\u3053\ufffd" + end + + it "replaces invalid encoding in source using a specified replacement even when a fallback is given" do + encoded = "ã¡\xE3\x81\xFF".encode("UTF-16LE", invalid: :replace, replace: "foo", fallback: -> c { "bar" }) + encoded.should == "\u3061foofoo".encode("UTF-16LE") + encoded.encode("UTF-8").should == "ã¡foofoo" + end + + it "replaces undefined encoding in destination with default replacement" do + encoded = "B\ufffd".encode(Encoding::US_ASCII, undef: :replace) + encoded.should == "B?".encode(Encoding::US_ASCII) + encoded.encode("UTF-8").should == "B?" + end + + it "replaces undefined encoding in destination with a specified replacement" do + encoded = "B\ufffd".encode(Encoding::US_ASCII, undef: :replace, replace: "foo") + encoded.should == "Bfoo".encode(Encoding::US_ASCII) + encoded.encode("UTF-8").should == "Bfoo" + end + + it "replaces undefined encoding in destination with a specified replacement even if a fallback is given" do + encoded = "B\ufffd".encode(Encoding::US_ASCII, undef: :replace, replace: "foo", fallback: proc {|x| "bar"}) + encoded.should == "Bfoo".encode(Encoding::US_ASCII) + encoded.encode("UTF-8").should == "Bfoo" + end + + it "replaces undefined encoding in destination using a fallback proc" do + encoded = "B\ufffd".encode(Encoding::US_ASCII, fallback: proc {|x| "bar"}) + encoded.should == "Bbar".encode(Encoding::US_ASCII) + encoded.encode("UTF-8").should == "Bbar" + end + + it "replaces invalid encoding in source using replace even when fallback is given as proc" do + encoded = "ã¡\xE3\x81\xFF".encode("UTF-16LE", invalid: :replace, replace: "foo", fallback: proc {|x| "bar"}) + encoded.should == "\u3061foofoo".encode("UTF-16LE") + encoded.encode("UTF-8").should == "ã¡foofoo" + end + end + + describe "when passed to, from" do + it "returns a copy in the destination encoding when both encodings are the same" do + str = "ã‚".dup.force_encoding("binary") + encoded = str.encode("utf-8", "utf-8") + + encoded.should_not equal(str) + encoded.should == str.force_encoding("utf-8") + encoded.encoding.should == Encoding::UTF_8 + end + + it "returns the transcoded string" do + str = "\x00\x00\x00\x1F" + str.encode(Encoding::UTF_8, Encoding::UTF_16BE).should == "\u0000\u001f" + end + end + + describe "when passed to, options" do + it "returns a copy when the destination encoding is the same as the String encoding" do + str = "ã‚" + encoded = str.encode(Encoding::UTF_8, undef: :replace) + encoded.should_not equal(str) + encoded.should == str + end + end + + describe "when passed to, from, options" do + it "returns a copy when both encodings are the same" do + str = "ã‚" + encoded = str.encode("utf-8", "utf-8", invalid: :replace) + encoded.should_not equal(str) + encoded.should == str + end + + it "returns a copy in the destination encoding when both encodings are the same" do + str = "ã‚".dup.force_encoding("binary") + encoded = str.encode("utf-8", "utf-8", invalid: :replace) + + encoded.should_not equal(str) + encoded.should == str.force_encoding("utf-8") + encoded.encoding.should == Encoding::UTF_8 + end + end +end + +describe "String#encode!" do + before :each do + @external = Encoding.default_external + @internal = Encoding.default_internal + end + + after :each do + Encoding.default_external = @external + Encoding.default_internal = @internal + end + + it_behaves_like :string_encode, :encode! + + it "raises a FrozenError when called on a frozen String" do + -> { "foo".freeze.encode!("euc-jp") }.should raise_error(FrozenError) + end + + # http://redmine.ruby-lang.org/issues/show/1836 + it "raises a FrozenError when called on a frozen String when it's a no-op" do + -> { "foo".freeze.encode!("utf-8") }.should raise_error(FrozenError) + end + + describe "when passed no options" do + it "returns self when Encoding.default_internal is nil" do + Encoding.default_internal = nil + str = +"ã‚" + str.encode!.should equal(str) + end + + it "returns self for a ASCII-only String when Encoding.default_internal is nil" do + Encoding.default_internal = nil + str = +"abc" + str.encode!.should equal(str) + end + end + + describe "when passed options" do + it "returns self for ASCII-only String when Encoding.default_internal is nil" do + Encoding.default_internal = nil + str = +"abc" + str.encode!(invalid: :replace).should equal(str) + end + end + + describe "when passed to encoding" do + it "returns self" do + str = +"abc" + result = str.encode!(Encoding::BINARY) + result.encoding.should equal(Encoding::BINARY) + result.should equal(str) + end + end + + describe "when passed to, from" do + it "returns self" do + str = +"ã‚ã‚" + result = str.encode!("euc-jp", "utf-8") + result.encoding.should equal(Encoding::EUC_JP) + result.should equal(str) + end + end +end diff --git a/spec/ruby/core/string/encoding_spec.rb b/spec/ruby/core/string/encoding_spec.rb new file mode 100644 index 0000000000..f6e8fd3470 --- /dev/null +++ b/spec/ruby/core/string/encoding_spec.rb @@ -0,0 +1,184 @@ +# -*- encoding: us-ascii -*- +require_relative '../../spec_helper' +require_relative 'fixtures/iso-8859-9-encoding' + +describe "String#encoding" do + it "returns an Encoding object" do + String.new.encoding.should be_an_instance_of(Encoding) + end + + it "is equal to the source encoding by default" do + s = StringSpecs::ISO88599Encoding.new + s.cedilla.encoding.should == s.source_encoding + s.cedilla.encode("utf-8").should == 350.chr(Encoding::UTF_8) # S-cedilla + end + + it "returns the given encoding if #force_encoding has been called" do + "a".dup.force_encoding(Encoding::SHIFT_JIS).encoding.should == Encoding::SHIFT_JIS + end + + it "returns the given encoding if #encode!has been called" do + "a".dup.encode!(Encoding::SHIFT_JIS).encoding.should == Encoding::SHIFT_JIS + end +end + +describe "String#encoding for US-ASCII Strings" do + it "returns US-ASCII if self is US-ASCII" do + "a".encoding.should == Encoding::US_ASCII + end + + it "returns US-ASCII if self is US-ASCII only, despite the default internal encoding being different" do + default_internal = Encoding.default_internal + Encoding.default_internal = Encoding::UTF_8 + "a".encoding.should == Encoding::US_ASCII + Encoding.default_internal = default_internal + end + + it "returns US-ASCII if self is US-ASCII only, despite the default external encoding being different" do + default_external = Encoding.default_external + Encoding.default_external = Encoding::UTF_8 + "a".encoding.should == Encoding::US_ASCII + Encoding.default_external = default_external + end + + it "returns US-ASCII if self is US-ASCII only, despite the default internal and external encodings being different" do + default_internal = Encoding.default_internal + default_external = Encoding.default_external + Encoding.default_internal = Encoding::UTF_8 + Encoding.default_external = Encoding::UTF_8 + "a".encoding.should == Encoding::US_ASCII + Encoding.default_external = default_external + Encoding.default_internal = default_internal + end + + it "returns US-ASCII if self is US-ASCII only, despite the default encodings being different" do + default_internal = Encoding.default_internal + default_external = Encoding.default_external + Encoding.default_internal = Encoding::UTF_8 + Encoding.default_external = Encoding::UTF_8 + "a".encoding.should == Encoding::US_ASCII + Encoding.default_external = default_external + Encoding.default_internal = default_internal + end + +end + +describe "String#encoding for Strings with \\u escapes" do + it "returns UTF-8" do + "\u{4040}".encoding.should == Encoding::UTF_8 + end + + it "returns US-ASCII if self is US-ASCII only" do + s = "\u{40}" + s.ascii_only?.should be_true + s.encoding.should == Encoding::US_ASCII + end + + it "returns UTF-8 if self isn't US-ASCII only" do + s = "\u{4076}\u{619}" + s.ascii_only?.should be_false + s.encoding.should == Encoding::UTF_8 + end + + it "is not affected by the default internal encoding" do + default_internal = Encoding.default_internal + Encoding.default_internal = Encoding::ISO_8859_15 + "\u{5050}".encoding.should == Encoding::UTF_8 + "\u{50}".encoding.should == Encoding::US_ASCII + Encoding.default_internal = default_internal + end + + it "is not affected by the default external encoding" do + default_external = Encoding.default_external + Encoding.default_external = Encoding::SHIFT_JIS + "\u{50}".encoding.should == Encoding::US_ASCII + "\u{5050}".encoding.should == Encoding::UTF_8 + Encoding.default_external = default_external + end + + it "is not affected by both the default internal and external encoding being set at the same time" do + default_internal = Encoding.default_internal + default_external = Encoding.default_external + Encoding.default_internal = Encoding::EUC_JP + Encoding.default_external = Encoding::SHIFT_JIS + "\u{50}".encoding.should == Encoding::US_ASCII + "\u{507}".encoding.should == Encoding::UTF_8 + Encoding.default_external = default_external + Encoding.default_internal = default_internal + end + + it "returns the given encoding if #force_encoding has been called" do + "\u{20}".dup.force_encoding(Encoding::SHIFT_JIS).encoding.should == Encoding::SHIFT_JIS + "\u{2020}".dup.force_encoding(Encoding::SHIFT_JIS).encoding.should == Encoding::SHIFT_JIS + end + + it "returns the given encoding if #encode!has been called" do + "\u{20}".dup.encode!(Encoding::SHIFT_JIS).encoding.should == Encoding::SHIFT_JIS + "\u{2020}".dup.encode!(Encoding::SHIFT_JIS).encoding.should == Encoding::SHIFT_JIS + end +end + +describe "String#encoding for Strings with \\x escapes" do + + it "returns US-ASCII if self is US-ASCII only" do + s = "\x61" + s.ascii_only?.should be_true + s.encoding.should == Encoding::US_ASCII + end + + it "returns BINARY when an escape creates a byte with the 8th bit set if the source encoding is US-ASCII" do + __ENCODING__.should == Encoding::US_ASCII + str = " " + str.encoding.should == Encoding::US_ASCII + str += [0xDF].pack('C') + str.ascii_only?.should be_false + str.encoding.should == Encoding::BINARY + end + + # TODO: Deal with case when the byte in question isn't valid in the source + # encoding? + it "returns the source encoding when an escape creates a byte with the 8th bit set if the source encoding isn't US-ASCII" do + fixture = StringSpecs::ISO88599Encoding.new + fixture.source_encoding.should == Encoding::ISO8859_9 + fixture.x_escape.ascii_only?.should be_false + fixture.x_escape.encoding.should == Encoding::ISO8859_9 + end + + it "is not affected by the default internal encoding" do + default_internal = Encoding.default_internal + Encoding.default_internal = Encoding::ISO_8859_15 + "\x50".encoding.should == Encoding::US_ASCII + "\x50".encoding.should == Encoding::US_ASCII + Encoding.default_internal = default_internal + end + + it "is not affected by the default external encoding" do + default_external = Encoding.default_external + Encoding.default_external = Encoding::SHIFT_JIS + "\x50".encoding.should == Encoding::US_ASCII + [0xD4].pack('C').encoding.should == Encoding::BINARY + Encoding.default_external = default_external + end + + it "is not affected by both the default internal and external encoding being set at the same time" do + default_internal = Encoding.default_internal + default_external = Encoding.default_external + Encoding.default_internal = Encoding::EUC_JP + Encoding.default_external = Encoding::SHIFT_JIS + x50 = "\x50" + x50.encoding.should == Encoding::US_ASCII + [0xD4].pack('C').encoding.should == Encoding::BINARY + Encoding.default_external = default_external + Encoding.default_internal = default_internal + end + + it "returns the given encoding if #force_encoding has been called" do + "\x50".dup.force_encoding(Encoding::SHIFT_JIS).encoding.should == Encoding::SHIFT_JIS + [212].pack('C').force_encoding(Encoding::ISO_8859_9).encoding.should == Encoding::ISO_8859_9 + end + + it "returns the given encoding if #encode!has been called" do + "\x50".dup.encode!(Encoding::SHIFT_JIS).encoding.should == Encoding::SHIFT_JIS + "x\00".dup.encode!(Encoding::UTF_8).encoding.should == Encoding::UTF_8 + end +end diff --git a/spec/ruby/core/string/end_with_spec.rb b/spec/ruby/core/string/end_with_spec.rb new file mode 100644 index 0000000000..ac4fff72ad --- /dev/null +++ b/spec/ruby/core/string/end_with_spec.rb @@ -0,0 +1,8 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative '../../shared/string/end_with' + +describe "String#end_with?" do + it_behaves_like :end_with, :to_s +end diff --git a/spec/ruby/core/string/eql_spec.rb b/spec/ruby/core/string/eql_spec.rb new file mode 100644 index 0000000000..397974d9fb --- /dev/null +++ b/spec/ruby/core/string/eql_spec.rb @@ -0,0 +1,21 @@ +require_relative '../../spec_helper' +require_relative 'shared/eql' + +describe "String#eql?" do + it_behaves_like :string_eql_value, :eql? + + describe "when given a non-String" do + it "returns false" do + 'hello'.should_not eql(5) + not_supported_on :opal do + 'hello'.should_not eql(:hello) + end + 'hello'.should_not eql(mock('x')) + end + + it "does not try to call #to_str on the given argument" do + (obj = mock('x')).should_not_receive(:to_str) + 'hello'.should_not eql(obj) + end + end +end diff --git a/spec/ruby/core/string/equal_value_spec.rb b/spec/ruby/core/string/equal_value_spec.rb new file mode 100644 index 0000000000..b9c9c372f8 --- /dev/null +++ b/spec/ruby/core/string/equal_value_spec.rb @@ -0,0 +1,8 @@ +require_relative '../../spec_helper' +require_relative 'shared/eql' +require_relative 'shared/equal_value' + +describe "String#==" do + it_behaves_like :string_eql_value, :== + it_behaves_like :string_equal_value, :== +end diff --git a/spec/ruby/core/string/fixtures/classes.rb b/spec/ruby/core/string/fixtures/classes.rb new file mode 100644 index 0000000000..26fcd51b5d --- /dev/null +++ b/spec/ruby/core/string/fixtures/classes.rb @@ -0,0 +1,60 @@ +class Object + # This helper is defined here rather than in MSpec because + # it is only used in #unpack specs. + def unpack_format(count=nil, repeat=nil) + format = "#{instance_variable_get(:@method)}#{count}" + format *= repeat if repeat + format.dup # because it may then become tainted + end +end + +module StringSpecs + class MyString < String; end + class MyArray < Array; end + class MyRange < Range; end + + class SubString < String + attr_reader :special + + def initialize(str=nil) + @special = str + end + end + + class InitializeString < String + attr_reader :ivar + + def initialize(other) + super + @ivar = 1 + end + + def initialize_copy(other) + ScratchPad.record object_id + end + end + + module StringModule + def repr + 1 + end + end + + class StringWithRaisingConstructor < String + def initialize(str) + raise ArgumentError.new('constructor was called') unless str == 'silly:string' + self.replace(str) + end + end + + class SpecialVarProcessor + def process(match) + if $~ != nil + str = $~[0] + else + str = "unset" + end + "<#{str}>" + end + end +end diff --git a/spec/ruby/core/string/fixtures/freeze_magic_comment.rb b/spec/ruby/core/string/fixtures/freeze_magic_comment.rb new file mode 100644 index 0000000000..2b87a16328 --- /dev/null +++ b/spec/ruby/core/string/fixtures/freeze_magic_comment.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +print (+ 'frozen string').frozen? ? 'immutable' : 'mutable' diff --git a/spec/ruby/core/string/fixtures/iso-8859-9-encoding.rb b/spec/ruby/core/string/fixtures/iso-8859-9-encoding.rb new file mode 100644 index 0000000000..cfa91dedc3 --- /dev/null +++ b/spec/ruby/core/string/fixtures/iso-8859-9-encoding.rb @@ -0,0 +1,9 @@ +# -*- encoding: iso-8859-9 -*- +module StringSpecs + class ISO88599Encoding + def source_encoding; __ENCODING__; end + def x_escape; [0xDF].pack('C').force_encoding("iso-8859-9"); end + def ascii_only; "glark"; end + def cedilla; "Þ"; end # S-cedilla + end +end diff --git a/spec/ruby/core/string/fixtures/to_c.rb b/spec/ruby/core/string/fixtures/to_c.rb new file mode 100644 index 0000000000..7776933263 --- /dev/null +++ b/spec/ruby/core/string/fixtures/to_c.rb @@ -0,0 +1,5 @@ +module StringSpecs + def self.to_c_method(string) + string.to_c + end +end diff --git a/spec/ruby/core/string/force_encoding_spec.rb b/spec/ruby/core/string/force_encoding_spec.rb new file mode 100644 index 0000000000..2259dcf3cf --- /dev/null +++ b/spec/ruby/core/string/force_encoding_spec.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: false +require_relative '../../spec_helper' + +describe "String#force_encoding" do + it "accepts a String as the name of an Encoding" do + "abc".force_encoding('shift_jis').encoding.should == Encoding::Shift_JIS + end + + describe "with a special encoding name" do + before :each do + @original_encoding = Encoding.default_internal + end + + after :each do + Encoding.default_internal = @original_encoding + end + + it "accepts valid special encoding names" do + Encoding.default_internal = "US-ASCII" + "abc".force_encoding("internal").encoding.should == Encoding::US_ASCII + end + + it "defaults to BINARY if special encoding name is not set" do + Encoding.default_internal = nil + "abc".force_encoding("internal").encoding.should == Encoding::BINARY + end + end + + it "accepts an Encoding instance" do + "abc".force_encoding(Encoding::SHIFT_JIS).encoding.should == Encoding::Shift_JIS + end + + it "calls #to_str to convert an object to an encoding name" do + obj = mock("force_encoding") + obj.should_receive(:to_str).and_return("utf-8") + + "abc".force_encoding(obj).encoding.should == Encoding::UTF_8 + end + + it "raises a TypeError if #to_str does not return a String" do + obj = mock("force_encoding") + obj.should_receive(:to_str).and_return(1) + + -> { "abc".force_encoding(obj) }.should raise_error(TypeError) + end + + it "raises a TypeError if passed nil" do + -> { "abc".force_encoding(nil) }.should raise_error(TypeError) + end + + it "returns self" do + str = "abc" + str.force_encoding('utf-8').should equal(str) + end + + it "sets the encoding even if the String contents are invalid in that encoding" do + str = "\u{9765}" + str.force_encoding('euc-jp') + str.encoding.should == Encoding::EUC_JP + str.valid_encoding?.should be_false + end + + it "does not transcode self" do + str = "é" + str.dup.force_encoding('utf-16le').should_not == str.encode('utf-16le') + end + + it "raises a FrozenError if self is frozen" do + str = "abcd".freeze + -> { str.force_encoding(str.encoding) }.should raise_error(FrozenError) + end +end diff --git a/spec/ruby/core/string/freeze_spec.rb b/spec/ruby/core/string/freeze_spec.rb new file mode 100644 index 0000000000..2e8e70386d --- /dev/null +++ b/spec/ruby/core/string/freeze_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: false +require_relative '../../spec_helper' + +describe "String#freeze" do + + it "produces the same object whenever called on an instance of a literal in the source" do + "abc".freeze.should equal "abc".freeze + end + + it "doesn't produce the same object for different instances of literals in the source" do + "abc".should_not equal "abc" + end + + it "being a special form doesn't change the value of defined?" do + defined?("abc".freeze).should == "method" + end + +end diff --git a/spec/ruby/core/string/getbyte_spec.rb b/spec/ruby/core/string/getbyte_spec.rb new file mode 100644 index 0000000000..27b7d826ea --- /dev/null +++ b/spec/ruby/core/string/getbyte_spec.rb @@ -0,0 +1,69 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' + +describe "String#getbyte" do + it "returns an Integer if given a valid index" do + "a".getbyte(0).should be_kind_of(Integer) + end + + it "starts indexing at 0" do + "b".getbyte(0).should == 98 + + # copy-on-write case + _str1, str2 = "fooXbar".split("X") + str2.getbyte(0).should == 98 + end + + it "counts from the end of the String if given a negative argument" do + "glark".getbyte(-1).should == "glark".getbyte(4) + + # copy-on-write case + _str1, str2 = "fooXbar".split("X") + str2.getbyte(-1).should == 114 + end + + it "returns an Integer between 0 and 255" do + "\x00".getbyte(0).should == 0 + [0xFF].pack('C').getbyte(0).should == 255 + 256.chr('utf-8').getbyte(0).should == 196 + 256.chr('utf-8').getbyte(1).should == 128 + end + + it "regards a multi-byte character as having multiple bytes" do + chr = "\u{998}" + chr.bytesize.should == 3 + chr.getbyte(0).should == 224 + chr.getbyte(1).should == 166 + chr.getbyte(2).should == 152 + end + + it "mirrors the output of #bytes" do + xDE = [0xDE].pack('C').force_encoding('utf-8') + str = "UTF-8 (\u{9865}} characters and hex escapes (#{xDE})" + str.bytes.to_a.each_with_index do |byte, index| + str.getbyte(index).should == byte + end + end + + it "interprets bytes relative to the String's encoding" do + str = "\u{333}" + str.encode('utf-8').getbyte(0).should_not == str.encode('utf-16le').getbyte(0) + end + + it "returns nil for out-of-bound indexes" do + "g".getbyte(1).should be_nil + end + + it "regards the empty String as containing no bytes" do + "".getbyte(0).should be_nil + end + + it "raises an ArgumentError unless given one argument" do + -> { "glark".getbyte }.should raise_error(ArgumentError) + -> { "food".getbyte(0,0) }.should raise_error(ArgumentError) + end + + it "raises a TypeError unless its argument can be coerced into an Integer" do + -> { "a".getbyte('a') }.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/string/grapheme_clusters_spec.rb b/spec/ruby/core/string/grapheme_clusters_spec.rb new file mode 100644 index 0000000000..380a245083 --- /dev/null +++ b/spec/ruby/core/string/grapheme_clusters_spec.rb @@ -0,0 +1,14 @@ +require_relative "../../spec_helper" +require_relative 'shared/chars' +require_relative 'shared/grapheme_clusters' + +describe "String#grapheme_clusters" do + it_behaves_like :string_chars, :grapheme_clusters + it_behaves_like :string_grapheme_clusters, :grapheme_clusters + + it "returns an array when no block given" do + string = "ab\u{1f3f3}\u{fe0f}\u{200d}\u{1f308}\u{1F43E}" + string.grapheme_clusters.should == ['a', 'b', "\u{1f3f3}\u{fe0f}\u{200d}\u{1f308}", "\u{1F43E}"] + + end +end diff --git a/spec/ruby/core/string/gsub_spec.rb b/spec/ruby/core/string/gsub_spec.rb new file mode 100644 index 0000000000..0d9f32eca2 --- /dev/null +++ b/spec/ruby/core/string/gsub_spec.rb @@ -0,0 +1,615 @@ +# -*- encoding: utf-8 -*- +# frozen_string_literal: false +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe :string_gsub_named_capture, shared: true do + it "replaces \\k named backreferences with the regexp's corresponding capture" do + str = "hello" + + str.gsub(/(?<foo>[aeiou])/, '<\k<foo>>').should == "h<e>ll<o>" + str.gsub(/(?<foo>.)/, '\k<foo>\k<foo>').should == "hheelllloo" + end +end + +describe "String#gsub with pattern and replacement" do + it "inserts the replacement around every character when the pattern collapses" do + "hello".gsub(//, ".").should == ".h.e.l.l.o." + end + + it "respects unicode when the pattern collapses" do + str = "ã“ã«ã¡ã‚" + reg = %r!! + + str.gsub(reg, ".").should == ".ã“.ã«.ã¡.ã‚." + end + + it "doesn't freak out when replacing ^" do + "Text\n".gsub(/^/, ' ').should == " Text\n" + "Text\nFoo".gsub(/^/, ' ').should == " Text\n Foo" + end + + it "returns a copy of self with all occurrences of pattern replaced with replacement" do + "hello".gsub(/[aeiou]/, '*').should == "h*ll*" + + str = "hello homely world. hah!" + str.gsub(/\Ah\S+\s*/, "huh? ").should == "huh? homely world. hah!" + + str = "¿por qué?" + str.gsub(/([a-z\d]*)/, "*").should == "*¿** **é*?*" + end + + it "ignores a block if supplied" do + "food".gsub(/f/, "g") { "w" }.should == "good" + end + + it "supports \\G which matches at the beginning of the remaining (non-matched) string" do + str = "hello homely world. hah!" + str.gsub(/\Gh\S+\s*/, "huh? ").should == "huh? huh? world. hah!" + end + + it "supports /i for ignoring case" do + str = "Hello. How happy are you?" + str.gsub(/h/i, "j").should == "jello. jow jappy are you?" + str.gsub(/H/i, "j").should == "jello. jow jappy are you?" + end + + it "doesn't interpret regexp metacharacters if pattern is a string" do + "12345".gsub('\d', 'a').should == "12345" + '\d'.gsub('\d', 'a').should == "a" + end + + it "replaces \\1 sequences with the regexp's corresponding capture" do + str = "hello" + + str.gsub(/([aeiou])/, '<\1>').should == "h<e>ll<o>" + str.gsub(/(.)/, '\1\1').should == "hheelllloo" + + str.gsub(/.(.?)/, '<\0>(\1)').should == "<he>(e)<ll>(l)<o>()" + + str.gsub(/.(.)+/, '\1').should == "o" + + str = "ABCDEFGHIJKLabcdefghijkl" + re = /#{"(.)" * 12}/ + str.gsub(re, '\1').should == "Aa" + str.gsub(re, '\9').should == "Ii" + # Only the first 9 captures can be accessed in MRI + str.gsub(re, '\10').should == "A0a0" + end + + it "treats \\1 sequences without corresponding captures as empty strings" do + str = "hello!" + + str.gsub("", '<\1>').should == "<>h<>e<>l<>l<>o<>!<>" + str.gsub("h", '<\1>').should == "<>ello!" + + str.gsub(//, '<\1>').should == "<>h<>e<>l<>l<>o<>!<>" + str.gsub(/./, '\1\2\3').should == "" + str.gsub(/.(.{20})?/, '\1').should == "" + end + + it "replaces \\& and \\0 with the complete match" do + str = "hello!" + + str.gsub("", '<\0>').should == "<>h<>e<>l<>l<>o<>!<>" + str.gsub("", '<\&>').should == "<>h<>e<>l<>l<>o<>!<>" + str.gsub("he", '<\0>').should == "<he>llo!" + str.gsub("he", '<\&>').should == "<he>llo!" + str.gsub("l", '<\0>').should == "he<l><l>o!" + str.gsub("l", '<\&>').should == "he<l><l>o!" + + str.gsub(//, '<\0>').should == "<>h<>e<>l<>l<>o<>!<>" + str.gsub(//, '<\&>').should == "<>h<>e<>l<>l<>o<>!<>" + str.gsub(/../, '<\0>').should == "<he><ll><o!>" + str.gsub(/../, '<\&>').should == "<he><ll><o!>" + str.gsub(/(.)./, '<\0>').should == "<he><ll><o!>" + end + + it "replaces \\` with everything before the current match" do + str = "hello!" + + str.gsub("", '<\`>').should == "<>h<h>e<he>l<hel>l<hell>o<hello>!<hello!>" + str.gsub("h", '<\`>').should == "<>ello!" + str.gsub("l", '<\`>').should == "he<he><hel>o!" + str.gsub("!", '<\`>').should == "hello<hello>" + + str.gsub(//, '<\`>').should == "<>h<h>e<he>l<hel>l<hell>o<hello>!<hello!>" + str.gsub(/../, '<\`>').should == "<><he><hell>" + end + + it "replaces \\' with everything after the current match" do + str = "hello!" + + str.gsub("", '<\\\'>').should == "<hello!>h<ello!>e<llo!>l<lo!>l<o!>o<!>!<>" + str.gsub("h", '<\\\'>').should == "<ello!>ello!" + str.gsub("ll", '<\\\'>').should == "he<o!>o!" + str.gsub("!", '<\\\'>').should == "hello<>" + + str.gsub(//, '<\\\'>').should == "<hello!>h<ello!>e<llo!>l<lo!>l<o!>o<!>!<>" + str.gsub(/../, '<\\\'>').should == "<llo!><o!><>" + end + + it "replaces \\+ with the last paren that actually matched" do + str = "hello!" + + str.gsub(/(.)(.)/, '\+').should == "el!" + str.gsub(/(.)(.)+/, '\+').should == "!" + str.gsub(/(.)()/, '\+').should == "" + str.gsub(/(.)(.{20})?/, '<\+>').should == "<h><e><l><l><o><!>" + + str = "ABCDEFGHIJKLabcdefghijkl" + re = /#{"(.)" * 12}/ + str.gsub(re, '\+').should == "Ll" + end + + it "treats \\+ as an empty string if there was no captures" do + "hello!".gsub(/./, '\+').should == "" + end + + it "maps \\\\ in replacement to \\" do + "hello".gsub(/./, '\\\\').should == '\\' * 5 + end + + it "leaves unknown \\x escapes in replacement untouched" do + "hello".gsub(/./, '\\x').should == '\\x' * 5 + "hello".gsub(/./, '\\y').should == '\\y' * 5 + end + + it "leaves \\ at the end of replacement untouched" do + "hello".gsub(/./, 'hah\\').should == 'hah\\' * 5 + end + + it_behaves_like :string_gsub_named_capture, :gsub + + it "handles pattern collapse" do + str = "ã“ã«ã¡ã‚" + reg = %r!! + str.gsub(reg, ".").should == ".ã“.ã«.ã¡.ã‚." + end + + it "tries to convert pattern to a string using to_str" do + pattern = mock('.') + def pattern.to_str() "." end + + "hello.".gsub(pattern, "!").should == "hello!" + end + + it "raises a TypeError when pattern can't be converted to a string" do + -> { "hello".gsub([], "x") }.should raise_error(TypeError) + -> { "hello".gsub(Object.new, "x") }.should raise_error(TypeError) + -> { "hello".gsub(nil, "x") }.should raise_error(TypeError) + end + + it "tries to convert replacement to a string using to_str" do + replacement = mock('hello_replacement') + def replacement.to_str() "hello_replacement" end + + "hello".gsub(/hello/, replacement).should == "hello_replacement" + end + + it "raises a TypeError when replacement can't be converted to a string" do + -> { "hello".gsub(/[aeiou]/, []) }.should raise_error(TypeError) + -> { "hello".gsub(/[aeiou]/, Object.new) }.should raise_error(TypeError) + -> { "hello".gsub(/[aeiou]/, nil) }.should raise_error(TypeError) + end + + it "returns String instances when called on a subclass" do + StringSpecs::MyString.new("").gsub(//, "").should be_an_instance_of(String) + StringSpecs::MyString.new("").gsub(/foo/, "").should be_an_instance_of(String) + StringSpecs::MyString.new("foo").gsub(/foo/, "").should be_an_instance_of(String) + StringSpecs::MyString.new("foo").gsub("foo", "").should be_an_instance_of(String) + end + + it "sets $~ to MatchData of last match and nil when there's none" do + 'hello.'.gsub('hello', 'x') + $~[0].should == 'hello' + + 'hello.'.gsub('not', 'x') + $~.should == nil + + 'hello.'.gsub(/.(.)/, 'x') + $~[0].should == 'o.' + + 'hello.'.gsub(/not/, 'x') + $~.should == nil + end + + it "handles a pattern in a superset encoding" do + result = 'abc'.force_encoding(Encoding::US_ASCII).gsub('é', 'è') + result.should == 'abc' + result.encoding.should == Encoding::US_ASCII + end + + it "handles a pattern in a subset encoding" do + result = 'été'.gsub('t'.force_encoding(Encoding::US_ASCII), 'u') + result.should == 'éué' + result.encoding.should == Encoding::UTF_8 + end +end + +describe "String#gsub with pattern and Hash" do + it "returns a copy of self with all occurrences of pattern replaced with the value of the corresponding hash key" do + "hello".gsub(/./, 'l' => 'L').should == "LL" + "hello!".gsub(/(.)(.)/, 'he' => 'she ', 'll' => 'said').should == 'she said' + "hello".gsub('l', 'l' => 'el').should == 'heelelo' + end + + it "ignores keys that don't correspond to matches" do + "hello".gsub(/./, 'z' => 'L', 'h' => 'b', 'o' => 'ow').should == "bow" + end + + it "returns an empty string if the pattern matches but the hash specifies no replacements" do + "hello".gsub(/./, 'z' => 'L').should == "" + end + + it "ignores non-String keys" do + "tattoo".gsub(/(tt)/, 'tt' => 'b', tt: 'z').should == "taboo" + end + + it "uses a key's value as many times as needed" do + "food".gsub(/o/, 'o' => '0').should == "f00d" + end + + it "uses the hash's default value for missing keys" do + hsh = {} + hsh.default='?' + hsh['o'] = '0' + "food".gsub(/./, hsh).should == "?00?" + end + + it "coerces the hash values with #to_s" do + hsh = {} + hsh.default=[] + hsh['o'] = 0 + obj = mock('!') + obj.should_receive(:to_s).and_return('!') + hsh['!'] = obj + "food!".gsub(/./, hsh).should == "[]00[]!" + end + + it "uses the hash's value set from default_proc for missing keys" do + hsh = {} + hsh.default_proc = -> k, v { 'lamb' } + "food!".gsub(/./, hsh).should == "lamblamblamblamblamb" + end + + it "sets $~ to MatchData of last match and nil when there's none for access from outside" do + 'hello.'.gsub('l', 'l' => 'L') + $~.begin(0).should == 3 + $~[0].should == 'l' + + 'hello.'.gsub('not', 'ot' => 'to') + $~.should == nil + + 'hello.'.gsub(/.(.)/, 'o' => ' hole') + $~[0].should == 'o.' + + 'hello.'.gsub(/not/, 'z' => 'glark') + $~.should == nil + end + + it "doesn't interpolate special sequences like \\1 for the block's return value" do + repl = '\& \0 \1 \` \\\' \+ \\\\ foo' + "hello".gsub(/(.+)/, 'hello' => repl ).should == repl + end +end + +describe "String#gsub! with pattern and Hash" do + + it "returns self with all occurrences of pattern replaced with the value of the corresponding hash key" do + "hello".gsub!(/./, 'l' => 'L').should == "LL" + "hello!".gsub!(/(.)(.)/, 'he' => 'she ', 'll' => 'said').should == 'she said' + "hello".gsub!('l', 'l' => 'el').should == 'heelelo' + end + + it "ignores keys that don't correspond to matches" do + "hello".gsub!(/./, 'z' => 'L', 'h' => 'b', 'o' => 'ow').should == "bow" + end + + it "replaces self with an empty string if the pattern matches but the hash specifies no replacements" do + "hello".gsub!(/./, 'z' => 'L').should == "" + end + + it "ignores non-String keys" do + "hello".gsub!(/(ll)/, 'll' => 'r', ll: 'z').should == "hero" + end + + it "uses a key's value as many times as needed" do + "food".gsub!(/o/, 'o' => '0').should == "f00d" + end + + it "uses the hash's default value for missing keys" do + hsh = {} + hsh.default='?' + hsh['o'] = '0' + "food".gsub!(/./, hsh).should == "?00?" + end + + it "coerces the hash values with #to_s" do + hsh = {} + hsh.default=[] + hsh['o'] = 0 + obj = mock('!') + obj.should_receive(:to_s).and_return('!') + hsh['!'] = obj + "food!".gsub!(/./, hsh).should == "[]00[]!" + end + + it "uses the hash's value set from default_proc for missing keys" do + hsh = {} + hsh.default_proc = -> k, v { 'lamb' } + "food!".gsub!(/./, hsh).should == "lamblamblamblamblamb" + end + + it "sets $~ to MatchData of last match and nil when there's none for access from outside" do + 'hello.'.gsub!('l', 'l' => 'L') + $~.begin(0).should == 3 + $~[0].should == 'l' + + 'hello.'.gsub!('not', 'ot' => 'to') + $~.should == nil + + 'hello.'.gsub!(/.(.)/, 'o' => ' hole') + $~[0].should == 'o.' + + 'hello.'.gsub!(/not/, 'z' => 'glark') + $~.should == nil + end + + it "doesn't interpolate special sequences like \\1 for the block's return value" do + repl = '\& \0 \1 \` \\\' \+ \\\\ foo' + "hello".gsub!(/(.+)/, 'hello' => repl ).should == repl + end +end + +describe "String#gsub with pattern and block" do + it "returns a copy of self with all occurrences of pattern replaced with the block's return value" do + "hello".gsub(/./) { |s| s.succ + ' ' }.should == "i f m m p " + "hello!".gsub(/(.)(.)/) { |*a| a.inspect }.should == '["he"]["ll"]["o!"]' + "hello".gsub('l') { 'x'}.should == 'hexxo' + end + + it "sets $~ for access from the block" do + str = "hello" + str.gsub(/([aeiou])/) { "<#{$~[1]}>" }.should == "h<e>ll<o>" + str.gsub(/([aeiou])/) { "<#{$1}>" }.should == "h<e>ll<o>" + str.gsub("l") { "<#{$~[0]}>" }.should == "he<l><l>o" + + offsets = [] + + str.gsub(/([aeiou])/) do + md = $~ + md.string.should == str + offsets << md.offset(0) + str + end.should == "hhellollhello" + + offsets.should == [[1, 2], [4, 5]] + end + + it "does not set $~ for procs created from methods" do + str = "hello" + str.gsub("l", &StringSpecs::SpecialVarProcessor.new.method(:process)).should == "he<unset><unset>o" + end + + it "restores $~ after leaving the block" do + [/./, "l"].each do |pattern| + old_md = nil + "hello".gsub(pattern) do + old_md = $~ + "ok".match(/./) + "x" + end + + $~[0].should == old_md[0] + $~.string.should == "hello" + end + end + + it "sets $~ to MatchData of last match and nil when there's none for access from outside" do + 'hello.'.gsub('l') { 'x' } + $~.begin(0).should == 3 + $~[0].should == 'l' + + 'hello.'.gsub('not') { 'x' } + $~.should == nil + + 'hello.'.gsub(/.(.)/) { 'x' } + $~[0].should == 'o.' + + 'hello.'.gsub(/not/) { 'x' } + $~.should == nil + end + + it "doesn't interpolate special sequences like \\1 for the block's return value" do + repl = '\& \0 \1 \` \\\' \+ \\\\ foo' + "hello".gsub(/(.+)/) { repl }.should == repl + end + + it "converts the block's return value to a string using to_s" do + replacement = mock('hello_replacement') + def replacement.to_s() "hello_replacement" end + + "hello".gsub(/hello/) { replacement }.should == "hello_replacement" + + obj = mock('ok') + def obj.to_s() "ok" end + + "hello".gsub(/.+/) { obj }.should == "ok" + end + + it "uses the compatible encoding if they are compatible" do + s = "hello" + s2 = "#{195.chr}#{192.chr}#{195.chr}" + + s.gsub(/l/) { |bar| 195.chr }.encoding.should == Encoding::BINARY + s2.gsub("#{192.chr}") { |bar| "hello" }.encoding.should == Encoding::BINARY + end + + it "raises an Encoding::CompatibilityError if the encodings are not compatible" do + s = "hllëllo" + s2 = "hellö" + + -> { s.gsub(/l/) { |bar| "РуÑÑкий".force_encoding("iso-8859-5") } }.should raise_error(Encoding::CompatibilityError) + -> { s2.gsub(/l/) { |bar| "РуÑÑкий".force_encoding("iso-8859-5") } }.should raise_error(Encoding::CompatibilityError) + end + + it "replaces the incompatible part properly even if the encodings are not compatible" do + s = "hllëllo" + + s.gsub(/ë/) { |bar| "РуÑÑкий".force_encoding("iso-8859-5") }.encoding.should == Encoding::ISO_8859_5 + end + + not_supported_on :opal do + it "raises an ArgumentError if encoding is not valid" do + x92 = [0x92].pack('C').force_encoding('utf-8') + -> { "a#{x92}b".gsub(/[^\x00-\x7f]/u, '') }.should raise_error(ArgumentError) + end + end +end + +describe "String#gsub with pattern and without replacement and block" do + it "returns an enumerator" do + enum = "abca".gsub(/a/) + enum.should be_an_instance_of(Enumerator) + enum.to_a.should == ["a", "a"] + end + + describe "returned Enumerator" do + describe "size" do + it "should return nil" do + "abca".gsub(/a/).size.should == nil + end + end + end +end + +describe "String#gsub with a string pattern" do + it "handles multibyte characters" do + "é".gsub("é", "â").should == "â" + "aé".gsub("é", "â").should == "aâ" + "éa".gsub("é", "â").should == "âa" + end +end + +describe "String#gsub! with pattern and replacement" do + it "modifies self in place and returns self" do + a = "hello" + a.gsub!(/[aeiou]/, '*').should equal(a) + a.should == "h*ll*" + end + + it "modifies self in place with multi-byte characters and returns self" do + a = "¿por qué?" + a.gsub!(/([a-z\d]*)/, "*").should equal(a) + a.should == "*¿** **é*?*" + end + + it "returns nil if no modifications were made" do + a = "hello" + a.gsub!(/z/, '*').should == nil + a.gsub!(/z/, 'z').should == nil + a.should == "hello" + end + + # See [ruby-core:23666] + it "raises a FrozenError when self is frozen" do + s = "hello" + s.freeze + + -> { s.gsub!(/ROAR/, "x") }.should raise_error(FrozenError) + -> { s.gsub!(/e/, "e") }.should raise_error(FrozenError) + -> { s.gsub!(/[aeiou]/, '*') }.should raise_error(FrozenError) + end + + it "handles a pattern in a superset encoding" do + string = 'abc'.force_encoding(Encoding::US_ASCII) + + result = string.gsub!('é', 'è') + + result.should == nil + string.should == 'abc' + string.encoding.should == Encoding::US_ASCII + end + + it "handles a pattern in a subset encoding" do + string = 'été' + pattern = 't'.force_encoding(Encoding::US_ASCII) + + result = string.gsub!(pattern, 'u') + + result.should == string + string.should == 'éué' + string.encoding.should == Encoding::UTF_8 + end +end + +describe "String#gsub! with pattern and block" do + it "modifies self in place and returns self" do + a = "hello" + a.gsub!(/[aeiou]/) { '*' }.should equal(a) + a.should == "h*ll*" + end + + it "returns nil if no modifications were made" do + a = "hello" + a.gsub!(/z/) { '*' }.should == nil + a.gsub!(/z/) { 'z' }.should == nil + a.should == "hello" + end + + # See [ruby-core:23663] + it "raises a FrozenError when self is frozen" do + s = "hello" + s.freeze + + -> { s.gsub!(/ROAR/) { "x" } }.should raise_error(FrozenError) + -> { s.gsub!(/e/) { "e" } }.should raise_error(FrozenError) + -> { s.gsub!(/[aeiou]/) { '*' } }.should raise_error(FrozenError) + end + + it "uses the compatible encoding if they are compatible" do + s = "hello" + s2 = "#{195.chr}#{192.chr}#{195.chr}" + + s.gsub!(/l/) { |bar| 195.chr }.encoding.should == Encoding::BINARY + s2.gsub!("#{192.chr}") { |bar| "hello" }.encoding.should == Encoding::BINARY + end + + it "raises an Encoding::CompatibilityError if the encodings are not compatible" do + s = "hllëllo" + s2 = "hellö" + + -> { s.gsub!(/l/) { |bar| "РуÑÑкий".force_encoding("iso-8859-5") } }.should raise_error(Encoding::CompatibilityError) + -> { s2.gsub!(/l/) { |bar| "РуÑÑкий".force_encoding("iso-8859-5") } }.should raise_error(Encoding::CompatibilityError) + end + + it "replaces the incompatible part properly even if the encodings are not compatible" do + s = "hllëllo" + + s.gsub!(/ë/) { |bar| "РуÑÑкий".force_encoding("iso-8859-5") }.encoding.should == Encoding::ISO_8859_5 + end + + not_supported_on :opal do + it "raises an ArgumentError if encoding is not valid" do + x92 = [0x92].pack('C').force_encoding('utf-8') + -> { "a#{x92}b".gsub!(/[^\x00-\x7f]/u, '') }.should raise_error(ArgumentError) + end + end +end + +describe "String#gsub! with pattern and without replacement and block" do + it "returns an enumerator" do + enum = "abca".gsub!(/a/) + enum.should be_an_instance_of(Enumerator) + enum.to_a.should == ["a", "a"] + end + + describe "returned Enumerator" do + describe "size" do + it "should return nil" do + "abca".gsub!(/a/).size.should == nil + end + end + end +end diff --git a/spec/ruby/core/string/hash_spec.rb b/spec/ruby/core/string/hash_spec.rb new file mode 100644 index 0000000000..0b26214b55 --- /dev/null +++ b/spec/ruby/core/string/hash_spec.rb @@ -0,0 +1,9 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "String#hash" do + it "returns a hash based on a string's length and content" do + "abc".hash.should == "abc".hash + "abc".hash.should_not == "cba".hash + end +end diff --git a/spec/ruby/core/string/hex_spec.rb b/spec/ruby/core/string/hex_spec.rb new file mode 100644 index 0000000000..364e915681 --- /dev/null +++ b/spec/ruby/core/string/hex_spec.rb @@ -0,0 +1,49 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +# TODO: Move actual results to String#to_int() and spec in terms of it +describe "String#hex" do + it "treats leading characters of self as a string of hex digits" do + "0a".hex.should == 10 + "0o".hex.should == 0 + "0x".hex.should == 0 + "A_BAD_BABE".hex.should == 0xABADBABE + "0b1010".hex.should == "b1010".hex + "0d500".hex.should == "d500".hex + "abcdefG".hex.should == 0xabcdef + end + + it "does not accept a sequence of underscores as part of a number" do + "a__b".hex.should == 0xa + "a____b".hex.should == 0xa + "a___f".hex.should == 0xa + end + + it "takes an optional sign" do + "-1234".hex.should == -4660 + "+1234".hex.should == 4660 + end + + it "takes an optional 0x" do + "0x0a".hex.should == 10 + "0a".hex.should == 10 + end + + it "requires that the sign is in front of the 0x if present" do + "-0x1".hex.should == -1 + "0x-1".hex.should == 0 + end + + it "returns 0 on error" do + "".hex.should == 0 + "+-5".hex.should == 0 + "wombat".hex.should == 0 + "0x0x42".hex.should == 0 + end + + it "returns 0 if sequence begins with underscore" do + "_a".hex.should == 0 + "___b".hex.should == 0 + "___0xc".hex.should == 0 + end +end diff --git a/spec/ruby/core/string/include_spec.rb b/spec/ruby/core/string/include_spec.rb new file mode 100644 index 0000000000..9781140a55 --- /dev/null +++ b/spec/ruby/core/string/include_spec.rb @@ -0,0 +1,49 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "String#include? with String" do + it "returns true if self contains other_str" do + "hello".include?("lo").should == true + "hello".include?("ol").should == false + end + + it "ignores subclass differences" do + "hello".include?(StringSpecs::MyString.new("lo")).should == true + StringSpecs::MyString.new("hello").include?("lo").should == true + StringSpecs::MyString.new("hello").include?(StringSpecs::MyString.new("lo")).should == true + end + + it "returns true if both strings are empty" do + "".should.include?("") + "".dup.force_encoding("EUC-JP").should.include?("") + "".should.include?("".dup.force_encoding("EUC-JP")) + "".dup.force_encoding("EUC-JP").should.include?("".dup.force_encoding("EUC-JP")) + end + + it "returns true if the RHS is empty" do + "a".should.include?("") + "a".dup.force_encoding("EUC-JP").should.include?("") + "a".should.include?("".dup.force_encoding("EUC-JP")) + "a".dup.force_encoding("EUC-JP").should.include?("".dup.force_encoding("EUC-JP")) + end + + it "tries to convert other to string using to_str" do + other = mock('lo') + other.should_receive(:to_str).and_return("lo") + + "hello".include?(other).should == true + end + + it "raises a TypeError if other can't be converted to string" do + -> { "hello".include?([]) }.should raise_error(TypeError) + -> { "hello".include?('h'.ord) }.should raise_error(TypeError) + -> { "hello".include?(mock('x')) }.should raise_error(TypeError) + end + + it "raises an Encoding::CompatibilityError if the encodings are incompatible" do + pat = "ã‚¢".encode Encoding::EUC_JP + -> do + "ã‚れ".include?(pat) + end.should raise_error(Encoding::CompatibilityError) + end +end diff --git a/spec/ruby/core/string/index_spec.rb b/spec/ruby/core/string/index_spec.rb new file mode 100644 index 0000000000..835263a2cd --- /dev/null +++ b/spec/ruby/core/string/index_spec.rb @@ -0,0 +1,350 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "String#index" do + it "raises a TypeError if passed nil" do + -> { "abc".index nil }.should raise_error(TypeError) + end + + it "raises a TypeError if passed a boolean" do + -> { "abc".index true }.should raise_error(TypeError) + end + + it "raises a TypeError if passed a Symbol" do + -> { "abc".index :a }.should raise_error(TypeError) + end + + it "calls #to_str to convert the first argument" do + char = mock("string index char") + char.should_receive(:to_str).and_return("b") + "abc".index(char).should == 1 + end + + it "calls #to_int to convert the second argument" do + offset = mock("string index offset") + offset.should_receive(:to_int).and_return(1) + "abc".index("c", offset).should == 2 + end + + it "raises a TypeError if passed an Integer" do + -> { "abc".index 97 }.should raise_error(TypeError) + end +end + +describe "String#index with String" do + it "behaves the same as String#index(char) for one-character strings" do + "blablabla hello cruel world...!".split("").uniq.each do |str| + chr = str[0] + str.index(str).should == str.index(chr) + + 0.upto(str.size + 1) do |start| + str.index(str, start).should == str.index(chr, start) + end + + (-str.size - 1).upto(-1) do |start| + str.index(str, start).should == str.index(chr, start) + end + end + end + + it "returns the index of the first occurrence of the given substring" do + "blablabla".index("").should == 0 + "blablabla".index("b").should == 0 + "blablabla".index("bla").should == 0 + "blablabla".index("blabla").should == 0 + "blablabla".index("blablabla").should == 0 + + "blablabla".index("l").should == 1 + "blablabla".index("la").should == 1 + "blablabla".index("labla").should == 1 + "blablabla".index("lablabla").should == 1 + + "blablabla".index("a").should == 2 + "blablabla".index("abla").should == 2 + "blablabla".index("ablabla").should == 2 + end + + it "doesn't set $~" do + $~ = nil + + 'hello.'.index('ll') + $~.should == nil + end + + it "ignores string subclasses" do + "blablabla".index(StringSpecs::MyString.new("bla")).should == 0 + StringSpecs::MyString.new("blablabla").index("bla").should == 0 + StringSpecs::MyString.new("blablabla").index(StringSpecs::MyString.new("bla")).should == 0 + end + + it "starts the search at the given offset" do + "blablabla".index("bl", 0).should == 0 + "blablabla".index("bl", 1).should == 3 + "blablabla".index("bl", 2).should == 3 + "blablabla".index("bl", 3).should == 3 + + "blablabla".index("bla", 0).should == 0 + "blablabla".index("bla", 1).should == 3 + "blablabla".index("bla", 2).should == 3 + "blablabla".index("bla", 3).should == 3 + + "blablabla".index("blab", 0).should == 0 + "blablabla".index("blab", 1).should == 3 + "blablabla".index("blab", 2).should == 3 + "blablabla".index("blab", 3).should == 3 + + "blablabla".index("la", 1).should == 1 + "blablabla".index("la", 2).should == 4 + "blablabla".index("la", 3).should == 4 + "blablabla".index("la", 4).should == 4 + + "blablabla".index("lab", 1).should == 1 + "blablabla".index("lab", 2).should == 4 + "blablabla".index("lab", 3).should == 4 + "blablabla".index("lab", 4).should == 4 + + "blablabla".index("ab", 2).should == 2 + "blablabla".index("ab", 3).should == 5 + "blablabla".index("ab", 4).should == 5 + "blablabla".index("ab", 5).should == 5 + + "blablabla".index("", 0).should == 0 + "blablabla".index("", 1).should == 1 + "blablabla".index("", 2).should == 2 + "blablabla".index("", 7).should == 7 + "blablabla".index("", 8).should == 8 + "blablabla".index("", 9).should == 9 + end + + it "starts the search at offset + self.length if offset is negative" do + str = "blablabla" + + ["bl", "bla", "blab", "la", "lab", "ab", ""].each do |needle| + (-str.length .. -1).each do |offset| + str.index(needle, offset).should == + str.index(needle, offset + str.length) + end + end + end + + it "returns nil if the substring isn't found" do + "blablabla".index("B").should == nil + "blablabla".index("z").should == nil + "blablabla".index("BLA").should == nil + "blablabla".index("blablablabla").should == nil + "blablabla".index("", 10).should == nil + + "hello".index("he", 1).should == nil + "hello".index("he", 2).should == nil + "I’ve got a multibyte character.\n".index("\n\n").should == nil + end + + it "returns the character index of a multibyte character" do + "ã‚りãŒã¨ã†".index("ãŒ").should == 2 + end + + it "returns the character index after offset" do + "ã‚れã‚れ".index("ã‚", 1).should == 2 + "ã‚りãŒã¨ã†ã‚りãŒã¨ã†".index("ãŒ", 3).should == 7 + end + + it "returns the character index after a partial first match" do + "</</h".index("</h").should == 2 + end + + it "raises an Encoding::CompatibilityError if the encodings are incompatible" do + char = "れ".encode Encoding::EUC_JP + -> do + "ã‚れ".index char + end.should raise_error(Encoding::CompatibilityError) + end + + it "handles a substring in a superset encoding" do + 'abc'.dup.force_encoding(Encoding::US_ASCII).index('é').should == nil + end + + it "handles a substring in a subset encoding" do + 'été'.index('t'.dup.force_encoding(Encoding::US_ASCII)).should == 1 + end + + it "raises an Encoding::CompatibilityError if the encodings are incompatible" do + str = 'abc'.dup.force_encoding("ISO-2022-JP") + pattern = 'b'.dup.force_encoding("EUC-JP") + + -> { str.index(pattern) }.should raise_error(Encoding::CompatibilityError, "incompatible character encodings: ISO-2022-JP and EUC-JP") + end +end + +describe "String#index with Regexp" do + it "behaves the same as String#index(string) for escaped string regexps" do + ["blablabla", "hello cruel world...!"].each do |str| + ["", "b", "bla", "lab", "o c", "d."].each do |needle| + regexp = Regexp.new(Regexp.escape(needle)) + str.index(regexp).should == str.index(needle) + + 0.upto(str.size + 1) do |start| + str.index(regexp, start).should == str.index(needle, start) + end + + (-str.size - 1).upto(-1) do |start| + str.index(regexp, start).should == str.index(needle, start) + end + end + end + end + + it "returns the index of the first match of regexp" do + "blablabla".index(/bla/).should == 0 + "blablabla".index(/BLA/i).should == 0 + + "blablabla".index(/.{0}/).should == 0 + "blablabla".index(/.{6}/).should == 0 + "blablabla".index(/.{9}/).should == 0 + + "blablabla".index(/.*/).should == 0 + "blablabla".index(/.+/).should == 0 + + "blablabla".index(/lab|b/).should == 0 + + not_supported_on :opal do + "blablabla".index(/\A/).should == 0 + "blablabla".index(/\Z/).should == 9 + "blablabla".index(/\z/).should == 9 + "blablabla\n".index(/\Z/).should == 9 + "blablabla\n".index(/\z/).should == 10 + end + + "blablabla".index(/^/).should == 0 + "\nblablabla".index(/^/).should == 0 + "b\nablabla".index(/$/).should == 1 + "bl\nablabla".index(/$/).should == 2 + + "blablabla".index(/.l./).should == 0 + end + + it "sets $~ to MatchData of match and nil when there's none" do + 'hello.'.index(/.(.)/) + $~[0].should == 'he' + + 'hello.'.index(/not/) + $~.should == nil + end + + ruby_bug "#20421", ""..."3.3" do + it "always clear $~" do + "a".index(/a/) + $~.should_not == nil + + string = "blablabla" + string.index(/bla/, string.length + 1) + $~.should == nil + end + end + + it "starts the search at the given offset" do + "blablabla".index(/.{0}/, 5).should == 5 + "blablabla".index(/.{1}/, 5).should == 5 + "blablabla".index(/.{2}/, 5).should == 5 + "blablabla".index(/.{3}/, 5).should == 5 + "blablabla".index(/.{4}/, 5).should == 5 + + "blablabla".index(/.{0}/, 3).should == 3 + "blablabla".index(/.{1}/, 3).should == 3 + "blablabla".index(/.{2}/, 3).should == 3 + "blablabla".index(/.{5}/, 3).should == 3 + "blablabla".index(/.{6}/, 3).should == 3 + + "blablabla".index(/.l./, 0).should == 0 + "blablabla".index(/.l./, 1).should == 3 + "blablabla".index(/.l./, 2).should == 3 + "blablabla".index(/.l./, 3).should == 3 + + "xblaxbla".index(/x./, 0).should == 0 + "xblaxbla".index(/x./, 1).should == 4 + "xblaxbla".index(/x./, 2).should == 4 + + not_supported_on :opal do + "blablabla\n".index(/\Z/, 9).should == 9 + end + end + + it "starts the search at offset + self.length if offset is negative" do + str = "blablabla" + + ["bl", "bla", "blab", "la", "lab", "ab", ""].each do |needle| + (-str.length .. -1).each do |offset| + str.index(needle, offset).should == + str.index(needle, offset + str.length) + end + end + end + + it "returns nil if the substring isn't found" do + "blablabla".index(/BLA/).should == nil + + "blablabla".index(/.{10}/).should == nil + "blaxbla".index(/.x/, 3).should == nil + "blaxbla".index(/..x/, 2).should == nil + end + + it "returns nil if the Regexp matches the empty string and the offset is out of range" do + "ruby".index(//,12).should be_nil + end + + it "supports \\G which matches at the given start offset" do + "helloYOU.".index(/\GYOU/, 5).should == 5 + "helloYOU.".index(/\GYOU/).should == nil + + re = /\G.+YOU/ + # The # marks where \G will match. + [ + ["#hi!YOUall.", 0], + ["h#i!YOUall.", 1], + ["hi#!YOUall.", 2], + ["hi!#YOUall.", nil] + ].each do |spec| + + start = spec[0].index("#") + str = spec[0].delete("#") + + str.index(re, start).should == spec[1] + end + end + + it "converts start_offset to an integer via to_int" do + obj = mock('1') + obj.should_receive(:to_int).and_return(1) + "RWOARW".index(/R./, obj).should == 4 + end + + it "returns the character index of a multibyte character" do + "ã‚りãŒã¨ã†".index(/ãŒ/).should == 2 + end + + it "returns the character index after offset" do + "ã‚れã‚れ".index(/ã‚/, 1).should == 2 + end + + it "treats the offset as a character index" do + "ã‚れã‚ã‚れ".index(/ã‚/, 3).should == 3 + end + + ruby_bug "#19763", ""..."3.3.0" do + it "raises an Encoding::CompatibilityError if the encodings are incompatible" do + re = Regexp.new "れ".encode(Encoding::EUC_JP) + -> do + "ã‚れ".index re + end.should raise_error(Encoding::CompatibilityError, "incompatible encoding regexp match (EUC-JP regexp with UTF-8 string)") + end + end + + # The exception message was incorrectly "incompatible character encodings: UTF-8 and EUC-JP" before 3.3.0 + # Still test that the right exception class is used before that. + it "raises an Encoding::CompatibilityError if the encodings are incompatible" do + re = Regexp.new "れ".encode(Encoding::EUC_JP) + -> do + "ã‚れ".index re + end.should raise_error(Encoding::CompatibilityError) + end +end diff --git a/spec/ruby/core/string/initialize_spec.rb b/spec/ruby/core/string/initialize_spec.rb new file mode 100644 index 0000000000..08734cc916 --- /dev/null +++ b/spec/ruby/core/string/initialize_spec.rb @@ -0,0 +1,26 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/replace' + +describe "String#initialize" do + it "is a private method" do + String.should have_private_instance_method(:initialize) + end + + describe "with no arguments" do + it "does not change self" do + s = "some string" + s.send :initialize + s.should == "some string" + end + + it "does not raise an exception when frozen" do + a = "hello".freeze + a.send(:initialize).should equal(a) + end + end + + describe "with an argument" do + it_behaves_like :string_replace, :initialize + end +end diff --git a/spec/ruby/core/string/insert_spec.rb b/spec/ruby/core/string/insert_spec.rb new file mode 100644 index 0000000000..483f3c9367 --- /dev/null +++ b/spec/ruby/core/string/insert_spec.rb @@ -0,0 +1,81 @@ +# -*- encoding: utf-8 -*- +# frozen_string_literal: false +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "String#insert with index, other" do + it "inserts other before the character at the given index" do + "abcd".insert(0, 'X').should == "Xabcd" + "abcd".insert(3, 'X').should == "abcXd" + "abcd".insert(4, 'X').should == "abcdX" + end + + it "modifies self in place" do + a = "abcd" + a.insert(4, 'X').should == "abcdX" + a.should == "abcdX" + end + + it "inserts after the given character on an negative count" do + "abcd".insert(-5, 'X').should == "Xabcd" + "abcd".insert(-3, 'X').should == "abXcd" + "abcd".insert(-1, 'X').should == "abcdX" + end + + it "raises an IndexError if the index is beyond string" do + -> { "abcd".insert(5, 'X') }.should raise_error(IndexError) + -> { "abcd".insert(-6, 'X') }.should raise_error(IndexError) + end + + it "converts index to an integer using to_int" do + other = mock('-3') + other.should_receive(:to_int).and_return(-3) + + "abcd".insert(other, "XYZ").should == "abXYZcd" + end + + it "converts other to a string using to_str" do + other = mock('XYZ') + other.should_receive(:to_str).and_return("XYZ") + + "abcd".insert(-3, other).should == "abXYZcd" + end + + it "raises a TypeError if other can't be converted to string" do + -> { "abcd".insert(-6, Object.new)}.should raise_error(TypeError) + -> { "abcd".insert(-6, []) }.should raise_error(TypeError) + -> { "abcd".insert(-6, mock('x')) }.should raise_error(TypeError) + end + + it "raises a FrozenError if self is frozen" do + str = "abcd".freeze + -> { str.insert(4, '') }.should raise_error(FrozenError) + -> { str.insert(4, 'X') }.should raise_error(FrozenError) + end + + it "inserts a character into a multibyte encoded string" do + "ã‚りãŒã¨ã†".insert(1, 'ü').should == "ã‚üりãŒã¨ã†" + end + + it "returns a String in the compatible encoding" do + str = "".force_encoding(Encoding::US_ASCII) + str.insert(0, "ã‚りãŒã¨ã†") + str.encoding.should == Encoding::UTF_8 + end + + it "raises an Encoding::CompatibilityError if the encodings are incompatible" do + pat = "ã‚¢".encode Encoding::EUC_JP + -> do + "ã‚れ".insert 0, pat + end.should raise_error(Encoding::CompatibilityError) + end + + it "should not call subclassed string methods" do + cls = Class.new(String) do + def replace(arg) + raise "should not call replace" + end + end + cls.new("abcd").insert(0, 'X').should == "Xabcd" + end +end diff --git a/spec/ruby/core/string/inspect_spec.rb b/spec/ruby/core/string/inspect_spec.rb new file mode 100644 index 0000000000..15db06c7f5 --- /dev/null +++ b/spec/ruby/core/string/inspect_spec.rb @@ -0,0 +1,520 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "String#inspect" do + it "does not return a subclass instance" do + StringSpecs::MyString.new.inspect.should be_an_instance_of(String) + end + + it "returns a string with special characters replaced with \\<char> notation" do + [ ["\a", '"\\a"'], + ["\b", '"\\b"'], + ["\t", '"\\t"'], + ["\n", '"\\n"'], + ["\v", '"\\v"'], + ["\f", '"\\f"'], + ["\r", '"\\r"'], + ["\e", '"\\e"'] + ].should be_computed_by(:inspect) + end + + it "returns a string with special characters replaced with \\<char> notation for UTF-16" do + pairs = [ + ["\a", '"\\a"'], + ["\b", '"\\b"'], + ["\t", '"\\t"'], + ["\n", '"\\n"'], + ["\v", '"\\v"'], + ["\f", '"\\f"'], + ["\r", '"\\r"'], + ["\e", '"\\e"'] + ].map { |str, result| [str.encode('UTF-16LE'), result] } + + pairs.should be_computed_by(:inspect) + end + + it "returns a string with \" and \\ escaped with a backslash" do + [ ["\"", '"\\""'], + ["\\", '"\\\\"'] + ].should be_computed_by(:inspect) + end + + it "returns a string with \\#<char> when # is followed by $, @, {" do + [ ["\#$", '"\\#$"'], + ["\#@", '"\\#@"'], + ["\#{", '"\\#{"'] + ].should be_computed_by(:inspect) + end + + it "returns a string with # not escaped when followed by any other character" do + [ ["#", '"#"'], + ["#1", '"#1"'] + ].should be_computed_by(:inspect) + end + + it "returns a string with printable non-alphanumeric characters unescaped" do + [ [" ", '" "'], + ["!", '"!"'], + ["$", '"$"'], + ["%", '"%"'], + ["&", '"&"'], + ["'", '"\'"'], + ["(", '"("'], + [")", '")"'], + ["*", '"*"'], + ["+", '"+"'], + [",", '","'], + ["-", '"-"'], + [".", '"."'], + ["/", '"/"'], + [":", '":"'], + [";", '";"'], + ["<", '"<"'], + ["=", '"="'], + [">", '">"'], + ["?", '"?"'], + ["@", '"@"'], + ["[", '"["'], + ["]", '"]"'], + ["^", '"^"'], + ["_", '"_"'], + ["`", '"`"'], + ["{", '"{"'], + ["|", '"|"'], + ["}", '"}"'], + ["~", '"~"'] + ].should be_computed_by(:inspect) + end + + it "returns a string with numeric characters unescaped" do + [ ["0", '"0"'], + ["1", '"1"'], + ["2", '"2"'], + ["3", '"3"'], + ["4", '"4"'], + ["5", '"5"'], + ["6", '"6"'], + ["7", '"7"'], + ["8", '"8"'], + ["9", '"9"'], + ].should be_computed_by(:inspect) + end + + it "returns a string with upper-case alpha characters unescaped" do + [ ["A", '"A"'], + ["B", '"B"'], + ["C", '"C"'], + ["D", '"D"'], + ["E", '"E"'], + ["F", '"F"'], + ["G", '"G"'], + ["H", '"H"'], + ["I", '"I"'], + ["J", '"J"'], + ["K", '"K"'], + ["L", '"L"'], + ["M", '"M"'], + ["N", '"N"'], + ["O", '"O"'], + ["P", '"P"'], + ["Q", '"Q"'], + ["R", '"R"'], + ["S", '"S"'], + ["T", '"T"'], + ["U", '"U"'], + ["V", '"V"'], + ["W", '"W"'], + ["X", '"X"'], + ["Y", '"Y"'], + ["Z", '"Z"'] + ].should be_computed_by(:inspect) + end + + it "returns a string with lower-case alpha characters unescaped" do + [ ["a", '"a"'], + ["b", '"b"'], + ["c", '"c"'], + ["d", '"d"'], + ["e", '"e"'], + ["f", '"f"'], + ["g", '"g"'], + ["h", '"h"'], + ["i", '"i"'], + ["j", '"j"'], + ["k", '"k"'], + ["l", '"l"'], + ["m", '"m"'], + ["n", '"n"'], + ["o", '"o"'], + ["p", '"p"'], + ["q", '"q"'], + ["r", '"r"'], + ["s", '"s"'], + ["t", '"t"'], + ["u", '"u"'], + ["v", '"v"'], + ["w", '"w"'], + ["x", '"x"'], + ["y", '"y"'], + ["z", '"z"'] + ].should be_computed_by(:inspect) + end + + it "returns a string with non-printing characters replaced by \\x notation" do + # Avoid the file encoding by computing the string with #chr. + [ [0001.chr, '"\\x01"'], + [0002.chr, '"\\x02"'], + [0003.chr, '"\\x03"'], + [0004.chr, '"\\x04"'], + [0005.chr, '"\\x05"'], + [0006.chr, '"\\x06"'], + [0016.chr, '"\\x0E"'], + [0017.chr, '"\\x0F"'], + [0020.chr, '"\\x10"'], + [0021.chr, '"\\x11"'], + [0022.chr, '"\\x12"'], + [0023.chr, '"\\x13"'], + [0024.chr, '"\\x14"'], + [0025.chr, '"\\x15"'], + [0026.chr, '"\\x16"'], + [0027.chr, '"\\x17"'], + [0030.chr, '"\\x18"'], + [0031.chr, '"\\x19"'], + [0032.chr, '"\\x1A"'], + [0034.chr, '"\\x1C"'], + [0035.chr, '"\\x1D"'], + [0036.chr, '"\\x1E"'], + [0037.chr, '"\\x1F"'], + [0177.chr, '"\\x7F"'], + [0200.chr, '"\\x80"'], + [0201.chr, '"\\x81"'], + [0202.chr, '"\\x82"'], + [0203.chr, '"\\x83"'], + [0204.chr, '"\\x84"'], + [0205.chr, '"\\x85"'], + [0206.chr, '"\\x86"'], + [0207.chr, '"\\x87"'], + [0210.chr, '"\\x88"'], + [0211.chr, '"\\x89"'], + [0212.chr, '"\\x8A"'], + [0213.chr, '"\\x8B"'], + [0214.chr, '"\\x8C"'], + [0215.chr, '"\\x8D"'], + [0216.chr, '"\\x8E"'], + [0217.chr, '"\\x8F"'], + [0220.chr, '"\\x90"'], + [0221.chr, '"\\x91"'], + [0222.chr, '"\\x92"'], + [0223.chr, '"\\x93"'], + [0224.chr, '"\\x94"'], + [0225.chr, '"\\x95"'], + [0226.chr, '"\\x96"'], + [0227.chr, '"\\x97"'], + [0230.chr, '"\\x98"'], + [0231.chr, '"\\x99"'], + [0232.chr, '"\\x9A"'], + [0233.chr, '"\\x9B"'], + [0234.chr, '"\\x9C"'], + [0235.chr, '"\\x9D"'], + [0236.chr, '"\\x9E"'], + [0237.chr, '"\\x9F"'], + [0240.chr, '"\\xA0"'], + [0241.chr, '"\\xA1"'], + [0242.chr, '"\\xA2"'], + [0243.chr, '"\\xA3"'], + [0244.chr, '"\\xA4"'], + [0245.chr, '"\\xA5"'], + [0246.chr, '"\\xA6"'], + [0247.chr, '"\\xA7"'], + [0250.chr, '"\\xA8"'], + [0251.chr, '"\\xA9"'], + [0252.chr, '"\\xAA"'], + [0253.chr, '"\\xAB"'], + [0254.chr, '"\\xAC"'], + [0255.chr, '"\\xAD"'], + [0256.chr, '"\\xAE"'], + [0257.chr, '"\\xAF"'], + [0260.chr, '"\\xB0"'], + [0261.chr, '"\\xB1"'], + [0262.chr, '"\\xB2"'], + [0263.chr, '"\\xB3"'], + [0264.chr, '"\\xB4"'], + [0265.chr, '"\\xB5"'], + [0266.chr, '"\\xB6"'], + [0267.chr, '"\\xB7"'], + [0270.chr, '"\\xB8"'], + [0271.chr, '"\\xB9"'], + [0272.chr, '"\\xBA"'], + [0273.chr, '"\\xBB"'], + [0274.chr, '"\\xBC"'], + [0275.chr, '"\\xBD"'], + [0276.chr, '"\\xBE"'], + [0277.chr, '"\\xBF"'], + [0300.chr, '"\\xC0"'], + [0301.chr, '"\\xC1"'], + [0302.chr, '"\\xC2"'], + [0303.chr, '"\\xC3"'], + [0304.chr, '"\\xC4"'], + [0305.chr, '"\\xC5"'], + [0306.chr, '"\\xC6"'], + [0307.chr, '"\\xC7"'], + [0310.chr, '"\\xC8"'], + [0311.chr, '"\\xC9"'], + [0312.chr, '"\\xCA"'], + [0313.chr, '"\\xCB"'], + [0314.chr, '"\\xCC"'], + [0315.chr, '"\\xCD"'], + [0316.chr, '"\\xCE"'], + [0317.chr, '"\\xCF"'], + [0320.chr, '"\\xD0"'], + [0321.chr, '"\\xD1"'], + [0322.chr, '"\\xD2"'], + [0323.chr, '"\\xD3"'], + [0324.chr, '"\\xD4"'], + [0325.chr, '"\\xD5"'], + [0326.chr, '"\\xD6"'], + [0327.chr, '"\\xD7"'], + [0330.chr, '"\\xD8"'], + [0331.chr, '"\\xD9"'], + [0332.chr, '"\\xDA"'], + [0333.chr, '"\\xDB"'], + [0334.chr, '"\\xDC"'], + [0335.chr, '"\\xDD"'], + [0336.chr, '"\\xDE"'], + [0337.chr, '"\\xDF"'], + [0340.chr, '"\\xE0"'], + [0341.chr, '"\\xE1"'], + [0342.chr, '"\\xE2"'], + [0343.chr, '"\\xE3"'], + [0344.chr, '"\\xE4"'], + [0345.chr, '"\\xE5"'], + [0346.chr, '"\\xE6"'], + [0347.chr, '"\\xE7"'], + [0350.chr, '"\\xE8"'], + [0351.chr, '"\\xE9"'], + [0352.chr, '"\\xEA"'], + [0353.chr, '"\\xEB"'], + [0354.chr, '"\\xEC"'], + [0355.chr, '"\\xED"'], + [0356.chr, '"\\xEE"'], + [0357.chr, '"\\xEF"'], + [0360.chr, '"\\xF0"'], + [0361.chr, '"\\xF1"'], + [0362.chr, '"\\xF2"'], + [0363.chr, '"\\xF3"'], + [0364.chr, '"\\xF4"'], + [0365.chr, '"\\xF5"'], + [0366.chr, '"\\xF6"'], + [0367.chr, '"\\xF7"'], + [0370.chr, '"\\xF8"'], + [0371.chr, '"\\xF9"'], + [0372.chr, '"\\xFA"'], + [0373.chr, '"\\xFB"'], + [0374.chr, '"\\xFC"'], + [0375.chr, '"\\xFD"'], + [0376.chr, '"\\xFE"'], + [0377.chr, '"\\xFF"'] + ].should be_computed_by(:inspect) + end + + it "returns a string with a NUL character replaced by \\x notation" do + 0.chr.inspect.should == '"\\x00"' + end + + it "uses \\x notation for broken UTF-8 sequences" do + "\xF0\x9F".inspect.should == '"\\xF0\\x9F"' + end + + it "works for broken US-ASCII strings" do + s = "©".dup.force_encoding("US-ASCII") + s.inspect.should == '"\xC2\xA9"' + end + + describe "when default external is UTF-8" do + before :each do + @extenc, Encoding.default_external = Encoding.default_external, Encoding::UTF_8 + end + + after :each do + Encoding.default_external = @extenc + end + + it "returns a string with non-printing characters replaced by \\u notation for Unicode strings" do + [ [0001.chr('utf-8'), '"\u0001"'], + [0002.chr('utf-8'), '"\u0002"'], + [0003.chr('utf-8'), '"\u0003"'], + [0004.chr('utf-8'), '"\u0004"'], + [0005.chr('utf-8'), '"\u0005"'], + [0006.chr('utf-8'), '"\u0006"'], + [0016.chr('utf-8'), '"\u000E"'], + [0017.chr('utf-8'), '"\u000F"'], + [0020.chr('utf-8'), '"\u0010"'], + [0021.chr('utf-8'), '"\u0011"'], + [0022.chr('utf-8'), '"\u0012"'], + [0023.chr('utf-8'), '"\u0013"'], + [0024.chr('utf-8'), '"\u0014"'], + [0025.chr('utf-8'), '"\u0015"'], + [0026.chr('utf-8'), '"\u0016"'], + [0027.chr('utf-8'), '"\u0017"'], + [0030.chr('utf-8'), '"\u0018"'], + [0031.chr('utf-8'), '"\u0019"'], + [0032.chr('utf-8'), '"\u001A"'], + [0034.chr('utf-8'), '"\u001C"'], + [0035.chr('utf-8'), '"\u001D"'], + [0036.chr('utf-8'), '"\u001E"'], + [0037.chr('utf-8'), '"\u001F"'], + [0177.chr('utf-8'), '"\u007F"'], + [0200.chr('utf-8'), '"\u0080"'], + [0201.chr('utf-8'), '"\u0081"'], + [0202.chr('utf-8'), '"\u0082"'], + [0203.chr('utf-8'), '"\u0083"'], + [0204.chr('utf-8'), '"\u0084"'], + [0206.chr('utf-8'), '"\u0086"'], + [0207.chr('utf-8'), '"\u0087"'], + [0210.chr('utf-8'), '"\u0088"'], + [0211.chr('utf-8'), '"\u0089"'], + [0212.chr('utf-8'), '"\u008A"'], + [0213.chr('utf-8'), '"\u008B"'], + [0214.chr('utf-8'), '"\u008C"'], + [0215.chr('utf-8'), '"\u008D"'], + [0216.chr('utf-8'), '"\u008E"'], + [0217.chr('utf-8'), '"\u008F"'], + [0220.chr('utf-8'), '"\u0090"'], + [0221.chr('utf-8'), '"\u0091"'], + [0222.chr('utf-8'), '"\u0092"'], + [0223.chr('utf-8'), '"\u0093"'], + [0224.chr('utf-8'), '"\u0094"'], + [0225.chr('utf-8'), '"\u0095"'], + [0226.chr('utf-8'), '"\u0096"'], + [0227.chr('utf-8'), '"\u0097"'], + [0230.chr('utf-8'), '"\u0098"'], + [0231.chr('utf-8'), '"\u0099"'], + [0232.chr('utf-8'), '"\u009A"'], + [0233.chr('utf-8'), '"\u009B"'], + [0234.chr('utf-8'), '"\u009C"'], + [0235.chr('utf-8'), '"\u009D"'], + [0236.chr('utf-8'), '"\u009E"'], + [0237.chr('utf-8'), '"\u009F"'], + ].should be_computed_by(:inspect) + end + + it "returns a string with a NUL character replaced by \\u notation" do + 0.chr('utf-8').inspect.should == '"\\u0000"' + end + + it "returns a string with extended characters for Unicode strings" do + [ [0240.chr('utf-8'), '" "'], + [0241.chr('utf-8'), '"¡"'], + [0242.chr('utf-8'), '"¢"'], + [0243.chr('utf-8'), '"£"'], + [0244.chr('utf-8'), '"¤"'], + [0245.chr('utf-8'), '"Â¥"'], + [0246.chr('utf-8'), '"¦"'], + [0247.chr('utf-8'), '"§"'], + [0250.chr('utf-8'), '"¨"'], + [0251.chr('utf-8'), '"©"'], + [0252.chr('utf-8'), '"ª"'], + [0253.chr('utf-8'), '"«"'], + [0254.chr('utf-8'), '"¬"'], + [0255.chr('utf-8'), '"Â"'], + [0256.chr('utf-8'), '"®"'], + [0257.chr('utf-8'), '"¯"'], + [0260.chr('utf-8'), '"°"'], + [0261.chr('utf-8'), '"±"'], + [0262.chr('utf-8'), '"²"'], + [0263.chr('utf-8'), '"³"'], + [0264.chr('utf-8'), '"´"'], + [0265.chr('utf-8'), '"µ"'], + [0266.chr('utf-8'), '"¶"'], + [0267.chr('utf-8'), '"·"'], + [0270.chr('utf-8'), '"¸"'], + [0271.chr('utf-8'), '"¹"'], + [0272.chr('utf-8'), '"º"'], + [0273.chr('utf-8'), '"»"'], + [0274.chr('utf-8'), '"¼"'], + [0275.chr('utf-8'), '"½"'], + [0276.chr('utf-8'), '"¾"'], + [0277.chr('utf-8'), '"¿"'], + [0300.chr('utf-8'), '"À"'], + [0301.chr('utf-8'), '"Ã"'], + [0302.chr('utf-8'), '"Â"'], + [0303.chr('utf-8'), '"Ã"'], + [0304.chr('utf-8'), '"Ä"'], + [0305.chr('utf-8'), '"Ã…"'], + [0306.chr('utf-8'), '"Æ"'], + [0307.chr('utf-8'), '"Ç"'], + [0310.chr('utf-8'), '"È"'], + [0311.chr('utf-8'), '"É"'], + [0312.chr('utf-8'), '"Ê"'], + [0313.chr('utf-8'), '"Ë"'], + [0314.chr('utf-8'), '"ÃŒ"'], + [0315.chr('utf-8'), '"Ã"'], + [0316.chr('utf-8'), '"ÃŽ"'], + [0317.chr('utf-8'), '"Ã"'], + [0320.chr('utf-8'), '"Ã"'], + [0321.chr('utf-8'), '"Ñ"'], + [0322.chr('utf-8'), '"Ã’"'], + [0323.chr('utf-8'), '"Ó"'], + [0324.chr('utf-8'), '"Ô"'], + [0325.chr('utf-8'), '"Õ"'], + [0326.chr('utf-8'), '"Ö"'], + [0327.chr('utf-8'), '"×"'], + [0330.chr('utf-8'), '"Ø"'], + [0331.chr('utf-8'), '"Ù"'], + [0332.chr('utf-8'), '"Ú"'], + [0333.chr('utf-8'), '"Û"'], + [0334.chr('utf-8'), '"Ü"'], + [0335.chr('utf-8'), '"Ã"'], + [0336.chr('utf-8'), '"Þ"'], + [0337.chr('utf-8'), '"ß"'], + [0340.chr('utf-8'), '"à "'], + [0341.chr('utf-8'), '"á"'], + [0342.chr('utf-8'), '"â"'], + [0343.chr('utf-8'), '"ã"'], + [0344.chr('utf-8'), '"ä"'], + [0345.chr('utf-8'), '"Ã¥"'], + [0346.chr('utf-8'), '"æ"'], + [0347.chr('utf-8'), '"ç"'], + [0350.chr('utf-8'), '"è"'], + [0351.chr('utf-8'), '"é"'], + [0352.chr('utf-8'), '"ê"'], + [0353.chr('utf-8'), '"ë"'], + [0354.chr('utf-8'), '"ì"'], + [0355.chr('utf-8'), '"Ã"'], + [0356.chr('utf-8'), '"î"'], + [0357.chr('utf-8'), '"ï"'], + [0360.chr('utf-8'), '"ð"'], + [0361.chr('utf-8'), '"ñ"'], + [0362.chr('utf-8'), '"ò"'], + [0363.chr('utf-8'), '"ó"'], + [0364.chr('utf-8'), '"ô"'], + [0365.chr('utf-8'), '"õ"'], + [0366.chr('utf-8'), '"ö"'], + [0367.chr('utf-8'), '"÷"'], + [0370.chr('utf-8'), '"ø"'], + [0371.chr('utf-8'), '"ù"'], + [0372.chr('utf-8'), '"ú"'], + [0373.chr('utf-8'), '"û"'], + [0374.chr('utf-8'), '"ü"'], + [0375.chr('utf-8'), '"ý"'], + [0376.chr('utf-8'), '"þ"'], + [0377.chr('utf-8'), '"ÿ"'] + ].should be_computed_by(:inspect) + end + end + + describe "when the string's encoding is different than the result's encoding" do + describe "and the string's encoding is ASCII-compatible but the characters are non-ASCII" do + it "returns a string with the non-ASCII characters replaced by \\x notation" do + "\u{3042}".encode("EUC-JP").inspect.should == '"\\x{A4A2}"' + end + end + + describe "and the string has both ASCII-compatible and ASCII-incompatible chars" do + it "returns a string with the non-ASCII characters replaced by \\u notation" do + "hello привет".encode("utf-16le").inspect.should == '"hello \\u043F\\u0440\\u0438\\u0432\\u0435\\u0442"' + end + end + end +end diff --git a/spec/ruby/core/string/intern_spec.rb b/spec/ruby/core/string/intern_spec.rb new file mode 100644 index 0000000000..cd7dad4359 --- /dev/null +++ b/spec/ruby/core/string/intern_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/to_sym' + +describe "String#intern" do + it_behaves_like :string_to_sym, :intern +end diff --git a/spec/ruby/core/string/length_spec.rb b/spec/ruby/core/string/length_spec.rb new file mode 100644 index 0000000000..98cee1f03d --- /dev/null +++ b/spec/ruby/core/string/length_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/length' + +describe "String#length" do + it_behaves_like :string_length, :length +end diff --git a/spec/ruby/core/string/lines_spec.rb b/spec/ruby/core/string/lines_spec.rb new file mode 100644 index 0000000000..40ab5f71d8 --- /dev/null +++ b/spec/ruby/core/string/lines_spec.rb @@ -0,0 +1,19 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/each_line' + +describe "String#lines" do + it_behaves_like :string_each_line, :lines + + it "returns an array when no block given" do + ary = "hello world".lines(' ') + ary.should == ["hello ", "world"] + end + + context "when `chomp` keyword argument is passed" do + it "removes new line characters" do + "hello \nworld\n".lines(chomp: true).should == ["hello ", "world"] + "hello \r\nworld\r\n".lines(chomp: true).should == ["hello ", "world"] + end + end +end diff --git a/spec/ruby/core/string/ljust_spec.rb b/spec/ruby/core/string/ljust_spec.rb new file mode 100644 index 0000000000..47324c59d2 --- /dev/null +++ b/spec/ruby/core/string/ljust_spec.rb @@ -0,0 +1,100 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "String#ljust with length, padding" do + it "returns a new string of specified length with self left justified and padded with padstr" do + "hello".ljust(20, '1234').should == "hello123412341234123" + + "".ljust(1, "abcd").should == "a" + "".ljust(2, "abcd").should == "ab" + "".ljust(3, "abcd").should == "abc" + "".ljust(4, "abcd").should == "abcd" + "".ljust(6, "abcd").should == "abcdab" + + "OK".ljust(3, "abcd").should == "OKa" + "OK".ljust(4, "abcd").should == "OKab" + "OK".ljust(6, "abcd").should == "OKabcd" + "OK".ljust(8, "abcd").should == "OKabcdab" + end + + it "pads with whitespace if no padstr is given" do + "hello".ljust(20).should == "hello " + end + + it "returns self if it's longer than or as long as the specified length" do + "".ljust(0).should == "" + "".ljust(-1).should == "" + "hello".ljust(4).should == "hello" + "hello".ljust(-1).should == "hello" + "this".ljust(3).should == "this" + "radiology".ljust(8, '-').should == "radiology" + end + + it "tries to convert length to an integer using to_int" do + "^".ljust(3.8, "_^").should == "^_^" + + obj = mock('3') + obj.should_receive(:to_int).and_return(3) + + "o".ljust(obj, "_o").should == "o_o" + end + + it "raises a TypeError when length can't be converted to an integer" do + -> { "hello".ljust("x") }.should raise_error(TypeError) + -> { "hello".ljust("x", "y") }.should raise_error(TypeError) + -> { "hello".ljust([]) }.should raise_error(TypeError) + -> { "hello".ljust(mock('x')) }.should raise_error(TypeError) + end + + it "tries to convert padstr to a string using to_str" do + padstr = mock('123') + padstr.should_receive(:to_str).and_return("123") + + "hello".ljust(10, padstr).should == "hello12312" + end + + it "raises a TypeError when padstr can't be converted" do + -> { "hello".ljust(20, []) }.should raise_error(TypeError) + -> { "hello".ljust(20, Object.new)}.should raise_error(TypeError) + -> { "hello".ljust(20, mock('x')) }.should raise_error(TypeError) + end + + it "raises an ArgumentError when padstr is empty" do + -> { "hello".ljust(10, '') }.should raise_error(ArgumentError) + end + + it "returns String instances when called on subclasses" do + StringSpecs::MyString.new("").ljust(10).should be_an_instance_of(String) + StringSpecs::MyString.new("foo").ljust(10).should be_an_instance_of(String) + StringSpecs::MyString.new("foo").ljust(10, StringSpecs::MyString.new("x")).should be_an_instance_of(String) + + "".ljust(10, StringSpecs::MyString.new("x")).should be_an_instance_of(String) + "foo".ljust(10, StringSpecs::MyString.new("x")).should be_an_instance_of(String) + end + + describe "with width" do + it "returns a String in the same encoding as the original" do + str = "abc".dup.force_encoding Encoding::IBM437 + result = str.ljust 5 + result.should == "abc " + result.encoding.should equal(Encoding::IBM437) + end + end + + describe "with width, pattern" do + it "returns a String in the compatible encoding" do + str = "abc".dup.force_encoding Encoding::IBM437 + result = str.ljust 5, "ã‚" + result.should == "abcã‚ã‚" + result.encoding.should equal(Encoding::UTF_8) + end + + it "raises an Encoding::CompatibilityError if the encodings are incompatible" do + pat = "ã‚¢".encode Encoding::EUC_JP + -> do + "ã‚れ".ljust 5, pat + end.should raise_error(Encoding::CompatibilityError) + end + end +end diff --git a/spec/ruby/core/string/lstrip_spec.rb b/spec/ruby/core/string/lstrip_spec.rb new file mode 100644 index 0000000000..c83650207e --- /dev/null +++ b/spec/ruby/core/string/lstrip_spec.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: false +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/strip' + +describe "String#lstrip" do + it_behaves_like :string_strip, :lstrip + + it "returns a copy of self with leading whitespace removed" do + " hello ".lstrip.should == "hello " + " hello world ".lstrip.should == "hello world " + "\n\r\t\n\v\r hello world ".lstrip.should == "hello world " + "hello".lstrip.should == "hello" + " ã“ã«ã¡ã‚".lstrip.should == "ã“ã«ã¡ã‚" + end + + it "works with lazy substrings" do + " hello "[1...-1].lstrip.should == "hello " + " hello world "[1...-1].lstrip.should == "hello world " + "\n\r\t\n\v\r hello world "[1...-1].lstrip.should == "hello world " + " ã“ã«ã¡ã‚ "[1...-1].lstrip.should == "ã“ã«ã¡ã‚" + end + + it "strips leading \\0" do + "\x00hello".lstrip.should == "hello" + "\000 \000hello\000 \000".lstrip.should == "hello\000 \000" + end +end + +describe "String#lstrip!" do + it "modifies self in place and returns self" do + a = " hello " + a.lstrip!.should equal(a) + a.should == "hello " + end + + it "returns nil if no modifications were made" do + a = "hello" + a.lstrip!.should == nil + a.should == "hello" + end + + it "makes a string empty if it is only whitespace" do + "".lstrip!.should == nil + " ".lstrip.should == "" + " ".lstrip.should == "" + end + + it "removes leading NULL bytes and whitespace" do + a = "\000 \000hello\000 \000" + a.lstrip! + a.should == "hello\000 \000" + end + + it "raises a FrozenError on a frozen instance that is modified" do + -> { " hello ".freeze.lstrip! }.should raise_error(FrozenError) + end + + # see [ruby-core:23657] + it "raises a FrozenError on a frozen instance that would not be modified" do + -> { "hello".freeze.lstrip! }.should raise_error(FrozenError) + -> { "".freeze.lstrip! }.should raise_error(FrozenError) + end + + it "raises an ArgumentError if the first non-space codepoint is invalid" do + s = "\xDFabc".force_encoding(Encoding::UTF_8) + s.valid_encoding?.should be_false + -> { s.lstrip! }.should raise_error(ArgumentError) + + s = " \xDFabc".force_encoding(Encoding::UTF_8) + s.valid_encoding?.should be_false + -> { s.lstrip! }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/string/match_spec.rb b/spec/ruby/core/string/match_spec.rb new file mode 100644 index 0000000000..5e988f34ca --- /dev/null +++ b/spec/ruby/core/string/match_spec.rb @@ -0,0 +1,167 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe :string_match_escaped_literal, shared: true do + not_supported_on :opal do + it "matches a literal Regexp that uses ASCII-only UTF-8 escape sequences" do + "a b".match(/([\u{20}-\u{7e}])/)[0].should == "a" + end + end +end + +describe "String#=~" do + it "behaves the same way as index() when given a regexp" do + ("rudder" =~ /udder/).should == "rudder".index(/udder/) + ("boat" =~ /[^fl]oat/).should == "boat".index(/[^fl]oat/) + ("bean" =~ /bag/).should == "bean".index(/bag/) + ("true" =~ /false/).should == "true".index(/false/) + end + + it "raises a TypeError if a obj is a string" do + -> { "some string" =~ "another string" }.should raise_error(TypeError) + -> { "a" =~ StringSpecs::MyString.new("b") }.should raise_error(TypeError) + end + + it "invokes obj.=~ with self if obj is neither a string nor regexp" do + str = "w00t" + obj = mock('x') + + obj.should_receive(:=~).with(str).any_number_of_times.and_return(true) + str.should =~ obj + + obj = mock('y') + obj.should_receive(:=~).with(str).any_number_of_times.and_return(false) + str.should_not =~ obj + end + + it "sets $~ to MatchData when there is a match and nil when there's none" do + 'hello' =~ /./ + $~[0].should == 'h' + + 'hello' =~ /not/ + $~.should == nil + end + + it "returns the character index of a found match" do + ("ã“ã«ã¡ã‚" =~ /ã«/).should == 1 + end + +end + +describe "String#match" do + it "matches the pattern against self" do + 'hello'.match(/(.)\1/)[0].should == 'll' + end + + it_behaves_like :string_match_escaped_literal, :match + + describe "with [pattern, position]" do + describe "when given a positive position" do + it "matches the pattern against self starting at an optional index" do + "01234".match(/(.).(.)/, 1).captures.should == ["1", "3"] + end + + it "uses the start as a character offset" do + "零一二三四".match(/(.).(.)/, 1).captures.should == ["一", "三"] + end + end + + describe "when given a negative position" do + it "matches the pattern against self starting at an optional index" do + "01234".match(/(.).(.)/, -4).captures.should == ["1", "3"] + end + + it "uses the start as a character offset" do + "零一二三四".match(/(.).(.)/, -4).captures.should == ["一", "三"] + end + end + end + + describe "when passed a block" do + it "yields the MatchData" do + "abc".match(/./) {|m| ScratchPad.record m } + ScratchPad.recorded.should be_kind_of(MatchData) + end + + it "returns the block result" do + "abc".match(/./) { :result }.should == :result + end + + it "does not yield if there is no match" do + ScratchPad.record [] + "b".match(/a/) {|m| ScratchPad << m } + ScratchPad.recorded.should == [] + end + end + + it "tries to convert pattern to a string via to_str" do + obj = mock('.') + def obj.to_str() "." end + "hello".match(obj)[0].should == "h" + + obj = mock('.') + def obj.respond_to?(type, *) true end + def obj.method_missing(*args) "." end + "hello".match(obj)[0].should == "h" + end + + it "raises a TypeError if pattern is not a regexp or a string" do + -> { 'hello'.match(10) }.should raise_error(TypeError) + not_supported_on :opal do + -> { 'hello'.match(:ell) }.should raise_error(TypeError) + end + end + + it "converts string patterns to regexps without escaping" do + 'hello'.match('(.)\1')[0].should == 'll' + end + + it "returns nil if there's no match" do + 'hello'.match('xx').should == nil + end + + it "matches \\G at the start of the string" do + 'hello'.match(/\Gh/)[0].should == 'h' + 'hello'.match(/\Go/).should == nil + end + + it "sets $~ to MatchData of match or nil when there is none" do + 'hello'.match(/./) + $~[0].should == 'h' + Regexp.last_match[0].should == 'h' + + 'hello'.match(/X/) + $~.should == nil + Regexp.last_match.should == nil + end + + it "calls match on the regular expression" do + regexp = /./.dup + regexp.should_receive(:match).and_return(:foo) + 'hello'.match(regexp).should == :foo + end +end + +describe "String#match?" do + before :each do + # Resetting Regexp.last_match + /DONTMATCH/.match '' + end + + context "when matches the given regex" do + it "returns true but does not set Regexp.last_match" do + 'string'.match?(/string/i).should be_true + Regexp.last_match.should be_nil + end + end + + it "returns false when does not match the given regex" do + 'string'.match?(/STRING/).should be_false + end + + it "takes matching position as the 2nd argument" do + 'string'.match?(/str/i, 0).should be_true + 'string'.match?(/str/i, 1).should be_false + end +end diff --git a/spec/ruby/core/string/modulo_spec.rb b/spec/ruby/core/string/modulo_spec.rb new file mode 100644 index 0000000000..46e0aa0f36 --- /dev/null +++ b/spec/ruby/core/string/modulo_spec.rb @@ -0,0 +1,797 @@ +require_relative '../../spec_helper' +require_relative '../kernel/shared/sprintf' +require_relative '../kernel/shared/sprintf_encoding' +require_relative 'fixtures/classes' +require_relative '../../shared/hash/key_error' + +describe "String#%" do + it_behaves_like :kernel_sprintf, -> format, *args { + format % args + } + + it_behaves_like :kernel_sprintf_encoding, -> format, *args { + format % args + } +end + +# TODO: these specs are mostly redundant with kernel/shared/sprintf.rb specs. +# These specs should be moved there and deduplicated. +describe "String#%" do + context "when key is missing from passed-in hash" do + it_behaves_like :key_error, -> obj, key { "%{#{key}}" % obj }, { a: 5 } + end + + it "formats multiple expressions" do + ("%b %x %d %s" % [10, 10, 10, 10]).should == "1010 a 10 10" + end + + it "formats expressions mid string" do + ("hello %s!" % "world").should == "hello world!" + end + + it "formats %% into %" do + ("%d%% %s" % [10, "of chickens!"]).should == "10% of chickens!" + end + + describe "output's encoding" do + it "is the same as the format string if passed value is encoding-compatible" do + [Encoding::BINARY, Encoding::US_ASCII, Encoding::UTF_8, Encoding::SHIFT_JIS].each do |encoding| + ("hello %s!".encode(encoding) % "world").encoding.should == encoding + end + end + + it "negotiates a compatible encoding if necessary" do + ("hello %s" % 195.chr).encoding.should == Encoding::BINARY + ("hello %s".encode("shift_jis") % "wörld").encoding.should == Encoding::UTF_8 + end + + it "raises if a compatible encoding can't be found" do + -> { "hello %s".encode("utf-8") % "world".encode("UTF-16LE") }.should raise_error(Encoding::CompatibilityError) + end + end + + it "raises an error if single % appears at the end" do + -> { ("%" % []) }.should raise_error(ArgumentError) + -> { ("foo%" % [])}.should raise_error(ArgumentError) + end + + ruby_version_is ""..."3.4" do + it "formats single % character before a newline as literal %" do + ("%\n" % []).should == "%\n" + ("foo%\n" % []).should == "foo%\n" + ("%\n.3f" % 1.2).should == "%\n.3f" + end + + it "formats single % character before a NUL as literal %" do + ("%\0" % []).should == "%\0" + ("foo%\0" % []).should == "foo%\0" + ("%\0.3f" % 1.2).should == "%\0.3f" + end + + it "raises an error if single % appears anywhere else" do + -> { (" % " % []) }.should raise_error(ArgumentError) + -> { ("foo%quux" % []) }.should raise_error(ArgumentError) + end + + it "raises an error if NULL or \\n appear anywhere else in the format string" do + begin + old_debug, $DEBUG = $DEBUG, false + + -> { "%.\n3f" % 1.2 }.should raise_error(ArgumentError) + -> { "%.3\nf" % 1.2 }.should raise_error(ArgumentError) + -> { "%.\03f" % 1.2 }.should raise_error(ArgumentError) + -> { "%.3\0f" % 1.2 }.should raise_error(ArgumentError) + ensure + $DEBUG = old_debug + end + end + end + + ruby_version_is "3.4" do + it "raises an ArgumentError if % is not followed by a conversion specifier" do + -> { "%" % [] }.should raise_error(ArgumentError) + -> { "%\n" % [] }.should raise_error(ArgumentError) + -> { "%\0" % [] }.should raise_error(ArgumentError) + -> { " % " % [] }.should raise_error(ArgumentError) + -> { "%.\n3f" % 1.2 }.should raise_error(ArgumentError) + -> { "%.3\nf" % 1.2 }.should raise_error(ArgumentError) + -> { "%.\03f" % 1.2 }.should raise_error(ArgumentError) + -> { "%.3\0f" % 1.2 }.should raise_error(ArgumentError) + end + end + + it "ignores unused arguments when $DEBUG is false" do + begin + old_debug = $DEBUG + $DEBUG = false + + ("" % [1, 2, 3]).should == "" + ("%s" % [1, 2, 3]).should == "1" + ensure + $DEBUG = old_debug + end + end + + it "raises an ArgumentError for unused arguments when $DEBUG is true" do + begin + old_debug = $DEBUG + $DEBUG = true + s = $stderr + $stderr = IOStub.new + + -> { "" % [1, 2, 3] }.should raise_error(ArgumentError) + -> { "%s" % [1, 2, 3] }.should raise_error(ArgumentError) + ensure + $DEBUG = old_debug + $stderr = s + end + end + + it "always allows unused arguments when positional argument style is used" do + begin + old_debug = $DEBUG + $DEBUG = false + + ("%2$s" % [1, 2, 3]).should == "2" + $DEBUG = true + ("%2$s" % [1, 2, 3]).should == "2" + ensure + $DEBUG = old_debug + end + end + + ruby_version_is ""..."3.4" do + it "replaces trailing absolute argument specifier without type with percent sign" do + ("hello %1$" % "foo").should == "hello %" + end + end + + ruby_version_is "3.4" do + it "raises an ArgumentError if absolute argument specifier is followed by a conversion specifier" do + -> { "hello %1$" % "foo" }.should raise_error(ArgumentError) + end + end + + it "raises an ArgumentError when given invalid argument specifiers" do + -> { "%1" % [] }.should raise_error(ArgumentError) + -> { "%+" % [] }.should raise_error(ArgumentError) + -> { "%-" % [] }.should raise_error(ArgumentError) + -> { "%#" % [] }.should raise_error(ArgumentError) + -> { "%0" % [] }.should raise_error(ArgumentError) + -> { "%*" % [] }.should raise_error(ArgumentError) + -> { "%." % [] }.should raise_error(ArgumentError) + -> { "%_" % [] }.should raise_error(ArgumentError) + -> { "%0$s" % "x" }.should raise_error(ArgumentError) + -> { "%*0$s" % [5, "x"] }.should raise_error(ArgumentError) + -> { "%*1$.*0$1$s" % [1, 2, 3] }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when multiple positional argument tokens are given for one format specifier" do + -> { "%1$1$s" % "foo" }.should raise_error(ArgumentError) + end + + it "respects positional arguments and precision tokens given for one format specifier" do + ("%2$1d" % [1, 0]).should == "0" + ("%2$1d" % [0, 1]).should == "1" + + ("%2$.2f" % [1, 0]).should == "0.00" + ("%2$.2f" % [0, 1]).should == "1.00" + end + + it "allows more than one digit of position" do + ("%50$d" % (0..100).to_a).should == "49" + end + + it "raises an ArgumentError when multiple width star tokens are given for one format specifier" do + -> { "%**s" % [5, 5, 5] }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when a width star token is seen after a width token" do + -> { "%5*s" % [5, 5] }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when multiple precision tokens are given" do + -> { "%.5.5s" % 5 }.should raise_error(ArgumentError) + -> { "%.5.*s" % [5, 5] }.should raise_error(ArgumentError) + -> { "%.*.5s" % [5, 5] }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when there are less arguments than format specifiers" do + ("foo" % []).should == "foo" + -> { "%s" % [] }.should raise_error(ArgumentError) + -> { "%s %s" % [1] }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when absolute and relative argument numbers are mixed" do + -> { "%s %1$s" % "foo" }.should raise_error(ArgumentError) + -> { "%1$s %s" % "foo" }.should raise_error(ArgumentError) + + -> { "%s %2$s" % ["foo", "bar"] }.should raise_error(ArgumentError) + -> { "%2$s %s" % ["foo", "bar"] }.should raise_error(ArgumentError) + + -> { "%*2$s" % [5, 5, 5] }.should raise_error(ArgumentError) + -> { "%*.*2$s" % [5, 5, 5] }.should raise_error(ArgumentError) + -> { "%*2$.*2$s" % [5, 5, 5] }.should raise_error(ArgumentError) + -> { "%*.*2$s" % [5, 5, 5] }.should raise_error(ArgumentError) + end + + it "allows reuse of the one argument multiple via absolute argument numbers" do + ("%1$s %1$s" % "foo").should == "foo foo" + ("%1$s %2$s %1$s %2$s" % ["foo", "bar"]).should == "foo bar foo bar" + end + + it "always interprets an array argument as a list of argument parameters" do + -> { "%p" % [] }.should raise_error(ArgumentError) + ("%p" % [1]).should == "1" + ("%p %p" % [1, 2]).should == "1 2" + end + + it "always interprets an array subclass argument as a list of argument parameters" do + -> { "%p" % StringSpecs::MyArray[] }.should raise_error(ArgumentError) + ("%p" % StringSpecs::MyArray[1]).should == "1" + ("%p %p" % StringSpecs::MyArray[1, 2]).should == "1 2" + end + + it "allows positional arguments for width star and precision star arguments" do + ("%*1$.*2$3$d" % [10, 5, 1]).should == " 00001" + end + + it "allows negative width to imply '-' flag" do + ("%*1$.*2$3$d" % [-10, 5, 1]).should == "00001 " + ("%-*1$.*2$3$d" % [10, 5, 1]).should == "00001 " + ("%-*1$.*2$3$d" % [-10, 5, 1]).should == "00001 " + end + + it "ignores negative precision" do + ("%*1$.*2$3$d" % [10, -5, 1]).should == " 1" + end + + it "allows a star to take an argument number to use as the width" do + ("%1$*2$s" % ["a", 8]).should == " a" + ("%1$*10$s" % ["a",0,0,0,0,0,0,0,0,8]).should == " a" + end + + it "calls to_int on width star and precision star tokens" do + w = mock('10') + w.should_receive(:to_int).and_return(10) + + p = mock('5') + p.should_receive(:to_int).and_return(5) + + ("%*.*f" % [w, p, 1]).should == " 1.00000" + + + w = mock('10') + w.should_receive(:to_int).and_return(10) + + p = mock('5') + p.should_receive(:to_int).and_return(5) + + ("%*.*d" % [w, p, 1]).should == " 00001" + end + + it "does not call #to_a to convert the argument" do + x = mock("string modulo to_a") + x.should_not_receive(:to_a) + x.should_receive(:to_s).and_return("x") + + ("%s" % x).should == "x" + end + + it "calls #to_ary to convert the argument" do + x = mock("string modulo to_ary") + x.should_not_receive(:to_s) + x.should_receive(:to_ary).and_return(["x"]) + + ("%s" % x).should == "x" + end + + it "wraps the object in an Array if #to_ary returns nil" do + x = mock("string modulo to_ary") + x.should_receive(:to_ary).and_return(nil) + x.should_receive(:to_s).and_return("x") + + ("%s" % x).should == "x" + end + + it "raises a TypeError if #to_ary does not return an Array" do + x = mock("string modulo to_ary") + x.should_receive(:to_ary).and_return("x") + + -> { "%s" % x }.should raise_error(TypeError) + end + + it "tries to convert the argument to Array by calling #to_ary" do + obj = mock('[1,2]') + def obj.to_ary() [1, 2] end + def obj.to_s() "obj" end + ("%s %s" % obj).should == "1 2" + ("%s" % obj).should == "1" + end + + it "doesn't return subclass instances when called on a subclass" do + universal = mock('0') + def universal.to_int() 0 end + def universal.to_str() "0" end + def universal.to_f() 0.0 end + + [ + "", "foo", + "%b", "%B", "%c", "%d", "%e", "%E", + "%f", "%g", "%G", "%i", "%o", "%p", + "%s", "%u", "%x", "%X" + ].each do |format| + (StringSpecs::MyString.new(format) % universal).should be_an_instance_of(String) + end + end + + it "supports binary formats using %b for positive numbers" do + ("%b" % 10).should == "1010" + ("% b" % 10).should == " 1010" + ("%1$b" % [10, 20]).should == "1010" + ("%#b" % 10).should == "0b1010" + ("%+b" % 10).should == "+1010" + ("%-9b" % 10).should == "1010 " + ("%05b" % 10).should == "01010" + ("%*b" % [10, 6]).should == " 110" + ("%*b" % [-10, 6]).should == "110 " + ("%.4b" % 2).should == "0010" + ("%.32b" % 2147483648).should == "10000000000000000000000000000000" + end + + it "supports binary formats using %b for negative numbers" do + ("%b" % -5).should == "..1011" + ("%0b" % -5).should == "..1011" + ("%.1b" % -5).should == "..1011" + ("%.7b" % -5).should == "..11011" + ("%.10b" % -5).should == "..11111011" + ("% b" % -5).should == "-101" + ("%+b" % -5).should == "-101" + not_supported_on :opal do + ("%b" % -(2 ** 64 + 5)).should == + "..101111111111111111111111111111111111111111111111111111111111111011" + end + end + + it "supports binary formats using %B with same behaviour as %b except for using 0B instead of 0b for #" do + ("%B" % 10).should == ("%b" % 10) + ("% B" % 10).should == ("% b" % 10) + ("%1$B" % [10, 20]).should == ("%1$b" % [10, 20]) + ("%+B" % 10).should == ("%+b" % 10) + ("%-9B" % 10).should == ("%-9b" % 10) + ("%05B" % 10).should == ("%05b" % 10) + ("%*B" % [10, 6]).should == ("%*b" % [10, 6]) + ("%*B" % [-10, 6]).should == ("%*b" % [-10, 6]) + + ("%B" % -5).should == ("%b" % -5) + ("%0B" % -5).should == ("%0b" % -5) + ("%.1B" % -5).should == ("%.1b" % -5) + ("%.7B" % -5).should == ("%.7b" % -5) + ("%.10B" % -5).should == ("%.10b" % -5) + ("% B" % -5).should == ("% b" % -5) + ("%+B" % -5).should == ("%+b" % -5) + not_supported_on :opal do + ("%B" % -(2 ** 64 + 5)).should == ("%b" % -(2 ** 64 + 5)) + end + + ("%#B" % 10).should == "0B1010" + end + + it "supports character formats using %c" do + ("%c" % 10).should == "\n" + ("%2$c" % [10, 11, 14]).should == "\v" + ("%-4c" % 10).should == "\n " + ("%*c" % [10, 3]).should == " \003" + ("%c" % 42).should == "*" + + -> { "%c" % Object }.should raise_error(TypeError) + end + + it "supports single character strings as argument for %c" do + ("%c" % 'A').should == "A" + end + + it "supports only the first character as argument for %c" do + ("%c" % 'AA').should == "A" + end + + it "calls to_str on argument for %c formats" do + obj = mock('A') + obj.should_receive(:to_str).and_return('A') + + ("%c" % obj).should == "A" + end + + it "calls #to_ary on argument for %c formats" do + obj = mock('65') + obj.should_receive(:to_ary).and_return([65]) + ("%c" % obj).should == ("%c" % [65]) + end + + it "calls #to_int on argument for %c formats, if the argument does not respond to #to_ary" do + obj = mock('65') + obj.should_receive(:to_int).and_return(65) + + ("%c" % obj).should == ("%c" % 65) + end + + %w(d i).each do |f| + format = "%" + f + + it "supports integer formats using #{format}" do + ("%#{f}" % 10).should == "10" + ("% #{f}" % 10).should == " 10" + ("%1$#{f}" % [10, 20]).should == "10" + ("%+#{f}" % 10).should == "+10" + ("%-7#{f}" % 10).should == "10 " + ("%04#{f}" % 10).should == "0010" + ("%*#{f}" % [10, 4]).should == " 4" + ("%6.4#{f}" % 123).should == " 0123" + end + + it "supports negative integers using #{format}" do + ("%#{f}" % -5).should == "-5" + ("%3#{f}" % -5).should == " -5" + ("%03#{f}" % -5).should == "-05" + ("%+03#{f}" % -5).should == "-05" + ("%+.2#{f}" % -5).should == "-05" + ("%-3#{f}" % -5).should == "-5 " + ("%6.4#{f}" % -123).should == " -0123" + end + + it "supports negative integers using #{format}, giving priority to `-`" do + ("%-03#{f}" % -5).should == "-5 " + ("%+-03#{f}" % -5).should == "-5 " + end + end + + it "supports float formats using %e" do + ("%e" % 10).should == "1.000000e+01" + ("% e" % 10).should == " 1.000000e+01" + ("%1$e" % 10).should == "1.000000e+01" + ("%#e" % 10).should == "1.000000e+01" + ("%+e" % 10).should == "+1.000000e+01" + ("%-7e" % 10).should == "1.000000e+01" + ("%05e" % 10).should == "1.000000e+01" + ("%*e" % [10, 9]).should == "9.000000e+00" + end + + it "supports float formats using %e, but Inf, -Inf, and NaN are not floats" do + ("%e" % 1e1020).should == "Inf" + ("%e" % -1e1020).should == "-Inf" + ("%e" % -Float::NAN).should == "NaN" + ("%e" % Float::NAN).should == "NaN" + end + + it "supports float formats using %E, but Inf, -Inf, and NaN are not floats" do + ("%E" % 1e1020).should == "Inf" + ("%E" % -1e1020).should == "-Inf" + ("%-10E" % 1e1020).should == "Inf " + ("%10E" % 1e1020).should == " Inf" + ("%+E" % 1e1020).should == "+Inf" + ("% E" % 1e1020).should == " Inf" + ("%E" % Float::NAN).should == "NaN" + ("%E" % -Float::NAN).should == "NaN" + end + + it "supports float formats using %E" do + ("%E" % 10).should == "1.000000E+01" + ("% E" % 10).should == " 1.000000E+01" + ("%1$E" % 10).should == "1.000000E+01" + ("%#E" % 10).should == "1.000000E+01" + ("%+E" % 10).should == "+1.000000E+01" + ("%-7E" % 10).should == "1.000000E+01" + ("%05E" % 10).should == "1.000000E+01" + ("%*E" % [10, 9]).should == "9.000000E+00" + end + + it "pads with spaces for %E with Inf, -Inf, and NaN" do + ("%010E" % -1e1020).should == " -Inf" + ("%010E" % 1e1020).should == " Inf" + ("%010E" % Float::NAN).should == " NaN" + end + + it "supports float formats using %f" do + ("%f" % 10).should == "10.000000" + ("% f" % 10).should == " 10.000000" + ("%1$f" % 10).should == "10.000000" + ("%#f" % 10).should == "10.000000" + ("%#0.3f" % 10).should == "10.000" + ("%+f" % 10).should == "+10.000000" + ("%-7f" % 10).should == "10.000000" + ("%05f" % 10).should == "10.000000" + ("%0.5f" % 10).should == "10.00000" + ("%*f" % [10, 9]).should == " 9.000000" + end + + it "supports float formats using %g" do + ("%g" % 10).should == "10" + ("% g" % 10).should == " 10" + ("%1$g" % 10).should == "10" + ("%#g" % 10).should == "10.0000" + ("%#.3g" % 10).should == "10.0" + ("%+g" % 10).should == "+10" + ("%-7g" % 10).should == "10 " + ("%05g" % 10).should == "00010" + ("%g" % 10**10).should == "1e+10" + ("%*g" % [10, 9]).should == " 9" + end + + it "supports float formats using %G" do + ("%G" % 10).should == "10" + ("% G" % 10).should == " 10" + ("%1$G" % 10).should == "10" + ("%#G" % 10).should == "10.0000" + ("%#.3G" % 10).should == "10.0" + ("%+G" % 10).should == "+10" + ("%-7G" % 10).should == "10 " + ("%05G" % 10).should == "00010" + ("%G" % 10**10).should == "1E+10" + ("%*G" % [10, 9]).should == " 9" + end + + it "supports octal formats using %o for positive numbers" do + ("%o" % 10).should == "12" + ("% o" % 10).should == " 12" + ("%1$o" % [10, 20]).should == "12" + ("%#o" % 10).should == "012" + ("%+o" % 10).should == "+12" + ("%-9o" % 10).should == "12 " + ("%05o" % 10).should == "00012" + ("%*o" % [10, 6]).should == " 6" + end + + it "supports octal formats using %o for negative numbers" do + # These are incredibly wrong. -05 == -5, not 7177777...whatever + ("%o" % -5).should == "..73" + ("%0o" % -5).should == "..73" + ("%.4o" % 20).should == "0024" + ("%.1o" % -5).should == "..73" + ("%.7o" % -5).should == "..77773" + ("%.10o" % -5).should == "..77777773" + + ("% o" % -26).should == "-32" + ("%+o" % -26).should == "-32" + not_supported_on :opal do + ("%o" % -(2 ** 64 + 5)).should == "..75777777777777777777773" + end + end + + it "supports inspect formats using %p" do + ("%p" % 10).should == "10" + ("%1$p" % [10, 5]).should == "10" + ("%-22p" % 10).should == "10 " + ("%*p" % [10, 10]).should == " 10" + ("%p" % {capture: 1}).should == {capture: 1}.inspect + ("%p" % "str").should == "\"str\"" + end + + it "calls inspect on arguments for %p format" do + obj = mock('obj') + def obj.inspect() "obj" end + ("%p" % obj).should == "obj" + + # undef is not working + # obj = mock('obj') + # class << obj; undef :inspect; end + # def obj.method_missing(*args) "obj" end + # ("%p" % obj).should == "obj" + end + + it "supports string formats using %s" do + ("%s" % "hello").should == "hello" + ("%s" % "").should == "" + ("%s" % 10).should == "10" + ("%1$s" % [10, 8]).should == "10" + ("%-5s" % 10).should == "10 " + ("%*s" % [10, 9]).should == " 9" + end + + it "respects a space padding request not as part of the width" do + x = "% -5s" % ["foo"] + x.should == "foo " + end + + it "calls to_s on non-String arguments for %s format" do + obj = mock('obj') + def obj.to_s() "obj" end + + ("%s" % obj).should == "obj" + + # undef doesn't work + # obj = mock('obj') + # class << obj; undef :to_s; end + # def obj.method_missing(*args) "obj" end + # + # ("%s" % obj).should == "obj" + end + + # MRI crashes on this one. + # See http://groups.google.com/group/ruby-core-google/t/c285c18cd94c216d + it "raises an ArgumentError for huge precisions for %s" do + block = -> { "%.25555555555555555555555555555555555555s" % "hello world" } + block.should raise_error(ArgumentError) + end + + # Note: %u has been changed to an alias for %d in 1.9. + it "supports unsigned formats using %u" do + ("%u" % 10).should == "10" + ("% u" % 10).should == " 10" + ("%1$u" % [10, 20]).should == "10" + ("%+u" % 10).should == "+10" + ("%-7u" % 10).should == "10 " + ("%04u" % 10).should == "0010" + ("%*u" % [10, 4]).should == " 4" + end + + it "formats negative values with a leading sign using %u" do + ("% u" % -26).should == "-26" + ("%+u" % -26).should == "-26" + end + + it "supports negative bignums with %u or %d" do + ("%u" % -(2 ** 64 + 5)).should == "-18446744073709551621" + ("%d" % -(2 ** 64 + 5)).should == "-18446744073709551621" + end + + it "supports hex formats using %x for positive numbers" do + ("%x" % 10).should == "a" + ("% x" % 10).should == " a" + ("%1$x" % [10, 20]).should == "a" + ("%#x" % 10).should == "0xa" + ("%+x" % 10).should == "+a" + ("%-9x" % 10).should == "a " + ("%05x" % 10).should == "0000a" + ("%*x" % [10, 6]).should == " 6" + ("%.4x" % 20).should == "0014" + ("%x" % 0xFFFFFFFF).should == "ffffffff" + end + + it "supports hex formats using %x for negative numbers" do + ("%x" % -5).should == "..fb" + ("%0x" % -5).should == "..fb" + ("%.1x" % -5).should == "..fb" + ("%.7x" % -5).should == "..ffffb" + ("%.10x" % -5).should == "..fffffffb" + ("% x" % -26).should == "-1a" + ("%+x" % -26).should == "-1a" + not_supported_on :opal do + ("%x" % -(2 ** 64 + 5)).should == "..fefffffffffffffffb" + end + end + + it "supports hex formats using %X for positive numbers" do + ("%X" % 10).should == "A" + ("% X" % 10).should == " A" + ("%1$X" % [10, 20]).should == "A" + ("%#X" % 10).should == "0XA" + ("%+X" % 10).should == "+A" + ("%-9X" % 10).should == "A " + ("%05X" % 10).should == "0000A" + ("%*X" % [10, 6]).should == " 6" + ("%X" % 0xFFFFFFFF).should == "FFFFFFFF" + end + + it "supports hex formats using %X for negative numbers" do + ("%X" % -5).should == "..FB" + ("%0X" % -5).should == "..FB" + ("%.1X" % -5).should == "..FB" + ("%.7X" % -5).should == "..FFFFB" + ("%.10X" % -5).should == "..FFFFFFFB" + ("% X" % -26).should == "-1A" + ("%+X" % -26).should == "-1A" + not_supported_on :opal do + ("%X" % -(2 ** 64 + 5)).should == "..FEFFFFFFFFFFFFFFFB" + end + end + + it "formats zero without prefix using %#x" do + ("%#x" % 0).should == "0" + end + + it "formats zero without prefix using %#X" do + ("%#X" % 0).should == "0" + end + + %w(b d i o u x X).each do |f| + format = "%" + f + + it "behaves as if calling Kernel#Integer for #{format} argument, if it does not respond to #to_ary" do + (format % "10").should == (format % Kernel.Integer("10")) + (format % "0x42").should == (format % Kernel.Integer("0x42")) + (format % "0b1101").should == (format % Kernel.Integer("0b1101")) + (format % "0b1101_0000").should == (format % Kernel.Integer("0b1101_0000")) + (format % "0777").should == (format % Kernel.Integer("0777")) + -> { + # see [ruby-core:14139] for more details + (format % "0777").should == (format % Kernel.Integer("0777")) + }.should_not raise_error(ArgumentError) + + -> { format % "0__7_7_7" }.should raise_error(ArgumentError) + + -> { format % "" }.should raise_error(ArgumentError) + -> { format % "x" }.should raise_error(ArgumentError) + -> { format % "5x" }.should raise_error(ArgumentError) + -> { format % "08" }.should raise_error(ArgumentError) + -> { format % "0b2" }.should raise_error(ArgumentError) + -> { format % "123__456" }.should raise_error(ArgumentError) + + obj = mock('5') + obj.should_receive(:to_i).and_return(5) + (format % obj).should == (format % 5) + + obj = mock('6') + obj.stub!(:to_i).and_return(5) + obj.should_receive(:to_int).and_return(6) + (format % obj).should == (format % 6) + end + end + + %w(e E f g G).each do |f| + format = "%" + f + + it "tries to convert the passed argument to an Array using #to_ary" do + obj = mock('3.14') + obj.should_receive(:to_ary).and_return([3.14]) + (format % obj).should == (format % [3.14]) + end + + it "behaves as if calling Kernel#Float for #{format} arguments, when the passed argument does not respond to #to_ary" do + (format % 10).should == (format % 10.0) + (format % "-10.4e-20").should == (format % -10.4e-20) + (format % ".5").should == (format % 0.5) + (format % "-.5").should == (format % -0.5) + + ruby_version_is "3.4" do + (format % "10.").should == (format % 10) + end + + # Something's strange with this spec: + # it works just fine in individual mode, but not when run as part of a group + (format % "10_1_0.5_5_5").should == (format % 1010.555) + + (format % "0777").should == (format % 777) + + -> { format % "" }.should raise_error(ArgumentError) + -> { format % "x" }.should raise_error(ArgumentError) + -> { format % "." }.should raise_error(ArgumentError) + -> { format % "5x" }.should raise_error(ArgumentError) + -> { format % "0b1" }.should raise_error(ArgumentError) + -> { format % "10e10.5" }.should raise_error(ArgumentError) + -> { format % "10__10" }.should raise_error(ArgumentError) + -> { format % "10.10__10" }.should raise_error(ArgumentError) + + obj = mock('5.0') + obj.should_receive(:to_f).and_return(5.0) + (format % obj).should == (format % 5.0) + end + + it "behaves as if calling Kernel#Float for #{format} arguments, when the passed argument is hexadecimal string" do + (format % "0xA").should == (format % 0xA) + end + end + + describe "when format string contains %{} sections" do + it "replaces %{} sections with values from passed-in hash" do + ("%{foo}bar" % {foo: 'oof'}).should == "oofbar" + end + + it "should raise ArgumentError if no hash given" do + -> {"%{foo}" % []}.should raise_error(ArgumentError) + end + end + + describe "when format string contains %<> formats" do + it "uses the named argument for the format's value" do + ("%<foo>d" % {foo: 1}).should == "1" + end + + it "raises KeyError if key is missing from passed-in hash" do + -> {"%<foo>d" % {}}.should raise_error(KeyError) + end + + it "should raise ArgumentError if no hash given" do + -> {"%<foo>" % []}.should raise_error(ArgumentError) + end + end +end diff --git a/spec/ruby/core/string/multiply_spec.rb b/spec/ruby/core/string/multiply_spec.rb new file mode 100644 index 0000000000..c15f670c46 --- /dev/null +++ b/spec/ruby/core/string/multiply_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative '../../shared/string/times' + +describe "String#*" do + it_behaves_like :string_times, :*, -> str, times { str * times } +end diff --git a/spec/ruby/core/string/new_spec.rb b/spec/ruby/core/string/new_spec.rb new file mode 100644 index 0000000000..ca678f5323 --- /dev/null +++ b/spec/ruby/core/string/new_spec.rb @@ -0,0 +1,61 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "String.new" do + it "returns an instance of String" do + str = String.new + str.should be_an_instance_of(String) + end + + it "accepts an encoding argument" do + xA4xA2 = [0xA4, 0xA2].pack('CC').force_encoding 'utf-8' + str = String.new(xA4xA2, encoding: 'euc-jp') + str.encoding.should == Encoding::EUC_JP + end + + it "accepts a capacity argument" do + String.new("", capacity: 100_000).should == "" + String.new("abc", capacity: 100_000).should == "abc" + end + + it "returns a fully-formed String" do + str = String.new + str.size.should == 0 + str << "more" + str.should == "more" + end + + it "returns a new string given a string argument" do + str1 = "test" + str = String.new(str1) + str.should be_an_instance_of(String) + str.should == str1 + str << "more" + str.should == "testmore" + end + + it "returns an instance of a subclass" do + a = StringSpecs::MyString.new("blah") + a.should be_an_instance_of(StringSpecs::MyString) + a.should == "blah" + end + + it "is called on subclasses" do + s = StringSpecs::SubString.new + s.special.should == nil + s.should == "" + + s = StringSpecs::SubString.new "subclass" + s.special.should == "subclass" + s.should == "" + end + + it "raises TypeError on inconvertible object" do + -> { String.new 5 }.should raise_error(TypeError) + -> { String.new nil }.should raise_error(TypeError) + end + + it "returns a binary String" do + String.new.encoding.should == Encoding::BINARY + end +end diff --git a/spec/ruby/core/string/next_spec.rb b/spec/ruby/core/string/next_spec.rb new file mode 100644 index 0000000000..fcd3e5ef90 --- /dev/null +++ b/spec/ruby/core/string/next_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/succ' + +describe "String#next" do + it_behaves_like :string_succ, :next +end + +describe "String#next!" do + it_behaves_like :string_succ_bang, :"next!" +end diff --git a/spec/ruby/core/string/oct_spec.rb b/spec/ruby/core/string/oct_spec.rb new file mode 100644 index 0000000000..7637692217 --- /dev/null +++ b/spec/ruby/core/string/oct_spec.rb @@ -0,0 +1,88 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +# Note: We can't completely spec this in terms of to_int() because hex() +# allows the base to be changed by a base specifier in the string. +# See http://groups.google.com/group/ruby-core-google/browse_frm/thread/b53e9c2003425703 +describe "String#oct" do + it "treats numeric digits as base-8 digits by default" do + "0".oct.should == 0 + "77".oct.should == 077 + "077".oct.should == 077 + end + + it "accepts numbers formatted as binary" do + "0b1010".oct.should == 0b1010 + end + + it "accepts numbers formatted as hexadecimal" do + "0xFF".oct.should == 0xFF + end + + it "accepts numbers formatted as decimal" do + "0d500".oct.should == 500 + end + + describe "with a leading minus sign" do + it "treats numeric digits as base-8 digits by default" do + "-12348".oct.should == -01234 + end + + it "accepts numbers formatted as binary" do + "-0b0101".oct.should == -0b0101 + end + + it "accepts numbers formatted as hexadecimal" do + "-0xEE".oct.should == -0xEE + end + + it "accepts numbers formatted as decimal" do + "-0d500".oct.should == -500 + end + end + + describe "with a leading plus sign" do + it "treats numeric digits as base-8 digits by default" do + "+12348".oct.should == 01234 + end + + it "accepts numbers formatted as binary" do + "+0b1010".oct.should == 0b1010 + end + + it "accepts numbers formatted as hexadecimal" do + "+0xFF".oct.should == 0xFF + end + + it "accepts numbers formatted as decimal" do + "+0d500".oct.should == 500 + end + end + + it "accepts a single underscore separating digits" do + "755_333".oct.should == 0755_333 + end + + it "does not accept a sequence of underscores as part of a number" do + "7__3".oct.should == 07 + "7___3".oct.should == 07 + "7__5".oct.should == 07 + end + + it "ignores characters that are incorrect for the base-8 digits" do + "0o".oct.should == 0 + "5678".oct.should == 0567 + end + + it "returns 0 if no characters can be interpreted as a base-8 number" do + "".oct.should == 0 + "+-5".oct.should == 0 + "wombat".oct.should == 0 + end + + it "returns 0 for strings with leading underscores" do + "_7".oct.should == 0 + "_07".oct.should == 0 + " _7".oct.should == 0 + end +end diff --git a/spec/ruby/core/string/ord_spec.rb b/spec/ruby/core/string/ord_spec.rb new file mode 100644 index 0000000000..35af3b5458 --- /dev/null +++ b/spec/ruby/core/string/ord_spec.rb @@ -0,0 +1,33 @@ +require_relative '../../spec_helper' + +describe "String#ord" do + it "returns an Integer" do + 'a'.ord.should be_an_instance_of(Integer) + end + + it "returns the codepoint of the first character in the String" do + 'a'.ord.should == 97 + end + + + it "ignores subsequent characters" do + "\u{287}a".ord.should == "\u{287}".ord + end + + it "understands multibyte characters" do + "\u{9879}".ord.should == 39033 + end + + it "is equivalent to #codepoints.first" do + "\u{981}\u{982}".ord.should == "\u{981}\u{982}".codepoints.first + end + + it "raises an ArgumentError if called on an empty String" do + -> { ''.ord }.should raise_error(ArgumentError) + end + + it "raises ArgumentError if the character is broken" do + s = "©".dup.force_encoding("US-ASCII") + -> { s.ord }.should raise_error(ArgumentError, "invalid byte sequence in US-ASCII") + end +end diff --git a/spec/ruby/core/string/partition_spec.rb b/spec/ruby/core/string/partition_spec.rb new file mode 100644 index 0000000000..d5370dcc73 --- /dev/null +++ b/spec/ruby/core/string/partition_spec.rb @@ -0,0 +1,63 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/partition' + +describe "String#partition with String" do + it_behaves_like :string_partition, :partition + + it "returns an array of substrings based on splitting on the given string" do + "hello world".partition("o").should == ["hell", "o", " world"] + end + + it "always returns 3 elements" do + "hello".partition("x").should == ["hello", "", ""] + "hello".partition("hello").should == ["", "hello", ""] + end + + it "accepts regexp" do + "hello!".partition(/l./).should == ["he", "ll", "o!"] + end + + it "sets global vars if regexp used" do + "hello!".partition(/(.l)(.o)/) + $1.should == "el" + $2.should == "lo" + end + + it "converts its argument using :to_str" do + find = mock('l') + find.should_receive(:to_str).and_return("l") + "hello".partition(find).should == ["he","l","lo"] + end + + it "raises an error if not convertible to string" do + ->{ "hello".partition(5) }.should raise_error(TypeError) + ->{ "hello".partition(nil) }.should raise_error(TypeError) + end + + it "takes precedence over a given block" do + "hello world".partition("o") { true }.should == ["hell", "o", " world"] + end + + it "handles a pattern in a superset encoding" do + string = "hello".dup.force_encoding(Encoding::US_ASCII) + + result = string.partition("é") + + result.should == ["hello", "", ""] + result[0].encoding.should == Encoding::US_ASCII + result[1].encoding.should == Encoding::US_ASCII + result[2].encoding.should == Encoding::US_ASCII + end + + it "handles a pattern in a subset encoding" do + pattern = "o".dup.force_encoding(Encoding::US_ASCII) + + result = "héllo world".partition(pattern) + + result.should == ["héll", "o", " world"] + result[0].encoding.should == Encoding::UTF_8 + result[1].encoding.should == Encoding::US_ASCII + result[2].encoding.should == Encoding::UTF_8 + end +end diff --git a/spec/ruby/core/string/plus_spec.rb b/spec/ruby/core/string/plus_spec.rb new file mode 100644 index 0000000000..9da17451c6 --- /dev/null +++ b/spec/ruby/core/string/plus_spec.rb @@ -0,0 +1,37 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/concat' + +describe "String#+" do + it_behaves_like :string_concat_encoding, :+ + it_behaves_like :string_concat_type_coercion, :+ + + it "returns a new string containing the given string concatenated to self" do + ("" + "").should == "" + ("" + "Hello").should == "Hello" + ("Hello" + "").should == "Hello" + ("Ruby !" + "= Rubinius").should == "Ruby != Rubinius" + end + + it "converts any non-String argument with #to_str" do + c = mock 'str' + c.should_receive(:to_str).any_number_of_times.and_return(' + 1 = 2') + + ("1" + c).should == '1 + 1 = 2' + end + + it "raises a TypeError when given any object that fails #to_str" do + -> { "" + Object.new }.should raise_error(TypeError) + -> { "" + 65 }.should raise_error(TypeError) + end + + it "doesn't return subclass instances" do + (StringSpecs::MyString.new("hello") + "").should be_an_instance_of(String) + (StringSpecs::MyString.new("hello") + "foo").should be_an_instance_of(String) + (StringSpecs::MyString.new("hello") + StringSpecs::MyString.new("foo")).should be_an_instance_of(String) + (StringSpecs::MyString.new("hello") + StringSpecs::MyString.new("")).should be_an_instance_of(String) + (StringSpecs::MyString.new("") + StringSpecs::MyString.new("")).should be_an_instance_of(String) + ("hello" + StringSpecs::MyString.new("foo")).should be_an_instance_of(String) + ("hello" + StringSpecs::MyString.new("")).should be_an_instance_of(String) + end +end diff --git a/spec/ruby/core/string/prepend_spec.rb b/spec/ruby/core/string/prepend_spec.rb new file mode 100644 index 0000000000..5248ea8056 --- /dev/null +++ b/spec/ruby/core/string/prepend_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: false +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "String#prepend" do + it "prepends the given argument to self and returns self" do + str = "world" + str.prepend("hello ").should equal(str) + str.should == "hello world" + end + + it "converts the given argument to a String using to_str" do + obj = mock("hello") + obj.should_receive(:to_str).and_return("hello") + a = " world!".prepend(obj) + a.should == "hello world!" + end + + it "raises a TypeError if the given argument can't be converted to a String" do + -> { "hello ".prepend [] }.should raise_error(TypeError) + -> { 'hello '.prepend mock('x') }.should raise_error(TypeError) + end + + it "raises a FrozenError when self is frozen" do + a = "hello" + a.freeze + + -> { a.prepend "" }.should raise_error(FrozenError) + -> { a.prepend "test" }.should raise_error(FrozenError) + end + + it "works when given a subclass instance" do + a = " world" + a.prepend StringSpecs::MyString.new("hello") + a.should == "hello world" + end + + it "takes multiple arguments" do + str = " world" + str.prepend "he", "", "llo" + str.should == "hello world" + end + + it "prepends the initial value when given arguments contain 2 self" do + str = "hello" + str.prepend str, str + str.should == "hellohellohello" + end + + it "returns self when given no arguments" do + str = "hello" + str.prepend.should equal(str) + str.should == "hello" + end +end diff --git a/spec/ruby/core/string/replace_spec.rb b/spec/ruby/core/string/replace_spec.rb new file mode 100644 index 0000000000..ef9bab4f3c --- /dev/null +++ b/spec/ruby/core/string/replace_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/replace' + +describe "String#replace" do + it_behaves_like :string_replace, :replace +end diff --git a/spec/ruby/core/string/reverse_spec.rb b/spec/ruby/core/string/reverse_spec.rb new file mode 100644 index 0000000000..aa6abe6036 --- /dev/null +++ b/spec/ruby/core/string/reverse_spec.rb @@ -0,0 +1,70 @@ +# encoding: utf-8 +# frozen_string_literal: false + +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "String#reverse" do + it "returns a new string with the characters of self in reverse order" do + "stressed".reverse.should == "desserts" + "m".reverse.should == "m" + "".reverse.should == "" + end + + it "returns String instances when called on a subclass" do + StringSpecs::MyString.new("stressed").reverse.should be_an_instance_of(String) + StringSpecs::MyString.new("m").reverse.should be_an_instance_of(String) + StringSpecs::MyString.new("").reverse.should be_an_instance_of(String) + end + + it "reverses a string with multi byte characters" do + "微軟æ£é»‘é«”".reverse.should == "體黑æ£è»Ÿå¾®" + end + + it "works with a broken string" do + str = "微軟\xDF\xDEæ£é»‘é«”".force_encoding(Encoding::UTF_8) + + str.valid_encoding?.should be_false + + str.reverse.should == "體黑æ£\xDE\xDF軟微" + end + + it "returns a String in the same encoding as self" do + "stressed".encode("US-ASCII").reverse.encoding.should == Encoding::US_ASCII + end +end + +describe "String#reverse!" do + it "reverses self in place and always returns self" do + a = "stressed" + a.reverse!.should equal(a) + a.should == "desserts" + + "".reverse!.should == "" + end + + it "raises a FrozenError on a frozen instance that is modified" do + -> { "anna".freeze.reverse! }.should raise_error(FrozenError) + -> { "hello".freeze.reverse! }.should raise_error(FrozenError) + end + + # see [ruby-core:23666] + it "raises a FrozenError on a frozen instance that would not be modified" do + -> { "".freeze.reverse! }.should raise_error(FrozenError) + end + + it "reverses a string with multi byte characters" do + str = "微軟æ£é»‘é«”" + str.reverse! + str.should == "體黑æ£è»Ÿå¾®" + end + + it "works with a broken string" do + str = "微軟\xDF\xDEæ£é»‘é«”".force_encoding(Encoding::UTF_8) + + str.valid_encoding?.should be_false + str.reverse! + + str.should == "體黑æ£\xDE\xDF軟微" + end +end diff --git a/spec/ruby/core/string/rindex_spec.rb b/spec/ruby/core/string/rindex_spec.rb new file mode 100644 index 0000000000..0863a9c3be --- /dev/null +++ b/spec/ruby/core/string/rindex_spec.rb @@ -0,0 +1,384 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "String#rindex with object" do + it "raises a TypeError if obj isn't a String or Regexp" do + not_supported_on :opal do + -> { "hello".rindex(:sym) }.should raise_error(TypeError) + end + -> { "hello".rindex(mock('x')) }.should raise_error(TypeError) + end + + it "raises a TypeError if obj is an Integer" do + -> { "hello".rindex(42) }.should raise_error(TypeError) + end + + it "doesn't try to convert obj to an integer via to_int" do + obj = mock('x') + obj.should_not_receive(:to_int) + -> { "hello".rindex(obj) }.should raise_error(TypeError) + end + + it "tries to convert obj to a string via to_str" do + obj = mock('lo') + def obj.to_str() "lo" end + "hello".rindex(obj).should == "hello".rindex("lo") + + obj = mock('o') + def obj.respond_to?(arg, *) true end + def obj.method_missing(*args) "o" end + "hello".rindex(obj).should == "hello".rindex("o") + end +end + +describe "String#rindex with String" do + it "behaves the same as String#rindex(char) for one-character strings" do + "blablabla hello cruel world...!".split("").uniq.each do |str| + chr = str[0] + str.rindex(str).should == str.rindex(chr) + + 0.upto(str.size + 1) do |start| + str.rindex(str, start).should == str.rindex(chr, start) + end + + (-str.size - 1).upto(-1) do |start| + str.rindex(str, start).should == str.rindex(chr, start) + end + end + end + + it "behaves the same as String#rindex(?char) for one-character strings" do + "blablabla hello cruel world...!".split("").uniq.each do |str| + chr = str[0] =~ / / ? str[0] : eval("?#{str[0]}") + str.rindex(str).should == str.rindex(chr) + + 0.upto(str.size + 1) do |start| + str.rindex(str, start).should == str.rindex(chr, start) + end + + (-str.size - 1).upto(-1) do |start| + str.rindex(str, start).should == str.rindex(chr, start) + end + end + end + + it "returns the index of the last occurrence of the given substring" do + "blablabla".rindex("").should == 9 + "blablabla".rindex("a").should == 8 + "blablabla".rindex("la").should == 7 + "blablabla".rindex("bla").should == 6 + "blablabla".rindex("abla").should == 5 + "blablabla".rindex("labla").should == 4 + "blablabla".rindex("blabla").should == 3 + "blablabla".rindex("ablabla").should == 2 + "blablabla".rindex("lablabla").should == 1 + "blablabla".rindex("blablabla").should == 0 + + "blablabla".rindex("l").should == 7 + "blablabla".rindex("bl").should == 6 + "blablabla".rindex("abl").should == 5 + "blablabla".rindex("labl").should == 4 + "blablabla".rindex("blabl").should == 3 + "blablabla".rindex("ablabl").should == 2 + "blablabla".rindex("lablabl").should == 1 + "blablabla".rindex("blablabl").should == 0 + + "blablabla".rindex("b").should == 6 + "blablabla".rindex("ab").should == 5 + "blablabla".rindex("lab").should == 4 + "blablabla".rindex("blab").should == 3 + "blablabla".rindex("ablab").should == 2 + "blablabla".rindex("lablab").should == 1 + "blablabla".rindex("blablab").should == 0 + end + + it "doesn't set $~" do + $~ = nil + + 'hello.'.rindex('ll') + $~.should == nil + end + + it "ignores string subclasses" do + "blablabla".rindex(StringSpecs::MyString.new("bla")).should == 6 + StringSpecs::MyString.new("blablabla").rindex("bla").should == 6 + StringSpecs::MyString.new("blablabla").rindex(StringSpecs::MyString.new("bla")).should == 6 + end + + it "starts the search at the given offset" do + "blablabla".rindex("bl", 0).should == 0 + "blablabla".rindex("bl", 1).should == 0 + "blablabla".rindex("bl", 2).should == 0 + "blablabla".rindex("bl", 3).should == 3 + + "blablabla".rindex("bla", 0).should == 0 + "blablabla".rindex("bla", 1).should == 0 + "blablabla".rindex("bla", 2).should == 0 + "blablabla".rindex("bla", 3).should == 3 + + "blablabla".rindex("blab", 0).should == 0 + "blablabla".rindex("blab", 1).should == 0 + "blablabla".rindex("blab", 2).should == 0 + "blablabla".rindex("blab", 3).should == 3 + "blablabla".rindex("blab", 6).should == 3 + "blablablax".rindex("blab", 6).should == 3 + + "blablabla".rindex("la", 1).should == 1 + "blablabla".rindex("la", 2).should == 1 + "blablabla".rindex("la", 3).should == 1 + "blablabla".rindex("la", 4).should == 4 + + "blablabla".rindex("lab", 1).should == 1 + "blablabla".rindex("lab", 2).should == 1 + "blablabla".rindex("lab", 3).should == 1 + "blablabla".rindex("lab", 4).should == 4 + + "blablabla".rindex("ab", 2).should == 2 + "blablabla".rindex("ab", 3).should == 2 + "blablabla".rindex("ab", 4).should == 2 + "blablabla".rindex("ab", 5).should == 5 + + "blablabla".rindex("", 0).should == 0 + "blablabla".rindex("", 1).should == 1 + "blablabla".rindex("", 2).should == 2 + "blablabla".rindex("", 7).should == 7 + "blablabla".rindex("", 8).should == 8 + "blablabla".rindex("", 9).should == 9 + "blablabla".rindex("", 10).should == 9 + end + + it "starts the search at offset + self.length if offset is negative" do + str = "blablabla" + + ["bl", "bla", "blab", "la", "lab", "ab", ""].each do |needle| + (-str.length .. -1).each do |offset| + str.rindex(needle, offset).should == + str.rindex(needle, offset + str.length) + end + end + end + + it "returns nil if the substring isn't found" do + "blablabla".rindex("B").should == nil + "blablabla".rindex("z").should == nil + "blablabla".rindex("BLA").should == nil + "blablabla".rindex("blablablabla").should == nil + + "hello".rindex("lo", 0).should == nil + "hello".rindex("lo", 1).should == nil + "hello".rindex("lo", 2).should == nil + + "hello".rindex("llo", 0).should == nil + "hello".rindex("llo", 1).should == nil + + "hello".rindex("el", 0).should == nil + "hello".rindex("ello", 0).should == nil + + "hello".rindex("", -6).should == nil + "hello".rindex("", -7).should == nil + + "hello".rindex("h", -6).should == nil + end + + it "tries to convert start_offset to an integer via to_int" do + obj = mock('5') + def obj.to_int() 5 end + "str".rindex("st", obj).should == 0 + + obj = mock('5') + def obj.respond_to?(arg, *) true end + def obj.method_missing(*args) 5 end + "str".rindex("st", obj).should == 0 + end + + it "raises a TypeError when given offset is nil" do + -> { "str".rindex("st", nil) }.should raise_error(TypeError) + end + + it "handles a substring in a superset encoding" do + 'abc'.dup.force_encoding(Encoding::US_ASCII).rindex('é').should == nil + end + + it "handles a substring in a subset encoding" do + 'été'.rindex('t'.dup.force_encoding(Encoding::US_ASCII)).should == 1 + end + + it "raises an Encoding::CompatibilityError if the encodings are incompatible" do + str = 'abc'.dup.force_encoding("ISO-2022-JP") + pattern = 'b'.dup.force_encoding("EUC-JP") + + -> { str.rindex(pattern) }.should raise_error(Encoding::CompatibilityError, "incompatible character encodings: ISO-2022-JP and EUC-JP") + end +end + +describe "String#rindex with Regexp" do + it "behaves the same as String#rindex(string) for escaped string regexps" do + ["blablabla", "hello cruel world...!"].each do |str| + ["", "b", "bla", "lab", "o c", "d."].each do |needle| + regexp = Regexp.new(Regexp.escape(needle)) + str.rindex(regexp).should == str.rindex(needle) + + 0.upto(str.size + 1) do |start| + str.rindex(regexp, start).should == str.rindex(needle, start) + end + + (-str.size - 1).upto(-1) do |start| + str.rindex(regexp, start).should == str.rindex(needle, start) + end + end + end + end + + it "returns the index of the first match from the end of string of regexp" do + "blablabla".rindex(/bla/).should == 6 + "blablabla".rindex(/BLA/i).should == 6 + + "blablabla".rindex(/.{0}/).should == 9 + "blablabla".rindex(/.{1}/).should == 8 + "blablabla".rindex(/.{2}/).should == 7 + "blablabla".rindex(/.{6}/).should == 3 + "blablabla".rindex(/.{9}/).should == 0 + + "blablabla".rindex(/.*/).should == 9 + "blablabla".rindex(/.+/).should == 8 + + "blablabla".rindex(/bla|a/).should == 8 + + not_supported_on :opal do + "blablabla".rindex(/\A/).should == 0 + "blablabla".rindex(/\Z/).should == 9 + "blablabla".rindex(/\z/).should == 9 + "blablabla\n".rindex(/\Z/).should == 10 + "blablabla\n".rindex(/\z/).should == 10 + end + + "blablabla".rindex(/^/).should == 0 + not_supported_on :opal do + "\nblablabla".rindex(/^/).should == 1 + "b\nlablabla".rindex(/^/).should == 2 + end + "blablabla".rindex(/$/).should == 9 + + "blablabla".rindex(/.l./).should == 6 + end + + it "sets $~ to MatchData of match and nil when there's none" do + 'hello.'.rindex(/.(.)/) + $~[0].should == 'o.' + + 'hello.'.rindex(/not/) + $~.should == nil + end + + it "starts the search at the given offset" do + "blablabla".rindex(/.{0}/, 5).should == 5 + "blablabla".rindex(/.{1}/, 5).should == 5 + "blablabla".rindex(/.{2}/, 5).should == 5 + "blablabla".rindex(/.{3}/, 5).should == 5 + "blablabla".rindex(/.{4}/, 5).should == 5 + + "blablabla".rindex(/.{0}/, 3).should == 3 + "blablabla".rindex(/.{1}/, 3).should == 3 + "blablabla".rindex(/.{2}/, 3).should == 3 + "blablabla".rindex(/.{5}/, 3).should == 3 + "blablabla".rindex(/.{6}/, 3).should == 3 + + "blablabla".rindex(/.l./, 0).should == 0 + "blablabla".rindex(/.l./, 1).should == 0 + "blablabla".rindex(/.l./, 2).should == 0 + "blablabla".rindex(/.l./, 3).should == 3 + + "blablablax".rindex(/.x/, 10).should == 8 + "blablablax".rindex(/.x/, 9).should == 8 + "blablablax".rindex(/.x/, 8).should == 8 + + "blablablax".rindex(/..x/, 10).should == 7 + "blablablax".rindex(/..x/, 9).should == 7 + "blablablax".rindex(/..x/, 8).should == 7 + "blablablax".rindex(/..x/, 7).should == 7 + + not_supported_on :opal do + "blablabla\n".rindex(/\Z/, 9).should == 9 + end + end + + it "starts the search at offset + self.length if offset is negative" do + str = "blablabla" + + ["bl", "bla", "blab", "la", "lab", "ab", ""].each do |needle| + (-str.length .. -1).each do |offset| + str.rindex(needle, offset).should == + str.rindex(needle, offset + str.length) + end + end + end + + it "returns nil if the substring isn't found" do + "blablabla".rindex(/BLA/).should == nil + "blablabla".rindex(/.{10}/).should == nil + "blablablax".rindex(/.x/, 7).should == nil + "blablablax".rindex(/..x/, 6).should == nil + + not_supported_on :opal do + "blablabla".rindex(/\Z/, 5).should == nil + "blablabla".rindex(/\z/, 5).should == nil + "blablabla\n".rindex(/\z/, 9).should == nil + end + end + + not_supported_on :opal do + it "supports \\G which matches at the given start offset" do + "helloYOU.".rindex(/YOU\G/, 8).should == 5 + "helloYOU.".rindex(/YOU\G/).should == nil + + idx = "helloYOUall!".index("YOU") + re = /YOU.+\G.+/ + # The # marks where \G will match. + [ + ["helloYOU#all.", nil], + ["helloYOUa#ll.", idx], + ["helloYOUal#l.", idx], + ["helloYOUall#.", idx], + ["helloYOUall.#", nil] + ].each do |i| + start = i[0].index("#") + str = i[0].delete("#") + + str.rindex(re, start).should == i[1] + end + end + end + + it "tries to convert start_offset to an integer via to_int" do + obj = mock('5') + def obj.to_int() 5 end + "str".rindex(/../, obj).should == 1 + + obj = mock('5') + def obj.respond_to?(arg, *) true end + def obj.method_missing(*args); 5; end + "str".rindex(/../, obj).should == 1 + end + + it "raises a TypeError when given offset is nil" do + -> { "str".rindex(/../, nil) }.should raise_error(TypeError) + end + + it "returns the reverse character index of a multibyte character" do + "ã‚りãŒã‚ŠãŒã¨ã†".rindex("ãŒ").should == 4 + "ã‚りãŒã‚ŠãŒã¨ã†".rindex(/ãŒ/).should == 4 + end + + it "returns the character index before the finish" do + "ã‚りãŒã‚ŠãŒã¨ã†".rindex("ãŒ", 3).should == 2 + "ã‚りãŒã‚ŠãŒã¨ã†".rindex(/ãŒ/, 3).should == 2 + end + + it "raises an Encoding::CompatibilityError if the encodings are incompatible" do + re = Regexp.new "れ".encode(Encoding::EUC_JP) + -> do + "ã‚れ".rindex re + end.should raise_error(Encoding::CompatibilityError, "incompatible encoding regexp match (EUC-JP regexp with UTF-8 string)") + end +end diff --git a/spec/ruby/core/string/rjust_spec.rb b/spec/ruby/core/string/rjust_spec.rb new file mode 100644 index 0000000000..4ad3e54aea --- /dev/null +++ b/spec/ruby/core/string/rjust_spec.rb @@ -0,0 +1,100 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "String#rjust with length, padding" do + it "returns a new string of specified length with self right justified and padded with padstr" do + "hello".rjust(20, '1234').should == "123412341234123hello" + + "".rjust(1, "abcd").should == "a" + "".rjust(2, "abcd").should == "ab" + "".rjust(3, "abcd").should == "abc" + "".rjust(4, "abcd").should == "abcd" + "".rjust(6, "abcd").should == "abcdab" + + "OK".rjust(3, "abcd").should == "aOK" + "OK".rjust(4, "abcd").should == "abOK" + "OK".rjust(6, "abcd").should == "abcdOK" + "OK".rjust(8, "abcd").should == "abcdabOK" + end + + it "pads with whitespace if no padstr is given" do + "hello".rjust(20).should == " hello" + end + + it "returns self if it's longer than or as long as the specified length" do + "".rjust(0).should == "" + "".rjust(-1).should == "" + "hello".rjust(4).should == "hello" + "hello".rjust(-1).should == "hello" + "this".rjust(3).should == "this" + "radiology".rjust(8, '-').should == "radiology" + end + + it "tries to convert length to an integer using to_int" do + "^".rjust(3.8, "^_").should == "^_^" + + obj = mock('3') + obj.should_receive(:to_int).and_return(3) + + "o".rjust(obj, "o_").should == "o_o" + end + + it "raises a TypeError when length can't be converted to an integer" do + -> { "hello".rjust("x") }.should raise_error(TypeError) + -> { "hello".rjust("x", "y") }.should raise_error(TypeError) + -> { "hello".rjust([]) }.should raise_error(TypeError) + -> { "hello".rjust(mock('x')) }.should raise_error(TypeError) + end + + it "tries to convert padstr to a string using to_str" do + padstr = mock('123') + padstr.should_receive(:to_str).and_return("123") + + "hello".rjust(10, padstr).should == "12312hello" + end + + it "raises a TypeError when padstr can't be converted" do + -> { "hello".rjust(20, []) }.should raise_error(TypeError) + -> { "hello".rjust(20, Object.new)}.should raise_error(TypeError) + -> { "hello".rjust(20, mock('x')) }.should raise_error(TypeError) + end + + it "raises an ArgumentError when padstr is empty" do + -> { "hello".rjust(10, '') }.should raise_error(ArgumentError) + end + + it "returns String instances when called on subclasses" do + StringSpecs::MyString.new("").rjust(10).should be_an_instance_of(String) + StringSpecs::MyString.new("foo").rjust(10).should be_an_instance_of(String) + StringSpecs::MyString.new("foo").rjust(10, StringSpecs::MyString.new("x")).should be_an_instance_of(String) + + "".rjust(10, StringSpecs::MyString.new("x")).should be_an_instance_of(String) + "foo".rjust(10, StringSpecs::MyString.new("x")).should be_an_instance_of(String) + end + + describe "with width" do + it "returns a String in the same encoding as the original" do + str = "abc".dup.force_encoding Encoding::IBM437 + result = str.rjust 5 + result.should == " abc" + result.encoding.should equal(Encoding::IBM437) + end + end + + describe "with width, pattern" do + it "returns a String in the compatible encoding" do + str = "abc".dup.force_encoding Encoding::IBM437 + result = str.rjust 5, "ã‚" + result.should == "ã‚ã‚abc" + result.encoding.should equal(Encoding::UTF_8) + end + + it "raises an Encoding::CompatibilityError if the encodings are incompatible" do + pat = "ã‚¢".encode Encoding::EUC_JP + -> do + "ã‚れ".rjust 5, pat + end.should raise_error(Encoding::CompatibilityError) + end + end +end diff --git a/spec/ruby/core/string/rpartition_spec.rb b/spec/ruby/core/string/rpartition_spec.rb new file mode 100644 index 0000000000..cef0384c73 --- /dev/null +++ b/spec/ruby/core/string/rpartition_spec.rb @@ -0,0 +1,71 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/partition' + +describe "String#rpartition with String" do + it_behaves_like :string_partition, :rpartition + + it "returns an array of substrings based on splitting on the given string" do + "hello world".rpartition("o").should == ["hello w", "o", "rld"] + end + + it "always returns 3 elements" do + "hello".rpartition("x").should == ["", "", "hello"] + "hello".rpartition("hello").should == ["", "hello", ""] + end + + it "returns original string if regexp doesn't match" do + "hello".rpartition("/x/").should == ["", "", "hello"] + end + + it "returns new object if doesn't match" do + str = "hello" + str.rpartition("/no_match/").last.should_not.equal?(str) + end + + it "handles multibyte string correctly" do + "ユーザ@ドメイン".rpartition(/@/).should == ["ユーザ", "@", "ドメイン"] + end + + it "accepts regexp" do + "hello!".rpartition(/l./).should == ["hel", "lo", "!"] + end + + it "affects $~" do + matched_string = "hello!".rpartition(/l./)[1] + matched_string.should == $~[0] + end + + it "converts its argument using :to_str" do + find = mock('l') + find.should_receive(:to_str).and_return("l") + "hello".rpartition(find).should == ["hel","l","o"] + end + + it "raises an error if not convertible to string" do + ->{ "hello".rpartition(5) }.should raise_error(TypeError) + ->{ "hello".rpartition(nil) }.should raise_error(TypeError) + end + + it "handles a pattern in a superset encoding" do + string = "hello".dup.force_encoding(Encoding::US_ASCII) + + result = string.rpartition("é") + + result.should == ["", "", "hello"] + result[0].encoding.should == Encoding::US_ASCII + result[1].encoding.should == Encoding::US_ASCII + result[2].encoding.should == Encoding::US_ASCII + end + + it "handles a pattern in a subset encoding" do + pattern = "o".dup.force_encoding(Encoding::US_ASCII) + + result = "héllo world".rpartition(pattern) + + result.should == ["héllo w", "o", "rld"] + result[0].encoding.should == Encoding::UTF_8 + result[1].encoding.should == Encoding::US_ASCII + result[2].encoding.should == Encoding::UTF_8 + end +end diff --git a/spec/ruby/core/string/rstrip_spec.rb b/spec/ruby/core/string/rstrip_spec.rb new file mode 100644 index 0000000000..55773f5238 --- /dev/null +++ b/spec/ruby/core/string/rstrip_spec.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: false +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/strip' + +describe "String#rstrip" do + it_behaves_like :string_strip, :rstrip + + it "returns a copy of self with trailing whitespace removed" do + " hello ".rstrip.should == " hello" + " hello world ".rstrip.should == " hello world" + " hello world \n\r\t\n\v\r".rstrip.should == " hello world" + "hello".rstrip.should == "hello" + "hello\x00".rstrip.should == "hello" + "ã“ã«ã¡ã‚ ".rstrip.should == "ã“ã«ã¡ã‚" + end + + it "works with lazy substrings" do + " hello "[1...-1].rstrip.should == " hello" + " hello world "[1...-1].rstrip.should == " hello world" + " hello world \n\r\t\n\v\r"[1...-1].rstrip.should == " hello world" + " ã“ã«ã¡ã‚ "[1...-1].rstrip.should == "ã“ã«ã¡ã‚" + end + + it "returns a copy of self with all trailing whitespace and NULL bytes removed" do + "\x00 \x00hello\x00 \x00".rstrip.should == "\x00 \x00hello" + end +end + +describe "String#rstrip!" do + it "modifies self in place and returns self" do + a = " hello " + a.rstrip!.should equal(a) + a.should == " hello" + end + + it "modifies self removing trailing NULL bytes and whitespace" do + a = "\x00 \x00hello\x00 \x00" + a.rstrip! + a.should == "\x00 \x00hello" + end + + it "returns nil if no modifications were made" do + a = "hello" + a.rstrip!.should == nil + a.should == "hello" + end + + it "makes a string empty if it is only whitespace" do + "".rstrip!.should == nil + " ".rstrip.should == "" + " ".rstrip.should == "" + end + + it "removes trailing NULL bytes and whitespace" do + a = "\000 goodbye \000" + a.rstrip! + a.should == "\000 goodbye" + end + + it "raises a FrozenError on a frozen instance that is modified" do + -> { " hello ".freeze.rstrip! }.should raise_error(FrozenError) + end + + # see [ruby-core:23666] + it "raises a FrozenError on a frozen instance that would not be modified" do + -> { "hello".freeze.rstrip! }.should raise_error(FrozenError) + -> { "".freeze.rstrip! }.should raise_error(FrozenError) + end + + it "raises an Encoding::CompatibilityError if the last non-space codepoint is invalid" do + s = "abc\xDF".force_encoding(Encoding::UTF_8) + s.valid_encoding?.should be_false + -> { s.rstrip! }.should raise_error(Encoding::CompatibilityError) + + s = "abc\xDF ".force_encoding(Encoding::UTF_8) + s.valid_encoding?.should be_false + -> { s.rstrip! }.should raise_error(Encoding::CompatibilityError) + end +end diff --git a/spec/ruby/core/string/scan_spec.rb b/spec/ruby/core/string/scan_spec.rb new file mode 100644 index 0000000000..bbe843b591 --- /dev/null +++ b/spec/ruby/core/string/scan_spec.rb @@ -0,0 +1,173 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "String#scan" do + it "returns an array containing all matches" do + "cruel world".scan(/\w+/).should == ["cruel", "world"] + "cruel world".scan(/.../).should == ["cru", "el ", "wor"] + + # Edge case + "hello".scan(//).should == ["", "", "", "", "", ""] + "".scan(//).should == [""] + end + + it "respects unicode when the pattern collapses to nothing" do + str = "ã“ã«ã¡ã‚" + reg = %r!! + str.scan(reg).should == ["", "", "", "", ""] + end + + it "stores groups as arrays in the returned arrays" do + "hello".scan(/()/).should == [[""]] * 6 + "hello".scan(/()()/).should == [["", ""]] * 6 + "cruel world".scan(/(...)/).should == [["cru"], ["el "], ["wor"]] + "cruel world".scan(/(..)(..)/).should == [["cr", "ue"], ["l ", "wo"]] + end + + it "scans for occurrences of the string if pattern is a string" do + "one two one two".scan('one').should == ["one", "one"] + "hello.".scan('.').should == ['.'] + end + + it "sets $~ to MatchData of last match and nil when there's none" do + 'hello.'.scan(/.(.)/) + $~[0].should == 'o.' + + 'hello.'.scan(/not/) + $~.should == nil + + 'hello.'.scan('l') + $~.begin(0).should == 3 + $~[0].should == 'l' + + 'hello.'.scan('not') + $~.should == nil + end + + it "supports \\G which matches the end of the previous match / string start for first match" do + "one two one two".scan(/\G\w+/).should == ["one"] + "one two one two".scan(/\G\w+\s*/).should == ["one ", "two ", "one ", "two"] + "one two one two".scan(/\G\s*\w+/).should == ["one", " two", " one", " two"] + end + + it "tries to convert pattern to a string via to_str" do + obj = mock('o') + obj.should_receive(:to_str).and_return("o") + "o_o".scan(obj).should == ["o", "o"] + end + + it "raises a TypeError if pattern isn't a Regexp and can't be converted to a String" do + -> { "cruel world".scan(5) }.should raise_error(TypeError) + not_supported_on :opal do + -> { "cruel world".scan(:test) }.should raise_error(TypeError) + end + -> { "cruel world".scan(mock('x')) }.should raise_error(TypeError) + end + + # jruby/jruby#5513 + it "does not raise any errors when passed a multi-byte string" do + "ã‚ã‚ã‚aaaã‚ã‚ã‚".scan("ã‚ã‚ã‚").should == ["ã‚ã‚ã‚", "ã‚ã‚ã‚"] + end + + it "returns Strings in the same encoding as self" do + "cruel world".encode("US-ASCII").scan(/\w+/).each do |s| + s.encoding.should == Encoding::US_ASCII + end + end +end + +describe "String#scan with pattern and block" do + it "returns self" do + s = "foo" + s.scan(/./) {}.should equal(s) + s.scan(/roar/) {}.should equal(s) + end + + it "passes each match to the block as one argument: an array" do + a = [] + "cruel world".scan(/\w+/) { |*w| a << w } + a.should == [["cruel"], ["world"]] + end + + it "passes groups to the block as one argument: an array" do + a = [] + "cruel world".scan(/(..)(..)/) { |w| a << w } + a.should == [["cr", "ue"], ["l ", "wo"]] + end + + it "sets $~ for access from the block" do + str = "hello" + + matches = [] + offsets = [] + + str.scan(/([aeiou])/) do + md = $~ + md.string.should == str + matches << md.to_a + offsets << md.offset(0) + str + end + + matches.should == [["e", "e"], ["o", "o"]] + offsets.should == [[1, 2], [4, 5]] + + matches = [] + offsets = [] + + str.scan("l") do + md = $~ + md.string.should == str + matches << md.to_a + offsets << md.offset(0) + str + end + + matches.should == [["l"], ["l"]] + offsets.should == [[2, 3], [3, 4]] + end + + it "restores $~ after leaving the block" do + [/./, "l"].each do |pattern| + old_md = nil + "hello".scan(pattern) do + old_md = $~ + "ok".match(/./) + "x" + end + + $~[0].should == old_md[0] + $~.string.should == "hello" + end + end + + it "sets $~ to MatchData of last match and nil when there's none for access from outside" do + 'hello.'.scan('l') { 'x' } + $~.begin(0).should == 3 + $~[0].should == 'l' + + 'hello.'.scan('not') { 'x' } + $~.should == nil + + 'hello.'.scan(/.(.)/) { 'x' } + $~[0].should == 'o.' + + 'hello.'.scan(/not/) { 'x' } + $~.should == nil + end + + it "passes block arguments as individual arguments when blocks are provided" do + "a b c\na b c\na b c".scan(/(\w*) (\w*) (\w*)/) do |first,second,third| + first.should == 'a'; + second.should == 'b'; + third.should == 'c'; + end + end + + it "yields String instances for subclasses" do + a = [] + StringSpecs::MyString.new("abc").scan(/./) { |s| a << s.class } + a.should == [String, String, String] + end +end diff --git a/spec/ruby/core/string/scrub_spec.rb b/spec/ruby/core/string/scrub_spec.rb new file mode 100644 index 0000000000..b9ef0f1a16 --- /dev/null +++ b/spec/ruby/core/string/scrub_spec.rb @@ -0,0 +1,164 @@ +# -*- encoding: utf-8 -*- +# frozen_string_literal: false +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "String#scrub with a default replacement" do + it "returns self for valid strings" do + input = "foo" + + input.scrub.should == input + end + + it "replaces invalid byte sequences" do + x81 = [0x81].pack('C').force_encoding('utf-8') + "abc\u3042#{x81}".scrub.should == "abc\u3042\uFFFD" + end + + it "replaces invalid byte sequences in lazy substrings" do + x81 = [0x81].pack('C').force_encoding('utf-8') + "abc\u3042#{x81}def"[1...-1].scrub.should == "bc\u3042\uFFFDde" + end + + it "returns a copy of self when the input encoding is BINARY" do + input = "foo".encode('BINARY') + + input.scrub.should == "foo" + end + + it "replaces invalid byte sequences when using ASCII as the input encoding" do + xE3x80 = [0xE3, 0x80].pack('CC').force_encoding 'utf-8' + input = "abc\u3042#{xE3x80}".force_encoding('ASCII') + input.scrub.should == "abc?????" + end + + it "returns a String in the same encoding as self" do + x81 = [0x81].pack('C').force_encoding('utf-8') + "abc\u3042#{x81}".scrub.encoding.should == Encoding::UTF_8 + end + + it "returns String instances when called on a subclass" do + StringSpecs::MyString.new("foo").scrub.should be_an_instance_of(String) + input = [0x81].pack('C').force_encoding('utf-8') + StringSpecs::MyString.new(input).scrub.should be_an_instance_of(String) + end +end + +describe "String#scrub with a custom replacement" do + it "returns self for valid strings" do + input = "foo" + + input.scrub("*").should == input + end + + it "replaces invalid byte sequences" do + x81 = [0x81].pack('C').force_encoding('utf-8') + "abc\u3042#{x81}".scrub("*").should == "abc\u3042*" + end + + it "replaces invalid byte sequences in frozen strings" do + x81 = [0x81].pack('C').force_encoding('utf-8') + (-"abc\u3042#{x81}").scrub("*").should == "abc\u3042*" + + leading_surrogate = [0x00, 0xD8] + utf16_str = ("abc".encode('UTF-16LE').bytes + leading_surrogate).pack('c*').force_encoding('UTF-16LE') + (-(utf16_str)).scrub("*".encode('UTF-16LE')).should == "abc*".encode('UTF-16LE') + end + + it "replaces an incomplete character at the end with a single replacement" do + xE3x80 = [0xE3, 0x80].pack('CC').force_encoding 'utf-8' + xE3x80.scrub("*").should == "*" + end + + it "raises ArgumentError for replacements with an invalid encoding" do + x81 = [0x81].pack('C').force_encoding('utf-8') + xE4 = [0xE4].pack('C').force_encoding('utf-8') + block = -> { "foo#{x81}".scrub(xE4) } + + block.should raise_error(ArgumentError) + end + + it "returns a String in the same encoding as self" do + x81 = [0x81].pack('C').force_encoding('utf-8') + "abc\u3042#{x81}".scrub("*").encoding.should == Encoding::UTF_8 + end + + it "raises TypeError when a non String replacement is given" do + x81 = [0x81].pack('C').force_encoding('utf-8') + block = -> { "foo#{x81}".scrub(1) } + + block.should raise_error(TypeError) + end + + it "returns String instances when called on a subclass" do + StringSpecs::MyString.new("foo").scrub("*").should be_an_instance_of(String) + input = [0x81].pack('C').force_encoding('utf-8') + StringSpecs::MyString.new(input).scrub("*").should be_an_instance_of(String) + end +end + +describe "String#scrub with a block" do + it "returns self for valid strings" do + input = "foo" + + input.scrub { |b| "*" }.should == input + end + + it "replaces invalid byte sequences" do + xE3x80 = [0xE3, 0x80].pack('CC').force_encoding 'utf-8' + replaced = "abc\u3042#{xE3x80}".scrub { |b| "<#{b.unpack("H*")[0]}>" } + + replaced.should == "abc\u3042<e380>" + end + + it "replaces invalid byte sequences using a custom encoding" do + x80x80 = [0x80, 0x80].pack('CC').force_encoding 'utf-8' + replaced = x80x80.scrub do |bad| + bad.encode(Encoding::UTF_8, Encoding::Windows_1252) + end + + replaced.should == "€€" + end + + it "returns String instances when called on a subclass" do + StringSpecs::MyString.new("foo").scrub { |b| "*" }.should be_an_instance_of(String) + input = [0x81].pack('C').force_encoding('utf-8') + StringSpecs::MyString.new(input).scrub { |b| "<#{b.unpack("H*")[0]}>" }.should be_an_instance_of(String) + end +end + +describe "String#scrub!" do + it "modifies self for valid strings" do + x81 = [0x81].pack('C').force_encoding('utf-8') + input = "a#{x81}" + input.scrub! + input.should == "a\uFFFD" + end + + it "accepts blocks" do + x81 = [0x81].pack('C').force_encoding('utf-8') + input = "a#{x81}" + input.scrub! { |b| "<?>" } + input.should == "a<?>" + end + + it "maintains the state of frozen strings that are already valid" do + input = "a" + input.freeze + input.scrub! + input.frozen?.should be_true + end + + it "preserves the instance variables of already valid strings" do + input = "a" + input.instance_variable_set(:@a, 'b') + input.scrub! + input.instance_variable_get(:@a).should == 'b' + end + + it "accepts a frozen string as a replacement" do + input = "a\xE2" + input.scrub!('.'.freeze) + input.should == 'a.' + end +end diff --git a/spec/ruby/core/string/setbyte_spec.rb b/spec/ruby/core/string/setbyte_spec.rb new file mode 100644 index 0000000000..85403ca62c --- /dev/null +++ b/spec/ruby/core/string/setbyte_spec.rb @@ -0,0 +1,112 @@ +# -*- encoding: utf-8 -*- +# frozen_string_literal: false +require_relative '../../spec_helper' + +describe "String#setbyte" do + it "returns an Integer" do + "a".setbyte(0,1).should be_kind_of(Integer) + end + + it "modifies the receiver" do + str = "glark" + old_id = str.object_id + str.setbyte(0,88) + str.object_id.should == old_id + end + + it "changes the byte at the given index to the new byte" do + str = "a" + str.setbyte(0,98) + str.should == 'b' + + # copy-on-write case + str1, str2 = "fooXbar".split("X") + str2.setbyte(0, 50) + str2.should == "2ar" + str1.should == "foo" + end + + it "allows changing bytes in multi-byte characters" do + str = "\u{915}" + str.setbyte(1,254) + str.getbyte(1).should == 254 + end + + it "can invalidate a String's encoding" do + str = "glark" + str.valid_encoding?.should be_true + str.setbyte(2,253) + str.valid_encoding?.should be_false + + str = "ABC" + str.setbyte(0, 0x20) # ' ' + str.should.valid_encoding? + str.setbyte(0, 0xE3) + str.should_not.valid_encoding? + end + + it "regards a negative index as counting from the end of the String" do + str = "hedgehog" + str.setbyte(-3, 108) + str.should == "hedgelog" + + # copy-on-write case + str1, str2 = "fooXbar".split("X") + str2.setbyte(-1, 50) + str2.should == "ba2" + str1.should == "foo" + end + + it "raises an IndexError if the index is greater than the String bytesize" do + -> { "?".setbyte(1, 97) }.should raise_error(IndexError) + end + + it "raises an IndexError if the negative index is greater magnitude than the String bytesize" do + -> { "???".setbyte(-5, 97) }.should raise_error(IndexError) + end + + it "sets a byte at an index greater than String size" do + chr = "\u{998}" + chr.bytesize.should == 3 + chr.setbyte(2, 150) + chr.should == "\xe0\xa6\x96" + end + + it "does not modify the original string when using String.new" do + str1 = "hedgehog" + str2 = String.new(str1) + str2.setbyte(0, 108) + str2.should == "ledgehog" + str2.should_not == "hedgehog" + str1.should == "hedgehog" + str1.should_not == "ledgehog" + end + + it "raises a FrozenError if self is frozen" do + str = "cold".freeze + str.frozen?.should be_true + -> { str.setbyte(3,96) }.should raise_error(FrozenError) + end + + it "raises a TypeError unless the second argument is an Integer" do + -> { "a".setbyte(0,'a') }.should raise_error(TypeError) + end + + it "calls #to_int to convert the index" do + index = mock("setbyte index") + index.should_receive(:to_int).and_return(1) + + str = "hat" + str.setbyte(index, "i".ord) + str.should == "hit" + end + + it "calls to_int to convert the value" do + value = mock("setbyte value") + value.should_receive(:to_int).and_return("i".ord) + + str = "hat" + str.setbyte(1, value) + str.should == "hit" + end +end diff --git a/spec/ruby/core/string/shared/byte_index_common.rb b/spec/ruby/core/string/shared/byte_index_common.rb new file mode 100644 index 0000000000..3de1453f4f --- /dev/null +++ b/spec/ruby/core/string/shared/byte_index_common.rb @@ -0,0 +1,63 @@ +# -*- encoding: utf-8 -*- +require_relative '../../../spec_helper' + +describe :byte_index_common, shared: true do + describe "raises on type errors" do + it "raises a TypeError if passed nil" do + -> { "abc".send(@method, nil) }.should raise_error(TypeError, "no implicit conversion of nil into String") + end + + it "raises a TypeError if passed a boolean" do + -> { "abc".send(@method, true) }.should raise_error(TypeError, "no implicit conversion of true into String") + end + + it "raises a TypeError if passed a Symbol" do + not_supported_on :opal do + -> { "abc".send(@method, :a) }.should raise_error(TypeError, "no implicit conversion of Symbol into String") + end + end + + it "raises a TypeError if passed a Symbol" do + obj = mock('x') + obj.should_not_receive(:to_int) + -> { "hello".send(@method, obj) }.should raise_error(TypeError, "no implicit conversion of MockObject into String") + end + + it "raises a TypeError if passed an Integer" do + -> { "abc".send(@method, 97) }.should raise_error(TypeError, "no implicit conversion of Integer into String") + end + end + + describe "with multibyte codepoints" do + it "raises an IndexError when byte offset lands in the middle of a multibyte character" do + -> { "ã‚".send(@method, "", 1) }.should raise_error(IndexError, "offset 1 does not land on character boundary") + -> { "ã‚".send(@method, "", 2) }.should raise_error(IndexError, "offset 2 does not land on character boundary") + -> { "ã‚".send(@method, "", -1) }.should raise_error(IndexError, "offset 2 does not land on character boundary") + -> { "ã‚".send(@method, "", -2) }.should raise_error(IndexError, "offset 1 does not land on character boundary") + end + + it "raises an Encoding::CompatibilityError if the encodings are incompatible" do + re = Regexp.new "れ".encode(Encoding::EUC_JP) + -> do + "ã‚れ".send(@method, re) + end.should raise_error(Encoding::CompatibilityError, "incompatible encoding regexp match (EUC-JP regexp with UTF-8 string)") + end + end + + describe "with global variables" do + it "doesn't set $~ for non regex search" do + $~ = nil + + 'hello.'.send(@method, 'll') + $~.should == nil + end + + it "sets $~ to MatchData of match and nil when there's none" do + 'hello.'.send(@method, /.e./) + $~[0].should == 'hel' + + 'hello.'.send(@method, /not/) + $~.should == nil + end + end +end diff --git a/spec/ruby/core/string/shared/chars.rb b/spec/ruby/core/string/shared/chars.rb new file mode 100644 index 0000000000..c730643cf4 --- /dev/null +++ b/spec/ruby/core/string/shared/chars.rb @@ -0,0 +1,66 @@ +# -*- encoding: utf-8 -*- +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' + +describe :string_chars, shared: true do + it "passes each char in self to the given block" do + a = [] + "hello".send(@method) { |c| a << c } + a.should == ['h', 'e', 'l', 'l', 'o'] + end + + it "returns self" do + s = StringSpecs::MyString.new "hello" + s.send(@method){}.should equal(s) + end + + + it "is unicode aware" do + "\303\207\342\210\202\303\251\306\222g".send(@method).to_a.should == + ["\303\207", "\342\210\202", "\303\251", "\306\222", "g"] + end + + it "returns characters in the same encoding as self" do + "&%".dup.force_encoding('Shift_JIS').send(@method).to_a.all? {|c| c.encoding.name.should == 'Shift_JIS'} + "&%".encode('BINARY').send(@method).to_a.all? {|c| c.encoding.should == Encoding::BINARY } + end + + it "works with multibyte characters" do + s = "\u{8987}".dup.force_encoding("UTF-8") + s.bytesize.should == 3 + s.send(@method).to_a.should == [s] + end + + it "works if the String's contents is invalid for its encoding" do + xA4 = [0xA4].pack('C') + xA4.force_encoding('UTF-8') + xA4.valid_encoding?.should be_false + xA4.send(@method).to_a.should == [xA4.force_encoding("UTF-8")] + end + + it "returns a different character if the String is transcoded" do + s = "\u{20AC}".dup.force_encoding('UTF-8') + s.encode('UTF-8').send(@method).to_a.should == ["\u{20AC}".dup.force_encoding('UTF-8')] + s.encode('iso-8859-15').send(@method).to_a.should == [[0xA4].pack('C').force_encoding('iso-8859-15')] + s.encode('iso-8859-15').encode('UTF-8').send(@method).to_a.should == ["\u{20AC}".dup.force_encoding('UTF-8')] + end + + it "uses the String's encoding to determine what characters it contains" do + s = +"\u{24B62}" + + s.force_encoding('UTF-8').send(@method).to_a.should == [ + s.force_encoding('UTF-8') + ] + s.force_encoding('BINARY').send(@method).to_a.should == [ + [0xF0].pack('C').force_encoding('BINARY'), + [0xA4].pack('C').force_encoding('BINARY'), + [0xAD].pack('C').force_encoding('BINARY'), + [0xA2].pack('C').force_encoding('BINARY') + ] + s.force_encoding('SJIS').send(@method).to_a.should == [ + [0xF0,0xA4].pack('CC').force_encoding('SJIS'), + [0xAD].pack('C').force_encoding('SJIS'), + [0xA2].pack('C').force_encoding('SJIS') + ] + end +end diff --git a/spec/ruby/core/string/shared/codepoints.rb b/spec/ruby/core/string/shared/codepoints.rb new file mode 100644 index 0000000000..1c28ba3d5e --- /dev/null +++ b/spec/ruby/core/string/shared/codepoints.rb @@ -0,0 +1,62 @@ +# encoding: binary +describe :string_codepoints, shared: true do + it "returns self" do + s = "foo" + result = s.send(@method) {} + result.should equal s + end + + it "raises an ArgumentError when self has an invalid encoding and a method is called on the returned Enumerator" do + s = "\xDF".dup.force_encoding(Encoding::UTF_8) + s.valid_encoding?.should be_false + -> { s.send(@method).to_a }.should raise_error(ArgumentError) + end + + it "yields each codepoint to the block if one is given" do + codepoints = [] + "abcd".send(@method) do |codepoint| + codepoints << codepoint + end + codepoints.should == [97, 98, 99, 100] + end + + it "raises an ArgumentError if self's encoding is invalid and a block is given" do + s = "\xDF".dup.force_encoding(Encoding::UTF_8) + s.valid_encoding?.should be_false + -> { s.send(@method) { } }.should raise_error(ArgumentError) + end + + it "yields codepoints as Integers" do + "glark\u{20}".send(@method).to_a.each do |codepoint| + codepoint.should be_an_instance_of(Integer) + end + end + + it "yields one codepoint for each character" do + s = "\u{9876}\u{28}\u{1987}" + s.send(@method).to_a.size.should == s.chars.to_a.size + end + + it "works for multibyte characters" do + s = "\u{9819}" + s.bytesize.should == 3 + s.send(@method).to_a.should == [38937] + end + + it "yields the codepoints corresponding to the character's position in the String's encoding" do + "\u{787}".send(@method).to_a.should == [1927] + end + + it "round-trips to the original String using Integer#chr" do + s = "\u{13}\u{7711}\u{1010}" + s2 = +"" + s.send(@method) {|n| s2 << n.chr(Encoding::UTF_8)} + s.should == s2 + end + + it "is synonymous with #bytes for Strings which are single-byte optimizable" do + s = "(){}".encode('ascii') + s.ascii_only?.should be_true + s.send(@method).to_a.should == s.bytes.to_a + end +end diff --git a/spec/ruby/core/string/shared/concat.rb b/spec/ruby/core/string/shared/concat.rb new file mode 100644 index 0000000000..dded9a69e7 --- /dev/null +++ b/spec/ruby/core/string/shared/concat.rb @@ -0,0 +1,159 @@ +# frozen_string_literal: false +describe :string_concat, shared: true do + it "concatenates the given argument to self and returns self" do + str = 'hello ' + str.send(@method, 'world').should equal(str) + str.should == "hello world" + end + + it "raises a FrozenError when self is frozen" do + a = "hello" + a.freeze + + -> { a.send(@method, "") }.should raise_error(FrozenError) + -> { a.send(@method, "test") }.should raise_error(FrozenError) + end + + it "returns a String when given a subclass instance" do + a = "hello" + a.send(@method, StringSpecs::MyString.new(" world")) + a.should == "hello world" + a.should be_an_instance_of(String) + end + + it "returns an instance of same class when called on a subclass" do + str = StringSpecs::MyString.new("hello") + str.send(@method, " world") + str.should == "hello world" + str.should be_an_instance_of(StringSpecs::MyString) + end + + describe "with Integer" do + it "concatenates the argument interpreted as a codepoint" do + b = "".send(@method, 33) + b.should == "!" + + b.encode!(Encoding::UTF_8) + b.send(@method, 0x203D) + b.should == "!\u203D" + end + + # #5855 + it "returns a BINARY string if self is US-ASCII and the argument is between 128-255 (inclusive)" do + a = ("".encode(Encoding::US_ASCII).send(@method, 128)) + a.encoding.should == Encoding::BINARY + a.should == 128.chr + + a = ("".encode(Encoding::US_ASCII).send(@method, 255)) + a.encoding.should == Encoding::BINARY + a.should == 255.chr + end + + it "raises RangeError if the argument is an invalid codepoint for self's encoding" do + -> { "".encode(Encoding::US_ASCII).send(@method, 256) }.should raise_error(RangeError) + -> { "".encode(Encoding::EUC_JP).send(@method, 0x81) }.should raise_error(RangeError) + end + + it "raises RangeError if the argument is negative" do + -> { "".send(@method, -200) }.should raise_error(RangeError) + -> { "".send(@method, -bignum_value) }.should raise_error(RangeError) + end + + it "doesn't call to_int on its argument" do + x = mock('x') + x.should_not_receive(:to_int) + + -> { "".send(@method, x) }.should raise_error(TypeError) + end + + it "raises a FrozenError when self is frozen" do + a = "hello" + a.freeze + + -> { a.send(@method, 0) }.should raise_error(FrozenError) + -> { a.send(@method, 33) }.should raise_error(FrozenError) + end + end +end + +describe :string_concat_encoding, shared: true do + describe "when self is in an ASCII-incompatible encoding incompatible with the argument's encoding" do + it "uses self's encoding if both are empty" do + "".encode("UTF-16LE").send(@method, "").encoding.should == Encoding::UTF_16LE + end + + it "uses self's encoding if the argument is empty" do + "x".encode("UTF-16LE").send(@method, "").encoding.should == Encoding::UTF_16LE + end + + it "uses the argument's encoding if self is empty" do + "".encode("UTF-16LE").send(@method, "x".encode("UTF-8")).encoding.should == Encoding::UTF_8 + end + + it "raises Encoding::CompatibilityError if neither are empty" do + -> { "x".encode("UTF-16LE").send(@method, "y".encode("UTF-8")) }.should raise_error(Encoding::CompatibilityError) + end + end + + describe "when the argument is in an ASCII-incompatible encoding incompatible with self's encoding" do + it "uses self's encoding if both are empty" do + "".encode("UTF-8").send(@method, "".encode("UTF-16LE")).encoding.should == Encoding::UTF_8 + end + + it "uses self's encoding if the argument is empty" do + "x".encode("UTF-8").send(@method, "".encode("UTF-16LE")).encoding.should == Encoding::UTF_8 + end + + it "uses the argument's encoding if self is empty" do + "".encode("UTF-8").send(@method, "x".encode("UTF-16LE")).encoding.should == Encoding::UTF_16LE + end + + it "raises Encoding::CompatibilityError if neither are empty" do + -> { "x".encode("UTF-8").send(@method, "y".encode("UTF-16LE")) }.should raise_error(Encoding::CompatibilityError) + end + end + + describe "when self and the argument are in different ASCII-compatible encodings" do + it "uses self's encoding if both are ASCII-only" do + "abc".encode("UTF-8").send(@method, "123".encode("SHIFT_JIS")).encoding.should == Encoding::UTF_8 + end + + it "uses self's encoding if the argument is ASCII-only" do + "\u00E9".encode("UTF-8").send(@method, "123".encode("ISO-8859-1")).encoding.should == Encoding::UTF_8 + end + + it "uses the argument's encoding if self is ASCII-only" do + "abc".encode("UTF-8").send(@method, "\u00E9".encode("ISO-8859-1")).encoding.should == Encoding::ISO_8859_1 + end + + it "raises Encoding::CompatibilityError if neither are ASCII-only" do + -> { "\u00E9".encode("UTF-8").send(@method, "\u00E9".encode("ISO-8859-1")) }.should raise_error(Encoding::CompatibilityError) + end + end + + describe "when self is BINARY and argument is US-ASCII" do + it "uses BINARY encoding" do + "abc".encode("BINARY").send(@method, "123".encode("US-ASCII")).encoding.should == Encoding::BINARY + end + end +end + +describe :string_concat_type_coercion, shared: true do + it "converts the given argument to a String using to_str" do + obj = mock('world!') + obj.should_receive(:to_str).and_return("world!") + a = 'hello '.send(@method, obj) + a.should == 'hello world!' + end + + it "raises a TypeError if the given argument can't be converted to a String" do + -> { 'hello '.send(@method, []) }.should raise_error(TypeError) + -> { 'hello '.send(@method, mock('x')) }.should raise_error(TypeError) + end + + it "raises a NoMethodError if the given argument raises a NoMethodError during type coercion to a String" do + obj = mock('world!') + obj.should_receive(:to_str).and_raise(NoMethodError) + -> { 'hello '.send(@method, obj) }.should raise_error(NoMethodError) + end +end diff --git a/spec/ruby/core/string/shared/dedup.rb b/spec/ruby/core/string/shared/dedup.rb new file mode 100644 index 0000000000..1ffd6aa0fd --- /dev/null +++ b/spec/ruby/core/string/shared/dedup.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: false +describe :string_dedup, shared: true do + it 'returns self if the String is frozen' do + input = 'foo'.freeze + output = input.send(@method) + + output.should equal(input) + output.should.frozen? + end + + it 'returns a frozen copy if the String is not frozen' do + input = 'foo' + output = input.send(@method) + + output.should.frozen? + output.should_not equal(input) + output.should == 'foo' + end + + it "returns the same object for equal unfrozen strings" do + origin = "this is a string" + dynamic = %w(this is a string).join(' ') + + origin.should_not equal(dynamic) + origin.send(@method).should equal(dynamic.send(@method)) + end + + it "returns the same object when it's called on the same String literal" do + "unfrozen string".send(@method).should equal("unfrozen string".send(@method)) + "unfrozen string".send(@method).should_not equal("another unfrozen string".send(@method)) + end + + it "deduplicates frozen strings" do + dynamic = %w(this string is frozen).join(' ').freeze + + dynamic.should_not equal("this string is frozen".freeze) + + dynamic.send(@method).should equal("this string is frozen".freeze) + dynamic.send(@method).should equal("this string is frozen".send(@method).freeze) + end + + it "does not deduplicate a frozen string when it has instance variables" do + dynamic = %w(this string is frozen).join(' ') + dynamic.instance_variable_set(:@a, 1) + dynamic.freeze + + dynamic.send(@method).should_not equal("this string is frozen".freeze) + dynamic.send(@method).should_not equal("this string is frozen".send(@method).freeze) + dynamic.send(@method).should equal(dynamic) + end +end diff --git a/spec/ruby/core/string/shared/each_char_without_block.rb b/spec/ruby/core/string/shared/each_char_without_block.rb new file mode 100644 index 0000000000..397100ce0e --- /dev/null +++ b/spec/ruby/core/string/shared/each_char_without_block.rb @@ -0,0 +1,26 @@ +# -*- encoding: utf-8 -*- +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' + +describe :string_each_char_without_block, shared: true do + describe "when no block is given" do + it "returns an enumerator" do + enum = "hello".send(@method) + enum.should be_an_instance_of(Enumerator) + enum.to_a.should == ['h', 'e', 'l', 'l', 'o'] + end + + describe "returned enumerator" do + describe "size" do + it "should return the size of the string" do + str = "hello" + str.send(@method).size.should == str.size + str = "ola" + str.send(@method).size.should == str.size + str = "\303\207\342\210\202\303\251\306\222g" + str.send(@method).size.should == str.size + end + end + end + end +end diff --git a/spec/ruby/core/string/shared/each_codepoint_without_block.rb b/spec/ruby/core/string/shared/each_codepoint_without_block.rb new file mode 100644 index 0000000000..c88e5c54c7 --- /dev/null +++ b/spec/ruby/core/string/shared/each_codepoint_without_block.rb @@ -0,0 +1,33 @@ +# encoding: binary +describe :string_each_codepoint_without_block, shared: true do + describe "when no block is given" do + it "returns an Enumerator" do + "".send(@method).should be_an_instance_of(Enumerator) + end + + it "returns an Enumerator even when self has an invalid encoding" do + s = "\xDF".dup.force_encoding(Encoding::UTF_8) + s.valid_encoding?.should be_false + s.send(@method).should be_an_instance_of(Enumerator) + end + + describe "returned Enumerator" do + describe "size" do + it "should return the size of the string" do + str = "hello" + str.send(@method).size.should == str.size + str = "ola" + str.send(@method).size.should == str.size + str = "\303\207\342\210\202\303\251\306\222g" + str.send(@method).size.should == str.size + end + + it "should return the size of the string even when the string has an invalid encoding" do + s = "\xDF".dup.force_encoding(Encoding::UTF_8) + s.valid_encoding?.should be_false + s.send(@method).size.should == 1 + end + end + end + end +end diff --git a/spec/ruby/core/string/shared/each_line.rb b/spec/ruby/core/string/shared/each_line.rb new file mode 100644 index 0000000000..231a6d9d4f --- /dev/null +++ b/spec/ruby/core/string/shared/each_line.rb @@ -0,0 +1,162 @@ +describe :string_each_line, shared: true do + it "splits using default newline separator when none is specified" do + a = [] + "one\ntwo\r\nthree".send(@method) { |s| a << s } + a.should == ["one\n", "two\r\n", "three"] + + b = [] + "hello\n\n\nworld".send(@method) { |s| b << s } + b.should == ["hello\n", "\n", "\n", "world"] + + c = [] + "\n\n\n\n\n".send(@method) {|s| c << s} + c.should == ["\n", "\n", "\n", "\n", "\n"] + end + + it "splits self using the supplied record separator and passes each substring to the block" do + a = [] + "one\ntwo\r\nthree".send(@method, "\n") { |s| a << s } + a.should == ["one\n", "two\r\n", "three"] + + b = [] + "hello\nworld".send(@method, 'l') { |s| b << s } + b.should == [ "hel", "l", "o\nworl", "d" ] + + c = [] + "hello\n\n\nworld".send(@method, "\n") { |s| c << s } + c.should == ["hello\n", "\n", "\n", "world"] + end + + it "splits strings containing multibyte characters" do + s = <<~EOS + foo + 🤡🤡🤡🤡🤡🤡🤡 + bar + baz + EOS + + b = [] + s.send(@method) { |part| b << part } + b.should == ["foo\n", "🤡🤡🤡🤡🤡🤡🤡\n", "bar\n", "baz\n"] + end + + it "passes self as a whole to the block if the separator is nil" do + a = [] + "one\ntwo\r\nthree".send(@method, nil) { |s| a << s } + a.should == ["one\ntwo\r\nthree"] + end + + it "yields paragraphs (broken by 2 or more successive newlines) when passed '' and replaces multiple newlines with only two ones" do + a = [] + "hello\nworld\n\n\nand\nuniverse\n\n\n\n\n".send(@method, '') { |s| a << s } + a.should == ["hello\nworld\n\n", "and\nuniverse\n\n"] + + a = [] + "hello\nworld\n\n\nand\nuniverse\n\n\n\n\ndog".send(@method, '') { |s| a << s } + a.should == ["hello\nworld\n\n", "and\nuniverse\n\n", "dog"] + end + + describe "uses $/" do + before :each do + @before_separator = $/ + end + + after :each do + suppress_warning {$/ = @before_separator} + end + + it "as the separator when none is given" do + [ + "", "x", "x\ny", "x\ry", "x\r\ny", "x\n\r\r\ny", + "hello hullo bello" + ].each do |str| + ["", "llo", "\n", "\r", nil].each do |sep| + expected = [] + str.send(@method, sep) { |x| expected << x } + + suppress_warning {$/ = sep} + + actual = [] + suppress_warning {str.send(@method) { |x| actual << x }} + + actual.should == expected + end + end + end + end + + it "yields String instances for subclasses" do + a = [] + StringSpecs::MyString.new("hello\nworld").send(@method) { |s| a << s.class } + a.should == [String, String] + end + + it "returns self" do + s = "hello\nworld" + (s.send(@method) {}).should equal(s) + end + + it "tries to convert the separator to a string using to_str" do + separator = mock('l') + separator.should_receive(:to_str).and_return("l") + + a = [] + "hello\nworld".send(@method, separator) { |s| a << s } + a.should == [ "hel", "l", "o\nworl", "d" ] + end + + it "does not care if the string is modified while substituting" do + str = +"hello\nworld." + out = [] + str.send(@method){|x| out << x; str[-1] = '!' }.should == "hello\nworld!" + out.should == ["hello\n", "world."] + end + + it "returns Strings in the same encoding as self" do + "one\ntwo\r\nthree".encode("US-ASCII").send(@method) do |s| + s.encoding.should == Encoding::US_ASCII + end + end + + it "raises a TypeError when the separator can't be converted to a string" do + -> { "hello world".send(@method, false) {} }.should raise_error(TypeError) + -> { "hello world".send(@method, mock('x')) {} }.should raise_error(TypeError) + end + + it "accepts a string separator" do + "hello world".send(@method, ?o).to_a.should == ["hello", " wo", "rld"] + end + + it "raises a TypeError when the separator is a symbol" do + -> { "hello world".send(@method, :o).to_a }.should raise_error(TypeError) + end + + context "when `chomp` keyword argument is passed" do + it "removes new line characters when separator is not specified" do + a = [] + "hello \nworld\n".send(@method, chomp: true) { |s| a << s } + a.should == ["hello ", "world"] + + a = [] + "hello \r\nworld\r\n".send(@method, chomp: true) { |s| a << s } + a.should == ["hello ", "world"] + end + + it "removes only specified separator" do + a = [] + "hello world".send(@method, ' ', chomp: true) { |s| a << s } + a.should == ["hello", "world"] + end + + # https://bugs.ruby-lang.org/issues/14257 + it "ignores new line characters when separator is specified" do + a = [] + "hello\n world\n".send(@method, ' ', chomp: true) { |s| a << s } + a.should == ["hello\n", "world\n"] + + a = [] + "hello\r\n world\r\n".send(@method, ' ', chomp: true) { |s| a << s } + a.should == ["hello\r\n", "world\r\n"] + end + end +end diff --git a/spec/ruby/core/string/shared/each_line_without_block.rb b/spec/ruby/core/string/shared/each_line_without_block.rb new file mode 100644 index 0000000000..8e08b0390c --- /dev/null +++ b/spec/ruby/core/string/shared/each_line_without_block.rb @@ -0,0 +1,17 @@ +describe :string_each_line_without_block, shared: true do + describe "when no block is given" do + it "returns an enumerator" do + enum = "hello world".send(@method, ' ') + enum.should be_an_instance_of(Enumerator) + enum.to_a.should == ["hello ", "world"] + end + + describe "returned Enumerator" do + describe "size" do + it "should return nil" do + "hello world".send(@method, ' ').size.should == nil + end + end + end + end +end diff --git a/spec/ruby/core/string/shared/encode.rb b/spec/ruby/core/string/shared/encode.rb new file mode 100644 index 0000000000..9466308886 --- /dev/null +++ b/spec/ruby/core/string/shared/encode.rb @@ -0,0 +1,432 @@ +# -*- encoding: utf-8 -*- +# frozen_string_literal: false +describe :string_encode, shared: true do + describe "when passed no options" do + it "transcodes to Encoding.default_internal when set" do + Encoding.default_internal = Encoding::UTF_8 + str = [0xA4, 0xA2].pack('CC').force_encoding Encoding::EUC_JP + str.send(@method).should == "ã‚" + end + + it "transcodes a 7-bit String despite no generic converting being available" do + -> do + Encoding::Converter.new Encoding::Emacs_Mule, Encoding::BINARY + end.should raise_error(Encoding::ConverterNotFoundError) + + Encoding.default_internal = Encoding::Emacs_Mule + str = "\x79".force_encoding Encoding::BINARY + + str.send(@method).should == "y".force_encoding(Encoding::BINARY) + end + + it "raises an Encoding::ConverterNotFoundError when no conversion is possible" do + Encoding.default_internal = Encoding::Emacs_Mule + str = [0x80].pack('C').force_encoding Encoding::BINARY + -> { str.send(@method) }.should raise_error(Encoding::ConverterNotFoundError) + end + end + + describe "when passed to encoding" do + it "accepts a String argument" do + str = [0xA4, 0xA2].pack('CC').force_encoding Encoding::EUC_JP + str.send(@method, "utf-8").should == "ã‚" + end + + it "calls #to_str to convert the object to an Encoding" do + enc = mock("string encode encoding") + enc.should_receive(:to_str).and_return("utf-8") + + str = [0xA4, 0xA2].pack('CC').force_encoding Encoding::EUC_JP + str.send(@method, enc).should == "ã‚" + end + + it "transcodes to the passed encoding" do + str = [0xA4, 0xA2].pack('CC').force_encoding Encoding::EUC_JP + str.send(@method, Encoding::UTF_8).should == "ã‚" + end + + it "transcodes Japanese multibyte characters" do + str = "ã‚ã„ã†ãˆãŠ" + str.send(@method, Encoding::ISO_2022_JP).should == + "\e\x24\x42\x24\x22\x24\x24\x24\x26\x24\x28\x24\x2A\e\x28\x42".force_encoding(Encoding::ISO_2022_JP) + end + + it "transcodes a 7-bit String despite no generic converting being available" do + -> do + Encoding::Converter.new Encoding::Emacs_Mule, Encoding::BINARY + end.should raise_error(Encoding::ConverterNotFoundError) + + str = "\x79".force_encoding Encoding::BINARY + str.send(@method, Encoding::Emacs_Mule).should == "y".force_encoding(Encoding::BINARY) + end + + it "raises an Encoding::ConverterNotFoundError when no conversion is possible" do + str = [0x80].pack('C').force_encoding Encoding::BINARY + -> do + str.send(@method, Encoding::Emacs_Mule) + end.should raise_error(Encoding::ConverterNotFoundError) + end + + it "raises an Encoding::ConverterNotFoundError for an invalid encoding" do + -> do + "abc".send(@method, "xyz") + end.should raise_error(Encoding::ConverterNotFoundError) + end + end + + describe "when passed options" do + it "does not process transcoding options if not transcoding" do + result = "ã‚\ufffdã‚".send(@method, undef: :replace) + result.should == "ã‚\ufffdã‚" + end + + it "calls #to_hash to convert the object" do + options = mock("string encode options") + options.should_receive(:to_hash).and_return({ undef: :replace }) + + result = "ã‚\ufffdã‚".send(@method, **options) + result.should == "ã‚\ufffdã‚" + end + + it "transcodes to Encoding.default_internal when set" do + Encoding.default_internal = Encoding::UTF_8 + str = [0xA4, 0xA2].pack('CC').force_encoding Encoding::EUC_JP + str.send(@method, invalid: :replace).should == "ã‚" + end + + it "raises an Encoding::ConverterNotFoundError when no conversion is possible despite 'invalid: :replace, undef: :replace'" do + Encoding.default_internal = Encoding::Emacs_Mule + str = [0x80].pack('C').force_encoding Encoding::BINARY + -> do + str.send(@method, invalid: :replace, undef: :replace) + end.should raise_error(Encoding::ConverterNotFoundError) + end + + it "replaces invalid characters when replacing Emacs-Mule encoded strings" do + got = [0x80].pack('C').force_encoding('Emacs-Mule').send(@method, invalid: :replace) + + got.should == "?".encode('Emacs-Mule') + end + end + + describe "when passed to, from" do + it "transcodes between the encodings ignoring the String encoding" do + str = "ã‚" + result = [0xA6, 0xD0, 0x8F, 0xAB, 0xE4, 0x8F, 0xAB, 0xB1].pack('C8') + result.force_encoding Encoding::EUC_JP + str.send(@method, "euc-jp", "ibm437").should == result + end + + it "calls #to_str to convert the from object to an Encoding" do + enc = mock("string encode encoding") + enc.should_receive(:to_str).and_return("ibm437") + + str = "ã‚" + result = [0xA6, 0xD0, 0x8F, 0xAB, 0xE4, 0x8F, 0xAB, 0xB1].pack('C8') + result.force_encoding Encoding::EUC_JP + + str.send(@method, "euc-jp", enc).should == result + end + end + + describe "when passed to, options" do + it "replaces undefined characters in the destination encoding" do + result = "ã‚?ã‚".send(@method, Encoding::EUC_JP, undef: :replace) + # testing for: "\xA4\xA2?\xA4\xA2" + xA4xA2 = [0xA4, 0xA2].pack('CC') + result.should == "#{xA4xA2}?#{xA4xA2}".force_encoding("euc-jp") + end + + it "replaces invalid characters in the destination encoding" do + xFF = [0xFF].pack('C').force_encoding('utf-8') + "ab#{xFF}c".send(@method, Encoding::ISO_8859_1, invalid: :replace).should == "ab?c" + end + + it "calls #to_hash to convert the options object" do + options = mock("string encode options") + options.should_receive(:to_hash).and_return({ undef: :replace }) + + result = "ã‚?ã‚".send(@method, Encoding::EUC_JP, **options) + xA4xA2 = [0xA4, 0xA2].pack('CC').force_encoding('utf-8') + result.should == "#{xA4xA2}?#{xA4xA2}".force_encoding("euc-jp") + end + end + + describe "when passed to, from, options" do + it "replaces undefined characters in the destination encoding" do + str = "ã‚?ã‚".force_encoding Encoding::BINARY + result = str.send(@method, "euc-jp", "utf-8", undef: :replace) + xA4xA2 = [0xA4, 0xA2].pack('CC').force_encoding('utf-8') + result.should == "#{xA4xA2}?#{xA4xA2}".force_encoding("euc-jp") + end + + it "replaces invalid characters in the destination encoding" do + xFF = [0xFF].pack('C').force_encoding('utf-8') + str = "ab#{xFF}c".force_encoding Encoding::BINARY + str.send(@method, "iso-8859-1", "utf-8", invalid: :replace).should == "ab?c" + end + + it "calls #to_str to convert the to object to an encoding" do + to = mock("string encode to encoding") + to.should_receive(:to_str).and_return("iso-8859-1") + + xFF = [0xFF].pack('C').force_encoding('utf-8') + str = "ab#{xFF}c".force_encoding Encoding::BINARY + str.send(@method, to, "utf-8", invalid: :replace).should == "ab?c" + end + + it "calls #to_str to convert the from object to an encoding" do + from = mock("string encode to encoding") + from.should_receive(:to_str).and_return("utf-8") + + xFF = [0xFF].pack('C').force_encoding('utf-8') + str = "ab#{xFF}c".force_encoding Encoding::BINARY + str.send(@method, "iso-8859-1", from, invalid: :replace).should == "ab?c" + end + + it "calls #to_hash to convert the options object" do + options = mock("string encode options") + options.should_receive(:to_hash).and_return({ invalid: :replace }) + + xFF = [0xFF].pack('C').force_encoding('utf-8') + str = "ab#{xFF}c".force_encoding Encoding::BINARY + str.send(@method, "iso-8859-1", "utf-8", **options).should == "ab?c" + end + end + + describe "given the fallback option" do + context "given a hash" do + it "looks up the replacement value from the hash" do + encoded = "B\ufffd".encode(Encoding::US_ASCII, fallback: { "\ufffd" => "bar" }) + encoded.should == "Bbar" + end + + it "calls to_str on the returned value" do + obj = Object.new + obj.should_receive(:to_str).and_return("bar") + encoded = "B\ufffd".encode(Encoding::US_ASCII, fallback: { "\ufffd" => obj }) + encoded.should == "Bbar" + end + + it "does not call to_s on the returned value" do + obj = Object.new + obj.should_not_receive(:to_s) + -> { + "B\ufffd".encode(Encoding::US_ASCII, fallback: { "\ufffd" => obj }) + }.should raise_error(TypeError, "no implicit conversion of Object into String") + end + + it "raises an error if the key is not present in the hash" do + -> { + "B\ufffd".encode(Encoding::US_ASCII, fallback: { "foo" => "bar" }) + }.should raise_error(Encoding::UndefinedConversionError, "U+FFFD from UTF-8 to US-ASCII") + end + + it "raises an error if the value is itself invalid" do + -> { + "B\ufffd".encode(Encoding::US_ASCII, fallback: { "\ufffd" => "\uffee" }) + }.should raise_error(ArgumentError, "too big fallback string") + end + + it "uses the hash's default value if set" do + hash = {} + hash.default = "bar" + encoded = "B\ufffd".encode(Encoding::US_ASCII, fallback: hash) + encoded.should == "Bbar" + end + + it "uses the result of calling default_proc if set" do + hash = {} + hash.default_proc = -> _, _ { "bar" } + encoded = "B\ufffd".encode(Encoding::US_ASCII, fallback: hash) + encoded.should == "Bbar" + end + end + + context "given an object inheriting from Hash" do + before do + klass = Class.new(Hash) + @hash_like = klass.new + @hash_like["\ufffd"] = "bar" + end + + it "looks up the replacement value from the object" do + encoded = "B\ufffd".encode(Encoding::US_ASCII, fallback: @hash_like) + encoded.should == "Bbar" + end + end + + context "given an object responding to []" do + before do + klass = Class.new do + def [](c) = c.bytes.inspect + end + @hash_like = klass.new + end + + it "calls [] on the object, passing the invalid character" do + encoded = "B\ufffd".encode(Encoding::US_ASCII, fallback: @hash_like) + encoded.should == "B[239, 191, 189]" + end + end + + context "given an object not responding to []" do + before do + @non_hash_like = Object.new + end + + it "raises an error" do + -> { + "B\ufffd".encode(Encoding::US_ASCII, fallback: @non_hash_like) + }.should raise_error(Encoding::UndefinedConversionError, "U+FFFD from UTF-8 to US-ASCII") + end + end + + context "given a proc" do + it "calls the proc to get the replacement value, passing in the invalid character" do + encoded = "B\ufffd".encode(Encoding::US_ASCII, fallback: proc { |c| c.bytes.inspect }) + encoded.should == "B[239, 191, 189]" + end + + it "calls to_str on the returned value" do + obj = Object.new + obj.should_receive(:to_str).and_return("bar") + encoded = "B\ufffd".encode(Encoding::US_ASCII, fallback: proc { |c| obj }) + encoded.should == "Bbar" + end + + it "does not call to_s on the returned value" do + obj = Object.new + obj.should_not_receive(:to_s) + -> { + "B\ufffd".encode(Encoding::US_ASCII, fallback: proc { |c| obj }) + }.should raise_error(TypeError, "no implicit conversion of Object into String") + end + + it "raises an error if the returned value is itself invalid" do + -> { + "B\ufffd".encode(Encoding::US_ASCII, fallback: -> c { "\uffee" }) + }.should raise_error(ArgumentError, "too big fallback string") + end + end + + context "given a lambda" do + it "calls the lambda to get the replacement value, passing in the invalid character" do + encoded = "B\ufffd".encode(Encoding::US_ASCII, fallback: -> c { c.bytes.inspect }) + encoded.should == "B[239, 191, 189]" + end + + it "calls to_str on the returned value" do + obj = Object.new + obj.should_receive(:to_str).and_return("bar") + encoded = "B\ufffd".encode(Encoding::US_ASCII, fallback: -> c { obj }) + encoded.should == "Bbar" + end + + it "does not call to_s on the returned value" do + obj = Object.new + obj.should_not_receive(:to_s) + -> { + "B\ufffd".encode(Encoding::US_ASCII, fallback: -> c { obj }) + }.should raise_error(TypeError, "no implicit conversion of Object into String") + end + + it "raises an error if the returned value is itself invalid" do + -> { + "B\ufffd".encode(Encoding::US_ASCII, fallback: -> c { "\uffee" }) + }.should raise_error(ArgumentError, "too big fallback string") + end + end + + context "given a method" do + def replace(c) = c.bytes.inspect + def replace_bad(c) = "\uffee" + + def replace_to_str(c) + obj = Object.new + obj.should_receive(:to_str).and_return("bar") + obj + end + + def replace_to_s(c) + obj = Object.new + obj.should_not_receive(:to_s) + obj + end + + it "calls the method to get the replacement value, passing in the invalid character" do + encoded = "B\ufffd".encode(Encoding::US_ASCII, fallback: method(:replace)) + encoded.should == "B[239, 191, 189]" + end + + it "calls to_str on the returned value" do + encoded = "B\ufffd".encode(Encoding::US_ASCII, fallback: method(:replace_to_str)) + encoded.should == "Bbar" + end + + it "does not call to_s on the returned value" do + -> { + "B\ufffd".encode(Encoding::US_ASCII, fallback: method(:replace_to_s)) + }.should raise_error(TypeError, "no implicit conversion of Object into String") + end + + it "raises an error if the returned value is itself invalid" do + -> { + "B\ufffd".encode(Encoding::US_ASCII, fallback: method(:replace_bad)) + }.should raise_error(ArgumentError, "too big fallback string") + end + end + end + + describe "given the xml: :text option" do + it "replaces all instances of '&' with '&'" do + '& and &'.send(@method, "UTF-8", xml: :text).should == '& and &' + end + + it "replaces all instances of '<' with '<'" do + '< and <'.send(@method, "UTF-8", xml: :text).should == '< and <' + end + + it "replaces all instances of '>' with '>'" do + '> and >'.send(@method, "UTF-8", xml: :text).should == '> and >' + end + + it "does not replace '\"'" do + '" and "'.send(@method, "UTF-8", xml: :text).should == '" and "' + end + + it "replaces undefined characters with their upper-case hexadecimal numeric character references" do + 'ürst'.send(@method, Encoding::US_ASCII, xml: :text).should == 'ürst' + end + end + + describe "given the xml: :attr option" do + it "surrounds the encoded text with double-quotes" do + 'abc'.send(@method, "UTF-8", xml: :attr).should == '"abc"' + end + + it "replaces all instances of '&' with '&'" do + '& and &'.send(@method, "UTF-8", xml: :attr).should == '"& and &"' + end + + it "replaces all instances of '<' with '<'" do + '< and <'.send(@method, "UTF-8", xml: :attr).should == '"< and <"' + end + + it "replaces all instances of '>' with '>'" do + '> and >'.send(@method, "UTF-8", xml: :attr).should == '"> and >"' + end + + it "replaces all instances of '\"' with '"'" do + '" and "'.send(@method, "UTF-8", xml: :attr).should == '"" and ""' + end + + it "replaces undefined characters with their upper-case hexadecimal numeric character references" do + 'ürst'.send(@method, Encoding::US_ASCII, xml: :attr).should == '"ürst"' + end + end + + it "raises ArgumentError if the value of the :xml option is not :text or :attr" do + -> { ''.send(@method, "UTF-8", xml: :other) }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/string/shared/eql.rb b/spec/ruby/core/string/shared/eql.rb new file mode 100644 index 0000000000..d5af337d53 --- /dev/null +++ b/spec/ruby/core/string/shared/eql.rb @@ -0,0 +1,38 @@ +# encoding: binary +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' + +describe :string_eql_value, shared: true do + it "returns true if self <=> string returns 0" do + 'hello'.send(@method, 'hello').should be_true + end + + it "returns false if self <=> string does not return 0" do + "more".send(@method, "MORE").should be_false + "less".send(@method, "greater").should be_false + end + + it "ignores encoding difference of compatible string" do + "hello".dup.force_encoding("utf-8").send(@method, "hello".dup.force_encoding("iso-8859-1")).should be_true + end + + it "considers encoding difference of incompatible string" do + "\xff".dup.force_encoding("utf-8").send(@method, "\xff".dup.force_encoding("iso-8859-1")).should be_false + end + + it "considers encoding compatibility" do + "abcd".dup.force_encoding("utf-8").send(@method, "abcd".dup.force_encoding("utf-32le")).should be_false + end + + it "ignores subclass differences" do + a = "hello" + b = StringSpecs::MyString.new("hello") + + a.send(@method, b).should be_true + b.send(@method, a).should be_true + end + + it "returns true when comparing 2 empty strings but one is not ASCII-compatible" do + "".send(@method, "".dup.force_encoding('iso-2022-jp')).should == true + end +end diff --git a/spec/ruby/core/string/shared/equal_value.rb b/spec/ruby/core/string/shared/equal_value.rb new file mode 100644 index 0000000000..fccafb5821 --- /dev/null +++ b/spec/ruby/core/string/shared/equal_value.rb @@ -0,0 +1,29 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' + +describe :string_equal_value, shared: true do + it "returns false if obj does not respond to to_str" do + 'hello'.send(@method, 5).should be_false + not_supported_on :opal do + 'hello'.send(@method, :hello).should be_false + end + 'hello'.send(@method, mock('x')).should be_false + end + + it "returns obj == self if obj responds to to_str" do + obj = Object.new + + # String#== merely checks if #to_str is defined. It does + # not call it. + obj.stub!(:to_str) + + # Don't use @method for :== in `obj.should_receive(:==)` + obj.should_receive(:==).and_return(true) + + 'hello'.send(@method, obj).should be_true + end + + it "is not fooled by NUL characters" do + "abc\0def".send(@method, "abc\0xyz").should be_false + end +end diff --git a/spec/ruby/core/string/shared/grapheme_clusters.rb b/spec/ruby/core/string/shared/grapheme_clusters.rb new file mode 100644 index 0000000000..8b666868b1 --- /dev/null +++ b/spec/ruby/core/string/shared/grapheme_clusters.rb @@ -0,0 +1,16 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' + +describe :string_grapheme_clusters, shared: true do + it "passes each grapheme cluster in self to the given block" do + a = [] + # test string: abc[rainbow flag emoji][paw prints] + "ab\u{1f3f3}\u{fe0f}\u{200d}\u{1f308}\u{1F43E}".send(@method) { |c| a << c } + a.should == ['a', 'b', "\u{1f3f3}\u{fe0f}\u{200d}\u{1f308}", "\u{1F43E}"] + end + + it "returns self" do + s = StringSpecs::MyString.new "ab\u{1f3f3}\u{fe0f}\u{200d}\u{1f308}\u{1F43E}" + s.send(@method) {}.should equal(s) + end +end diff --git a/spec/ruby/core/string/shared/length.rb b/spec/ruby/core/string/shared/length.rb new file mode 100644 index 0000000000..ae572ba755 --- /dev/null +++ b/spec/ruby/core/string/shared/length.rb @@ -0,0 +1,55 @@ +# encoding: utf-8 + +describe :string_length, shared: true do + it "returns the length of self" do + "".send(@method).should == 0 + "\x00".send(@method).should == 1 + "one".send(@method).should == 3 + "two".send(@method).should == 3 + "three".send(@method).should == 5 + "four".send(@method).should == 4 + end + + it "returns the length of a string in different encodings" do + utf8_str = 'ã“ã«ã¡ã‚' * 100 + utf8_str.send(@method).should == 400 + utf8_str.encode(Encoding::UTF_32BE).send(@method).should == 400 + utf8_str.encode(Encoding::SHIFT_JIS).send(@method).should == 400 + end + + it "returns the length of the new self after encoding is changed" do + str = +'ã“ã«ã¡ã‚' + str.send(@method) + + str.force_encoding('BINARY').send(@method).should == 12 + end + + it "returns the correct length after force_encoding(BINARY)" do + utf8 = "ã‚" + ascii = "a" + concat = utf8 + ascii + + concat.encoding.should == Encoding::UTF_8 + concat.bytesize.should == 4 + + concat.send(@method).should == 2 + concat.force_encoding(Encoding::ASCII_8BIT) + concat.send(@method).should == 4 + end + + it "adds 1 for every invalid byte in UTF-8" do + "\xF4\x90\x80\x80".send(@method).should == 4 + "a\xF4\x90\x80\x80b".send(@method).should == 6 + "é\xF4\x90\x80\x80è".send(@method).should == 6 + end + + it "adds 1 (and not 2) for a incomplete surrogate in UTF-16" do + "\x00\xd8".dup.force_encoding("UTF-16LE").send(@method).should == 1 + "\xd8\x00".dup.force_encoding("UTF-16BE").send(@method).should == 1 + end + + it "adds 1 for a broken sequence in UTF-32" do + "\x04\x03\x02\x01".dup.force_encoding("UTF-32LE").send(@method).should == 1 + "\x01\x02\x03\x04".dup.force_encoding("UTF-32BE").send(@method).should == 1 + end +end diff --git a/spec/ruby/core/string/shared/partition.rb b/spec/ruby/core/string/shared/partition.rb new file mode 100644 index 0000000000..4cac149ce5 --- /dev/null +++ b/spec/ruby/core/string/shared/partition.rb @@ -0,0 +1,33 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' + +describe :string_partition, shared: true do + it "returns String instances when called on a subclass" do + StringSpecs::MyString.new("hello").send(@method, "l").each do |item| + item.should be_an_instance_of(String) + end + + StringSpecs::MyString.new("hello").send(@method, "x").each do |item| + item.should be_an_instance_of(String) + end + + StringSpecs::MyString.new("hello").send(@method, /l./).each do |item| + item.should be_an_instance_of(String) + end + end + + it "returns before- and after- parts in the same encoding as self" do + strings = "hello".encode("US-ASCII").send(@method, "ello") + strings[0].encoding.should == Encoding::US_ASCII + strings[2].encoding.should == Encoding::US_ASCII + + strings = "hello".encode("US-ASCII").send(@method, /ello/) + strings[0].encoding.should == Encoding::US_ASCII + strings[2].encoding.should == Encoding::US_ASCII + end + + it "returns the matching part in the separator's encoding" do + strings = "hello".encode("US-ASCII").send(@method, "ello") + strings[1].encoding.should == Encoding::UTF_8 + end +end diff --git a/spec/ruby/core/string/shared/replace.rb b/spec/ruby/core/string/shared/replace.rb new file mode 100644 index 0000000000..24dac0eb27 --- /dev/null +++ b/spec/ruby/core/string/shared/replace.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: false +describe :string_replace, shared: true do + it "returns self" do + a = "a" + a.send(@method, "b").should equal(a) + end + + it "replaces the content of self with other" do + a = "some string" + a.send(@method, "another string") + a.should == "another string" + end + + it "replaces the encoding of self with that of other" do + a = "".encode("UTF-16LE") + b = "".encode("UTF-8") + a.send(@method, b) + a.encoding.should == Encoding::UTF_8 + end + + it "carries over the encoding invalidity" do + a = "\u{8765}".force_encoding('ascii') + "".send(@method, a).valid_encoding?.should be_false + end + + it "tries to convert other to string using to_str" do + other = mock('x') + other.should_receive(:to_str).and_return("converted to a string") + "hello".send(@method, other).should == "converted to a string" + end + + it "raises a TypeError if other can't be converted to string" do + -> { "hello".send(@method, 123) }.should raise_error(TypeError) + -> { "hello".send(@method, []) }.should raise_error(TypeError) + -> { "hello".send(@method, mock('x')) }.should raise_error(TypeError) + end + + it "raises a FrozenError on a frozen instance that is modified" do + a = "hello".freeze + -> { a.send(@method, "world") }.should raise_error(FrozenError) + end + + # see [ruby-core:23666] + it "raises a FrozenError on a frozen instance when self-replacing" do + a = "hello".freeze + -> { a.send(@method, a) }.should raise_error(FrozenError) + end +end diff --git a/spec/ruby/core/string/shared/slice.rb b/spec/ruby/core/string/shared/slice.rb new file mode 100644 index 0000000000..7b9b9f6a14 --- /dev/null +++ b/spec/ruby/core/string/shared/slice.rb @@ -0,0 +1,517 @@ +describe :string_slice, shared: true do + it "returns the character code of the character at the given index" do + "hello".send(@method, 0).should == ?h + "hello".send(@method, -1).should == ?o + end + + it "returns nil if index is outside of self" do + "hello".send(@method, 20).should == nil + "hello".send(@method, -20).should == nil + + "".send(@method, 0).should == nil + "".send(@method, -1).should == nil + end + + it "calls to_int on the given index" do + "hello".send(@method, 0.5).should == ?h + + obj = mock('1') + obj.should_receive(:to_int).and_return(1) + "hello".send(@method, obj).should == ?e + end + + it "raises a TypeError if the given index is nil" do + -> { "hello".send(@method, nil) }.should raise_error(TypeError) + end + + it "raises a TypeError if the given index can't be converted to an Integer" do + -> { "hello".send(@method, mock('x')) }.should raise_error(TypeError) + -> { "hello".send(@method, {}) }.should raise_error(TypeError) + -> { "hello".send(@method, []) }.should raise_error(TypeError) + end + + it "raises a RangeError if the index is too big" do + -> { "hello".send(@method, bignum_value) }.should raise_error(RangeError) + end +end + +describe :string_slice_index_length, shared: true do + it "returns the substring starting at the given index with the given length" do + "hello there".send(@method, 0,0).should == "" + "hello there".send(@method, 0,1).should == "h" + "hello there".send(@method, 0,3).should == "hel" + "hello there".send(@method, 0,6).should == "hello " + "hello there".send(@method, 0,9).should == "hello the" + "hello there".send(@method, 0,12).should == "hello there" + + "hello there".send(@method, 1,0).should == "" + "hello there".send(@method, 1,1).should == "e" + "hello there".send(@method, 1,3).should == "ell" + "hello there".send(@method, 1,6).should == "ello t" + "hello there".send(@method, 1,9).should == "ello ther" + "hello there".send(@method, 1,12).should == "ello there" + + "hello there".send(@method, 3,0).should == "" + "hello there".send(@method, 3,1).should == "l" + "hello there".send(@method, 3,3).should == "lo " + "hello there".send(@method, 3,6).should == "lo the" + "hello there".send(@method, 3,9).should == "lo there" + + "hello there".send(@method, 4,0).should == "" + "hello there".send(@method, 4,3).should == "o t" + "hello there".send(@method, 4,6).should == "o ther" + "hello there".send(@method, 4,9).should == "o there" + + "foo".send(@method, 2,1).should == "o" + "foo".send(@method, 3,0).should == "" + "foo".send(@method, 3,1).should == "" + + "".send(@method, 0,0).should == "" + "".send(@method, 0,1).should == "" + + "x".send(@method, 0,0).should == "" + "x".send(@method, 0,1).should == "x" + "x".send(@method, 1,0).should == "" + "x".send(@method, 1,1).should == "" + + "x".send(@method, -1,0).should == "" + "x".send(@method, -1,1).should == "x" + + "hello there".send(@method, -3,2).should == "er" + end + + it "returns a string with the same encoding as self" do + s = "hello there" + s.send(@method, 1, 9).encoding.should == s.encoding + + a = "hello".dup.force_encoding("binary") + b = " there".dup.force_encoding("ISO-8859-1") + c = (a + b).force_encoding(Encoding::US_ASCII) + + c.send(@method, 0, 5).encoding.should == Encoding::US_ASCII + c.send(@method, 5, 6).encoding.should == Encoding::US_ASCII + c.send(@method, 1, 3).encoding.should == Encoding::US_ASCII + c.send(@method, 8, 2).encoding.should == Encoding::US_ASCII + c.send(@method, 1, 10).encoding.should == Encoding::US_ASCII + end + + it "returns nil if the offset falls outside of self" do + "hello there".send(@method, 20,3).should == nil + "hello there".send(@method, -20,3).should == nil + + "".send(@method, 1,0).should == nil + "".send(@method, 1,1).should == nil + + "".send(@method, -1,0).should == nil + "".send(@method, -1,1).should == nil + + "x".send(@method, 2,0).should == nil + "x".send(@method, 2,1).should == nil + + "x".send(@method, -2,0).should == nil + "x".send(@method, -2,1).should == nil + + "x".send(@method, fixnum_max, 1).should == nil + end + + it "returns nil if the length is negative" do + "hello there".send(@method, 4,-3).should == nil + "hello there".send(@method, -4,-3).should == nil + end + + platform_is pointer_size: 64 do + it "returns nil if the length is negative big value" do + "hello there".send(@method, 4, -(1 << 31)).should == nil + + # by some reason length < -(1 << 31) on CI on Windows leads to + # 'RangeError: bignum too big to convert into `long'' error + platform_is_not :windows do + "hello there".send(@method, 4, -(1 << 63)).should == nil + end + end + end + + it "calls to_int on the given index and the given length" do + "hello".send(@method, 0.5, 1).should == "h" + "hello".send(@method, 0.5, 2.5).should == "he" + "hello".send(@method, 1, 2.5).should == "el" + + obj = mock('2') + obj.should_receive(:to_int).exactly(4).times.and_return(2) + + "hello".send(@method, obj, 1).should == "l" + "hello".send(@method, obj, obj).should == "ll" + "hello".send(@method, 0, obj).should == "he" + end + + it "raises a TypeError when idx or length can't be converted to an integer" do + -> { "hello".send(@method, mock('x'), 0) }.should raise_error(TypeError) + -> { "hello".send(@method, 0, mock('x')) }.should raise_error(TypeError) + + # I'm deliberately including this here. + # It means that str.send(@method, other, idx) isn't supported. + -> { "hello".send(@method, "", 0) }.should raise_error(TypeError) + end + + it "raises a TypeError when the given index or the given length is nil" do + -> { "hello".send(@method, 1, nil) }.should raise_error(TypeError) + -> { "hello".send(@method, nil, 1) }.should raise_error(TypeError) + -> { "hello".send(@method, nil, nil) }.should raise_error(TypeError) + end + + it "raises a RangeError if the index or length is too big" do + -> { "hello".send(@method, bignum_value, 1) }.should raise_error(RangeError) + -> { "hello".send(@method, 0, bignum_value) }.should raise_error(RangeError) + end + + it "raises a RangeError if the index or length is too small" do + -> { "hello".send(@method, -bignum_value, 1) }.should raise_error(RangeError) + -> { "hello".send(@method, 0, -bignum_value) }.should raise_error(RangeError) + end + + it "returns String instances" do + s = StringSpecs::MyString.new("hello") + s.send(@method, 0,0).should be_an_instance_of(String) + s.send(@method, 0,4).should be_an_instance_of(String) + s.send(@method, 1,4).should be_an_instance_of(String) + end + + it "handles repeated application" do + "hello world".send(@method, 6, 5).send(@method, 0, 1).should == 'w' + "hello world".send(@method, 6, 5).send(@method, 0, 5).should == 'world' + + "hello world".send(@method, 6, 5).send(@method, 1, 1).should == 'o' + "hello world".send(@method, 6, 5).send(@method, 1, 4).should == 'orld' + + "hello world".send(@method, 6, 5).send(@method, 4, 1).should == 'd' + "hello world".send(@method, 6, 5).send(@method, 5, 0).should == '' + + "hello world".send(@method, 6, 0).send(@method, -1, 0).should == nil + "hello world".send(@method, 6, 0).send(@method, 1, 1).should == nil + end +end + +describe :string_slice_range, shared: true do + it "returns the substring given by the offsets of the range" do + "hello there".send(@method, 1..1).should == "e" + "hello there".send(@method, 1..3).should == "ell" + "hello there".send(@method, 1...3).should == "el" + "hello there".send(@method, -4..-2).should == "her" + "hello there".send(@method, -4...-2).should == "he" + "hello there".send(@method, 5..-1).should == " there" + "hello there".send(@method, 5...-1).should == " ther" + + "".send(@method, 0..0).should == "" + + "x".send(@method, 0..0).should == "x" + "x".send(@method, 0..1).should == "x" + "x".send(@method, 0...1).should == "x" + "x".send(@method, 0..-1).should == "x" + + "x".send(@method, 1..1).should == "" + "x".send(@method, 1..-1).should == "" + end + + it "returns a String in the same encoding as self" do + "hello there".encode("US-ASCII").send(@method, 1..1).encoding.should == Encoding::US_ASCII + end + + it "returns nil if the beginning of the range falls outside of self" do + "hello there".send(@method, 12..-1).should == nil + "hello there".send(@method, 20..25).should == nil + "hello there".send(@method, 20..1).should == nil + "hello there".send(@method, -20..1).should == nil + "hello there".send(@method, -20..-1).should == nil + + "".send(@method, -1..-1).should == nil + "".send(@method, -1...-1).should == nil + "".send(@method, -1..0).should == nil + "".send(@method, -1...0).should == nil + end + + it "returns an empty string if range.begin is inside self and > real end" do + "hello there".send(@method, 1...1).should == "" + "hello there".send(@method, 4..2).should == "" + "hello".send(@method, 4..-4).should == "" + "hello there".send(@method, -5..-6).should == "" + "hello there".send(@method, -2..-4).should == "" + "hello there".send(@method, -5..-6).should == "" + "hello there".send(@method, -5..2).should == "" + + "".send(@method, 0...0).should == "" + "".send(@method, 0..-1).should == "" + "".send(@method, 0...-1).should == "" + + "x".send(@method, 0...0).should == "" + "x".send(@method, 0...-1).should == "" + "x".send(@method, 1...1).should == "" + "x".send(@method, 1...-1).should == "" + end + + it "returns String instances" do + s = StringSpecs::MyString.new("hello") + s.send(@method, 0...0).should be_an_instance_of(String) + s.send(@method, 0..4).should be_an_instance_of(String) + s.send(@method, 1..4).should be_an_instance_of(String) + end + + it "calls to_int on range arguments" do + from = mock('from') + to = mock('to') + + # So we can construct a range out of them... + from.should_receive(:<=>).twice.and_return(0) + + from.should_receive(:to_int).twice.and_return(1) + to.should_receive(:to_int).twice.and_return(-2) + + "hello there".send(@method, from..to).should == "ello ther" + "hello there".send(@method, from...to).should == "ello the" + end + + it "works with Range subclasses" do + a = "GOOD" + range_incl = StringSpecs::MyRange.new(1, 2) + range_excl = StringSpecs::MyRange.new(-3, -1, true) + + a.send(@method, range_incl).should == "OO" + a.send(@method, range_excl).should == "OO" + end + + it "handles repeated application" do + "hello world".send(@method, 6..11).send(@method, 0..0).should == 'w' + "hello world".send(@method, 6..11).send(@method, 0..4).should == 'world' + + "hello world".send(@method, 6..11).send(@method, 1..1).should == 'o' + "hello world".send(@method, 6..11).send(@method, 1..4).should == 'orld' + + "hello world".send(@method, 6..11).send(@method, 4..4).should == 'd' + "hello world".send(@method, 6..11).send(@method, 5..4).should == '' + + "hello world".send(@method, 6..5).send(@method, -1..-1).should == nil + "hello world".send(@method, 6..5).send(@method, 1..1).should == nil + end + + it "raises a type error if a range is passed with a length" do + ->{ "hello".send(@method, 1..2, 1) }.should raise_error(TypeError) + end + + it "raises a RangeError if one of the bound is too big" do + -> { "hello".send(@method, bignum_value..(bignum_value + 1)) }.should raise_error(RangeError) + -> { "hello".send(@method, 0..bignum_value) }.should raise_error(RangeError) + end + + it "works with endless ranges" do + "hello there".send(@method, eval("(2..)")).should == "llo there" + "hello there".send(@method, eval("(2...)")).should == "llo there" + "hello there".send(@method, eval("(-4..)")).should == "here" + "hello there".send(@method, eval("(-4...)")).should == "here" + end + + it "works with beginless ranges" do + "hello there".send(@method, (..5)).should == "hello " + "hello there".send(@method, (...5)).should == "hello" + "hello there".send(@method, (..-4)).should == "hello th" + "hello there".send(@method, (...-4)).should == "hello t" + "hello there".send(@method, (...nil)).should == "hello there" + end +end + +describe :string_slice_regexp, shared: true do + it "returns the matching portion of self" do + "hello there".send(@method, /[aeiou](.)\1/).should == "ell" + "".send(@method, //).should == "" + end + + it "returns nil if there is no match" do + "hello there".send(@method, /xyz/).should == nil + end + + it "returns a String in the same encoding as self" do + "hello there".encode("US-ASCII").send(@method, /[aeiou](.)\1/).encoding.should == Encoding::US_ASCII + end + + it "returns String instances" do + s = StringSpecs::MyString.new("hello") + s.send(@method, //).should be_an_instance_of(String) + s.send(@method, /../).should be_an_instance_of(String) + end + + it "sets $~ to MatchData when there is a match and nil when there's none" do + 'hello'.send(@method, /./) + $~[0].should == 'h' + + 'hello'.send(@method, /not/) + $~.should == nil + end +end + +describe :string_slice_regexp_index, shared: true do + it "returns the capture for the given index" do + "hello there".send(@method, /[aeiou](.)\1/, 0).should == "ell" + "hello there".send(@method, /[aeiou](.)\1/, 1).should == "l" + "hello there".send(@method, /[aeiou](.)\1/, -1).should == "l" + + "har".send(@method, /(.)(.)(.)/, 0).should == "har" + "har".send(@method, /(.)(.)(.)/, 1).should == "h" + "har".send(@method, /(.)(.)(.)/, 2).should == "a" + "har".send(@method, /(.)(.)(.)/, 3).should == "r" + "har".send(@method, /(.)(.)(.)/, -1).should == "r" + "har".send(@method, /(.)(.)(.)/, -2).should == "a" + "har".send(@method, /(.)(.)(.)/, -3).should == "h" + end + + it "returns nil if there is no match" do + "hello there".send(@method, /(what?)/, 1).should == nil + end + + it "returns nil if the index is larger than the number of captures" do + "hello there".send(@method, /hello (.)/, 2).should == nil + # You can't refer to 0 using negative indices + "hello there".send(@method, /hello (.)/, -2).should == nil + end + + it "returns nil if there is no capture for the given index" do + "hello there".send(@method, /[aeiou](.)\1/, 2).should == nil + end + + it "returns nil if the given capture group was not matched but still sets $~" do + "test".send(@method, /te(z)?/, 1).should == nil + $~[0].should == "te" + $~[1].should == nil + end + + it "returns a String in the same encoding as self" do + "hello there".encode("US-ASCII").send(@method, /[aeiou](.)\1/, 0).encoding.should == Encoding::US_ASCII + end + + it "calls to_int on the given index" do + obj = mock('2') + obj.should_receive(:to_int).and_return(2) + + "har".send(@method, /(.)(.)(.)/, 1.5).should == "h" + "har".send(@method, /(.)(.)(.)/, obj).should == "a" + end + + it "raises a TypeError when the given index can't be converted to Integer" do + -> { "hello".send(@method, /(.)(.)(.)/, mock('x')) }.should raise_error(TypeError) + -> { "hello".send(@method, /(.)(.)(.)/, {}) }.should raise_error(TypeError) + -> { "hello".send(@method, /(.)(.)(.)/, []) }.should raise_error(TypeError) + end + + it "raises a TypeError when the given index is nil" do + -> { "hello".send(@method, /(.)(.)(.)/, nil) }.should raise_error(TypeError) + end + + it "returns String instances" do + s = StringSpecs::MyString.new("hello") + s.send(@method, /(.)(.)/, 0).should be_an_instance_of(String) + s.send(@method, /(.)(.)/, 1).should be_an_instance_of(String) + end + + it "sets $~ to MatchData when there is a match and nil when there's none" do + 'hello'.send(@method, /.(.)/, 0) + $~[0].should == 'he' + + 'hello'.send(@method, /.(.)/, 1) + $~[1].should == 'e' + + 'hello'.send(@method, /not/, 0) + $~.should == nil + end +end + +describe :string_slice_string, shared: true do + it "returns other_str if it occurs in self" do + s = "lo" + "hello there".send(@method, s).should == s + end + + it "doesn't set $~" do + $~ = nil + + 'hello'.send(@method, 'll') + $~.should == nil + end + + it "returns nil if there is no match" do + "hello there".send(@method, "bye").should == nil + end + + it "doesn't call to_str on its argument" do + o = mock('x') + o.should_not_receive(:to_str) + + -> { "hello".send(@method, o) }.should raise_error(TypeError) + end + + it "returns a String instance when given a subclass instance" do + s = StringSpecs::MyString.new("el") + r = "hello".send(@method, s) + r.should == "el" + r.should be_an_instance_of(String) + end +end + +describe :string_slice_regexp_group, shared: true do + not_supported_on :opal do + it "returns the capture for the given name" do + "hello there".send(@method, /(?<g>[aeiou](.))/, 'g').should == "el" + "hello there".send(@method, /[aeiou](?<g>.)/, 'g').should == "l" + + "har".send(@method, /(?<g>(.)(.)(.))/, 'g').should == "har" + "har".send(@method, /(?<h>.)(.)(.)/, 'h').should == "h" + "har".send(@method, /(.)(?<a>.)(.)/, 'a').should == "a" + "har".send(@method, /(.)(.)(?<r>.)/, 'r').should == "r" + "har".send(@method, /(?<h>.)(?<a>.)(?<r>.)/, 'r').should == "r" + end + + it "returns the last capture for duplicate names" do + "hello there".send(@method, /(?<g>h)(?<g>.)/, 'g').should == "e" + "hello there".send(@method, /(?<g>h)(?<g>.)(?<f>.)/, 'g').should == "e" + end + + it "returns the innermost capture for nested duplicate names" do + "hello there".send(@method, /(?<g>h(?<g>.))/, 'g').should == "e" + end + + it "returns nil if there is no match" do + "hello there".send(@method, /(?<whut>what?)/, 'whut').should be_nil + end + + it "raises an IndexError if there is no capture for the given name" do + -> do + "hello there".send(@method, /[aeiou](.)\1/, 'non') + end.should raise_error(IndexError) + end + + it "raises a TypeError when the given name is not a String" do + -> { "hello".send(@method, /(?<q>.)/, mock('x')) }.should raise_error(TypeError) + -> { "hello".send(@method, /(?<q>.)/, {}) }.should raise_error(TypeError) + -> { "hello".send(@method, /(?<q>.)/, []) }.should raise_error(TypeError) + end + + it "raises an IndexError when given the empty String as a group name" do + -> { "hello".send(@method, /(?<q>)/, '') }.should raise_error(IndexError) + end + + it "returns String instances" do + s = StringSpecs::MyString.new("hello") + s.send(@method, /(?<q>.)/, 'q').should be_an_instance_of(String) + end + + it "sets $~ to MatchData when there is a match and nil when there's none" do + 'hello'.send(@method, /(?<hi>.(.))/, 'hi') + $~[0].should == 'he' + + 'hello'.send(@method, /(?<non>not)/, 'non') + $~.should be_nil + end + end +end + +describe :string_slice_symbol, shared: true do + it "raises TypeError" do + -> { 'hello'.send(@method, :hello) }.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/string/shared/strip.rb b/spec/ruby/core/string/shared/strip.rb new file mode 100644 index 0000000000..3af77b50fe --- /dev/null +++ b/spec/ruby/core/string/shared/strip.rb @@ -0,0 +1,14 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' + +describe :string_strip, shared: true do + it "returns a String in the same encoding as self" do + " hello ".encode("US-ASCII").send(@method).encoding.should == Encoding::US_ASCII + end + + it "returns String instances when called on a subclass" do + StringSpecs::MyString.new(" hello ").send(@method).should be_an_instance_of(String) + StringSpecs::MyString.new(" ").send(@method).should be_an_instance_of(String) + StringSpecs::MyString.new("").send(@method).should be_an_instance_of(String) + end +end diff --git a/spec/ruby/core/string/shared/succ.rb b/spec/ruby/core/string/shared/succ.rb new file mode 100644 index 0000000000..7c68345f10 --- /dev/null +++ b/spec/ruby/core/string/shared/succ.rb @@ -0,0 +1,87 @@ +# encoding: binary +describe :string_succ, shared: true do + it "returns an empty string for empty strings" do + "".send(@method).should == "" + end + + it "returns the successor by increasing the rightmost alphanumeric (digit => digit, letter => letter with same case)" do + "abcd".send(@method).should == "abce" + "THX1138".send(@method).should == "THX1139" + + "<<koala>>".send(@method).should == "<<koalb>>" + "==A??".send(@method).should == "==B??" + end + + it "increases non-alphanumerics (via ascii rules) if there are no alphanumerics" do + "***".send(@method).should == "**+" + "**`".send(@method).should == "**a" + end + + it "increases the next best alphanumeric (jumping over non-alphanumerics) if there is a carry" do + "dz".send(@method).should == "ea" + "HZ".send(@method).should == "IA" + "49".send(@method).should == "50" + + "izz".send(@method).should == "jaa" + "IZZ".send(@method).should == "JAA" + "699".send(@method).should == "700" + + "6Z99z99Z".send(@method).should == "7A00a00A" + + "1999zzz".send(@method).should == "2000aaa" + "NZ/[]ZZZ9999".send(@method).should == "OA/[]AAA0000" + end + + it "increases the next best character if there is a carry for non-alphanumerics" do + "(\xFF".send(@method).should == ")\x00" + "`\xFF".send(@method).should == "a\x00" + "<\xFF\xFF".send(@method).should == "=\x00\x00" + end + + it "adds an additional character (just left to the last increased one) if there is a carry and no character left to increase" do + "z".send(@method).should == "aa" + "Z".send(@method).should == "AA" + "9".send(@method).should == "10" + + "zz".send(@method).should == "aaa" + "ZZ".send(@method).should == "AAA" + "99".send(@method).should == "100" + + "9Z99z99Z".send(@method).should == "10A00a00A" + + "ZZZ9999".send(@method).should == "AAAA0000" + "/[]9999".send(@method).should == "/[]10000" + "/[]ZZZ9999".send(@method).should == "/[]AAAA0000" + "Z/[]ZZZ9999".send(@method).should == "AA/[]AAA0000" + + # non-alphanumeric cases + "\xFF".send(@method).should == "\x01\x00" + "\xFF\xFF".send(@method).should == "\x01\x00\x00" + end + + it "returns String instances when called on a subclass" do + StringSpecs::MyString.new("").send(@method).should be_an_instance_of(String) + StringSpecs::MyString.new("a").send(@method).should be_an_instance_of(String) + StringSpecs::MyString.new("z").send(@method).should be_an_instance_of(String) + end + + it "returns a String in the same encoding as self" do + "z".encode("US-ASCII").send(@method).encoding.should == Encoding::US_ASCII + end +end + +describe :string_succ_bang, shared: true do + it "is equivalent to succ, but modifies self in place (still returns self)" do + ["", "abcd", "THX1138"].each do |s| + s = +s + r = s.dup.send(@method) + s.send(@method).should equal(s) + s.should == r + end + end + + it "raises a FrozenError if self is frozen" do + -> { "".freeze.send(@method) }.should raise_error(FrozenError) + -> { "abcd".freeze.send(@method) }.should raise_error(FrozenError) + end +end diff --git a/spec/ruby/core/string/shared/to_s.rb b/spec/ruby/core/string/shared/to_s.rb new file mode 100644 index 0000000000..4b87a6cbe1 --- /dev/null +++ b/spec/ruby/core/string/shared/to_s.rb @@ -0,0 +1,13 @@ +describe :string_to_s, shared: true do + it "returns self when self.class == String" do + a = "a string" + a.should equal(a.send(@method)) + end + + it "returns a new instance of String when called on a subclass" do + a = StringSpecs::MyString.new("a string") + s = a.send(@method) + s.should == "a string" + s.should be_an_instance_of(String) + end +end diff --git a/spec/ruby/core/string/shared/to_sym.rb b/spec/ruby/core/string/shared/to_sym.rb new file mode 100644 index 0000000000..833eae100e --- /dev/null +++ b/spec/ruby/core/string/shared/to_sym.rb @@ -0,0 +1,72 @@ +describe :string_to_sym, shared: true do + it "returns the symbol corresponding to self" do + "Koala".send(@method).should equal :Koala + 'cat'.send(@method).should equal :cat + '@cat'.send(@method).should equal :@cat + 'cat and dog'.send(@method).should equal :"cat and dog" + "abc=".send(@method).should equal :abc= + end + + it "does not special case +(binary) and -(binary)" do + "+(binary)".send(@method).should equal :"+(binary)" + "-(binary)".send(@method).should equal :"-(binary)" + end + + it "does not special case certain operators" do + "!@".send(@method).should equal :"!@" + "~@".send(@method).should equal :"~@" + "!(unary)".send(@method).should equal :"!(unary)" + "~(unary)".send(@method).should equal :"~(unary)" + "+(unary)".send(@method).should equal :"+(unary)" + "-(unary)".send(@method).should equal :"-(unary)" + end + + it "returns a US-ASCII Symbol for a UTF-8 String containing only US-ASCII characters" do + sym = "foobar".send(@method) + sym.encoding.should == Encoding::US_ASCII + sym.should equal :"foobar" + end + + it "returns a US-ASCII Symbol for a binary String containing only US-ASCII characters" do + sym = "foobar".b.send(@method) + sym.encoding.should == Encoding::US_ASCII + sym.should equal :"foobar" + end + + it "returns a UTF-8 Symbol for a UTF-8 String containing non US-ASCII characters" do + sym = "il était une fois".send(@method) + sym.encoding.should == Encoding::UTF_8 + sym.should equal :"il était une #{'fois'}" + end + + it "returns a UTF-16LE Symbol for a UTF-16LE String containing non US-ASCII characters" do + utf16_str = "UtéF16".encode(Encoding::UTF_16LE) + sym = utf16_str.send(@method) + sym.encoding.should == Encoding::UTF_16LE + sym.to_s.should == utf16_str + end + + it "returns a binary Symbol for a binary String containing non US-ASCII characters" do + binary_string = "binarÃ".b + sym = binary_string.send(@method) + sym.encoding.should == Encoding::BINARY + sym.to_s.should == binary_string + end + + it "ignores existing symbols with different encoding" do + source = "fée" + + iso_symbol = source.dup.force_encoding(Encoding::ISO_8859_1).send(@method) + iso_symbol.encoding.should == Encoding::ISO_8859_1 + binary_symbol = source.dup.force_encoding(Encoding::BINARY).send(@method) + binary_symbol.encoding.should == Encoding::BINARY + end + + it "raises an EncodingError for UTF-8 String containing invalid bytes" do + invalid_utf8 = "\xC3" + invalid_utf8.should_not.valid_encoding? + -> { + invalid_utf8.send(@method) + }.should raise_error(EncodingError, 'invalid symbol in encoding UTF-8 :"\xC3"') + end +end diff --git a/spec/ruby/core/string/size_spec.rb b/spec/ruby/core/string/size_spec.rb new file mode 100644 index 0000000000..9e1f40c5ae --- /dev/null +++ b/spec/ruby/core/string/size_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/length' + +describe "String#size" do + it_behaves_like :string_length, :size +end diff --git a/spec/ruby/core/string/slice_spec.rb b/spec/ruby/core/string/slice_spec.rb new file mode 100644 index 0000000000..5aba2d3be0 --- /dev/null +++ b/spec/ruby/core/string/slice_spec.rb @@ -0,0 +1,390 @@ +# -*- encoding: utf-8 -*- +# frozen_string_literal: false +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/slice' + +describe "String#slice" do + it_behaves_like :string_slice, :slice +end + +describe "String#slice with index, length" do + it_behaves_like :string_slice_index_length, :slice +end + +describe "String#slice with Range" do + it_behaves_like :string_slice_range, :slice +end + +describe "String#slice with Regexp" do + it_behaves_like :string_slice_regexp, :slice +end + +describe "String#slice with Regexp, index" do + it_behaves_like :string_slice_regexp_index, :slice +end + +describe "String#slice with Regexp, group" do + it_behaves_like :string_slice_regexp_group, :slice +end + +describe "String#slice with String" do + it_behaves_like :string_slice_string, :slice +end + +describe "String#slice with Symbol" do + it_behaves_like :string_slice_symbol, :slice +end + +describe "String#slice! with index" do + it "deletes and return the char at the given position" do + a = "hello" + a.slice!(1).should == ?e + a.should == "hllo" + a.slice!(-1).should == ?o + a.should == "hll" + end + + it "returns nil if idx is outside of self" do + a = "hello" + a.slice!(20).should == nil + a.should == "hello" + a.slice!(-20).should == nil + a.should == "hello" + end + + it "raises a FrozenError if self is frozen" do + -> { "hello".freeze.slice!(1) }.should raise_error(FrozenError) + -> { "hello".freeze.slice!(10) }.should raise_error(FrozenError) + -> { "".freeze.slice!(0) }.should raise_error(FrozenError) + end + + it "calls to_int on index" do + "hello".slice!(0.5).should == ?h + + obj = mock('1') + obj.should_receive(:to_int).at_least(1).and_return(1) + "hello".slice!(obj).should == ?e + + obj = mock('1') + obj.should_receive(:respond_to?).at_least(1).with(:to_int, true).and_return(true) + obj.should_receive(:method_missing).at_least(1).with(:to_int).and_return(1) + "hello".slice!(obj).should == ?e + end + + + it "returns the character given by the character index" do + "hellö there".slice!(1).should == "e" + "hellö there".slice!(4).should == "ö" + "hellö there".slice!(6).should == "t" + end + +end + +describe "String#slice! with index, length" do + it "deletes and returns the substring at idx and the given length" do + a = "hello" + a.slice!(1, 2).should == "el" + a.should == "hlo" + + a.slice!(1, 0).should == "" + a.should == "hlo" + + a.slice!(-2, 4).should == "lo" + a.should == "h" + end + + it "returns nil if the given position is out of self" do + a = "hello" + a.slice(10, 3).should == nil + a.should == "hello" + + a.slice(-10, 20).should == nil + a.should == "hello" + end + + it "returns nil if the length is negative" do + a = "hello" + a.slice(4, -3).should == nil + a.should == "hello" + end + + it "raises a FrozenError if self is frozen" do + -> { "hello".freeze.slice!(1, 2) }.should raise_error(FrozenError) + -> { "hello".freeze.slice!(10, 3) }.should raise_error(FrozenError) + -> { "hello".freeze.slice!(-10, 3)}.should raise_error(FrozenError) + -> { "hello".freeze.slice!(4, -3) }.should raise_error(FrozenError) + -> { "hello".freeze.slice!(10, 3) }.should raise_error(FrozenError) + -> { "hello".freeze.slice!(-10, 3)}.should raise_error(FrozenError) + -> { "hello".freeze.slice!(4, -3) }.should raise_error(FrozenError) + end + + it "calls to_int on idx and length" do + "hello".slice!(0.5, 2.5).should == "he" + + obj = mock('2') + def obj.to_int() 2 end + "hello".slice!(obj, obj).should == "ll" + + obj = mock('2') + def obj.respond_to?(name, *) name == :to_int; end + def obj.method_missing(name, *) name == :to_int ? 2 : super; end + "hello".slice!(obj, obj).should == "ll" + end + + it "returns String instances" do + s = StringSpecs::MyString.new("hello") + s.slice!(0, 0).should be_an_instance_of(String) + s.slice!(0, 4).should be_an_instance_of(String) + end + + it "returns the substring given by the character offsets" do + "hellö there".slice!(1,0).should == "" + "hellö there".slice!(1,3).should == "ell" + "hellö there".slice!(1,6).should == "ellö t" + "hellö there".slice!(1,9).should == "ellö ther" + end + + it "treats invalid bytes as single bytes" do + xE6xCB = [0xE6,0xCB].pack('CC').force_encoding('utf-8') + "a#{xE6xCB}b".slice!(1, 2).should == xE6xCB + end +end + +describe "String#slice! Range" do + it "deletes and return the substring given by the offsets of the range" do + a = "hello" + a.slice!(1..3).should == "ell" + a.should == "ho" + a.slice!(0..0).should == "h" + a.should == "o" + a.slice!(0...0).should == "" + a.should == "o" + + # Edge Case? + "hello".slice!(-3..-9).should == "" + end + + it "returns nil if the given range is out of self" do + a = "hello" + a.slice!(-6..-9).should == nil + a.should == "hello" + + b = "hello" + b.slice!(10..20).should == nil + b.should == "hello" + end + + it "returns String instances" do + s = StringSpecs::MyString.new("hello") + s.slice!(0...0).should be_an_instance_of(String) + s.slice!(0..4).should be_an_instance_of(String) + end + + it "calls to_int on range arguments" do + from = mock('from') + to = mock('to') + + # So we can construct a range out of them... + def from.<=>(o) 0 end + def to.<=>(o) 0 end + + def from.to_int() 1 end + def to.to_int() -2 end + + "hello there".slice!(from..to).should == "ello ther" + + from = mock('from') + to = mock('to') + + def from.<=>(o) 0 end + def to.<=>(o) 0 end + + def from.respond_to?(name, *) name == :to_int; end + def from.method_missing(name) name == :to_int ? 1 : super; end + def to.respond_to?(name, *) name == :to_int; end + def to.method_missing(name) name == :to_int ? -2 : super; end + + "hello there".slice!(from..to).should == "ello ther" + end + + it "works with Range subclasses" do + a = "GOOD" + range_incl = StringSpecs::MyRange.new(1, 2) + + a.slice!(range_incl).should == "OO" + end + + + it "returns the substring given by the character offsets of the range" do + "hellö there".slice!(1..1).should == "e" + "hellö there".slice!(1..3).should == "ell" + "hellö there".slice!(1...3).should == "el" + "hellö there".slice!(-4..-2).should == "her" + "hellö there".slice!(-4...-2).should == "he" + "hellö there".slice!(5..-1).should == " there" + "hellö there".slice!(5...-1).should == " ther" + end + + + it "raises a FrozenError on a frozen instance that is modified" do + -> { "hello".freeze.slice!(1..3) }.should raise_error(FrozenError) + end + + # see redmine #1551 + it "raises a FrozenError on a frozen instance that would not be modified" do + -> { "hello".freeze.slice!(10..20)}.should raise_error(FrozenError) + end +end + +describe "String#slice! with Regexp" do + it "deletes and returns the first match from self" do + s = "this is a string" + s.slice!(/s.*t/).should == 's is a st' + s.should == 'thiring' + + c = "hello hello" + c.slice!(/llo/).should == "llo" + c.should == "he hello" + end + + it "returns nil if there was no match" do + s = "this is a string" + s.slice!(/zzz/).should == nil + s.should == "this is a string" + end + + it "returns String instances" do + s = StringSpecs::MyString.new("hello") + s.slice!(//).should be_an_instance_of(String) + s.slice!(/../).should be_an_instance_of(String) + end + + it "returns the matching portion of self with a multi byte character" do + "hëllo there".slice!(/[ë](.)\1/).should == "ëll" + "".slice!(//).should == "" + end + + it "sets $~ to MatchData when there is a match and nil when there's none" do + 'hello'.slice!(/./) + $~[0].should == 'h' + + 'hello'.slice!(/not/) + $~.should == nil + end + + it "raises a FrozenError on a frozen instance that is modified" do + -> { "this is a string".freeze.slice!(/s.*t/) }.should raise_error(FrozenError) + end + + it "raises a FrozenError on a frozen instance that would not be modified" do + -> { "this is a string".freeze.slice!(/zzz/) }.should raise_error(FrozenError) + end +end + +describe "String#slice! with Regexp, index" do + it "deletes and returns the capture for idx from self" do + str = "hello there" + str.slice!(/[aeiou](.)\1/, 0).should == "ell" + str.should == "ho there" + str.slice!(/(t)h/, 1).should == "t" + str.should == "ho here" + end + + it "returns nil if there was no match" do + s = "this is a string" + s.slice!(/x(zzz)/, 1).should == nil + s.should == "this is a string" + end + + it "returns nil if there is no capture for idx" do + "hello there".slice!(/[aeiou](.)\1/, 2).should == nil + # You can't refer to 0 using negative indices + "hello there".slice!(/[aeiou](.)\1/, -2).should == nil + end + + it "accepts a Float for capture index" do + "har".slice!(/(.)(.)(.)/, 1.5).should == "h" + end + + it "calls #to_int to convert an Object to capture index" do + obj = mock('2') + obj.should_receive(:to_int).at_least(1).times.and_return(2) + + "har".slice!(/(.)(.)(.)/, obj).should == "a" + end + + it "returns String instances" do + s = StringSpecs::MyString.new("hello") + s.slice!(/(.)(.)/, 0).should be_an_instance_of(String) + s.slice!(/(.)(.)/, 1).should be_an_instance_of(String) + end + + it "returns the encoding aware capture for the given index" do + "hÃ¥r".slice!(/(.)(.)(.)/, 0).should == "hÃ¥r" + "hÃ¥r".slice!(/(.)(.)(.)/, 1).should == "h" + "hÃ¥r".slice!(/(.)(.)(.)/, 2).should == "Ã¥" + "hÃ¥r".slice!(/(.)(.)(.)/, 3).should == "r" + "hÃ¥r".slice!(/(.)(.)(.)/, -1).should == "r" + "hÃ¥r".slice!(/(.)(.)(.)/, -2).should == "Ã¥" + "hÃ¥r".slice!(/(.)(.)(.)/, -3).should == "h" + end + + it "sets $~ to MatchData when there is a match and nil when there's none" do + 'hello'[/.(.)/, 0] + $~[0].should == 'he' + + 'hello'[/.(.)/, 1] + $~[1].should == 'e' + + 'hello'[/not/, 0] + $~.should == nil + end + + it "raises a FrozenError if self is frozen" do + -> { "this is a string".freeze.slice!(/s.*t/) }.should raise_error(FrozenError) + -> { "this is a string".freeze.slice!(/zzz/, 0)}.should raise_error(FrozenError) + -> { "this is a string".freeze.slice!(/(.)/, 2)}.should raise_error(FrozenError) + end +end + +describe "String#slice! with String" do + it "removes and returns the first occurrence of other_str from self" do + c = "hello hello" + c.slice!('llo').should == "llo" + c.should == "he hello" + end + + it "doesn't set $~" do + $~ = nil + + 'hello'.slice!('ll') + $~.should == nil + end + + it "returns nil if self does not contain other" do + a = "hello" + a.slice!('zzz').should == nil + a.should == "hello" + end + + it "doesn't call to_str on its argument" do + o = mock('x') + o.should_not_receive(:to_str) + + -> { "hello".slice!(o) }.should raise_error(TypeError) + end + + it "returns a subclass instance when given a subclass instance" do + s = StringSpecs::MyString.new("el") + r = "hello".slice!(s) + r.should == "el" + r.should be_an_instance_of(String) + end + + it "raises a FrozenError if self is frozen" do + -> { "hello hello".freeze.slice!('llo') }.should raise_error(FrozenError) + -> { "this is a string".freeze.slice!('zzz')}.should raise_error(FrozenError) + -> { "this is a string".freeze.slice!('zzz')}.should raise_error(FrozenError) + end +end diff --git a/spec/ruby/core/string/split_spec.rb b/spec/ruby/core/string/split_spec.rb new file mode 100644 index 0000000000..3c6d1864d1 --- /dev/null +++ b/spec/ruby/core/string/split_spec.rb @@ -0,0 +1,546 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "String#split with String" do + it "throws an ArgumentError if the string is not a valid" do + s = "\xDF".dup.force_encoding(Encoding::UTF_8) + + -> { s.split }.should raise_error(ArgumentError) + -> { s.split(':') }.should raise_error(ArgumentError) + end + + it "throws an ArgumentError if the pattern is not a valid string" do + str = 'проверка' + broken_str = "\xDF".dup.force_encoding(Encoding::UTF_8) + + -> { str.split(broken_str) }.should raise_error(ArgumentError) + end + + it "splits on multibyte characters" do + "ã‚りãŒã‚ŠãŒã¨ã†".split("ãŒ").should == ["ã‚り", "り", "ã¨ã†"] + end + + it "returns an array of substrings based on splitting on the given string" do + "mellow yellow".split("ello").should == ["m", "w y", "w"] + end + + it "suppresses trailing empty fields when limit isn't given or 0" do + "1,2,,3,4,,".split(',').should == ["1", "2", "", "3", "4"] + "1,2,,3,4,,".split(',', 0).should == ["1", "2", "", "3", "4"] + " a b c\nd ".split(" ").should == ["", "a", "b", "c\nd"] + " a ã‚ c\nd ".split(" ").should == ["", "a", "ã‚", "c\nd"] + "hai".split("hai").should == [] + ",".split(",").should == [] + ",".split(",", 0).should == [] + "ã‚".split("ã‚").should == [] + "ã‚".split("ã‚", 0).should == [] + end + + it "does not suppress trailing empty fields when a positive limit is given" do + " 1 2 ".split(" ", 2).should == ["1", "2 "] + " 1 2 ".split(" ", 3).should == ["1", "2", ""] + " 1 2 ".split(" ", 4).should == ["1", "2", ""] + " 1 ã‚ ".split(" ", 2).should == ["1", "ã‚ "] + " 1 ã‚ ".split(" ", 3).should == ["1", "ã‚", ""] + " 1 ã‚ ".split(" ", 4).should == ["1", "ã‚", ""] + + "1,2,".split(',', 2).should == ["1", "2,"] + "1,2,".split(',', 3).should == ["1", "2", ""] + "1,2,".split(',', 4).should == ["1", "2", ""] + "1,ã‚,".split(',', 2).should == ["1", "ã‚,"] + "1,ã‚,".split(',', 3).should == ["1", "ã‚", ""] + "1,ã‚,".split(',', 4).should == ["1", "ã‚", ""] + + "1 2 ".split(/ /, 2).should == ["1", "2 "] + "1 2 ".split(/ /, 3).should == ["1", "2", ""] + "1 2 ".split(/ /, 4).should == ["1", "2", ""] + "1 ã‚ ".split(/ /, 2).should == ["1", "ã‚ "] + "1 ã‚ ".split(/ /, 3).should == ["1", "ã‚", ""] + "1 ã‚ ".split(/ /, 4).should == ["1", "ã‚", ""] + end + + it "returns an array with one entry if limit is 1: the original string" do + "hai".split("hai", 1).should == ["hai"] + "x.y.z".split(".", 1).should == ["x.y.z"] + "hello world ".split(" ", 1).should == ["hello world "] + "hi!".split("", 1).should == ["hi!"] + end + + it "returns at most limit fields when limit > 1" do + "hai".split("hai", 2).should == ["", ""] + + "1,2".split(",", 3).should == ["1", "2"] + + "1,2,,3,4,,".split(',', 2).should == ["1", "2,,3,4,,"] + "1,2,,3,4,,".split(',', 3).should == ["1", "2", ",3,4,,"] + "1,2,,3,4,,".split(',', 4).should == ["1", "2", "", "3,4,,"] + "1,2,,3,4,,".split(',', 5).should == ["1", "2", "", "3", "4,,"] + "1,2,,3,4,,".split(',', 6).should == ["1", "2", "", "3", "4", ","] + + "x".split('x', 2).should == ["", ""] + "xx".split('x', 2).should == ["", "x"] + "xx".split('x', 3).should == ["", "", ""] + "xxx".split('x', 2).should == ["", "xx"] + "xxx".split('x', 3).should == ["", "", "x"] + "xxx".split('x', 4).should == ["", "", "", ""] + end + + it "doesn't suppress or limit fields when limit is negative" do + "1,2,,3,4,,".split(',', -1).should == ["1", "2", "", "3", "4", "", ""] + "1,2,,3,4,,".split(',', -5).should == ["1", "2", "", "3", "4", "", ""] + " a b c\nd ".split(" ", -1).should == ["", "a", "b", "c\nd", ""] + ",".split(",", -1).should == ["", ""] + end + + it "raises a RangeError when the limit is larger than int" do + -> { "a,b".split(" ", 2147483649) }.should raise_error(RangeError) + end + + it "defaults to $; when string isn't given or nil" do + suppress_warning do + old_fs = $; + begin + [",", ":", "", "XY", nil].each do |fs| + $; = fs + + ["x,y,z,,,", "1:2:", "aXYbXYcXY", ""].each do |str| + expected = str.split(fs || " ") + + str.split(nil).should == expected + str.split.should == expected + + str.split(nil, -1).should == str.split(fs || " ", -1) + str.split(nil, 0).should == str.split(fs || " ", 0) + str.split(nil, 2).should == str.split(fs || " ", 2) + end + end + ensure + $; = old_fs + end + end + + context "when $; is not nil" do + before do + suppress_warning do + @old_value, $; = $;, 'foobar' + end + end + + after do + $; = @old_value + end + + it "warns" do + -> { "".split }.should complain(/warning: \$; is set to non-nil value/) + end + end + end + + it "ignores leading and continuous whitespace when string is a single space" do + " now's the time ".split(' ').should == ["now's", "the", "time"] + " now's the time ".split(' ', -1).should == ["now's", "the", "time", ""] + " now's the time ".split(' ', 3).should == ["now's", "the", "time "] + + "\t\n a\t\tb \n\r\r\nc\v\vd\v ".split(' ').should == ["a", "b", "c", "d"] + "a\x00a b".split(' ').should == ["a\x00a", "b"] + end + + describe "when limit is zero" do + it "ignores leading and continuous whitespace when string is a single space" do + " now's the time ".split(' ', 0).should == ["now's", "the", "time"] + end + end + + it "splits between characters when its argument is an empty string" do + "hi!".split("").should == ["h", "i", "!"] + "hi!".split("", -1).should == ["h", "i", "!", ""] + "hi!".split("", 0).should == ["h", "i", "!"] + "hi!".split("", 1).should == ["hi!"] + "hi!".split("", 2).should == ["h", "i!"] + "hi!".split("", 3).should == ["h", "i", "!"] + "hi!".split("", 4).should == ["h", "i", "!", ""] + "hi!".split("", 5).should == ["h", "i", "!", ""] + end + + it "tries converting its pattern argument to a string via to_str" do + obj = mock('::') + obj.should_receive(:to_str).and_return("::") + + "hello::world".split(obj).should == ["hello", "world"] + end + + it "tries converting limit to an integer via to_int" do + obj = mock('2') + obj.should_receive(:to_int).and_return(2) + + "1.2.3.4".split(".", obj).should == ["1", "2.3.4"] + end + + it "doesn't set $~" do + $~ = nil + "x.y.z".split(".") + $~.should == nil + end + + it "returns the original string if no matches are found" do + "foo".split("bar").should == ["foo"] + "foo".split("bar", -1).should == ["foo"] + "foo".split("bar", 0).should == ["foo"] + "foo".split("bar", 1).should == ["foo"] + "foo".split("bar", 2).should == ["foo"] + "foo".split("bar", 3).should == ["foo"] + end + + it "returns String instances based on self" do + ["", "x.y.z.", " x y "].each do |str| + ["", ".", " "].each do |pat| + [-1, 0, 1, 2].each do |limit| + StringSpecs::MyString.new(str).split(pat, limit).each do |x| + x.should be_an_instance_of(String) + end + + str.split(StringSpecs::MyString.new(pat), limit).each do |x| + x.should be_an_instance_of(String) + end + end + end + end + end + + it "returns an empty array when whitespace is split on whitespace" do + " ".split(" ").should == [] + " \n ".split(" ").should == [] + " ".split(" ").should == [] + " \t ".split(" ").should == [] + end + + it "doesn't split on non-ascii whitespace" do + "a\u{2008}b".split(" ").should == ["a\u{2008}b"] + end + + it "returns Strings in the same encoding as self" do + strings = "hello world".encode("US-ASCII").split(" ") + + strings[0].encoding.should == Encoding::US_ASCII + strings[1].encoding.should == Encoding::US_ASCII + end +end + +describe "String#split with Regexp" do + it "throws an ArgumentError if the string is not a valid" do + s = "\xDF".dup.force_encoding(Encoding::UTF_8) + + -> { s.split(/./) }.should raise_error(ArgumentError) + end + + it "divides self on regexp matches" do + " now's the time".split(/ /).should == ["", "now's", "", "the", "time"] + " x\ny ".split(/ /).should == ["", "x\ny"] + "1, 2.34,56, 7".split(/,\s*/).should == ["1", "2.34", "56", "7"] + "1x2X3".split(/x/i).should == ["1", "2", "3"] + end + + it "treats negative limits as no limit" do + "".split(%r!/+!, -1).should == [] + end + + it "suppresses trailing empty fields when limit isn't given or 0" do + "1,2,,3,4,,".split(/,/).should == ["1", "2", "", "3", "4"] + "1,2,,3,4,,".split(/,/, 0).should == ["1", "2", "", "3", "4"] + " a b c\nd ".split(/\s+/).should == ["", "a", "b", "c", "d"] + "hai".split(/hai/).should == [] + ",".split(/,/).should == [] + ",".split(/,/, 0).should == [] + end + + it "returns an array with one entry if limit is 1: the original string" do + "hai".split(/hai/, 1).should == ["hai"] + "xAyBzC".split(/[A-Z]/, 1).should == ["xAyBzC"] + "hello world ".split(/\s+/, 1).should == ["hello world "] + "hi!".split(//, 1).should == ["hi!"] + end + + it "returns at most limit fields when limit > 1" do + "hai".split(/hai/, 2).should == ["", ""] + + "1,2".split(/,/, 3).should == ["1", "2"] + + "1,2,,3,4,,".split(/,/, 2).should == ["1", "2,,3,4,,"] + "1,2,,3,4,,".split(/,/, 3).should == ["1", "2", ",3,4,,"] + "1,2,,3,4,,".split(/,/, 4).should == ["1", "2", "", "3,4,,"] + "1,2,,3,4,,".split(/,/, 5).should == ["1", "2", "", "3", "4,,"] + "1,2,,3,4,,".split(/,/, 6).should == ["1", "2", "", "3", "4", ","] + + "x".split(/x/, 2).should == ["", ""] + "xx".split(/x/, 2).should == ["", "x"] + "xx".split(/x/, 3).should == ["", "", ""] + "xxx".split(/x/, 2).should == ["", "xx"] + "xxx".split(/x/, 3).should == ["", "", "x"] + "xxx".split(/x/, 4).should == ["", "", "", ""] + end + + it "doesn't suppress or limit fields when limit is negative" do + "1,2,,3,4,,".split(/,/, -1).should == ["1", "2", "", "3", "4", "", ""] + "1,2,,3,4,,".split(/,/, -5).should == ["1", "2", "", "3", "4", "", ""] + " a b c\nd ".split(/\s+/, -1).should == ["", "a", "b", "c", "d", ""] + ",".split(/,/, -1).should == ["", ""] + end + + it "defaults to $; when regexp isn't given or nil" do + suppress_warning do + old_fs = $; + begin + [/,/, /:/, //, /XY/, /./].each do |fs| + $; = fs + + ["x,y,z,,,", "1:2:", "aXYbXYcXY", ""].each do |str| + expected = str.split(fs) + + str.split(nil).should == expected + str.split.should == expected + + str.split(nil, -1).should == str.split(fs, -1) + str.split(nil, 0).should == str.split(fs, 0) + str.split(nil, 2).should == str.split(fs, 2) + end + end + ensure + $; = old_fs + end + end + end + + it "splits between characters when regexp matches a zero-length string" do + "hello".split(//).should == ["h", "e", "l", "l", "o"] + "hello".split(//, -1).should == ["h", "e", "l", "l", "o", ""] + "hello".split(//, 0).should == ["h", "e", "l", "l", "o"] + "hello".split(//, 1).should == ["hello"] + "hello".split(//, 2).should == ["h", "ello"] + "hello".split(//, 5).should == ["h", "e", "l", "l", "o"] + "hello".split(//, 6).should == ["h", "e", "l", "l", "o", ""] + "hello".split(//, 7).should == ["h", "e", "l", "l", "o", ""] + + "hi mom".split(/\s*/).should == ["h", "i", "m", "o", "m"] + + "AABCCBAA".split(/(?=B)/).should == ["AA", "BCC", "BAA"] + "AABCCBAA".split(/(?=B)/, -1).should == ["AA", "BCC", "BAA"] + "AABCCBAA".split(/(?=B)/, 2).should == ["AA", "BCCBAA"] + end + + it "respects unicode when splitting between characters" do + str = "ã“ã«ã¡ã‚" + reg = %r!! + ary = str.split(reg) + ary.size.should == 4 + ary.should == ["ã“", "ã«", "ã¡", "ã‚"] + end + + it "respects the encoding of the regexp when splitting between characters" do + str = "\303\202" + ary = str.split(//u) + ary.size.should == 1 + ary.should == ["\303\202"] + end + + it "includes all captures in the result array" do + "hello".split(/(el)/).should == ["h", "el", "lo"] + "hi!".split(/()/).should == ["h", "", "i", "", "!"] + "hi!".split(/()/, -1).should == ["h", "", "i", "", "!", "", ""] + "hello".split(/((el))()/).should == ["h", "el", "el", "", "lo"] + "AabB".split(/([a-z])+/).should == ["A", "b", "B"] + end + + it "applies the limit to the number of split substrings, without counting captures" do + "aBaBa".split(/(B)()()/, 2).should == ["a", "B", "", "", "aBa"] + end + + it "does not include non-matching captures in the result array" do + "hello".split(/(el)|(xx)/).should == ["h", "el", "lo"] + end + + it "tries converting limit to an integer via to_int" do + obj = mock('2') + obj.should_receive(:to_int).and_return(2) + + "1.2.3.4".split(".", obj).should == ["1", "2.3.4"] + end + + it "returns a type error if limit can't be converted to an integer" do + -> {"1.2.3.4".split(".", "three")}.should raise_error(TypeError) + -> {"1.2.3.4".split(".", nil) }.should raise_error(TypeError) + end + + it "doesn't set $~" do + $~ = nil + "x:y:z".split(/:/) + $~.should == nil + end + + it "returns the original string if no matches are found" do + "foo".split(/bar/).should == ["foo"] + "foo".split(/bar/, -1).should == ["foo"] + "foo".split(/bar/, 0).should == ["foo"] + "foo".split(/bar/, 1).should == ["foo"] + "foo".split(/bar/, 2).should == ["foo"] + "foo".split(/bar/, 3).should == ["foo"] + end + + it "returns String instances based on self" do + ["", "x:y:z:", " x y "].each do |str| + [//, /:/, /\s+/].each do |pat| + [-1, 0, 1, 2].each do |limit| + StringSpecs::MyString.new(str).split(pat, limit).each do |x| + x.should be_an_instance_of(String) + end + end + end + end + end + + it "returns Strings in the same encoding as self" do + ary = "а б в".split + encodings = ary.map { |s| s.encoding } + encodings.should == [Encoding::UTF_8, Encoding::UTF_8, Encoding::UTF_8] + end + + it "splits a string on each character for a multibyte encoding and empty split" do + "That's why efficiency could not be helped".split("").size.should == 39 + end + + it "returns an ArgumentError if an invalid UTF-8 string is supplied" do + broken_str = +'проверка' # in russian, means "test" + broken_str.force_encoding('binary') + broken_str.chop! + broken_str.force_encoding('utf-8') + ->{ broken_str.split(/\r\n|\r|\n/) }.should raise_error(ArgumentError) + end + + # See https://bugs.ruby-lang.org/issues/12689 and https://github.com/jruby/jruby/issues/4868 + it "allows concurrent Regexp calls in a shared context" do + str = 'a,b,c,d,e' + + p = proc { str.split(/,/) } + results = 10.times.map { Thread.new { x = nil; 100.times { x = p.call }; x } }.map(&:value) + + results.should == [%w[a b c d e]] * 10 + end + + context "when a block is given" do + it "yields each split substring with default pattern" do + a = [] + returned_object = "chunky bacon".split { |str| a << str.capitalize } + + returned_object.should == "chunky bacon" + a.should == ["Chunky", "Bacon"] + end + + it "yields each split substring with default pattern for a lazy substring" do + a = [] + returned_object = "chunky bacon"[1...-1].split { |str| a << str.capitalize } + + returned_object.should == "hunky baco" + a.should == ["Hunky", "Baco"] + end + + it "yields each split substring with default pattern for a non-ASCII string" do + a = [] + returned_object = "l'été arrive bientôt".split { |str| a << str } + + returned_object.should == "l'été arrive bientôt" + a.should == ["l'été", "arrive", "bientôt"] + end + + it "yields each split substring with default pattern for a non-ASCII lazy substring" do + a = [] + returned_object = "l'été arrive bientôt"[1...-1].split { |str| a << str } + + returned_object.should == "'été arrive bientô" + a.should == ["'été", "arrive", "bientô"] + end + + it "yields the string when limit is 1" do + a = [] + returned_object = "chunky bacon".split("", 1) { |str| a << str.capitalize } + + returned_object.should == "chunky bacon" + a.should == ["Chunky bacon"] + end + + it "yields each split letter" do + a = [] + returned_object = "chunky".split("", 0) { |str| a << str.capitalize } + + returned_object.should == "chunky" + a.should == %w(C H U N K Y) + end + + it "yields each split substring with a pattern" do + a = [] + returned_object = "chunky-bacon".split("-", 0) { |str| a << str.capitalize } + + returned_object.should == "chunky-bacon" + a.should == ["Chunky", "Bacon"] + end + + it "yields each split substring with empty regexp pattern" do + a = [] + returned_object = "chunky".split(//) { |str| a << str.capitalize } + + returned_object.should == "chunky" + a.should == %w(C H U N K Y) + end + + it "yields each split substring with empty regexp pattern and limit" do + a = [] + returned_object = "chunky".split(//, 3) { |str| a << str.capitalize } + + returned_object.should == "chunky" + a.should == %w(C H Unky) + end + + it "yields each split substring with a regexp pattern" do + a = [] + returned_object = "chunky:bacon".split(/:/) { |str| a << str.capitalize } + + returned_object.should == "chunky:bacon" + a.should == ["Chunky", "Bacon"] + end + + it "returns a string as is (and doesn't call block) if it is empty" do + a = [] + returned_object = "".split { |str| a << str.capitalize } + + returned_object.should == "" + a.should == [] + end + end + + describe "for a String subclass" do + it "yields instances of String" do + a = [] + StringSpecs::MyString.new("a|b").split("|") { |str| a << str } + first, last = a + + first.should be_an_instance_of(String) + first.should == "a" + + last.should be_an_instance_of(String) + last.should == "b" + end + end + + it "raises a TypeError when not called with nil, String, or Regexp" do + -> { "hello".split(42) }.should raise_error(TypeError) + -> { "hello".split(:ll) }.should raise_error(TypeError) + -> { "hello".split(false) }.should raise_error(TypeError) + -> { "hello".split(Object.new) }.should raise_error(TypeError) + end + + it "returns Strings in the same encoding as self" do + strings = "hello world".encode("US-ASCII").split(/ /) + + strings[0].encoding.should == Encoding::US_ASCII + strings[1].encoding.should == Encoding::US_ASCII + end +end diff --git a/spec/ruby/core/string/squeeze_spec.rb b/spec/ruby/core/string/squeeze_spec.rb new file mode 100644 index 0000000000..981d480684 --- /dev/null +++ b/spec/ruby/core/string/squeeze_spec.rb @@ -0,0 +1,111 @@ +# encoding: binary +# frozen_string_literal: false +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +# TODO: rewrite all these specs + +describe "String#squeeze" do + it "returns new string where runs of the same character are replaced by a single character when no args are given" do + "yellow moon".squeeze.should == "yelow mon" + end + + it "only squeezes chars that are in the intersection of all sets given" do + "woot squeeze cheese".squeeze("eost", "queo").should == "wot squeze chese" + " now is the".squeeze(" ").should == " now is the" + end + + it "negates sets starting with ^" do + s = "<<subbookkeeper!!!>>" + s.squeeze("beko", "^e").should == s.squeeze("bko") + s.squeeze("^<bek!>").should == s.squeeze("o") + s.squeeze("^o").should == s.squeeze("<bek!>") + s.squeeze("^").should == s + "^__^".squeeze("^^").should == "^_^" + "((^^__^^))".squeeze("_^").should == "((^_^))" + end + + it "squeezes all chars in a sequence" do + s = "--subbookkeeper--" + s.squeeze("\x00-\xFF").should == s.squeeze + s.squeeze("bk-o").should == s.squeeze("bklmno") + s.squeeze("b-e").should == s.squeeze("bcde") + s.squeeze("e-").should == "-subbookkeper-" + s.squeeze("-e").should == "-subbookkeper-" + s.squeeze("---").should == "-subbookkeeper-" + "ook--001122".squeeze("--2").should == "ook-012" + "ook--(())".squeeze("(--").should == "ook-()" + s.squeeze("^b-e").should == "-subbokeeper-" + "^^__^^".squeeze("^^-^").should == "^^_^^" + "^^--^^".squeeze("^---").should == "^--^" + + s.squeeze("b-dk-o-").should == "-subokeeper-" + s.squeeze("-b-dk-o").should == "-subokeeper-" + s.squeeze("b-d-k-o").should == "-subokeeper-" + + s.squeeze("bc-e").should == "--subookkeper--" + s.squeeze("^bc-e").should == "-subbokeeper-" + + "AABBCCaabbcc[[]]".squeeze("A-a").should == "ABCabbcc[]" + end + + it "raises an ArgumentError when the parameter is out of sequence" do + s = "--subbookkeeper--" + -> { s.squeeze("e-b") }.should raise_error(ArgumentError) + -> { s.squeeze("^e-b") }.should raise_error(ArgumentError) + end + + it "tries to convert each set arg to a string using to_str" do + other_string = mock('lo') + other_string.should_receive(:to_str).and_return("lo") + + other_string2 = mock('o') + other_string2.should_receive(:to_str).and_return("o") + + "hello room".squeeze(other_string, other_string2).should == "hello rom" + end + + it "returns a String in the same encoding as self" do + "yellow moon".encode("US-ASCII").squeeze.encoding.should == Encoding::US_ASCII + "yellow moon".encode("US-ASCII").squeeze("a").encoding.should == Encoding::US_ASCII + end + + it "raises a TypeError when one set arg can't be converted to a string" do + -> { "hello world".squeeze([]) }.should raise_error(TypeError) + -> { "hello world".squeeze(Object.new)}.should raise_error(TypeError) + -> { "hello world".squeeze(mock('x')) }.should raise_error(TypeError) + end + + it "returns String instances when called on a subclass" do + StringSpecs::MyString.new("oh no!!!").squeeze("!").should be_an_instance_of(String) + end +end + +describe "String#squeeze!" do + it "modifies self in place and returns self" do + a = "yellow moon" + a.squeeze!.should equal(a) + a.should == "yelow mon" + end + + it "returns nil if no modifications were made" do + a = "squeeze" + a.squeeze!("u", "sq").should == nil + a.squeeze!("q").should == nil + a.should == "squeeze" + end + + it "raises an ArgumentError when the parameter is out of sequence" do + s = "--subbookkeeper--" + -> { s.squeeze!("e-b") }.should raise_error(ArgumentError) + -> { s.squeeze!("^e-b") }.should raise_error(ArgumentError) + end + + it "raises a FrozenError when self is frozen" do + a = "yellow moon" + a.freeze + + -> { a.squeeze!("") }.should raise_error(FrozenError) + -> { a.squeeze! }.should raise_error(FrozenError) + end +end diff --git a/spec/ruby/core/string/start_with_spec.rb b/spec/ruby/core/string/start_with_spec.rb new file mode 100644 index 0000000000..35e33b46a6 --- /dev/null +++ b/spec/ruby/core/string/start_with_spec.rb @@ -0,0 +1,27 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative '../../shared/string/start_with' + +describe "String#start_with?" do + it_behaves_like :start_with, :to_s + + # Here and not in the shared examples because this is invalid as a Symbol + it "matches part of a character with the same part" do + "\xA9".should.start_with?("\xA9") # A9 is not a character head for UTF-8 + end + + ruby_version_is ""..."3.3" do + it "does not check we are matching only part of a character" do + "\xe3\x81\x82".size.should == 1 + "\xe3\x81\x82".should.start_with?("\xe3") + end + end + + ruby_version_is "3.3" do # #19784 + it "checks we are matching only part of a character" do + "\xe3\x81\x82".size.should == 1 + "\xe3\x81\x82".should_not.start_with?("\xe3") + end + end +end diff --git a/spec/ruby/core/string/string_spec.rb b/spec/ruby/core/string/string_spec.rb new file mode 100644 index 0000000000..cdefbbecbd --- /dev/null +++ b/spec/ruby/core/string/string_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' + +describe "String" do + it "includes Comparable" do + String.include?(Comparable).should == true + end +end diff --git a/spec/ruby/core/string/strip_spec.rb b/spec/ruby/core/string/strip_spec.rb new file mode 100644 index 0000000000..edb6ea3b44 --- /dev/null +++ b/spec/ruby/core/string/strip_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: false +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/strip' + +describe "String#strip" do + it_behaves_like :string_strip, :strip + + it "returns a new string with leading and trailing whitespace removed" do + " hello ".strip.should == "hello" + " hello world ".strip.should == "hello world" + "\tgoodbye\r\v\n".strip.should == "goodbye" + end + + it "returns a copy of self without leading and trailing NULL bytes and whitespace" do + " \x00 goodbye \x00 ".strip.should == "goodbye" + end +end + +describe "String#strip!" do + it "modifies self in place and returns self" do + a = " hello " + a.strip!.should equal(a) + a.should == "hello" + + a = "\tgoodbye\r\v\n" + a.strip! + a.should == "goodbye" + end + + it "returns nil if no modifications where made" do + a = "hello" + a.strip!.should == nil + a.should == "hello" + end + + it "makes a string empty if it is only whitespace" do + "".strip!.should == nil + " ".strip.should == "" + " ".strip.should == "" + end + + it "removes leading and trailing NULL bytes and whitespace" do + a = "\000 goodbye \000" + a.strip! + a.should == "goodbye" + end + + it "raises a FrozenError on a frozen instance that is modified" do + -> { " hello ".freeze.strip! }.should raise_error(FrozenError) + end + + # see #1552 + it "raises a FrozenError on a frozen instance that would not be modified" do + -> {"hello".freeze.strip! }.should raise_error(FrozenError) + -> {"".freeze.strip! }.should raise_error(FrozenError) + end +end diff --git a/spec/ruby/core/string/sub_spec.rb b/spec/ruby/core/string/sub_spec.rb new file mode 100644 index 0000000000..6ff28ec851 --- /dev/null +++ b/spec/ruby/core/string/sub_spec.rb @@ -0,0 +1,512 @@ +# frozen_string_literal: false +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "String#sub with pattern, replacement" do + it "returns a copy of self when no modification is made" do + a = "hello" + b = a.sub(/w.*$/, "*") + + b.should_not equal(a) + b.should == "hello" + end + + it "returns a copy of self with all occurrences of pattern replaced with replacement" do + "hello".sub(/[aeiou]/, '*').should == "h*llo" + "hello".sub(//, ".").should == ".hello" + end + + it "ignores a block if supplied" do + "food".sub(/f/, "g") { "w" }.should == "good" + end + + it "supports \\G which matches at the beginning of the string" do + "hello world!".sub(/\Ghello/, "hi").should == "hi world!" + end + + it "supports /i for ignoring case" do + "Hello".sub(/h/i, "j").should == "jello" + "hello".sub(/H/i, "j").should == "jello" + end + + it "doesn't interpret regexp metacharacters if pattern is a string" do + "12345".sub('\d', 'a').should == "12345" + '\d'.sub('\d', 'a').should == "a" + end + + it "replaces \\1 sequences with the regexp's corresponding capture" do + str = "hello" + + str.sub(/([aeiou])/, '<\1>').should == "h<e>llo" + str.sub(/(.)/, '\1\1').should == "hhello" + + str.sub(/.(.?)/, '<\0>(\1)').should == "<he>(e)llo" + + str.sub(/.(.)+/, '\1').should == "o" + + str = "ABCDEFGHIJKL" + re = /#{"(.)" * 12}/ + str.sub(re, '\1').should == "A" + str.sub(re, '\9').should == "I" + # Only the first 9 captures can be accessed in MRI + str.sub(re, '\10').should == "A0" + end + + it "treats \\1 sequences without corresponding captures as empty strings" do + str = "hello!" + + str.sub("", '<\1>').should == "<>hello!" + str.sub("h", '<\1>').should == "<>ello!" + + str.sub(//, '<\1>').should == "<>hello!" + str.sub(/./, '\1\2\3').should == "ello!" + str.sub(/.(.{20})?/, '\1').should == "ello!" + end + + it "replaces \\& and \\0 with the complete match" do + str = "hello!" + + str.sub("", '<\0>').should == "<>hello!" + str.sub("", '<\&>').should == "<>hello!" + str.sub("he", '<\0>').should == "<he>llo!" + str.sub("he", '<\&>').should == "<he>llo!" + str.sub("l", '<\0>').should == "he<l>lo!" + str.sub("l", '<\&>').should == "he<l>lo!" + + str.sub(//, '<\0>').should == "<>hello!" + str.sub(//, '<\&>').should == "<>hello!" + str.sub(/../, '<\0>').should == "<he>llo!" + str.sub(/../, '<\&>').should == "<he>llo!" + str.sub(/(.)./, '<\0>').should == "<he>llo!" + end + + it "replaces \\` with everything before the current match" do + str = "hello!" + + str.sub("", '<\`>').should == "<>hello!" + str.sub("h", '<\`>').should == "<>ello!" + str.sub("l", '<\`>').should == "he<he>lo!" + str.sub("!", '<\`>').should == "hello<hello>" + + str.sub(//, '<\`>').should == "<>hello!" + str.sub(/..o/, '<\`>').should == "he<he>!" + end + + it "replaces \\' with everything after the current match" do + str = "hello!" + + str.sub("", '<\\\'>').should == "<hello!>hello!" + str.sub("h", '<\\\'>').should == "<ello!>ello!" + str.sub("ll", '<\\\'>').should == "he<o!>o!" + str.sub("!", '<\\\'>').should == "hello<>" + + str.sub(//, '<\\\'>').should == "<hello!>hello!" + str.sub(/../, '<\\\'>').should == "<llo!>llo!" + end + + it "replaces \\\\\\+ with \\\\+" do + "x".sub(/x/, '\\\+').should == "\\+" + end + + it "replaces \\+ with the last paren that actually matched" do + str = "hello!" + + str.sub(/(.)(.)/, '\+').should == "ello!" + str.sub(/(.)(.)+/, '\+').should == "!" + str.sub(/(.)()/, '\+').should == "ello!" + str.sub(/(.)(.{20})?/, '<\+>').should == "<h>ello!" + + str = "ABCDEFGHIJKL" + re = /#{"(.)" * 12}/ + str.sub(re, '\+').should == "L" + end + + it "treats \\+ as an empty string if there was no captures" do + "hello!".sub(/./, '\+').should == "ello!" + end + + it "maps \\\\ in replacement to \\" do + "hello".sub(/./, '\\\\').should == '\\ello' + end + + it "leaves unknown \\x escapes in replacement untouched" do + "hello".sub(/./, '\\x').should == '\\xello' + "hello".sub(/./, '\\y').should == '\\yello' + end + + it "leaves \\ at the end of replacement untouched" do + "hello".sub(/./, 'hah\\').should == 'hah\\ello' + end + + it "tries to convert pattern to a string using to_str" do + pattern = mock('.') + pattern.should_receive(:to_str).and_return(".") + + "hello.".sub(pattern, "!").should == "hello!" + end + + not_supported_on :opal do + it "raises a TypeError when pattern is a Symbol" do + -> { "hello".sub(:woot, "x") }.should raise_error(TypeError) + end + end + + it "raises a TypeError when pattern is an Array" do + -> { "hello".sub([], "x") }.should raise_error(TypeError) + end + + it "raises a TypeError when pattern can't be converted to a string" do + -> { "hello".sub(Object.new, nil) }.should raise_error(TypeError) + end + + it "tries to convert replacement to a string using to_str" do + replacement = mock('hello_replacement') + replacement.should_receive(:to_str).and_return("hello_replacement") + + "hello".sub(/hello/, replacement).should == "hello_replacement" + end + + it "raises a TypeError when replacement can't be converted to a string" do + -> { "hello".sub(/[aeiou]/, []) }.should raise_error(TypeError) + -> { "hello".sub(/[aeiou]/, 99) }.should raise_error(TypeError) + end + + it "returns String instances when called on a subclass" do + StringSpecs::MyString.new("").sub(//, "").should be_an_instance_of(String) + StringSpecs::MyString.new("").sub(/foo/, "").should be_an_instance_of(String) + StringSpecs::MyString.new("foo").sub(/foo/, "").should be_an_instance_of(String) + StringSpecs::MyString.new("foo").sub("foo", "").should be_an_instance_of(String) + end + + it "sets $~ to MatchData of match and nil when there's none" do + 'hello.'.sub('hello', 'x') + $~[0].should == 'hello' + + 'hello.'.sub('not', 'x') + $~.should == nil + + 'hello.'.sub(/.(.)/, 'x') + $~[0].should == 'he' + + 'hello.'.sub(/not/, 'x') + $~.should == nil + end + + it "replaces \\\\\\1 with \\1" do + "ababa".sub(/(b)/, '\\\1').should == "a\\1aba" + end + + it "replaces \\\\\\\\1 with \\1" do + "ababa".sub(/(b)/, '\\\\1').should == "a\\1aba" + end + + it "replaces \\\\\\\\\\1 with \\" do + "ababa".sub(/(b)/, '\\\\\1').should == "a\\baba" + end + + it "handles a pattern in a superset encoding" do + result = 'abc'.force_encoding(Encoding::US_ASCII).sub('é', 'è') + result.should == 'abc' + result.encoding.should == Encoding::US_ASCII + end + + it "handles a pattern in a subset encoding" do + result = 'été'.sub('t'.force_encoding(Encoding::US_ASCII), 'u') + result.should == 'éué' + result.encoding.should == Encoding::UTF_8 + end +end + +describe "String#sub with pattern and block" do + it "returns a copy of self with the first occurrences of pattern replaced with the block's return value" do + "hi".sub(/./) { |s| s + ' ' }.should == "h i" + "hi!".sub(/(.)(.)/) { |*a| a.inspect }.should == '["hi"]!' + end + + it "sets $~ for access from the block" do + str = "hello" + str.sub(/([aeiou])/) { "<#{$~[1]}>" }.should == "h<e>llo" + str.sub(/([aeiou])/) { "<#{$1}>" }.should == "h<e>llo" + str.sub("l") { "<#{$~[0]}>" }.should == "he<l>lo" + + offsets = [] + + str.sub(/([aeiou])/) do + md = $~ + md.string.should == str + offsets << md.offset(0) + str + end.should == "hhellollo" + + offsets.should == [[1, 2]] + end + + it "sets $~ to MatchData of last match and nil when there's none for access from outside" do + 'hello.'.sub('l') { 'x' } + $~.begin(0).should == 2 + $~[0].should == 'l' + + 'hello.'.sub('not') { 'x' } + $~.should == nil + + 'hello.'.sub(/.(.)/) { 'x' } + $~[0].should == 'he' + + 'hello.'.sub(/not/) { 'x' } + $~.should == nil + end + + it "doesn't raise a RuntimeError if the string is modified while substituting" do + str = "hello" + str.sub(//) { str[0] = 'x' }.should == "xhello" + str.should == "xello" + end + + it "doesn't interpolate special sequences like \\1 for the block's return value" do + repl = '\& \0 \1 \` \\\' \+ \\\\ foo' + "hello".sub(/(.+)/) { repl }.should == repl + end + + it "converts the block's return value to a string using to_s" do + obj = mock('hello_replacement') + obj.should_receive(:to_s).and_return("hello_replacement") + "hello".sub(/hello/) { obj }.should == "hello_replacement" + + obj = mock('ok') + obj.should_receive(:to_s).and_return("ok") + "hello".sub(/.+/) { obj }.should == "ok" + end +end + +describe "String#sub! with pattern, replacement" do + it "modifies self in place and returns self" do + a = "hello" + a.sub!(/[aeiou]/, '*').should equal(a) + a.should == "h*llo" + end + + it "returns nil if no modifications were made" do + a = "hello" + a.sub!(/z/, '*').should == nil + a.sub!(/z/, 'z').should == nil + a.should == "hello" + end + + it "raises a FrozenError when self is frozen" do + s = "hello" + s.freeze + + -> { s.sub!(/ROAR/, "x") }.should raise_error(FrozenError) + -> { s.sub!(/e/, "e") }.should raise_error(FrozenError) + -> { s.sub!(/[aeiou]/, '*') }.should raise_error(FrozenError) + end + + it "handles a pattern in a superset encoding" do + string = 'abc'.force_encoding(Encoding::US_ASCII) + + result = string.sub!('é', 'è') + + result.should == nil + string.should == 'abc' + string.encoding.should == Encoding::US_ASCII + end + + it "handles a pattern in a subset encoding" do + string = 'été' + pattern = 't'.force_encoding(Encoding::US_ASCII) + + result = string.sub!(pattern, 'u') + + result.should == string + string.should == 'éué' + string.encoding.should == Encoding::UTF_8 + end +end + +describe "String#sub! with pattern and block" do + it "modifies self in place and returns self" do + a = "hello" + a.sub!(/[aeiou]/) { '*' }.should equal(a) + a.should == "h*llo" + end + + it "sets $~ for access from the block" do + str = "hello" + str.dup.sub!(/([aeiou])/) { "<#{$~[1]}>" }.should == "h<e>llo" + str.dup.sub!(/([aeiou])/) { "<#{$1}>" }.should == "h<e>llo" + str.dup.sub!("l") { "<#{$~[0]}>" }.should == "he<l>lo" + + offsets = [] + + str.dup.sub!(/([aeiou])/) do + md = $~ + md.string.should == str + offsets << md.offset(0) + str + end.should == "hhellollo" + + offsets.should == [[1, 2]] + end + + it "returns nil if no modifications were made" do + a = "hello" + a.sub!(/z/) { '*' }.should == nil + a.sub!(/z/) { 'z' }.should == nil + a.should == "hello" + end + + it "raises a RuntimeError if the string is modified while substituting" do + str = "hello" + -> { str.sub!(//) { str << 'x' } }.should raise_error(RuntimeError) + end + + it "raises a FrozenError when self is frozen" do + s = "hello" + s.freeze + + -> { s.sub!(/ROAR/) { "x" } }.should raise_error(FrozenError) + -> { s.sub!(/e/) { "e" } }.should raise_error(FrozenError) + -> { s.sub!(/[aeiou]/) { '*' } }.should raise_error(FrozenError) + end +end + +describe "String#sub with pattern and Hash" do + + it "returns a copy of self with the first occurrence of pattern replaced with the value of the corresponding hash key" do + "hello".sub(/./, 'l' => 'L').should == "ello" + "hello!".sub(/(.)(.)/, 'he' => 'she ', 'll' => 'said').should == 'she llo!' + "hello".sub('l', 'l' => 'el').should == 'heello' + end + + it "removes keys that don't correspond to matches" do + "hello".sub(/./, 'z' => 'b', 'o' => 'ow').should == "ello" + end + + it "ignores non-String keys" do + "tattoo".sub(/(tt)/, 'tt' => 'b', tt: 'z').should == "taboo" + end + + it "uses a key's value only a single time" do + "food".sub(/o/, 'o' => '0').should == "f0od" + end + + it "uses the hash's default value for missing keys" do + hsh = {} + hsh.default='?' + hsh['o'] = '0' + "food".sub(/./, hsh).should == "?ood" + end + + it "coerces the hash values with #to_s" do + hsh = {} + hsh.default=[] + hsh['o'] = 0 + obj = mock('!') + obj.should_receive(:to_s).and_return('!') + hsh['f'] = obj + "food!".sub(/./, hsh).should == "!ood!" + end + + it "uses the hash's value set from default_proc for missing keys" do + hsh = {} + hsh.default_proc = -> k, v { 'lamb' } + "food!".sub(/./, hsh).should == "lambood!" + end + + it "sets $~ to MatchData of first match and nil when there's none for access from outside" do + 'hello.'.sub('l', 'l' => 'L') + $~.begin(0).should == 2 + $~[0].should == 'l' + + 'hello.'.sub('not', 'ot' => 'to') + $~.should == nil + + 'hello.'.sub(/.(.)/, 'o' => ' hole') + $~[0].should == 'he' + + 'hello.'.sub(/not/, 'z' => 'glark') + $~.should == nil + end + + it "doesn't interpolate special sequences like \\1 for the block's return value" do + repl = '\& \0 \1 \` \\\' \+ \\\\ foo' + "hello".sub(/(.+)/, 'hello' => repl ).should == repl + end + +end + +describe "String#sub! with pattern and Hash" do + + it "returns self with the first occurrence of pattern replaced with the value of the corresponding hash key" do + "hello".sub!(/./, 'l' => 'L').should == "ello" + "hello!".sub!(/(.)(.)/, 'he' => 'she ', 'll' => 'said').should == 'she llo!' + "hello".sub!('l', 'l' => 'el').should == 'heello' + end + + it "removes keys that don't correspond to matches" do + "hello".sub!(/./, 'z' => 'b', 'o' => 'ow').should == "ello" + end + + it "ignores non-String keys" do + "hello".sub!(/(ll)/, 'll' => 'r', ll: 'z').should == "hero" + end + + it "uses a key's value only a single time" do + "food".sub!(/o/, 'o' => '0').should == "f0od" + end + + it "uses the hash's default value for missing keys" do + hsh = {} + hsh.default='?' + hsh['o'] = '0' + "food".sub!(/./, hsh).should == "?ood" + end + + it "coerces the hash values with #to_s" do + hsh = {} + hsh.default=[] + hsh['o'] = 0 + obj = mock('!') + obj.should_receive(:to_s).and_return('!') + hsh['f'] = obj + "food!".sub!(/./, hsh).should == "!ood!" + end + + it "uses the hash's value set from default_proc for missing keys" do + hsh = {} + hsh.default_proc = -> k, v { 'lamb' } + "food!".sub!(/./, hsh).should == "lambood!" + end + + it "sets $~ to MatchData of first match and nil when there's none for access from outside" do + 'hello.'.sub!('l', 'l' => 'L') + $~.begin(0).should == 2 + $~[0].should == 'l' + + 'hello.'.sub!('not', 'ot' => 'to') + $~.should == nil + + 'hello.'.sub!(/.(.)/, 'o' => ' hole') + $~[0].should == 'he' + + 'hello.'.sub!(/not/, 'z' => 'glark') + $~.should == nil + end + + it "doesn't interpolate special sequences like \\1 for the block's return value" do + repl = '\& \0 \1 \` \\\' \+ \\\\ foo' + "hello".sub!(/(.+)/, 'hello' => repl ).should == repl + end +end + +describe "String#sub with pattern and without replacement and block" do + it "raises a ArgumentError" do + -> { "abca".sub(/a/) }.should raise_error(ArgumentError) + end +end + +describe "String#sub! with pattern and without replacement and block" do + it "raises a ArgumentError" do + -> { "abca".sub!(/a/) }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/string/succ_spec.rb b/spec/ruby/core/string/succ_spec.rb new file mode 100644 index 0000000000..65047e0aa2 --- /dev/null +++ b/spec/ruby/core/string/succ_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/succ' + +describe "String#succ" do + it_behaves_like :string_succ, :succ +end + +describe "String#succ!" do + it_behaves_like :string_succ_bang, :"succ!" +end diff --git a/spec/ruby/core/string/sum_spec.rb b/spec/ruby/core/string/sum_spec.rb new file mode 100644 index 0000000000..c283b7c254 --- /dev/null +++ b/spec/ruby/core/string/sum_spec.rb @@ -0,0 +1,22 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "String#sum" do + it "returns a basic n-bit checksum of the characters in self" do + "ruby".sum.should == 450 + "ruby".sum(8).should == 194 + "rubinius".sum(23).should == 881 + end + + it "tries to convert n to an integer using to_int" do + obj = mock('8') + obj.should_receive(:to_int).and_return(8) + + "hello".sum(obj).should == "hello".sum(8) + end + + it "returns sum of the bytes in self if n less or equal to zero" do + "xyz".sum(0).should == 363 + "xyz".sum(-10).should == 363 + end +end diff --git a/spec/ruby/core/string/swapcase_spec.rb b/spec/ruby/core/string/swapcase_spec.rb new file mode 100644 index 0000000000..011a213501 --- /dev/null +++ b/spec/ruby/core/string/swapcase_spec.rb @@ -0,0 +1,193 @@ +# -*- encoding: utf-8 -*- +# frozen_string_literal: false +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "String#swapcase" do + it "returns a new string with all uppercase chars from self converted to lowercase and vice versa" do + "Hello".swapcase.should == "hELLO" + "cYbEr_PuNk11".swapcase.should == "CyBeR_pUnK11" + "+++---111222???".swapcase.should == "+++---111222???" + end + + it "returns a String in the same encoding as self" do + "Hello".encode("US-ASCII").swapcase.encoding.should == Encoding::US_ASCII + end + + describe "full Unicode case mapping" do + it "works for all of Unicode with no option" do + "äÖü".swapcase.should == "ÄöÜ" + end + + it "updates string metadata" do + swapcased = "Aßet".swapcase + + swapcased.should == "aSSET" + swapcased.size.should == 5 + swapcased.bytesize.should == 5 + swapcased.ascii_only?.should be_true + end + end + + describe "ASCII-only case mapping" do + it "does not swapcase non-ASCII characters" do + "aßet".swapcase(:ascii).should == "AßET" + end + + it "works with substrings" do + "prefix aTé"[-3..-1].swapcase(:ascii).should == "Até" + end + end + + describe "full Unicode case mapping adapted for Turkic languages" do + it "swaps case of ASCII characters according to Turkic semantics" do + "aiS".swapcase(:turkic).should == "Aİs" + end + + it "allows Lithuanian as an extra option" do + "aiS".swapcase(:turkic, :lithuanian).should == "Aİs" + end + + it "does not allow any other additional option" do + -> { "aiS".swapcase(:turkic, :ascii) }.should raise_error(ArgumentError) + end + end + + describe "full Unicode case mapping adapted for Lithuanian" do + it "currently works the same as full Unicode case mapping" do + "Iß".swapcase(:lithuanian).should == "iSS" + end + + it "allows Turkic as an extra option (and applies Turkic semantics)" do + "iS".swapcase(:lithuanian, :turkic).should == "İs" + end + + it "does not allow any other additional option" do + -> { "aiS".swapcase(:lithuanian, :ascii) }.should raise_error(ArgumentError) + end + end + + it "does not allow the :fold option for upcasing" do + -> { "abc".swapcase(:fold) }.should raise_error(ArgumentError) + end + + it "does not allow invalid options" do + -> { "abc".swapcase(:invalid_option) }.should raise_error(ArgumentError) + end + + it "returns String instances when called on a subclass" do + StringSpecs::MyString.new("").swapcase.should be_an_instance_of(String) + StringSpecs::MyString.new("hello").swapcase.should be_an_instance_of(String) + end +end + +describe "String#swapcase!" do + it "modifies self in place" do + a = "cYbEr_PuNk11" + a.swapcase!.should equal(a) + a.should == "CyBeR_pUnK11" + end + + it "modifies self in place for non-ascii-compatible encodings" do + a = "cYbEr_PuNk11".encode("utf-16le") + a.swapcase! + a.should == "CyBeR_pUnK11".encode("utf-16le") + end + + describe "full Unicode case mapping" do + it "modifies self in place for all of Unicode with no option" do + a = "äÖü" + a.swapcase! + a.should == "ÄöÜ" + end + + it "works for non-ascii-compatible encodings" do + a = "äÖü".encode("utf-16le") + a.swapcase! + a.should == "ÄöÜ".encode("utf-16le") + end + + it "updates string metadata" do + swapcased = "Aßet" + swapcased.swapcase! + + swapcased.should == "aSSET" + swapcased.size.should == 5 + swapcased.bytesize.should == 5 + swapcased.ascii_only?.should be_true + end + end + + describe "modifies self in place for ASCII-only case mapping" do + it "does not swapcase non-ASCII characters" do + a = "aßet" + a.swapcase!(:ascii) + a.should == "AßET" + end + + it "works for non-ascii-compatible encodings" do + a = "aBc".encode("utf-16le") + a.swapcase!(:ascii) + a.should == "AbC".encode("utf-16le") + end + end + + describe "modifies self in place for full Unicode case mapping adapted for Turkic languages" do + it "swaps case of ASCII characters according to Turkic semantics" do + a = "aiS" + a.swapcase!(:turkic) + a.should == "Aİs" + end + + it "allows Lithuanian as an extra option" do + a = "aiS" + a.swapcase!(:turkic, :lithuanian) + a.should == "Aİs" + end + + it "does not allow any other additional option" do + -> { a = "aiS"; a.swapcase!(:turkic, :ascii) }.should raise_error(ArgumentError) + end + end + + describe "full Unicode case mapping adapted for Lithuanian" do + it "currently works the same as full Unicode case mapping" do + a = "Iß" + a.swapcase!(:lithuanian) + a.should == "iSS" + end + + it "allows Turkic as an extra option (and applies Turkic semantics)" do + a = "iS" + a.swapcase!(:lithuanian, :turkic) + a.should == "İs" + end + + it "does not allow any other additional option" do + -> { a = "aiS"; a.swapcase!(:lithuanian, :ascii) }.should raise_error(ArgumentError) + end + end + + it "does not allow the :fold option for upcasing" do + -> { a = "abc"; a.swapcase!(:fold) }.should raise_error(ArgumentError) + end + + it "does not allow invalid options" do + -> { a = "abc"; a.swapcase!(:invalid_option) }.should raise_error(ArgumentError) + end + + it "returns nil if no modifications were made" do + a = "+++---111222???" + a.swapcase!.should == nil + a.should == "+++---111222???" + + "".swapcase!.should == nil + end + + it "raises a FrozenError when self is frozen" do + ["", "hello"].each do |a| + a.freeze + -> { a.swapcase! }.should raise_error(FrozenError) + end + end +end diff --git a/spec/ruby/core/string/to_c_spec.rb b/spec/ruby/core/string/to_c_spec.rb new file mode 100644 index 0000000000..1813890e72 --- /dev/null +++ b/spec/ruby/core/string/to_c_spec.rb @@ -0,0 +1,53 @@ +require_relative '../../spec_helper' +require_relative '../../shared/kernel/complex' +require_relative 'fixtures/to_c' + +describe "String#to_c" do + it_behaves_like :kernel_complex, :to_c_method, StringSpecs +end + +describe "String#to_c" do + it "returns a complex number with 0 as the real part, 0 as the imaginary part for unrecognised Strings" do + 'ruby'.to_c.should == Complex(0, 0) + end + + it "ignores trailing garbage" do + '79+4iruby'.to_c.should == Complex(79, 4) + '7__9+4__0i'.to_c.should == Complex(7, 0) + end + + context "it treats special float value strings as characters" do + it "parses any string that starts with 'I' as 1i" do + 'Infinity'.to_c.should == Complex(0, 1) + '-Infinity'.to_c.should == Complex(0, -1) + 'Insecure'.to_c.should == Complex(0, 1) + '-Insecure'.to_c.should == Complex(0, -1) + end + + it "does not parse any numeric information in 'NaN'" do + 'NaN'.to_c.should == Complex(0, 0) + end + end + + it "allows null-byte" do + "1-2i\0".to_c.should == Complex(1, -2) + "1\0-2i".to_c.should == Complex(1, 0) + "\01-2i".to_c.should == Complex(0, 0) + end + + it "raises Encoding::CompatibilityError if String is in not ASCII-compatible encoding" do + -> { + '79+4i'.encode("UTF-16").to_c + }.should raise_error(Encoding::CompatibilityError, "ASCII incompatible encoding: UTF-16") + end + + it "treats a sequence of underscores as an end of Complex string" do + "5+3_1i".to_c.should == Complex(5, 31) + "5+3__1i".to_c.should == Complex(5) + "5+3___1i".to_c.should == Complex(5) + + "12_3".to_c.should == Complex(123) + "12__3".to_c.should == Complex(12) + "12___3".to_c.should == Complex(12) + end +end diff --git a/spec/ruby/core/string/to_f_spec.rb b/spec/ruby/core/string/to_f_spec.rb new file mode 100644 index 0000000000..abfd2517b6 --- /dev/null +++ b/spec/ruby/core/string/to_f_spec.rb @@ -0,0 +1,142 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +# src.scan(/[+-]?[\d_]+\.[\d_]+(e[+-]?[\d_]+)?\b|[+-]?[\d_]+e[+-]?[\d_]+\b/i) + +describe "String#to_f" do + it "treats leading characters of self as a floating point number" do + "123.45e1".to_f.should == 1234.5 + "45.67 degrees".to_f.should == 45.67 + "0".to_f.should == 0.0 + + ".5".to_f.should == 0.5 + ".5e1".to_f.should == 5.0 + "5.".to_f.should == 5.0 + "5e".to_f.should == 5.0 + "5E".to_f.should == 5.0 + end + + it "treats special float value strings as characters" do + "NaN".to_f.should == 0 + "Infinity".to_f.should == 0 + "-Infinity".to_f.should == 0 + end + + it "allows for varying case" do + "123.45e1".to_f.should == 1234.5 + "123.45E1".to_f.should == 1234.5 + end + + it "allows for varying signs" do + "+123.45e1".to_f.should == +123.45e1 + "-123.45e1".to_f.should == -123.45e1 + "123.45e+1".to_f.should == 123.45e+1 + "123.45e-1".to_f.should == 123.45e-1 + "+123.45e+1".to_f.should == +123.45e+1 + "+123.45e-1".to_f.should == +123.45e-1 + "-123.45e+1".to_f.should == -123.45e+1 + "-123.45e-1".to_f.should == -123.45e-1 + end + + it "allows for underscores, even in the decimal side" do + "1_234_567.890_1".to_f.should == 1_234_567.890_1 + end + + it "returns 0 for strings with leading underscores" do + "_9".to_f.should == 0 + end + + it "stops if the underscore is not followed or preceded by a number" do + "1__2".to_f.should == 1.0 + "1_.2".to_f.should == 1.0 + "1._2".to_f.should == 1.0 + "1.2_e2".to_f.should == 1.2 + "1.2e_2".to_f.should == 1.2 + "1_x2".to_f.should == 1.0 + "1x_2".to_f.should == 1.0 + "+_1".to_f.should == 0.0 + "-_1".to_f.should == 0.0 + end + + it "does not allow prefixes to autodetect the base" do + "0b10".to_f.should == 0 + "010".to_f.should == 10 + "0o10".to_f.should == 0 + "0d10".to_f.should == 0 + "0x10".to_f.should == 0 + end + + it "treats any non-numeric character other than '.', 'e' and '_' as terminals" do + "blah".to_f.should == 0 + "1b5".to_f.should == 1 + "1d5".to_f.should == 1 + "1o5".to_f.should == 1 + "1xx5".to_f.should == 1 + "x5".to_f.should == 0 + end + + it "takes an optional sign" do + "-45.67 degrees".to_f.should == -45.67 + "+45.67 degrees".to_f.should == 45.67 + "-5_5e-5_0".to_f.should == -55e-50 + "-".to_f.should == 0.0 + (1.0 / "-0".to_f).to_s.should == "-Infinity" + end + + it "treats a second 'e' as terminal" do + "1.234e1e2".to_f.should == 1.234e1 + end + + it "treats a second '.' as terminal" do + "1.2.3".to_f.should == 1.2 + end + + it "treats a '.' after an 'e' as terminal" do + "1.234e1.9".to_f.should == 1.234e1 + end + + it "returns 0.0 if the conversion fails" do + "bad".to_f.should == 0.0 + "thx1138".to_f.should == 0.0 + end + + it "ignores leading and trailing whitespace" do + " 1.2".to_f.should == 1.2 + "1.2 ".to_f.should == 1.2 + " 1.2 ".to_f.should == 1.2 + "\t1.2".to_f.should == 1.2 + "\n1.2".to_f.should == 1.2 + "\v1.2".to_f.should == 1.2 + "\f1.2".to_f.should == 1.2 + "\r1.2".to_f.should == 1.2 + end + + it "treats non-printable ASCII characters as terminals" do + "\0001.2".to_f.should == 0 + "\0011.2".to_f.should == 0 + "\0371.2".to_f.should == 0 + "\1771.2".to_f.should == 0 + "\2001.2".b.to_f.should == 0 + "\3771.2".b.to_f.should == 0 + end + + ruby_version_is "3.2.3" do + it "raises Encoding::CompatibilityError if String is in not ASCII-compatible encoding" do + -> { + '1.2'.encode("UTF-16").to_f + }.should raise_error(Encoding::CompatibilityError, "ASCII incompatible encoding: UTF-16") + end + end + + it "allows String representation without a fractional part" do + "1.".to_f.should == 1.0 + "+1.".to_f.should == 1.0 + "-1.".to_f.should == -1.0 + "1.e+0".to_f.should == 1.0 + "1.e+0".to_f.should == 1.0 + + ruby_bug "#20705", ""..."3.4" do + "1.e-2".to_f.should be_close(0.01, TOLERANCE) + end + end +end diff --git a/spec/ruby/core/string/to_i_spec.rb b/spec/ruby/core/string/to_i_spec.rb new file mode 100644 index 0000000000..39f69acda3 --- /dev/null +++ b/spec/ruby/core/string/to_i_spec.rb @@ -0,0 +1,349 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "String#to_i" do + it "returns 0 for strings with leading underscores" do + "_123".to_i.should == 0 + end + + it "ignores underscores in between the digits" do + "1_2_3asdf".to_i.should == 123 + end + + it "ignores multiple non-consecutive underscores when the first digit is 0" do + (2..16).each do |base| + "0_0_010".to_i(base).should == base; + end + end + + it "bails out at the first double underscore if the first digit is 0" do + (2..16).each do |base| + "010__1".to_i(base).should == base; + end + end + + it "ignores leading whitespaces" do + [ " 123", " 123", "\r\n\r\n123", "\t\t123", + "\r\n\t\n123", " \t\n\r\t 123"].each do |str| + str.to_i.should == 123 + end + end + + it "ignores subsequent invalid characters" do + "123asdf".to_i.should == 123 + "123#123".to_i.should == 123 + "123 456".to_i.should == 123 + end + + it "returns 0 if self is no valid integer-representation" do + [ "++2", "+-2", "--2" ].each do |str| + str.to_i.should == 0 + end + end + + it "accepts '+' at the beginning of a String" do + "+0d56".to_i.should == 56 + end + + it "interprets leading characters as a number in the given base" do + "100110010010".to_i(2).should == 0b100110010010 + "100110201001".to_i(3).should == 186409 + "103110201001".to_i(4).should == 5064769 + "103110241001".to_i(5).should == 55165126 + "153110241001".to_i(6).should == 697341529 + "153160241001".to_i(7).should == 3521513430 + "153160241701".to_i(8).should == 14390739905 + "853160241701".to_i(9).should == 269716550518 + "853160241791".to_i(10).should == 853160241791 + + "F00D_BE_1337".to_i(16).should == 0xF00D_BE_1337 + "-hello_world".to_i(32).should == -18306744 + "abcXYZ".to_i(36).should == 623741435 + + ("z" * 24).to_i(36).should == 22452257707354557240087211123792674815 + + "5e10".to_i.should == 5 + end + + it "auto-detects base 8 via leading 0 when base = 0" do + "01778".to_i(0).should == 0177 + "-01778".to_i(0).should == -0177 + end + + it "auto-detects base 2 via 0b when base = 0" do + "0b112".to_i(0).should == 0b11 + "-0b112".to_i(0).should == -0b11 + end + + it "auto-detects base 10 via 0d when base = 0" do + "0d19A".to_i(0).should == 19 + "-0d19A".to_i(0).should == -19 + end + + it "auto-detects base 8 via 0o when base = 0" do + "0o178".to_i(0).should == 0o17 + "-0o178".to_i(0).should == -0o17 + end + + it "auto-detects base 16 via 0x when base = 0" do + "0xFAZ".to_i(0).should == 0xFA + "-0xFAZ".to_i(0).should == -0xFA + end + + it "auto-detects base 10 with no base specifier when base = 0" do + "1234567890ABC".to_i(0).should == 1234567890 + "-1234567890ABC".to_i(0).should == -1234567890 + end + + it "doesn't handle foreign base specifiers when base is > 0" do + [2, 3, 4, 8, 10].each do |base| + "0111".to_i(base).should == "111".to_i(base) + + "0b11".to_i(base).should == (base == 2 ? 0b11 : 0) + "0d11".to_i(base).should == (base == 10 ? 0d11 : 0) + "0o11".to_i(base).should == (base == 8 ? 0o11 : 0) + "0xFA".to_i(base).should == 0 + end + + "0xD00D".to_i(16).should == 0xD00D + + "0b11".to_i(16).should == 0xb11 + "0d11".to_i(16).should == 0xd11 + "0o11".to_i(25).should == 15026 + "0x11".to_i(34).should == 38183 + + "0B11".to_i(16).should == 0xb11 + "0D11".to_i(16).should == 0xd11 + "0O11".to_i(25).should == 15026 + "0X11".to_i(34).should == 38183 + end + + it "tries to convert the base to an integer using to_int" do + obj = mock('8') + obj.should_receive(:to_int).and_return(8) + + "777".to_i(obj).should == 0777 + end + + it "requires that the sign if any appears before the base specifier" do + "0b-1".to_i( 2).should == 0 + "0d-1".to_i(10).should == 0 + "0o-1".to_i( 8).should == 0 + "0x-1".to_i(16).should == 0 + + "0b-1".to_i(2).should == 0 + "0o-1".to_i(8).should == 0 + "0d-1".to_i(10).should == 0 + "0x-1".to_i(16).should == 0 + end + + it "raises an ArgumentError for illegal bases (1, < 0 or > 36)" do + -> { "".to_i(1) }.should raise_error(ArgumentError) + -> { "".to_i(-1) }.should raise_error(ArgumentError) + -> { "".to_i(37) }.should raise_error(ArgumentError) + end + + it "returns an Integer for long strings with trailing spaces" do + "0 ".to_i.should == 0 + "0 ".to_i.should be_an_instance_of(Integer) + + "10 ".to_i.should == 10 + "10 ".to_i.should be_an_instance_of(Integer) + + "-10 ".to_i.should == -10 + "-10 ".to_i.should be_an_instance_of(Integer) + end + + it "returns an Integer for long strings with leading spaces" do + " 0".to_i.should == 0 + " 0".to_i.should be_an_instance_of(Integer) + + " 10".to_i.should == 10 + " 10".to_i.should be_an_instance_of(Integer) + + " -10".to_i.should == -10 + " -10".to_i.should be_an_instance_of(Integer) + end + + it "returns the correct Integer for long strings" do + "245789127594125924165923648312749312749327482".to_i.should == 245789127594125924165923648312749312749327482 + "-245789127594125924165923648312749312749327482".to_i.should == -245789127594125924165923648312749312749327482 + end +end + +describe "String#to_i with bases" do + it "parses a String in base 2" do + str = "10" * 50 + str.to_i(2).to_s(2).should == str + end + + it "parses a String in base 3" do + str = "120" * 33 + str.to_i(3).to_s(3).should == str + end + + it "parses a String in base 4" do + str = "1230" * 25 + str.to_i(4).to_s(4).should == str + end + + it "parses a String in base 5" do + str = "12340" * 20 + str.to_i(5).to_s(5).should == str + end + + it "parses a String in base 6" do + str = "123450" * 16 + str.to_i(6).to_s(6).should == str + end + + it "parses a String in base 7" do + str = "1234560" * 14 + str.to_i(7).to_s(7).should == str + end + + it "parses a String in base 8" do + str = "12345670" * 12 + str.to_i(8).to_s(8).should == str + end + + it "parses a String in base 9" do + str = "123456780" * 11 + str.to_i(9).to_s(9).should == str + end + + it "parses a String in base 10" do + str = "1234567890" * 10 + str.to_i(10).to_s(10).should == str + end + + it "parses a String in base 11" do + str = "1234567890a" * 9 + str.to_i(11).to_s(11).should == str + end + + it "parses a String in base 12" do + str = "1234567890ab" * 8 + str.to_i(12).to_s(12).should == str + end + + it "parses a String in base 13" do + str = "1234567890abc" * 7 + str.to_i(13).to_s(13).should == str + end + + it "parses a String in base 14" do + str = "1234567890abcd" * 7 + str.to_i(14).to_s(14).should == str + end + + it "parses a String in base 15" do + str = "1234567890abcde" * 6 + str.to_i(15).to_s(15).should == str + end + + it "parses a String in base 16" do + str = "1234567890abcdef" * 6 + str.to_i(16).to_s(16).should == str + end + + it "parses a String in base 17" do + str = "1234567890abcdefg" * 5 + str.to_i(17).to_s(17).should == str + end + + it "parses a String in base 18" do + str = "1234567890abcdefgh" * 5 + str.to_i(18).to_s(18).should == str + end + + it "parses a String in base 19" do + str = "1234567890abcdefghi" * 5 + str.to_i(19).to_s(19).should == str + end + + it "parses a String in base 20" do + str = "1234567890abcdefghij" * 5 + str.to_i(20).to_s(20).should == str + end + + it "parses a String in base 21" do + str = "1234567890abcdefghijk" * 4 + str.to_i(21).to_s(21).should == str + end + + it "parses a String in base 22" do + str = "1234567890abcdefghijkl" * 4 + str.to_i(22).to_s(22).should == str + end + + it "parses a String in base 23" do + str = "1234567890abcdefghijklm" * 4 + str.to_i(23).to_s(23).should == str + end + + it "parses a String in base 24" do + str = "1234567890abcdefghijklmn" * 4 + str.to_i(24).to_s(24).should == str + end + + it "parses a String in base 25" do + str = "1234567890abcdefghijklmno" * 4 + str.to_i(25).to_s(25).should == str + end + + it "parses a String in base 26" do + str = "1234567890abcdefghijklmnop" * 3 + str.to_i(26).to_s(26).should == str + end + + it "parses a String in base 27" do + str = "1234567890abcdefghijklmnopq" * 3 + str.to_i(27).to_s(27).should == str + end + + it "parses a String in base 28" do + str = "1234567890abcdefghijklmnopqr" * 3 + str.to_i(28).to_s(28).should == str + end + + it "parses a String in base 29" do + str = "1234567890abcdefghijklmnopqrs" * 3 + str.to_i(29).to_s(29).should == str + end + + it "parses a String in base 30" do + str = "1234567890abcdefghijklmnopqrst" * 3 + str.to_i(30).to_s(30).should == str + end + + it "parses a String in base 31" do + str = "1234567890abcdefghijklmnopqrstu" * 3 + str.to_i(31).to_s(31).should == str + end + + it "parses a String in base 32" do + str = "1234567890abcdefghijklmnopqrstuv" * 3 + str.to_i(32).to_s(32).should == str + end + + it "parses a String in base 33" do + str = "1234567890abcdefghijklmnopqrstuvw" * 3 + str.to_i(33).to_s(33).should == str + end + + it "parses a String in base 34" do + str = "1234567890abcdefghijklmnopqrstuvwx" * 2 + str.to_i(34).to_s(34).should == str + end + + it "parses a String in base 35" do + str = "1234567890abcdefghijklmnopqrstuvwxy" * 2 + str.to_i(35).to_s(35).should == str + end + + it "parses a String in base 36" do + str = "1234567890abcdefghijklmnopqrstuvwxyz" * 2 + str.to_i(36).to_s(36).should == str + end +end diff --git a/spec/ruby/core/string/to_r_spec.rb b/spec/ruby/core/string/to_r_spec.rb new file mode 100644 index 0000000000..4ffbb10d98 --- /dev/null +++ b/spec/ruby/core/string/to_r_spec.rb @@ -0,0 +1,62 @@ +require_relative '../../spec_helper' + +describe "String#to_r" do + it "returns a Rational object" do + String.new.to_r.should be_an_instance_of(Rational) + end + + it "returns (0/1) for the empty String" do + "".to_r.should == Rational(0, 1) + end + + it "returns (n/1) for a String starting with a decimal _n_" do + "2".to_r.should == Rational(2, 1) + "1765".to_r.should == Rational(1765, 1) + end + + it "ignores trailing characters" do + "2 foo".to_r.should == Rational(2, 1) + "1765, ".to_r.should == Rational(1765, 1) + end + + it "ignores leading spaces" do + " 2".to_r.should == Rational(2, 1) + " 1765, ".to_r.should == Rational(1765, 1) + end + + it "does not ignore arbitrary, non-numeric leading characters" do + "The rational form of 33 is...".to_r.should_not == Rational(33, 1) + "a1765, ".to_r.should_not == Rational(1765, 1) + end + + it "treats leading hyphen as minus signs" do + "-20".to_r.should == Rational(-20, 1) + end + + it "accepts leading plus signs" do + "+20".to_r.should == Rational(20, 1) + end + + it "does not treat a leading period without a numeric prefix as a decimal point" do + ".9".to_r.should_not == Rational(8106479329266893, 9007199254740992) + end + + it "understands decimal points" do + "3.33".to_r.should == Rational(333, 100) + "-3.33".to_r.should == Rational(-333, 100) + end + + it "ignores underscores between numbers" do + "190_22".to_r.should == Rational(19022, 1) + "-190_22.7".to_r.should == Rational(-190227, 10) + end + + it "understands a forward slash as separating the numerator from the denominator" do + "20/3".to_r.should == Rational(20, 3) + " -19.10/3".to_r.should == Rational(-191, 30) + end + + it "returns (0/1) for Strings it can't parse" do + "glark".to_r.should == Rational(0,1) + end +end diff --git a/spec/ruby/core/string/to_s_spec.rb b/spec/ruby/core/string/to_s_spec.rb new file mode 100644 index 0000000000..e5872745a8 --- /dev/null +++ b/spec/ruby/core/string/to_s_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/to_s' + +describe "String#to_s" do + it_behaves_like :string_to_s, :to_s +end diff --git a/spec/ruby/core/string/to_str_spec.rb b/spec/ruby/core/string/to_str_spec.rb new file mode 100644 index 0000000000..e24262a7ae --- /dev/null +++ b/spec/ruby/core/string/to_str_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/to_s' + +describe "String#to_str" do + it_behaves_like :string_to_s, :to_str +end diff --git a/spec/ruby/core/string/to_sym_spec.rb b/spec/ruby/core/string/to_sym_spec.rb new file mode 100644 index 0000000000..f9135211ce --- /dev/null +++ b/spec/ruby/core/string/to_sym_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/to_sym' + +describe "String#to_sym" do + it_behaves_like :string_to_sym, :to_sym +end diff --git a/spec/ruby/core/string/tr_s_spec.rb b/spec/ruby/core/string/tr_s_spec.rb new file mode 100644 index 0000000000..dd72da440c --- /dev/null +++ b/spec/ruby/core/string/tr_s_spec.rb @@ -0,0 +1,133 @@ +# -*- encoding: utf-8 -*- +# frozen_string_literal: false +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "String#tr_s" do + it "returns a string processed according to tr with newly duplicate characters removed" do + "hello".tr_s('l', 'r').should == "hero" + "hello".tr_s('el', '*').should == "h*o" + "hello".tr_s('el', 'hx').should == "hhxo" + "hello".tr_s('o', '.').should == "hell." + end + + it "accepts c1-c2 notation to denote ranges of characters" do + "hello".tr_s('a-y', 'b-z').should == "ifmp" + "123456789".tr_s("2-5", "abcdefg").should == "1abcd6789" + "hello ^--^".tr_s("e-", "__").should == "h_llo ^_^" + "hello ^--^".tr_s("---", "_").should == "hello ^_^" + end + + ruby_bug "#19769", ""..."3.3" do + it "accepts c1-c1 notation to denote range of one character" do + "hello".tr_s('e-e', 'x').should == "hxllo" + "123456789".tr_s("2-23","xy").should == "1xy456789" + "hello ^-^".tr_s("e-", "a-a_").should == "hallo ^_^" + "hello ^-^".tr_s("---o", "_a").should == "hella ^_^" + end + end + + it "pads to_str with its last char if it is shorter than from_string" do + "this".tr_s("this", "x").should == "x" + end + + it "translates chars not in from_string when it starts with a ^" do + "hello".tr_s('^aeiou', '*').should == "*e*o" + "123456789".tr_s("^345", "abc").should == "c345c" + "abcdefghijk".tr_s("^d-g", "9131").should == "1defg1" + + "hello ^_^".tr_s("a-e^e", ".").should == "h.llo ._." + "hello ^_^".tr_s("^^", ".").should == ".^.^" + "hello ^_^".tr_s("^", "x").should == "hello x_x" + "hello ^-^".tr_s("^-^", "x").should == "x^-^" + "hello ^-^".tr_s("^^-^", "x").should == "x^x^" + "hello ^-^".tr_s("^---", "x").should == "x-x" + "hello ^-^".tr_s("^---l-o", "x").should == "xllox-x" + end + + it "tries to convert from_str and to_str to strings using to_str" do + from_str = mock('ab') + from_str.should_receive(:to_str).and_return("ab") + + to_str = mock('AB') + to_str.should_receive(:to_str).and_return("AB") + + "bla".tr_s(from_str, to_str).should == "BlA" + end + + it "returns String instances when called on a subclass" do + StringSpecs::MyString.new("hello").tr_s("e", "a").should be_an_instance_of(String) + end + + # http://redmine.ruby-lang.org/issues/show/1839 + it "can replace a 7-bit ASCII character with a multibyte one" do + a = "uber" + a.encoding.should == Encoding::UTF_8 + b = a.tr_s("u","ü") + b.should == "über" + b.encoding.should == Encoding::UTF_8 + end + + it "can replace multiple 7-bit ASCII characters with a multibyte one" do + a = "uuuber" + a.encoding.should == Encoding::UTF_8 + b = a.tr_s("u","ü") + b.should == "über" + b.encoding.should == Encoding::UTF_8 + end + + it "can replace a multibyte character with a single byte one" do + a = "über" + a.encoding.should == Encoding::UTF_8 + b = a.tr_s("ü","u") + b.should == "uber" + b.encoding.should == Encoding::UTF_8 + end + + it "can replace multiple multibyte characters with a single byte one" do + a = "üüüber" + a.encoding.should == Encoding::UTF_8 + b = a.tr_s("ü","u") + b.should == "uber" + b.encoding.should == Encoding::UTF_8 + end + + it "does not replace a multibyte character where part of the bytes match the tr string" do + str = "æ¤Žåæ·±å¤" + a = "\u0080\u0082\u0083\u0084\u0085\u0086\u0087\u0088\u0089\u008A\u008B\u008C\u008E\u0091\u0092\u0093\u0094\u0095\u0096\u0097\u0098\u0099\u009A\u009B\u009C\u009E\u009F" + b = "€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“â€â€¢â€“—˜™š›œžŸ" + str.tr_s(a, b).should == "æ¤Žåæ·±å¤" + end + + +end + +describe "String#tr_s!" do + it "modifies self in place" do + s = "hello" + s.tr_s!("l", "r").should == "hero" + s.should == "hero" + end + + it "returns nil if no modification was made" do + s = "hello" + s.tr_s!("za", "yb").should == nil + s.tr_s!("", "").should == nil + s.should == "hello" + end + + it "does not modify self if from_str is empty" do + s = "hello" + s.tr_s!("", "").should == nil + s.should == "hello" + s.tr_s!("", "yb").should == nil + s.should == "hello" + end + + it "raises a FrozenError if self is frozen" do + s = "hello".freeze + -> { s.tr_s!("el", "ar") }.should raise_error(FrozenError) + -> { s.tr_s!("l", "r") }.should raise_error(FrozenError) + -> { s.tr_s!("", "") }.should raise_error(FrozenError) + end +end diff --git a/spec/ruby/core/string/tr_spec.rb b/spec/ruby/core/string/tr_spec.rb new file mode 100644 index 0000000000..75841a974f --- /dev/null +++ b/spec/ruby/core/string/tr_spec.rb @@ -0,0 +1,128 @@ +# -*- encoding: utf-8 -*- +# frozen_string_literal: false +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "String#tr" do + it "returns a new string with the characters from from_string replaced by the ones in to_string" do + "hello".tr('aeiou', '*').should == "h*ll*" + "hello".tr('el', 'ip').should == "hippo" + "Lisp".tr("Lisp", "Ruby").should == "Ruby" + end + + it "accepts c1-c2 notation to denote ranges of characters" do + "hello".tr('a-y', 'b-z').should == "ifmmp" + "123456789".tr("2-5","abcdefg").should == "1abcd6789" + "hello ^-^".tr("e-", "__").should == "h_llo ^_^" + "hello ^-^".tr("---", "_").should == "hello ^_^" + end + + ruby_bug "#19769", ""..."3.3" do + it "accepts c1-c1 notation to denote range of one character" do + "hello".tr('e-e', 'x').should == "hxllo" + "123456789".tr("2-23","xy").should == "1xy456789" + "hello ^-^".tr("e-", "a-a_").should == "hallo ^_^" + "hello ^-^".tr("---o", "_a").should == "hella ^_^" + end + end + + it "pads to_str with its last char if it is shorter than from_string" do + "this".tr("this", "x").should == "xxxx" + "hello".tr("a-z", "A-H.").should == "HE..." + end + + it "raises an ArgumentError a descending range in the replacement as containing just the start character" do + -> { "hello".tr("a-y", "z-b") }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError a descending range in the source as empty" do + -> { "hello".tr("l-a", "z") }.should raise_error(ArgumentError) + end + + it "translates chars not in from_string when it starts with a ^" do + "hello".tr('^aeiou', '*').should == "*e**o" + "123456789".tr("^345", "abc").should == "cc345cccc" + "abcdefghijk".tr("^d-g", "9131").should == "111defg1111" + + "hello ^_^".tr("a-e^e", ".").should == "h.llo ._." + "hello ^_^".tr("^^", ".").should == "......^.^" + "hello ^_^".tr("^", "x").should == "hello x_x" + "hello ^-^".tr("^-^", "x").should == "xxxxxx^-^" + "hello ^-^".tr("^^-^", "x").should == "xxxxxx^x^" + "hello ^-^".tr("^---", "x").should == "xxxxxxx-x" + "hello ^-^".tr("^---l-o", "x").should == "xxlloxx-x" + end + + it "supports non-injective replacements" do + "hello".tr("helo", "1212").should == "12112" + end + + it "tries to convert from_str and to_str to strings using to_str" do + from_str = mock('ab') + from_str.should_receive(:to_str).and_return("ab") + + to_str = mock('AB') + to_str.should_receive(:to_str).and_return("AB") + + "bla".tr(from_str, to_str).should == "BlA" + end + + it "returns Stringinstances when called on a subclass" do + StringSpecs::MyString.new("hello").tr("e", "a").should be_an_instance_of(String) + end + + # http://redmine.ruby-lang.org/issues/show/1839 + it "can replace a 7-bit ASCII character with a multibyte one" do + a = "uber" + a.encoding.should == Encoding::UTF_8 + b = a.tr("u","ü") + b.should == "über" + b.encoding.should == Encoding::UTF_8 + end + + it "can replace a multibyte character with a single byte one" do + a = "über" + a.encoding.should == Encoding::UTF_8 + b = a.tr("ü","u") + b.should == "uber" + b.encoding.should == Encoding::UTF_8 + end + + it "does not replace a multibyte character where part of the bytes match the tr string" do + str = "æ¤Žåæ·±å¤" + a = "\u0080\u0082\u0083\u0084\u0085\u0086\u0087\u0088\u0089\u008A\u008B\u008C\u008E\u0091\u0092\u0093\u0094\u0095\u0096\u0097\u0098\u0099\u009A\u009B\u009C\u009E\u009F" + b = "€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“â€â€¢â€“—˜™š›œžŸ" + str.tr(a, b).should == "æ¤Žåæ·±å¤" + end + +end + +describe "String#tr!" do + it "modifies self in place" do + s = "abcdefghijklmnopqR" + s.tr!("cdefg", "12").should == "ab12222hijklmnopqR" + s.should == "ab12222hijklmnopqR" + end + + it "returns nil if no modification was made" do + s = "hello" + s.tr!("za", "yb").should == nil + s.tr!("", "").should == nil + s.should == "hello" + end + + it "does not modify self if from_str is empty" do + s = "hello" + s.tr!("", "").should == nil + s.should == "hello" + s.tr!("", "yb").should == nil + s.should == "hello" + end + + it "raises a FrozenError if self is frozen" do + s = "abcdefghijklmnopqR".freeze + -> { s.tr!("cdefg", "12") }.should raise_error(FrozenError) + -> { s.tr!("R", "S") }.should raise_error(FrozenError) + -> { s.tr!("", "") }.should raise_error(FrozenError) + end +end diff --git a/spec/ruby/core/string/try_convert_spec.rb b/spec/ruby/core/string/try_convert_spec.rb new file mode 100644 index 0000000000..72ce5dd8b2 --- /dev/null +++ b/spec/ruby/core/string/try_convert_spec.rb @@ -0,0 +1,50 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "String.try_convert" do + it "returns the argument if it's a String" do + x = String.new + String.try_convert(x).should equal(x) + end + + it "returns the argument if it's a kind of String" do + x = StringSpecs::MyString.new + String.try_convert(x).should equal(x) + end + + it "returns nil when the argument does not respond to #to_str" do + String.try_convert(Object.new).should be_nil + end + + it "sends #to_str to the argument and returns the result if it's nil" do + obj = mock("to_str") + obj.should_receive(:to_str).and_return(nil) + String.try_convert(obj).should be_nil + end + + it "sends #to_str to the argument and returns the result if it's a String" do + x = String.new + obj = mock("to_str") + obj.should_receive(:to_str).and_return(x) + String.try_convert(obj).should equal(x) + end + + it "sends #to_str to the argument and returns the result if it's a kind of String" do + x = StringSpecs::MyString.new + obj = mock("to_str") + obj.should_receive(:to_str).and_return(x) + String.try_convert(obj).should equal(x) + end + + it "sends #to_str to the argument and raises TypeError if it's not a kind of String" do + obj = mock("to_str") + obj.should_receive(:to_str).and_return(Object.new) + -> { String.try_convert obj }.should raise_error(TypeError, "can't convert MockObject to String (MockObject#to_str gives Object)") + end + + it "does not rescue exceptions raised by #to_str" do + obj = mock("to_str") + obj.should_receive(:to_str).and_raise(RuntimeError) + -> { String.try_convert obj }.should raise_error(RuntimeError) + end +end diff --git a/spec/ruby/core/string/uminus_spec.rb b/spec/ruby/core/string/uminus_spec.rb new file mode 100644 index 0000000000..46d88f6704 --- /dev/null +++ b/spec/ruby/core/string/uminus_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/dedup' + +describe 'String#-@' do + it_behaves_like :string_dedup, :-@ +end diff --git a/spec/ruby/core/string/undump_spec.rb b/spec/ruby/core/string/undump_spec.rb new file mode 100644 index 0000000000..6ff220161c --- /dev/null +++ b/spec/ruby/core/string/undump_spec.rb @@ -0,0 +1,441 @@ +# encoding: utf-8 +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "String#undump" do + it "does not take into account if a string is frozen" do + '"foo"'.freeze.undump.should_not.frozen? + end + + it "always returns String instance" do + StringSpecs::MyString.new('"foo"').undump.should be_an_instance_of(String) + end + + it "strips outer \"" do + '"foo"'.undump.should == 'foo' + end + + it "returns a string with special characters in \\<char> notation replaced with the characters" do + [ ['"\\a"', "\a"], + ['"\\b"', "\b"], + ['"\\t"', "\t"], + ['"\\n"', "\n"], + ['"\\v"', "\v"], + ['"\\f"', "\f"], + ['"\\r"', "\r"], + ['"\\e"', "\e"] + ].should be_computed_by(:undump) + end + + it "returns a string with unescaped sequences \" and \\" do + [ ['"\\""' , "\""], + ['"\\\\"', "\\"] + ].should be_computed_by(:undump) + end + + it "returns a string with unescaped sequences \\#<char> when # is followed by $, @, {" do + [ ['"\\#$PATH"', "\#$PATH"], + ['"\\#@a"', "\#@a"], + ['"\\#@@a"', "\#@@a"], + ['"\\#{a}"', "\#{a}"] + ].should be_computed_by(:undump) + end + + it "returns a string with # not escaped when followed by any other character" do + [ ['"#"', '#'], + ['"#1"', '#1'] + ].should be_computed_by(:undump) + end + + it "returns a string with printable non-alphanumeric characters" do + [ ['" "', ' '], + ['"!"', '!'], + ['"$"', '$'], + ['"%"', '%'], + ['"&"', '&'], + ['"\'"', '\''], + ['"("', '('], + ['")"', ')'], + ['"*"', '*'], + ['"+"', '+'], + ['","', ','], + ['"-"', '-'], + ['"."', '.'], + ['"/"', '/'], + ['":"', ':'], + ['";"', ';'], + ['"<"', '<'], + ['"="', '='], + ['">"', '>'], + ['"?"', '?'], + ['"@"', '@'], + ['"["', '['], + ['"]"', ']'], + ['"^"', '^'], + ['"_"', '_'], + ['"`"', '`'], + ['"{"', '{'], + ['"|"', '|'], + ['"}"', '}'], + ['"~"', '~'] + ].should be_computed_by(:undump) + end + + it "returns a string with numeric characters unescaped" do + [ ['"0"', "0"], + ['"1"', "1"], + ['"2"', "2"], + ['"3"', "3"], + ['"4"', "4"], + ['"5"', "5"], + ['"6"', "6"], + ['"7"', "7"], + ['"8"', "8"], + ['"9"', "9"], + ].should be_computed_by(:undump) + end + + it "returns a string with upper-case alpha characters unescaped" do + [ ['"A"', 'A'], + ['"B"', 'B'], + ['"C"', 'C'], + ['"D"', 'D'], + ['"E"', 'E'], + ['"F"', 'F'], + ['"G"', 'G'], + ['"H"', 'H'], + ['"I"', 'I'], + ['"J"', 'J'], + ['"K"', 'K'], + ['"L"', 'L'], + ['"M"', 'M'], + ['"N"', 'N'], + ['"O"', 'O'], + ['"P"', 'P'], + ['"Q"', 'Q'], + ['"R"', 'R'], + ['"S"', 'S'], + ['"T"', 'T'], + ['"U"', 'U'], + ['"V"', 'V'], + ['"W"', 'W'], + ['"X"', 'X'], + ['"Y"', 'Y'], + ['"Z"', 'Z'] + ].should be_computed_by(:undump) + end + + it "returns a string with lower-case alpha characters unescaped" do + [ ['"a"', 'a'], + ['"b"', 'b'], + ['"c"', 'c'], + ['"d"', 'd'], + ['"e"', 'e'], + ['"f"', 'f'], + ['"g"', 'g'], + ['"h"', 'h'], + ['"i"', 'i'], + ['"j"', 'j'], + ['"k"', 'k'], + ['"l"', 'l'], + ['"m"', 'm'], + ['"n"', 'n'], + ['"o"', 'o'], + ['"p"', 'p'], + ['"q"', 'q'], + ['"r"', 'r'], + ['"s"', 's'], + ['"t"', 't'], + ['"u"', 'u'], + ['"v"', 'v'], + ['"w"', 'w'], + ['"x"', 'x'], + ['"y"', 'y'], + ['"z"', 'z'] + ].should be_computed_by(:undump) + end + + it "returns a string with \\x notation replaced with non-printing ASCII character" do + [ ['"\\x00"', 0000.chr.force_encoding('utf-8')], + ['"\\x01"', 0001.chr.force_encoding('utf-8')], + ['"\\x02"', 0002.chr.force_encoding('utf-8')], + ['"\\x03"', 0003.chr.force_encoding('utf-8')], + ['"\\x04"', 0004.chr.force_encoding('utf-8')], + ['"\\x05"', 0005.chr.force_encoding('utf-8')], + ['"\\x06"', 0006.chr.force_encoding('utf-8')], + ['"\\x0E"', 0016.chr.force_encoding('utf-8')], + ['"\\x0F"', 0017.chr.force_encoding('utf-8')], + ['"\\x10"', 0020.chr.force_encoding('utf-8')], + ['"\\x11"', 0021.chr.force_encoding('utf-8')], + ['"\\x12"', 0022.chr.force_encoding('utf-8')], + ['"\\x13"', 0023.chr.force_encoding('utf-8')], + ['"\\x14"', 0024.chr.force_encoding('utf-8')], + ['"\\x15"', 0025.chr.force_encoding('utf-8')], + ['"\\x16"', 0026.chr.force_encoding('utf-8')], + ['"\\x17"', 0027.chr.force_encoding('utf-8')], + ['"\\x18"', 0030.chr.force_encoding('utf-8')], + ['"\\x19"', 0031.chr.force_encoding('utf-8')], + ['"\\x1A"', 0032.chr.force_encoding('utf-8')], + ['"\\x1C"', 0034.chr.force_encoding('utf-8')], + ['"\\x1D"', 0035.chr.force_encoding('utf-8')], + ['"\\x1E"', 0036.chr.force_encoding('utf-8')], + ['"\\x1F"', 0037.chr.force_encoding('utf-8')], + ['"\\x7F"', 0177.chr.force_encoding('utf-8')], + ['"\\x80"', 0200.chr.force_encoding('utf-8')], + ['"\\x81"', 0201.chr.force_encoding('utf-8')], + ['"\\x82"', 0202.chr.force_encoding('utf-8')], + ['"\\x83"', 0203.chr.force_encoding('utf-8')], + ['"\\x84"', 0204.chr.force_encoding('utf-8')], + ['"\\x85"', 0205.chr.force_encoding('utf-8')], + ['"\\x86"', 0206.chr.force_encoding('utf-8')], + ['"\\x87"', 0207.chr.force_encoding('utf-8')], + ['"\\x88"', 0210.chr.force_encoding('utf-8')], + ['"\\x89"', 0211.chr.force_encoding('utf-8')], + ['"\\x8A"', 0212.chr.force_encoding('utf-8')], + ['"\\x8B"', 0213.chr.force_encoding('utf-8')], + ['"\\x8C"', 0214.chr.force_encoding('utf-8')], + ['"\\x8D"', 0215.chr.force_encoding('utf-8')], + ['"\\x8E"', 0216.chr.force_encoding('utf-8')], + ['"\\x8F"', 0217.chr.force_encoding('utf-8')], + ['"\\x90"', 0220.chr.force_encoding('utf-8')], + ['"\\x91"', 0221.chr.force_encoding('utf-8')], + ['"\\x92"', 0222.chr.force_encoding('utf-8')], + ['"\\x93"', 0223.chr.force_encoding('utf-8')], + ['"\\x94"', 0224.chr.force_encoding('utf-8')], + ['"\\x95"', 0225.chr.force_encoding('utf-8')], + ['"\\x96"', 0226.chr.force_encoding('utf-8')], + ['"\\x97"', 0227.chr.force_encoding('utf-8')], + ['"\\x98"', 0230.chr.force_encoding('utf-8')], + ['"\\x99"', 0231.chr.force_encoding('utf-8')], + ['"\\x9A"', 0232.chr.force_encoding('utf-8')], + ['"\\x9B"', 0233.chr.force_encoding('utf-8')], + ['"\\x9C"', 0234.chr.force_encoding('utf-8')], + ['"\\x9D"', 0235.chr.force_encoding('utf-8')], + ['"\\x9E"', 0236.chr.force_encoding('utf-8')], + ['"\\x9F"', 0237.chr.force_encoding('utf-8')], + ['"\\xA0"', 0240.chr.force_encoding('utf-8')], + ['"\\xA1"', 0241.chr.force_encoding('utf-8')], + ['"\\xA2"', 0242.chr.force_encoding('utf-8')], + ['"\\xA3"', 0243.chr.force_encoding('utf-8')], + ['"\\xA4"', 0244.chr.force_encoding('utf-8')], + ['"\\xA5"', 0245.chr.force_encoding('utf-8')], + ['"\\xA6"', 0246.chr.force_encoding('utf-8')], + ['"\\xA7"', 0247.chr.force_encoding('utf-8')], + ['"\\xA8"', 0250.chr.force_encoding('utf-8')], + ['"\\xA9"', 0251.chr.force_encoding('utf-8')], + ['"\\xAA"', 0252.chr.force_encoding('utf-8')], + ['"\\xAB"', 0253.chr.force_encoding('utf-8')], + ['"\\xAC"', 0254.chr.force_encoding('utf-8')], + ['"\\xAD"', 0255.chr.force_encoding('utf-8')], + ['"\\xAE"', 0256.chr.force_encoding('utf-8')], + ['"\\xAF"', 0257.chr.force_encoding('utf-8')], + ['"\\xB0"', 0260.chr.force_encoding('utf-8')], + ['"\\xB1"', 0261.chr.force_encoding('utf-8')], + ['"\\xB2"', 0262.chr.force_encoding('utf-8')], + ['"\\xB3"', 0263.chr.force_encoding('utf-8')], + ['"\\xB4"', 0264.chr.force_encoding('utf-8')], + ['"\\xB5"', 0265.chr.force_encoding('utf-8')], + ['"\\xB6"', 0266.chr.force_encoding('utf-8')], + ['"\\xB7"', 0267.chr.force_encoding('utf-8')], + ['"\\xB8"', 0270.chr.force_encoding('utf-8')], + ['"\\xB9"', 0271.chr.force_encoding('utf-8')], + ['"\\xBA"', 0272.chr.force_encoding('utf-8')], + ['"\\xBB"', 0273.chr.force_encoding('utf-8')], + ['"\\xBC"', 0274.chr.force_encoding('utf-8')], + ['"\\xBD"', 0275.chr.force_encoding('utf-8')], + ['"\\xBE"', 0276.chr.force_encoding('utf-8')], + ['"\\xBF"', 0277.chr.force_encoding('utf-8')], + ['"\\xC0"', 0300.chr.force_encoding('utf-8')], + ['"\\xC1"', 0301.chr.force_encoding('utf-8')], + ['"\\xC2"', 0302.chr.force_encoding('utf-8')], + ['"\\xC3"', 0303.chr.force_encoding('utf-8')], + ['"\\xC4"', 0304.chr.force_encoding('utf-8')], + ['"\\xC5"', 0305.chr.force_encoding('utf-8')], + ['"\\xC6"', 0306.chr.force_encoding('utf-8')], + ['"\\xC7"', 0307.chr.force_encoding('utf-8')], + ['"\\xC8"', 0310.chr.force_encoding('utf-8')], + ['"\\xC9"', 0311.chr.force_encoding('utf-8')], + ['"\\xCA"', 0312.chr.force_encoding('utf-8')], + ['"\\xCB"', 0313.chr.force_encoding('utf-8')], + ['"\\xCC"', 0314.chr.force_encoding('utf-8')], + ['"\\xCD"', 0315.chr.force_encoding('utf-8')], + ['"\\xCE"', 0316.chr.force_encoding('utf-8')], + ['"\\xCF"', 0317.chr.force_encoding('utf-8')], + ['"\\xD0"', 0320.chr.force_encoding('utf-8')], + ['"\\xD1"', 0321.chr.force_encoding('utf-8')], + ['"\\xD2"', 0322.chr.force_encoding('utf-8')], + ['"\\xD3"', 0323.chr.force_encoding('utf-8')], + ['"\\xD4"', 0324.chr.force_encoding('utf-8')], + ['"\\xD5"', 0325.chr.force_encoding('utf-8')], + ['"\\xD6"', 0326.chr.force_encoding('utf-8')], + ['"\\xD7"', 0327.chr.force_encoding('utf-8')], + ['"\\xD8"', 0330.chr.force_encoding('utf-8')], + ['"\\xD9"', 0331.chr.force_encoding('utf-8')], + ['"\\xDA"', 0332.chr.force_encoding('utf-8')], + ['"\\xDB"', 0333.chr.force_encoding('utf-8')], + ['"\\xDC"', 0334.chr.force_encoding('utf-8')], + ['"\\xDD"', 0335.chr.force_encoding('utf-8')], + ['"\\xDE"', 0336.chr.force_encoding('utf-8')], + ['"\\xDF"', 0337.chr.force_encoding('utf-8')], + ['"\\xE0"', 0340.chr.force_encoding('utf-8')], + ['"\\xE1"', 0341.chr.force_encoding('utf-8')], + ['"\\xE2"', 0342.chr.force_encoding('utf-8')], + ['"\\xE3"', 0343.chr.force_encoding('utf-8')], + ['"\\xE4"', 0344.chr.force_encoding('utf-8')], + ['"\\xE5"', 0345.chr.force_encoding('utf-8')], + ['"\\xE6"', 0346.chr.force_encoding('utf-8')], + ['"\\xE7"', 0347.chr.force_encoding('utf-8')], + ['"\\xE8"', 0350.chr.force_encoding('utf-8')], + ['"\\xE9"', 0351.chr.force_encoding('utf-8')], + ['"\\xEA"', 0352.chr.force_encoding('utf-8')], + ['"\\xEB"', 0353.chr.force_encoding('utf-8')], + ['"\\xEC"', 0354.chr.force_encoding('utf-8')], + ['"\\xED"', 0355.chr.force_encoding('utf-8')], + ['"\\xEE"', 0356.chr.force_encoding('utf-8')], + ['"\\xEF"', 0357.chr.force_encoding('utf-8')], + ['"\\xF0"', 0360.chr.force_encoding('utf-8')], + ['"\\xF1"', 0361.chr.force_encoding('utf-8')], + ['"\\xF2"', 0362.chr.force_encoding('utf-8')], + ['"\\xF3"', 0363.chr.force_encoding('utf-8')], + ['"\\xF4"', 0364.chr.force_encoding('utf-8')], + ['"\\xF5"', 0365.chr.force_encoding('utf-8')], + ['"\\xF6"', 0366.chr.force_encoding('utf-8')], + ['"\\xF7"', 0367.chr.force_encoding('utf-8')], + ['"\\xF8"', 0370.chr.force_encoding('utf-8')], + ['"\\xF9"', 0371.chr.force_encoding('utf-8')], + ['"\\xFA"', 0372.chr.force_encoding('utf-8')], + ['"\\xFB"', 0373.chr.force_encoding('utf-8')], + ['"\\xFC"', 0374.chr.force_encoding('utf-8')], + ['"\\xFD"', 0375.chr.force_encoding('utf-8')], + ['"\\xFE"', 0376.chr.force_encoding('utf-8')], + ['"\\xFF"', 0377.chr.force_encoding('utf-8')] + ].should be_computed_by(:undump) + end + + it "returns a string with \\u{} notation replaced with multi-byte UTF-8 characters" do + [ ['"\u{80}"', 0200.chr('utf-8')], + ['"\u{81}"', 0201.chr('utf-8')], + ['"\u{82}"', 0202.chr('utf-8')], + ['"\u{83}"', 0203.chr('utf-8')], + ['"\u{84}"', 0204.chr('utf-8')], + ['"\u{86}"', 0206.chr('utf-8')], + ['"\u{87}"', 0207.chr('utf-8')], + ['"\u{88}"', 0210.chr('utf-8')], + ['"\u{89}"', 0211.chr('utf-8')], + ['"\u{8a}"', 0212.chr('utf-8')], + ['"\u{8b}"', 0213.chr('utf-8')], + ['"\u{8c}"', 0214.chr('utf-8')], + ['"\u{8d}"', 0215.chr('utf-8')], + ['"\u{8e}"', 0216.chr('utf-8')], + ['"\u{8f}"', 0217.chr('utf-8')], + ['"\u{90}"', 0220.chr('utf-8')], + ['"\u{91}"', 0221.chr('utf-8')], + ['"\u{92}"', 0222.chr('utf-8')], + ['"\u{93}"', 0223.chr('utf-8')], + ['"\u{94}"', 0224.chr('utf-8')], + ['"\u{95}"', 0225.chr('utf-8')], + ['"\u{96}"', 0226.chr('utf-8')], + ['"\u{97}"', 0227.chr('utf-8')], + ['"\u{98}"', 0230.chr('utf-8')], + ['"\u{99}"', 0231.chr('utf-8')], + ['"\u{9a}"', 0232.chr('utf-8')], + ['"\u{9b}"', 0233.chr('utf-8')], + ['"\u{9c}"', 0234.chr('utf-8')], + ['"\u{9d}"', 0235.chr('utf-8')], + ['"\u{9e}"', 0236.chr('utf-8')], + ['"\u{9f}"', 0237.chr('utf-8')], + ].should be_computed_by(:undump) + end + + it "returns a string with \\uXXXX notation replaced with multi-byte UTF-8 characters" do + [ ['"\u0080"', 0200.chr('utf-8')], + ['"\u0081"', 0201.chr('utf-8')], + ['"\u0082"', 0202.chr('utf-8')], + ['"\u0083"', 0203.chr('utf-8')], + ['"\u0084"', 0204.chr('utf-8')], + ['"\u0086"', 0206.chr('utf-8')], + ['"\u0087"', 0207.chr('utf-8')], + ['"\u0088"', 0210.chr('utf-8')], + ['"\u0089"', 0211.chr('utf-8')], + ['"\u008a"', 0212.chr('utf-8')], + ['"\u008b"', 0213.chr('utf-8')], + ['"\u008c"', 0214.chr('utf-8')], + ['"\u008d"', 0215.chr('utf-8')], + ['"\u008e"', 0216.chr('utf-8')], + ['"\u008f"', 0217.chr('utf-8')], + ['"\u0090"', 0220.chr('utf-8')], + ['"\u0091"', 0221.chr('utf-8')], + ['"\u0092"', 0222.chr('utf-8')], + ['"\u0093"', 0223.chr('utf-8')], + ['"\u0094"', 0224.chr('utf-8')], + ['"\u0095"', 0225.chr('utf-8')], + ['"\u0096"', 0226.chr('utf-8')], + ['"\u0097"', 0227.chr('utf-8')], + ['"\u0098"', 0230.chr('utf-8')], + ['"\u0099"', 0231.chr('utf-8')], + ['"\u009a"', 0232.chr('utf-8')], + ['"\u009b"', 0233.chr('utf-8')], + ['"\u009c"', 0234.chr('utf-8')], + ['"\u009d"', 0235.chr('utf-8')], + ['"\u009e"', 0236.chr('utf-8')], + ['"\u009f"', 0237.chr('utf-8')], + ].should be_computed_by(:undump) + end + + it "undumps correctly string produced from non ASCII-compatible one" do + s = "\u{876}".encode('utf-16be') + s.dump.undump.should == s + + '"\\bv".force_encoding("UTF-16BE")'.undump.should == "\u0876".encode('utf-16be') + end + + it "returns a String in the same encoding as self" do + '"foo"'.encode("ISO-8859-1").undump.encoding.should == Encoding::ISO_8859_1 + '"foo"'.encode('windows-1251').undump.encoding.should == Encoding::Windows_1251 + end + + describe "Limitations" do + it "cannot undump non ASCII-compatible string" do + -> { '"foo"'.encode('utf-16le').undump }.should raise_error(Encoding::CompatibilityError) + end + end + + describe "invalid dump" do + it "raises RuntimeError exception if wrapping \" are missing" do + -> { 'foo'.undump }.should raise_error(RuntimeError, /invalid dumped string/) + -> { '"foo'.undump }.should raise_error(RuntimeError, /unterminated dumped string/) + -> { 'foo"'.undump }.should raise_error(RuntimeError, /invalid dumped string/) + -> { "'foo'".undump }.should raise_error(RuntimeError, /invalid dumped string/) + end + + it "raises RuntimeError if there is incorrect \\x sequence" do + -> { '"\x"'.undump }.should raise_error(RuntimeError, /invalid hex escape/) + -> { '"\\x3y"'.undump }.should raise_error(RuntimeError, /invalid hex escape/) + end + + it "raises RuntimeError in there is incorrect \\u sequence" do + -> { '"\\u"'.undump }.should raise_error(RuntimeError, /invalid Unicode escape/) + -> { '"\\u{"'.undump }.should raise_error(RuntimeError, /invalid Unicode escape/) + -> { '"\\u{3042"'.undump }.should raise_error(RuntimeError, /invalid Unicode escape/) + -> { '"\\u"'.undump }.should raise_error(RuntimeError, /invalid Unicode escape/) + end + + it "raises RuntimeError if there is malformed dump of non ASCII-compatible string" do + -> { '"".force_encoding("BINARY"'.undump }.should raise_error(RuntimeError, /invalid dumped string/) + -> { '"".force_encoding("Unknown")'.undump }.should raise_error(RuntimeError, /dumped string has unknown encoding name/) + -> { '"".force_encoding()'.undump }.should raise_error(RuntimeError, /invalid dumped string/) + end + + it "raises RuntimeError if string contains \0 character" do + -> { "\"foo\0\"".undump }.should raise_error(RuntimeError, /string contains null byte/) + end + + it "raises RuntimeError if string contains non ASCII character" do + -> { "\"\u3042\"".undump }.should raise_error(RuntimeError, /non-ASCII character detected/) + end + + it "raises RuntimeError if there are some excessive \"" do + -> { '" "" "'.undump }.should raise_error(RuntimeError, /invalid dumped string/) + end + end +end diff --git a/spec/ruby/core/string/unicode_normalize_spec.rb b/spec/ruby/core/string/unicode_normalize_spec.rb new file mode 100644 index 0000000000..2e7d22394a --- /dev/null +++ b/spec/ruby/core/string/unicode_normalize_spec.rb @@ -0,0 +1,116 @@ +# -*- encoding: utf-8 -*- +# frozen_string_literal: false +require_relative '../../spec_helper' + +# Examples taken from http://www.unicode.org/reports/tr15/#Norm_Forms + +describe "String#unicode_normalize" do + before :each do + @accented_f = "\u1e9b\u0323" + @angstrom = "\u212b" + @ohm = "\u2126" + end + + it "normalizes code points in the string according to the form that is specified" do + @accented_f.unicode_normalize(:nfc).should == "\u1e9b\u0323" + @accented_f.unicode_normalize(:nfd).should == "\u017f\u0323\u0307" + @accented_f.unicode_normalize(:nfkc).should == "\u1e69" + @accented_f.unicode_normalize(:nfkd).should == "\u0073\u0323\u0307" + end + + it "defaults to the nfc normalization form if no forms are specified" do + @accented_f.unicode_normalize.should == "\u1e9b\u0323" + @angstrom.unicode_normalize.should == "\u00c5" + @ohm.unicode_normalize.should == "\u03a9" + end + + # http://unicode.org/faq/normalization.html#6 + context "returns normalized form of string by default" do + it "03D3 (Ï“) GREEK UPSILON WITH ACUTE AND HOOK SYMBOL" do + "\u03D3".unicode_normalize(:nfc).should == "\u03D3" + "\u03D3".unicode_normalize(:nfd).should == "\u03D2\u0301" + "\u03D3".unicode_normalize(:nfkc).should == "\u038E" + "\u03D3".unicode_normalize(:nfkd).should == "\u03A5\u0301" + end + + it "03D4 (Ï”) GREEK UPSILON WITH DIAERESIS AND HOOK SYMBOL" do + "\u03D4".unicode_normalize(:nfc).should == "\u03D4" + "\u03D4".unicode_normalize(:nfd).should == "\u03D2\u0308" + "\u03D4".unicode_normalize(:nfkc).should == "\u03AB" + "\u03D4".unicode_normalize(:nfkd).should == "\u03A5\u0308" + end + + it "1E9B (ẛ) LATIN SMALL LETTER LONG S WITH DOT ABOVE" do + "\u1E9B".unicode_normalize(:nfc).should == "\u1E9B" + "\u1E9B".unicode_normalize(:nfd).should == "\u017F\u0307" + "\u1E9B".unicode_normalize(:nfkc).should == "\u1E61" + "\u1E9B".unicode_normalize(:nfkd).should == "\u0073\u0307" + end + end + + it "raises an Encoding::CompatibilityError if string is not in an unicode encoding" do + -> do + [0xE0].pack('C').force_encoding("ISO-8859-1").unicode_normalize(:nfd) + end.should raise_error(Encoding::CompatibilityError) + end + + it "raises an ArgumentError if the specified form is invalid" do + -> { + @angstrom.unicode_normalize(:invalid_form) + }.should raise_error(ArgumentError) + end +end + +describe "String#unicode_normalize!" do + it "normalizes code points and modifies the receiving string" do + angstrom = "\u212b" + angstrom.unicode_normalize! + angstrom.should == "\u00c5" + angstrom.should_not == "\u212b" + end + + it "modifies original string (nfc)" do + str = "a\u0300" + str.unicode_normalize!(:nfc) + + str.should_not == "a\u0300" + str.should == "à " + end + + it "modifies self in place (nfd)" do + str = "\u00E0" + str.unicode_normalize!(:nfd) + + str.should_not == "\u00E0" + str.should == "a\u0300" + end + + it "modifies self in place (nfkc)" do + str = "\u1E9B\u0323" + str.unicode_normalize!(:nfkc) + + str.should_not == "\u1E9B\u0323" + str.should == "\u1E69" + end + + it "modifies self in place (nfkd)" do + str = "\u1E9B\u0323" + str.unicode_normalize!(:nfkd) + + str.should_not == "\u1E9B\u0323" + str.should == "s\u0323\u0307" + end + + it "raises an Encoding::CompatibilityError if the string is not in an unicode encoding" do + -> { + [0xE0].pack('C').force_encoding("ISO-8859-1").unicode_normalize! + }.should raise_error(Encoding::CompatibilityError) + end + + it "raises an ArgumentError if the specified form is invalid" do + ohm = "\u2126" + -> { + ohm.unicode_normalize!(:invalid_form) + }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/string/unicode_normalized_spec.rb b/spec/ruby/core/string/unicode_normalized_spec.rb new file mode 100644 index 0000000000..91cf2086b2 --- /dev/null +++ b/spec/ruby/core/string/unicode_normalized_spec.rb @@ -0,0 +1,75 @@ +# -*- encoding: utf-8 -*- +# frozen_string_literal: false +require_relative '../../spec_helper' + +describe "String#unicode_normalized?" do + before :each do + @nfc_normalized_str = "\u1e9b\u0323" + @nfd_normalized_str = "\u017f\u0323\u0307" + @nfkc_normalized_str = "\u1e69" + @nfkd_normalized_str = "\u0073\u0323\u0307" + end + + it "returns true if string is in the specified normalization form" do + @nfc_normalized_str.unicode_normalized?(:nfc).should == true + @nfd_normalized_str.unicode_normalized?(:nfd).should == true + @nfkc_normalized_str.unicode_normalized?(:nfkc).should == true + @nfkd_normalized_str.unicode_normalized?(:nfkd).should == true + end + + it "returns false if string is not in the supplied normalization form" do + @nfd_normalized_str.unicode_normalized?(:nfc).should == false + @nfc_normalized_str.unicode_normalized?(:nfd).should == false + @nfc_normalized_str.unicode_normalized?(:nfkc).should == false + @nfc_normalized_str.unicode_normalized?(:nfkd).should == false + end + + it "defaults to the nfc normalization form if no forms are specified" do + @nfc_normalized_str.should.unicode_normalized? + @nfd_normalized_str.should_not.unicode_normalized? + end + + it "returns true if string is empty" do + "".should.unicode_normalized? + end + + it "returns true if string does not contain any unicode codepoints" do + "abc".should.unicode_normalized? + end + + it "raises an Encoding::CompatibilityError if the string is not in an unicode encoding" do + -> { @nfc_normalized_str.force_encoding("ISO-8859-1").unicode_normalized? }.should raise_error(Encoding::CompatibilityError) + end + + it "raises an ArgumentError if the specified form is invalid" do + -> { @nfc_normalized_str.unicode_normalized?(:invalid_form) }.should raise_error(ArgumentError) + end + + it "returns true if str is in Unicode normalization form (nfc)" do + str = "a\u0300" + str.unicode_normalized?(:nfc).should be_false + str.unicode_normalize!(:nfc) + str.unicode_normalized?(:nfc).should be_true + end + + it "returns true if str is in Unicode normalization form (nfd)" do + str = "a\u00E0" + str.unicode_normalized?(:nfd).should be_false + str.unicode_normalize!(:nfd) + str.unicode_normalized?(:nfd).should be_true + end + + it "returns true if str is in Unicode normalization form (nfkc)" do + str = "a\u0300" + str.unicode_normalized?(:nfkc).should be_false + str.unicode_normalize!(:nfkc) + str.unicode_normalized?(:nfkc).should be_true + end + + it "returns true if str is in Unicode normalization form (nfkd)" do + str = "a\u00E0" + str.unicode_normalized?(:nfkd).should be_false + str.unicode_normalize!(:nfkd) + str.unicode_normalized?(:nfkd).should be_true + end +end diff --git a/spec/ruby/core/string/unpack/a_spec.rb b/spec/ruby/core/string/unpack/a_spec.rb new file mode 100644 index 0000000000..a68e842e15 --- /dev/null +++ b/spec/ruby/core/string/unpack/a_spec.rb @@ -0,0 +1,66 @@ +# encoding: binary +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' +require_relative 'shared/string' +require_relative 'shared/taint' + +describe "String#unpack with format 'A'" do + it_behaves_like :string_unpack_basic, 'A' + it_behaves_like :string_unpack_no_platform, 'A' + it_behaves_like :string_unpack_string, 'A' + it_behaves_like :string_unpack_Aa, 'A' + it_behaves_like :string_unpack_taint, 'A' + + it "removes trailing space and NULL bytes from the decoded string" do + [ ["a\x00 b \x00", ["a\x00 b", ""]], + ["a\x00 b \x00 ", ["a\x00 b", ""]], + ["a\x00 b\x00 ", ["a\x00 b", ""]], + ["a\x00 b\x00", ["a\x00 b", ""]], + ["a\x00 b ", ["a\x00 b", ""]] + ].should be_computed_by(:unpack, "A*A") + end + + it "does not remove whitespace other than space" do + [ ["a\x00 b\x00\f", ["a\x00 b\x00\f"]], + ["a\x00 b\x00\n", ["a\x00 b\x00\n"]], + ["a\x00 b\x00\r", ["a\x00 b\x00\r"]], + ["a\x00 b\x00\t", ["a\x00 b\x00\t"]], + ["a\x00 b\x00\v", ["a\x00 b\x00\v"]], + ].should be_computed_by(:unpack, "A*") + end + + it "decodes into raw (ascii) string values" do + str = "str".dup.force_encoding('UTF-8').unpack("A*")[0] + str.encoding.should == Encoding::BINARY + end + +end + +describe "String#unpack with format 'a'" do + it_behaves_like :string_unpack_basic, 'a' + it_behaves_like :string_unpack_no_platform, 'a' + it_behaves_like :string_unpack_string, 'a' + it_behaves_like :string_unpack_Aa, 'a' + it_behaves_like :string_unpack_taint, 'a' + + it "does not remove trailing whitespace or NULL bytes from the decoded string" do + [ ["a\x00 b \x00", ["a\x00 b \x00"]], + ["a\x00 b \x00 ", ["a\x00 b \x00 "]], + ["a\x00 b\x00 ", ["a\x00 b\x00 "]], + ["a\x00 b\x00", ["a\x00 b\x00"]], + ["a\x00 b ", ["a\x00 b "]], + ["a\x00 b\f", ["a\x00 b\f"]], + ["a\x00 b\n", ["a\x00 b\n"]], + ["a\x00 b\r", ["a\x00 b\r"]], + ["a\x00 b\t", ["a\x00 b\t"]], + ["a\x00 b\v", ["a\x00 b\v"]] + ].should be_computed_by(:unpack, "a*") + end + + it "decodes into raw (ascii) string values" do + str = "".unpack("a*")[0] + str.encoding.should == Encoding::BINARY + end + +end diff --git a/spec/ruby/core/string/unpack/at_spec.rb b/spec/ruby/core/string/unpack/at_spec.rb new file mode 100644 index 0000000000..d4133c23ee --- /dev/null +++ b/spec/ruby/core/string/unpack/at_spec.rb @@ -0,0 +1,29 @@ +# encoding: binary +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' + +describe "String#unpack with format '@'" do + it_behaves_like :string_unpack_basic, '@' + it_behaves_like :string_unpack_no_platform, '@' + + it "moves the read index to the byte specified by the count" do + "\x01\x02\x03\x04".unpack("C3@2C").should == [1, 2, 3, 3] + end + + it "implicitly has a count of zero when count is not specified" do + "\x01\x02\x03\x04".unpack("C2@C").should == [1, 2, 1] + end + + it "has no effect when passed the '*' modifier" do + "\x01\x02\x03\x04".unpack("C2@*C").should == [1, 2, 3] + end + + it "positions the read index one beyond the last readable byte in the String" do + "\x01\x02\x03\x04".unpack("C2@4C").should == [1, 2, nil] + end + + it "raises an ArgumentError if the count exceeds the size of the String" do + -> { "\x01\x02\x03\x04".unpack("C2@5C") }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/string/unpack/b_spec.rb b/spec/ruby/core/string/unpack/b_spec.rb new file mode 100644 index 0000000000..b088f901fc --- /dev/null +++ b/spec/ruby/core/string/unpack/b_spec.rb @@ -0,0 +1,221 @@ +# encoding: binary +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' +require_relative 'shared/taint' + +describe "String#unpack with format 'B'" do + it_behaves_like :string_unpack_basic, 'B' + it_behaves_like :string_unpack_no_platform, 'B' + it_behaves_like :string_unpack_taint, 'B' + + it "decodes one bit from each byte for each format character starting with the most significant bit" do + [ ["\x00", "B", ["0"]], + ["\x80", "B", ["1"]], + ["\x0f", "B", ["0"]], + ["\x8f", "B", ["1"]], + ["\x7f", "B", ["0"]], + ["\xff", "B", ["1"]], + ["\x80\x00", "BB", ["1", "0"]], + ["\x8f\x00", "BB", ["1", "0"]], + ["\x80\x0f", "BB", ["1", "0"]], + ["\x80\x8f", "BB", ["1", "1"]], + ["\x80\x80", "BB", ["1", "1"]], + ["\x0f\x80", "BB", ["0", "1"]] + ].should be_computed_by(:unpack) + end + + it "decodes only the number of bits in the string when passed a count" do + "\x83".unpack("B25").should == ["10000011"] + end + + it "decodes multiple differing bit counts from a single string" do + str = "\xaa\xaa\xaa\xaa\x55\xaa\xd4\xc3\x6b\xd7\xaa\xd7\xc3\xd4\xaa\x6b\xd7\xaa" + array = str.unpack("B5B6B7B8B9B10B13B14B16B17") + array.should == ["10101", "101010", "1010101", "10101010", "010101011", + "1101010011", "0110101111010", "10101010110101", + "1100001111010100", "10101010011010111"] + end + + it "decodes a directive with a '*' modifier after a directive with a count modifier" do + "\xd4\xc3\x6b\xd7".unpack("B5B*").should == ["11010", "110000110110101111010111"] + end + + it "decodes a directive with a count modifier after a directive with a '*' modifier" do + "\xd4\xc3\x6b\xd7".unpack("B*B5").should == ["11010100110000110110101111010111", ""] + end + + it "decodes the number of bits specified by the count modifier" do + [ ["\x00", "B0", [""]], + ["\x80", "B1", ["1"]], + ["\x7f", "B2", ["01"]], + ["\x8f", "B3", ["100"]], + ["\x7f", "B4", ["0111"]], + ["\xff", "B5", ["11111"]], + ["\xf8", "B6", ["111110"]], + ["\x9c", "B7", ["1001110"]], + ["\xbd", "B8", ["10111101"]], + ["\x80\x80", "B9", ["100000001"]], + ["\x80\x70", "B10", ["1000000001"]], + ["\x80\x20", "B11", ["10000000001"]], + ["\x8f\x10", "B12", ["100011110001"]], + ["\x8f\x0f", "B13", ["1000111100001"]], + ["\x80\x0f", "B14", ["10000000000011"]], + ["\x80\x8f", "B15", ["100000001000111"]], + ["\x0f\x81", "B16", ["0000111110000001"]] + ].should be_computed_by(:unpack) + end + + it "decodes all the bits when passed the '*' modifier" do + [ ["", [""]], + ["\x00", ["00000000"]], + ["\x80", ["10000000"]], + ["\x7f", ["01111111"]], + ["\x81", ["10000001"]], + ["\x0f", ["00001111"]], + ["\x80\x80", ["1000000010000000"]], + ["\x8f\x10", ["1000111100010000"]], + ["\x00\x10", ["0000000000010000"]] + ].should be_computed_by(:unpack, "B*") + end + + it "adds an empty string for each element requested beyond the end of the String" do + [ ["", ["", "", ""]], + ["\x80", ["1", "", ""]], + ["\x80\x08", ["1", "0", ""]] + ].should be_computed_by(:unpack, "BBB") + end + + ruby_version_is ""..."3.3" do + it "ignores NULL bytes between directives" do + suppress_warning do + "\x80\x00".unpack("B\x00B").should == ["1", "0"] + end + end + end + + ruby_version_is "3.3" do + it "raise ArgumentError for NULL bytes between directives" do + -> { + "\x80\x00".unpack("B\x00B") + }.should raise_error(ArgumentError, /unknown unpack directive/) + end + end + + it "ignores spaces between directives" do + "\x80\x00".unpack("B B").should == ["1", "0"] + end + + it "decodes into US-ASCII string values" do + str = "s".dup.force_encoding('UTF-8').unpack("B*")[0] + str.encoding.name.should == 'US-ASCII' + end +end + +describe "String#unpack with format 'b'" do + it_behaves_like :string_unpack_basic, 'b' + it_behaves_like :string_unpack_no_platform, 'b' + it_behaves_like :string_unpack_taint, 'b' + + it "decodes one bit from each byte for each format character starting with the least significant bit" do + [ ["\x00", "b", ["0"]], + ["\x01", "b", ["1"]], + ["\xf0", "b", ["0"]], + ["\xf1", "b", ["1"]], + ["\xfe", "b", ["0"]], + ["\xff", "b", ["1"]], + ["\x01\x00", "bb", ["1", "0"]], + ["\xf1\x00", "bb", ["1", "0"]], + ["\x01\xf0", "bb", ["1", "0"]], + ["\x01\xf1", "bb", ["1", "1"]], + ["\x01\x01", "bb", ["1", "1"]], + ["\xf0\x01", "bb", ["0", "1"]] + ].should be_computed_by(:unpack) + end + + it "decodes only the number of bits in the string when passed a count" do + "\x83".unpack("b25").should == ["11000001"] + end + + it "decodes multiple differing bit counts from a single string" do + str = "\xaa\xaa\xaa\xaa\x55\xaa\xd4\xc3\x6b\xd7\xaa\xd7\xc3\xd4\xaa\x6b\xd7\xaa" + array = str.unpack("b5b6b7b8b9b10b13b14b16b17") + array.should == ["01010", "010101", "0101010", "01010101", "101010100", + "0010101111", "1101011011101", "01010101111010", + "1100001100101011", "01010101110101101"] + end + + it "decodes a directive with a '*' modifier after a directive with a count modifier" do + "\xd4\xc3\x6b\xd7".unpack("b5b*").should == ["00101", "110000111101011011101011"] + end + + it "decodes a directive with a count modifier after a directive with a '*' modifier" do + "\xd4\xc3\x6b\xd7".unpack("b*b5").should == ["00101011110000111101011011101011", ""] + end + + it "decodes the number of bits specified by the count modifier" do + [ ["\x00", "b0", [""]], + ["\x01", "b1", ["1"]], + ["\xfe", "b2", ["01"]], + ["\xfc", "b3", ["001"]], + ["\xf7", "b4", ["1110"]], + ["\xff", "b5", ["11111"]], + ["\xfe", "b6", ["011111"]], + ["\xce", "b7", ["0111001"]], + ["\xbd", "b8", ["10111101"]], + ["\x01\xff", "b9", ["100000001"]], + ["\x01\xfe", "b10", ["1000000001"]], + ["\x01\xfc", "b11", ["10000000001"]], + ["\xf1\xf8", "b12", ["100011110001"]], + ["\xe1\xf1", "b13", ["1000011110001"]], + ["\x03\xe0", "b14", ["11000000000001"]], + ["\x47\xc0", "b15", ["111000100000001"]], + ["\x81\x0f", "b16", ["1000000111110000"]] + ].should be_computed_by(:unpack) + end + + it "decodes all the bits when passed the '*' modifier" do + [ ["", [""]], + ["\x00", ["00000000"]], + ["\x80", ["00000001"]], + ["\x7f", ["11111110"]], + ["\x81", ["10000001"]], + ["\x0f", ["11110000"]], + ["\x80\x80", ["0000000100000001"]], + ["\x8f\x10", ["1111000100001000"]], + ["\x00\x10", ["0000000000001000"]] + ].should be_computed_by(:unpack, "b*") + end + + it "adds an empty string for each element requested beyond the end of the String" do + [ ["", ["", "", ""]], + ["\x01", ["1", "", ""]], + ["\x01\x80", ["1", "0", ""]] + ].should be_computed_by(:unpack, "bbb") + end + + ruby_version_is ""..."3.3" do + it "ignores NULL bytes between directives" do + suppress_warning do + "\x01\x00".unpack("b\x00b").should == ["1", "0"] + end + end + end + + ruby_version_is "3.3" do + it "raise ArgumentError for NULL bytes between directives" do + -> { + "\x01\x00".unpack("b\x00b") + }.should raise_error(ArgumentError, /unknown unpack directive/) + end + end + + it "ignores spaces between directives" do + "\x01\x00".unpack("b b").should == ["1", "0"] + end + + it "decodes into US-ASCII string values" do + str = "s".dup.force_encoding('UTF-8').unpack("b*")[0] + str.encoding.name.should == 'US-ASCII' + end +end diff --git a/spec/ruby/core/string/unpack/c_spec.rb b/spec/ruby/core/string/unpack/c_spec.rb new file mode 100644 index 0000000000..1e9548fb82 --- /dev/null +++ b/spec/ruby/core/string/unpack/c_spec.rb @@ -0,0 +1,75 @@ +# encoding: binary +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' + +describe :string_unpack_8bit, shared: true do + it "decodes one byte for a single format character" do + "abc".unpack(unpack_format()).should == [97] + end + + it "decodes two bytes for two format characters" do + "abc".unpack(unpack_format(nil, 2)).should == [97, 98] + end + + it "decodes the number of bytes requested by the count modifier" do + "abc".unpack(unpack_format(2)).should == [97, 98] + end + + it "decodes the remaining bytes when passed the '*' modifier" do + "abc".unpack(unpack_format('*')).should == [97, 98, 99] + end + + it "decodes the remaining bytes when passed the '*' modifier after another directive" do + "abc".unpack(unpack_format()+unpack_format('*')).should == [97, 98, 99] + end + + it "decodes zero bytes when no bytes remain and the '*' modifier is passed" do + "abc".unpack(unpack_format('*', 2)).should == [97, 98, 99] + end + + it "adds nil for each element requested beyond the end of the String" do + [ ["", [nil, nil, nil]], + ["a", [97, nil, nil]], + ["ab", [97, 98, nil]] + ].should be_computed_by(:unpack, unpack_format(3)) + end + + ruby_version_is ""..."3.3" do + it "ignores NULL bytes between directives" do + suppress_warning do + "abc".unpack(unpack_format("\000", 2)).should == [97, 98] + end + end + end + + ruby_version_is "3.3" do + it "raise ArgumentError for NULL bytes between directives" do + -> { + "abc".unpack(unpack_format("\000", 2)) + }.should raise_error(ArgumentError, /unknown unpack directive/) + end + end + + it "ignores spaces between directives" do + "abc".unpack(unpack_format(' ', 2)).should == [97, 98] + end +end + +describe "String#unpack with format 'C'" do + it_behaves_like :string_unpack_basic, 'C' + it_behaves_like :string_unpack_8bit, 'C' + + it "decodes a byte with most significant bit set as a positive number" do + "\xff\x80\x82".unpack('C*').should == [255, 128, 130] + end +end + +describe "String#unpack with format 'c'" do + it_behaves_like :string_unpack_basic, 'c' + it_behaves_like :string_unpack_8bit, 'c' + + it "decodes a byte with most significant bit set as a negative number" do + "\xff\x80\x82".unpack('c*').should == [-1, -128, -126] + end +end diff --git a/spec/ruby/core/string/unpack/comment_spec.rb b/spec/ruby/core/string/unpack/comment_spec.rb new file mode 100644 index 0000000000..050d2b7fc0 --- /dev/null +++ b/spec/ruby/core/string/unpack/comment_spec.rb @@ -0,0 +1,25 @@ +# encoding: binary +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' + +describe "String#unpack" do + it "ignores directives text from '#' to the first newline" do + "\x01\x02\x03".unpack("c#this is a comment\nc").should == [1, 2] + end + + it "ignores directives text from '#' to the end if no newline is present" do + "\x01\x02\x03".unpack("c#this is a comment c").should == [1] + end + + it "ignores comments at the start of the directives string" do + "\x01\x02\x03".unpack("#this is a comment\nc").should == [1] + end + + it "ignores the entire directive string if it is a comment" do + "\x01\x02\x03".unpack("#this is a comment c").should == [] + end + + it "ignores multiple comments" do + "\x01\x02\x03".unpack("c#comment\nc#comment\nc#c").should == [1, 2, 3] + end +end diff --git a/spec/ruby/core/string/unpack/d_spec.rb b/spec/ruby/core/string/unpack/d_spec.rb new file mode 100644 index 0000000000..0e4f57ec04 --- /dev/null +++ b/spec/ruby/core/string/unpack/d_spec.rb @@ -0,0 +1,28 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' +require_relative 'shared/float' + +little_endian do + describe "String#unpack with format 'D'" do + it_behaves_like :string_unpack_basic, 'D' + it_behaves_like :string_unpack_double_le, 'D' + end + + describe "String#unpack with format 'd'" do + it_behaves_like :string_unpack_basic, 'd' + it_behaves_like :string_unpack_double_le, 'd' + end +end + +big_endian do + describe "String#unpack with format 'D'" do + it_behaves_like :string_unpack_basic, 'D' + it_behaves_like :string_unpack_double_be, 'D' + end + + describe "String#unpack with format 'd'" do + it_behaves_like :string_unpack_basic, 'd' + it_behaves_like :string_unpack_double_be, 'd' + end +end diff --git a/spec/ruby/core/string/unpack/e_spec.rb b/spec/ruby/core/string/unpack/e_spec.rb new file mode 100644 index 0000000000..c958be1c8b --- /dev/null +++ b/spec/ruby/core/string/unpack/e_spec.rb @@ -0,0 +1,14 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' +require_relative 'shared/float' + +describe "String#unpack with format 'E'" do + it_behaves_like :string_unpack_basic, 'E' + it_behaves_like :string_unpack_double_le, 'E' +end + +describe "String#unpack with format 'e'" do + it_behaves_like :string_unpack_basic, 'e' + it_behaves_like :string_unpack_float_le, 'e' +end diff --git a/spec/ruby/core/string/unpack/f_spec.rb b/spec/ruby/core/string/unpack/f_spec.rb new file mode 100644 index 0000000000..ec8b9d435e --- /dev/null +++ b/spec/ruby/core/string/unpack/f_spec.rb @@ -0,0 +1,28 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' +require_relative 'shared/float' + +little_endian do + describe "String#unpack with format 'F'" do + it_behaves_like :string_unpack_basic, 'F' + it_behaves_like :string_unpack_float_le, 'F' + end + + describe "String#unpack with format 'f'" do + it_behaves_like :string_unpack_basic, 'f' + it_behaves_like :string_unpack_float_le, 'f' + end +end + +big_endian do + describe "String#unpack with format 'F'" do + it_behaves_like :string_unpack_basic, 'F' + it_behaves_like :string_unpack_float_be, 'F' + end + + describe "String#unpack with format 'f'" do + it_behaves_like :string_unpack_basic, 'f' + it_behaves_like :string_unpack_float_be, 'f' + end +end diff --git a/spec/ruby/core/string/unpack/g_spec.rb b/spec/ruby/core/string/unpack/g_spec.rb new file mode 100644 index 0000000000..ffc423b152 --- /dev/null +++ b/spec/ruby/core/string/unpack/g_spec.rb @@ -0,0 +1,14 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' +require_relative 'shared/float' + +describe "String#unpack with format 'G'" do + it_behaves_like :string_unpack_basic, 'G' + it_behaves_like :string_unpack_double_be, 'G' +end + +describe "String#unpack with format 'g'" do + it_behaves_like :string_unpack_basic, 'g' + it_behaves_like :string_unpack_float_be, 'g' +end diff --git a/spec/ruby/core/string/unpack/h_spec.rb b/spec/ruby/core/string/unpack/h_spec.rb new file mode 100644 index 0000000000..535836087d --- /dev/null +++ b/spec/ruby/core/string/unpack/h_spec.rb @@ -0,0 +1,159 @@ +# encoding: binary +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' +require_relative 'shared/taint' + +describe "String#unpack with format 'H'" do + it_behaves_like :string_unpack_basic, 'H' + it_behaves_like :string_unpack_no_platform, 'H' + it_behaves_like :string_unpack_taint, 'H' + + it "decodes one nibble from each byte for each format character starting with the most significant bit" do + [ ["\x8f", "H", ["8"]], + ["\xf8\x0f", "HH", ["f", "0"]] + ].should be_computed_by(:unpack) + end + + it "decodes only the number of nibbles in the string when passed a count" do + "\xca\xfe".unpack("H5").should == ["cafe"] + end + + it "decodes multiple differing nibble counts from a single string" do + array = "\xaa\x55\xaa\xd4\xc3\x6b\xd7\xaa\xd7".unpack("HH2H3H4H5") + array.should == ["a", "55", "aad", "c36b", "d7aad"] + end + + it "decodes a directive with a '*' modifier after a directive with a count modifier" do + "\xaa\x55\xaa\xd4\xc3\x6b".unpack("H3H*").should == ["aa5", "aad4c36b"] + end + + it "decodes a directive with a count modifier after a directive with a '*' modifier" do + "\xaa\x55\xaa\xd4\xc3\x6b".unpack("H*H3").should == ["aa55aad4c36b", ""] + end + + it "decodes the number of nibbles specified by the count modifier" do + [ ["\xab", "H0", [""]], + ["\x00", "H1", ["0"]], + ["\x01", "H2", ["01"]], + ["\x01\x23", "H3", ["012"]], + ["\x01\x23", "H4", ["0123"]], + ["\x01\x23\x45", "H5", ["01234"]] + ].should be_computed_by(:unpack) + end + + it "decodes all the nibbles when passed the '*' modifier" do + [ ["", [""]], + ["\xab", ["ab"]], + ["\xca\xfe", ["cafe"]], + ].should be_computed_by(:unpack, "H*") + end + + it "adds an empty string for each element requested beyond the end of the String" do + [ ["", ["", "", ""]], + ["\x01", ["0", "", ""]], + ["\x01\x80", ["0", "8", ""]] + ].should be_computed_by(:unpack, "HHH") + end + + ruby_version_is ""..."3.3" do + it "ignores NULL bytes between directives" do + suppress_warning do + "\x01\x10".unpack("H\x00H").should == ["0", "1"] + end + end + end + + ruby_version_is "3.3" do + it "raise ArgumentError for NULL bytes between directives" do + -> { + "\x01\x10".unpack("H\x00H") + }.should raise_error(ArgumentError, /unknown unpack directive/) + end + end + + it "ignores spaces between directives" do + "\x01\x10".unpack("H H").should == ["0", "1"] + end + + it "should make strings with US_ASCII encoding" do + "\x01".unpack("H")[0].encoding.should == Encoding::US_ASCII + end +end + +describe "String#unpack with format 'h'" do + it_behaves_like :string_unpack_basic, 'h' + it_behaves_like :string_unpack_no_platform, 'h' + it_behaves_like :string_unpack_taint, 'h' + + it "decodes one nibble from each byte for each format character starting with the least significant bit" do + [ ["\x8f", "h", ["f"]], + ["\xf8\x0f", "hh", ["8", "f"]] + ].should be_computed_by(:unpack) + end + + it "decodes only the number of nibbles in the string when passed a count" do + "\xac\xef".unpack("h5").should == ["cafe"] + end + + it "decodes multiple differing nibble counts from a single string" do + array = "\xaa\x55\xaa\xd4\xc3\x6b\xd7\xaa\xd7".unpack("hh2h3h4h5") + array.should == ["a", "55", "aa4", "3cb6", "7daa7"] + end + + it "decodes a directive with a '*' modifier after a directive with a count modifier" do + "\xba\x55\xaa\xd4\xc3\x6b".unpack("h3h*").should == ["ab5", "aa4d3cb6"] + end + + it "decodes a directive with a count modifier after a directive with a '*' modifier" do + "\xba\x55\xaa\xd4\xc3\x6b".unpack("h*h3").should == ["ab55aa4d3cb6", ""] + end + + it "decodes the number of nibbles specified by the count modifier" do + [ ["\xab", "h0", [""]], + ["\x00", "h1", ["0"]], + ["\x01", "h2", ["10"]], + ["\x01\x23", "h3", ["103"]], + ["\x01\x23", "h4", ["1032"]], + ["\x01\x23\x45", "h5", ["10325"]] + ].should be_computed_by(:unpack) + end + + it "decodes all the nibbles when passed the '*' modifier" do + [ ["", [""]], + ["\xab", ["ba"]], + ["\xac\xef", ["cafe"]], + ].should be_computed_by(:unpack, "h*") + end + + it "adds an empty string for each element requested beyond the end of the String" do + [ ["", ["", "", ""]], + ["\x01", ["1", "", ""]], + ["\x01\x80", ["1", "0", ""]] + ].should be_computed_by(:unpack, "hhh") + end + + ruby_version_is ""..."3.3" do + it "ignores NULL bytes between directives" do + suppress_warning do + "\x01\x10".unpack("h\x00h").should == ["1", "0"] + end + end + end + + ruby_version_is "3.3" do + it "raise ArgumentError for NULL bytes between directives" do + -> { + "\x01\x10".unpack("h\x00h") + }.should raise_error(ArgumentError, /unknown unpack directive/) + end + end + + it "ignores spaces between directives" do + "\x01\x10".unpack("h h").should == ["1", "0"] + end + + it "should make strings with US_ASCII encoding" do + "\x01".unpack("h")[0].encoding.should == Encoding::US_ASCII + end +end diff --git a/spec/ruby/core/string/unpack/i_spec.rb b/spec/ruby/core/string/unpack/i_spec.rb new file mode 100644 index 0000000000..b4bbba1923 --- /dev/null +++ b/spec/ruby/core/string/unpack/i_spec.rb @@ -0,0 +1,152 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' +require_relative 'shared/integer' + +describe "String#unpack with format 'I'" do + describe "with modifier '<'" do + it_behaves_like :string_unpack_32bit_le, 'I<' + it_behaves_like :string_unpack_32bit_le_unsigned, 'I<' + end + + describe "with modifier '<' and '_'" do + it_behaves_like :string_unpack_32bit_le, 'I<_' + it_behaves_like :string_unpack_32bit_le, 'I_<' + it_behaves_like :string_unpack_32bit_le_unsigned, 'I<_' + it_behaves_like :string_unpack_32bit_le_unsigned, 'I_<' + end + + describe "with modifier '<' and '!'" do + it_behaves_like :string_unpack_32bit_le, 'I<!' + it_behaves_like :string_unpack_32bit_le, 'I!<' + it_behaves_like :string_unpack_32bit_le_unsigned, 'I<!' + it_behaves_like :string_unpack_32bit_le_unsigned, 'I!<' + end + + describe "with modifier '>'" do + it_behaves_like :string_unpack_32bit_be, 'I>' + it_behaves_like :string_unpack_32bit_be_unsigned, 'I>' + end + + describe "with modifier '>' and '_'" do + it_behaves_like :string_unpack_32bit_be, 'I>_' + it_behaves_like :string_unpack_32bit_be, 'I_>' + it_behaves_like :string_unpack_32bit_be_unsigned, 'I>_' + it_behaves_like :string_unpack_32bit_be_unsigned, 'I_>' + end + + describe "with modifier '>' and '!'" do + it_behaves_like :string_unpack_32bit_be, 'I>!' + it_behaves_like :string_unpack_32bit_be, 'I!>' + it_behaves_like :string_unpack_32bit_be_unsigned, 'I>!' + it_behaves_like :string_unpack_32bit_be_unsigned, 'I!>' + end +end + +describe "String#unpack with format 'i'" do + describe "with modifier '<'" do + it_behaves_like :string_unpack_32bit_le, 'i<' + it_behaves_like :string_unpack_32bit_le_signed, 'i<' + end + + describe "with modifier '<' and '_'" do + it_behaves_like :string_unpack_32bit_le, 'i<_' + it_behaves_like :string_unpack_32bit_le, 'i_<' + it_behaves_like :string_unpack_32bit_le_signed, 'i<_' + it_behaves_like :string_unpack_32bit_le_signed, 'i_<' + end + + describe "with modifier '<' and '!'" do + it_behaves_like :string_unpack_32bit_le, 'i<!' + it_behaves_like :string_unpack_32bit_le, 'i!<' + it_behaves_like :string_unpack_32bit_le_signed, 'i<!' + it_behaves_like :string_unpack_32bit_le_signed, 'i!<' + end + + describe "with modifier '>'" do + it_behaves_like :string_unpack_32bit_be, 'i>' + it_behaves_like :string_unpack_32bit_be_signed, 'i>' + end + + describe "with modifier '>' and '_'" do + it_behaves_like :string_unpack_32bit_be, 'i>_' + it_behaves_like :string_unpack_32bit_be, 'i_>' + it_behaves_like :string_unpack_32bit_be_signed, 'i>_' + it_behaves_like :string_unpack_32bit_be_signed, 'i_>' + end + + describe "with modifier '>' and '!'" do + it_behaves_like :string_unpack_32bit_be, 'i>!' + it_behaves_like :string_unpack_32bit_be, 'i!>' + it_behaves_like :string_unpack_32bit_be_signed, 'i>!' + it_behaves_like :string_unpack_32bit_be_signed, 'i!>' + end +end + +little_endian do + describe "String#unpack with format 'I'" do + it_behaves_like :string_unpack_basic, 'I' + it_behaves_like :string_unpack_32bit_le, 'I' + it_behaves_like :string_unpack_32bit_le_unsigned, 'I' + end + + describe "String#unpack with format 'I' with modifier '_'" do + it_behaves_like :string_unpack_32bit_le, 'I_' + it_behaves_like :string_unpack_32bit_le_unsigned, 'I_' + end + + describe "String#unpack with format 'I' with modifier '!'" do + it_behaves_like :string_unpack_32bit_le, 'I!' + it_behaves_like :string_unpack_32bit_le_unsigned, 'I!' + end + + describe "String#unpack with format 'i'" do + it_behaves_like :string_unpack_basic, 'i' + it_behaves_like :string_unpack_32bit_le, 'i' + it_behaves_like :string_unpack_32bit_le_signed, 'i' + end + + describe "String#unpack with format 'i' with modifier '_'" do + it_behaves_like :string_unpack_32bit_le, 'i_' + it_behaves_like :string_unpack_32bit_le_signed, 'i_' + end + + describe "String#unpack with format 'i' with modifier '!'" do + it_behaves_like :string_unpack_32bit_le, 'i!' + it_behaves_like :string_unpack_32bit_le_signed, 'i!' + end +end + +big_endian do + describe "String#unpack with format 'I'" do + it_behaves_like :string_unpack_basic, 'I' + it_behaves_like :string_unpack_32bit_be, 'I' + it_behaves_like :string_unpack_32bit_be_unsigned, 'I' + end + + describe "String#unpack with format 'I' with modifier '_'" do + it_behaves_like :string_unpack_32bit_be, 'I_' + it_behaves_like :string_unpack_32bit_be_unsigned, 'I_' + end + + describe "String#unpack with format 'I' with modifier '!'" do + it_behaves_like :string_unpack_32bit_be, 'I!' + it_behaves_like :string_unpack_32bit_be_unsigned, 'I!' + end + + describe "String#unpack with format 'i'" do + it_behaves_like :string_unpack_basic, 'i' + it_behaves_like :string_unpack_32bit_be, 'i' + it_behaves_like :string_unpack_32bit_be_signed, 'i' + end + + describe "String#unpack with format 'i' with modifier '_'" do + it_behaves_like :string_unpack_32bit_be, 'i_' + it_behaves_like :string_unpack_32bit_be_signed, 'i_' + end + + describe "String#unpack with format 'i' with modifier '!'" do + it_behaves_like :string_unpack_32bit_be, 'i!' + it_behaves_like :string_unpack_32bit_be_signed, 'i!' + end +end diff --git a/spec/ruby/core/string/unpack/j_spec.rb b/spec/ruby/core/string/unpack/j_spec.rb new file mode 100644 index 0000000000..3c2baad642 --- /dev/null +++ b/spec/ruby/core/string/unpack/j_spec.rb @@ -0,0 +1,272 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' +require_relative 'shared/integer' + +platform_is pointer_size: 64 do + little_endian do + describe "String#unpack with format 'J'" do + describe "with modifier '_'" do + it_behaves_like :string_unpack_64bit_le, 'J_' + it_behaves_like :string_unpack_64bit_le_unsigned, 'J_' + end + + describe "with modifier '!'" do + it_behaves_like :string_unpack_64bit_le, 'J!' + it_behaves_like :string_unpack_64bit_le_unsigned, 'J!' + end + end + + describe "String#unpack with format 'j'" do + describe "with modifier '_'" do + it_behaves_like :string_unpack_64bit_le, 'j_' + it_behaves_like :string_unpack_64bit_le_signed, 'j_' + end + + describe "with modifier '!'" do + it_behaves_like :string_unpack_64bit_le, 'j!' + it_behaves_like :string_unpack_64bit_le_signed, 'j!' + end + end + end + + big_endian do + describe "String#unpack with format 'J'" do + describe "with modifier '_'" do + it_behaves_like :string_unpack_64bit_be, 'J_' + it_behaves_like :string_unpack_64bit_be_unsigned, 'J_' + end + + describe "with modifier '!'" do + it_behaves_like :string_unpack_64bit_be, 'J!' + it_behaves_like :string_unpack_64bit_be_unsigned, 'J!' + end + end + + describe "String#unpack with format 'j'" do + describe "with modifier '_'" do + it_behaves_like :string_unpack_64bit_be, 'j_' + it_behaves_like :string_unpack_64bit_be_signed, 'j_' + end + + describe "with modifier '!'" do + it_behaves_like :string_unpack_64bit_be, 'j!' + it_behaves_like :string_unpack_64bit_be_signed, 'j!' + end + end + end + + describe "String#unpack with format 'J'" do + describe "with modifier '<'" do + it_behaves_like :string_unpack_64bit_le, 'J<' + it_behaves_like :string_unpack_64bit_le_unsigned, 'J<' + end + + describe "with modifier '>'" do + it_behaves_like :string_unpack_64bit_be, 'J>' + it_behaves_like :string_unpack_64bit_be_unsigned, 'J>' + end + + describe "with modifier '<' and '_'" do + it_behaves_like :string_unpack_64bit_le, 'J<_' + it_behaves_like :string_unpack_64bit_le, 'J_<' + it_behaves_like :string_unpack_64bit_le_unsigned, 'J<_' + it_behaves_like :string_unpack_64bit_le_unsigned, 'J_<' + end + + describe "with modifier '<' and '!'" do + it_behaves_like :string_unpack_64bit_le, 'J<!' + it_behaves_like :string_unpack_64bit_le, 'J!<' + it_behaves_like :string_unpack_64bit_le_unsigned, 'J<!' + it_behaves_like :string_unpack_64bit_le_unsigned, 'J!<' + end + + describe "with modifier '>' and '_'" do + it_behaves_like :string_unpack_64bit_be, 'J>_' + it_behaves_like :string_unpack_64bit_be, 'J_>' + it_behaves_like :string_unpack_64bit_be_unsigned, 'J>_' + it_behaves_like :string_unpack_64bit_be_unsigned, 'J_>' + end + + describe "with modifier '>' and '!'" do + it_behaves_like :string_unpack_64bit_be, 'J>!' + it_behaves_like :string_unpack_64bit_be, 'J!>' + it_behaves_like :string_unpack_64bit_be_unsigned, 'J>!' + it_behaves_like :string_unpack_64bit_be_unsigned, 'J!>' + end + end + + describe "String#unpack with format 'j'" do + describe "with modifier '<'" do + it_behaves_like :string_unpack_64bit_le, 'j<' + it_behaves_like :string_unpack_64bit_le_signed, 'j<' + end + + describe "with modifier '>'" do + it_behaves_like :string_unpack_64bit_be, 'j>' + it_behaves_like :string_unpack_64bit_be_signed, 'j>' + end + + describe "with modifier '<' and '_'" do + it_behaves_like :string_unpack_64bit_le, 'j<_' + it_behaves_like :string_unpack_64bit_le, 'j_<' + it_behaves_like :string_unpack_64bit_le_signed, 'j<_' + it_behaves_like :string_unpack_64bit_le_signed, 'j_<' + end + + describe "with modifier '<' and '!'" do + it_behaves_like :string_unpack_64bit_le, 'j<!' + it_behaves_like :string_unpack_64bit_le, 'j!<' + it_behaves_like :string_unpack_64bit_le_signed, 'j<!' + it_behaves_like :string_unpack_64bit_le_signed, 'j!<' + end + + describe "with modifier '>' and '_'" do + it_behaves_like :string_unpack_64bit_be, 'j>_' + it_behaves_like :string_unpack_64bit_be, 'j_>' + it_behaves_like :string_unpack_64bit_be_signed, 'j>_' + it_behaves_like :string_unpack_64bit_be_signed, 'j_>' + end + + describe "with modifier '>' and '!'" do + it_behaves_like :string_unpack_64bit_be, 'j>!' + it_behaves_like :string_unpack_64bit_be, 'j!>' + it_behaves_like :string_unpack_64bit_be_signed, 'j>!' + it_behaves_like :string_unpack_64bit_be_signed, 'j!>' + end + end +end + +platform_is pointer_size: 32 do + little_endian do + describe "String#unpack with format 'J'" do + describe "with modifier '_'" do + it_behaves_like :string_unpack_32bit_le, 'J_' + it_behaves_like :string_unpack_32bit_le_unsigned, 'J_' + end + + describe "with modifier '!'" do + it_behaves_like :string_unpack_32bit_le, 'J!' + it_behaves_like :string_unpack_32bit_le_unsigned, 'J!' + end + end + + describe "String#unpack with format 'j'" do + describe "with modifier '_'" do + it_behaves_like :string_unpack_32bit_le, 'j_' + it_behaves_like :string_unpack_32bit_le_signed, 'j_' + end + + describe "with modifier '!'" do + it_behaves_like :string_unpack_32bit_le, 'j!' + it_behaves_like :string_unpack_32bit_le_signed, 'j!' + end + end + end + + big_endian do + describe "String#unpack with format 'J'" do + describe "with modifier '_'" do + it_behaves_like :string_unpack_32bit_be, 'J_' + it_behaves_like :string_unpack_32bit_be_unsigned, 'J_' + end + + describe "with modifier '!'" do + it_behaves_like :string_unpack_32bit_be, 'J!' + it_behaves_like :string_unpack_32bit_be_unsigned, 'J!' + end + end + + describe "String#unpack with format 'j'" do + describe "with modifier '_'" do + it_behaves_like :string_unpack_32bit_be, 'j_' + it_behaves_like :string_unpack_32bit_be_signed, 'j_' + end + + describe "with modifier '!'" do + it_behaves_like :string_unpack_32bit_be, 'j!' + it_behaves_like :string_unpack_32bit_be_signed, 'j!' + end + end + end + + describe "String#unpack with format 'J'" do + describe "with modifier '<'" do + it_behaves_like :string_unpack_32bit_le, 'J<' + it_behaves_like :string_unpack_32bit_le_unsigned, 'J<' + end + + describe "with modifier '>'" do + it_behaves_like :string_unpack_32bit_be, 'J>' + it_behaves_like :string_unpack_32bit_be_unsigned, 'J>' + end + + describe "with modifier '<' and '_'" do + it_behaves_like :string_unpack_32bit_le, 'J<_' + it_behaves_like :string_unpack_32bit_le, 'J_<' + it_behaves_like :string_unpack_32bit_le_unsigned, 'J<_' + it_behaves_like :string_unpack_32bit_le_unsigned, 'J_<' + end + + describe "with modifier '<' and '!'" do + it_behaves_like :string_unpack_32bit_le, 'J<!' + it_behaves_like :string_unpack_32bit_le, 'J!<' + it_behaves_like :string_unpack_32bit_le_unsigned, 'J<!' + it_behaves_like :string_unpack_32bit_le_unsigned, 'J!<' + end + + describe "with modifier '>' and '_'" do + it_behaves_like :string_unpack_32bit_be, 'J>_' + it_behaves_like :string_unpack_32bit_be, 'J_>' + it_behaves_like :string_unpack_32bit_be_unsigned, 'J>_' + it_behaves_like :string_unpack_32bit_be_unsigned, 'J_>' + end + + describe "with modifier '>' and '!'" do + it_behaves_like :string_unpack_32bit_be, 'J>!' + it_behaves_like :string_unpack_32bit_be, 'J!>' + it_behaves_like :string_unpack_32bit_be_unsigned, 'J>!' + it_behaves_like :string_unpack_32bit_be_unsigned, 'J!>' + end + end + + describe "String#unpack with format 'j'" do + describe "with modifier '<'" do + it_behaves_like :string_unpack_32bit_le, 'j<' + it_behaves_like :string_unpack_32bit_le_signed, 'j<' + end + + describe "with modifier '>'" do + it_behaves_like :string_unpack_32bit_be, 'j>' + it_behaves_like :string_unpack_32bit_be_signed, 'j>' + end + + describe "with modifier '<' and '_'" do + it_behaves_like :string_unpack_32bit_le, 'j<_' + it_behaves_like :string_unpack_32bit_le, 'j_<' + it_behaves_like :string_unpack_32bit_le_signed, 'j<_' + it_behaves_like :string_unpack_32bit_le_signed, 'j_<' + end + + describe "with modifier '<' and '!'" do + it_behaves_like :string_unpack_32bit_le, 'j<!' + it_behaves_like :string_unpack_32bit_le, 'j!<' + it_behaves_like :string_unpack_32bit_le_signed, 'j<!' + it_behaves_like :string_unpack_32bit_le_signed, 'j!<' + end + + describe "with modifier '>' and '_'" do + it_behaves_like :string_unpack_32bit_be, 'j>_' + it_behaves_like :string_unpack_32bit_be, 'j_>' + it_behaves_like :string_unpack_32bit_be_signed, 'j>_' + it_behaves_like :string_unpack_32bit_be_signed, 'j_>' + end + + describe "with modifier '>' and '!'" do + it_behaves_like :string_unpack_32bit_be, 'j>!' + it_behaves_like :string_unpack_32bit_be, 'j!>' + it_behaves_like :string_unpack_32bit_be_signed, 'j>!' + it_behaves_like :string_unpack_32bit_be_signed, 'j!>' + end + end +end diff --git a/spec/ruby/core/string/unpack/l_spec.rb b/spec/ruby/core/string/unpack/l_spec.rb new file mode 100644 index 0000000000..0adb567eca --- /dev/null +++ b/spec/ruby/core/string/unpack/l_spec.rb @@ -0,0 +1,265 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' +require_relative 'shared/integer' + +describe "String#unpack with format 'L'" do + describe "with modifier '<'" do + it_behaves_like :string_unpack_32bit_le, 'L<' + it_behaves_like :string_unpack_32bit_le_unsigned, 'L<' + end + + describe "with modifier '>'" do + it_behaves_like :string_unpack_32bit_be, 'L>' + it_behaves_like :string_unpack_32bit_be_unsigned, 'L>' + end + + platform_is c_long_size: 32 do + describe "with modifier '<' and '_'" do + it_behaves_like :string_unpack_32bit_le, 'L<_' + it_behaves_like :string_unpack_32bit_le, 'L_<' + it_behaves_like :string_unpack_32bit_le_unsigned, 'L<_' + it_behaves_like :string_unpack_32bit_le_unsigned, 'L_<' + end + + describe "with modifier '<' and '!'" do + it_behaves_like :string_unpack_32bit_le, 'L<!' + it_behaves_like :string_unpack_32bit_le, 'L!<' + it_behaves_like :string_unpack_32bit_le_unsigned, 'L<!' + it_behaves_like :string_unpack_32bit_le_unsigned, 'L!<' + end + + describe "with modifier '>' and '_'" do + it_behaves_like :string_unpack_32bit_be, 'L>_' + it_behaves_like :string_unpack_32bit_be, 'L_>' + it_behaves_like :string_unpack_32bit_be_unsigned, 'L>_' + it_behaves_like :string_unpack_32bit_be_unsigned, 'L_>' + end + + describe "with modifier '>' and '!'" do + it_behaves_like :string_unpack_32bit_be, 'L>!' + it_behaves_like :string_unpack_32bit_be, 'L!>' + it_behaves_like :string_unpack_32bit_be_unsigned, 'L>!' + it_behaves_like :string_unpack_32bit_be_unsigned, 'L!>' + end + end + + platform_is c_long_size: 64 do + describe "with modifier '<' and '_'" do + it_behaves_like :string_unpack_64bit_le, 'L<_' + it_behaves_like :string_unpack_64bit_le, 'L_<' + it_behaves_like :string_unpack_64bit_le_unsigned, 'L<_' + it_behaves_like :string_unpack_64bit_le_unsigned, 'L_<' + end + + describe "with modifier '<' and '!'" do + it_behaves_like :string_unpack_64bit_le, 'L<!' + it_behaves_like :string_unpack_64bit_le, 'L!<' + it_behaves_like :string_unpack_64bit_le_unsigned, 'L<!' + it_behaves_like :string_unpack_64bit_le_unsigned, 'L!<' + end + + describe "with modifier '>' and '_'" do + it_behaves_like :string_unpack_64bit_be, 'L>_' + it_behaves_like :string_unpack_64bit_be, 'L_>' + it_behaves_like :string_unpack_64bit_be_unsigned, 'L>_' + it_behaves_like :string_unpack_64bit_be_unsigned, 'L_>' + end + + describe "with modifier '>' and '!'" do + it_behaves_like :string_unpack_64bit_be, 'L>!' + it_behaves_like :string_unpack_64bit_be, 'L!>' + it_behaves_like :string_unpack_64bit_be_unsigned, 'L>!' + it_behaves_like :string_unpack_64bit_be_unsigned, 'L!>' + end + end +end + +describe "String#unpack with format 'l'" do + describe "with modifier '<'" do + it_behaves_like :string_unpack_32bit_le, 'l<' + it_behaves_like :string_unpack_32bit_le_signed, 'l<' + end + + describe "with modifier '>'" do + it_behaves_like :string_unpack_32bit_be, 'l>' + it_behaves_like :string_unpack_32bit_be_signed, 'l>' + end + + platform_is c_long_size: 32 do + describe "with modifier '<' and '_'" do + it_behaves_like :string_unpack_32bit_le, 'l<_' + it_behaves_like :string_unpack_32bit_le, 'l_<' + it_behaves_like :string_unpack_32bit_le_signed, 'l<_' + it_behaves_like :string_unpack_32bit_le_signed, 'l_<' + end + + describe "with modifier '<' and '!'" do + it_behaves_like :string_unpack_32bit_le, 'l<!' + it_behaves_like :string_unpack_32bit_le, 'l!<' + it_behaves_like :string_unpack_32bit_le_signed, 'l<!' + it_behaves_like :string_unpack_32bit_le_signed, 'l!<' + end + + describe "with modifier '>' and '_'" do + it_behaves_like :string_unpack_32bit_be, 'l>_' + it_behaves_like :string_unpack_32bit_be, 'l_>' + it_behaves_like :string_unpack_32bit_be_signed, 'l>_' + it_behaves_like :string_unpack_32bit_be_signed, 'l_>' + end + + describe "with modifier '>' and '!'" do + it_behaves_like :string_unpack_32bit_be, 'l>!' + it_behaves_like :string_unpack_32bit_be, 'l!>' + it_behaves_like :string_unpack_32bit_be_signed, 'l>!' + it_behaves_like :string_unpack_32bit_be_signed, 'l!>' + end + end + + platform_is c_long_size: 64 do + describe "with modifier '<' and '_'" do + it_behaves_like :string_unpack_64bit_le, 'l<_' + it_behaves_like :string_unpack_64bit_le, 'l_<' + it_behaves_like :string_unpack_64bit_le_signed, 'l<_' + it_behaves_like :string_unpack_64bit_le_signed, 'l_<' + end + + describe "with modifier '<' and '!'" do + it_behaves_like :string_unpack_64bit_le, 'l<!' + it_behaves_like :string_unpack_64bit_le, 'l!<' + it_behaves_like :string_unpack_64bit_le_signed, 'l<!' + it_behaves_like :string_unpack_64bit_le_signed, 'l!<' + end + + describe "with modifier '>' and '_'" do + it_behaves_like :string_unpack_64bit_be, 'l>_' + it_behaves_like :string_unpack_64bit_be, 'l_>' + it_behaves_like :string_unpack_64bit_be_signed, 'l>_' + it_behaves_like :string_unpack_64bit_be_signed, 'l_>' + end + + describe "with modifier '>' and '!'" do + it_behaves_like :string_unpack_64bit_be, 'l>!' + it_behaves_like :string_unpack_64bit_be, 'l!>' + it_behaves_like :string_unpack_64bit_be_signed, 'l>!' + it_behaves_like :string_unpack_64bit_be_signed, 'l!>' + end + end +end + +little_endian do + describe "String#unpack with format 'L'" do + it_behaves_like :string_unpack_basic, 'L' + it_behaves_like :string_unpack_32bit_le, 'L' + it_behaves_like :string_unpack_32bit_le_unsigned, 'L' + end + + describe "String#unpack with format 'l'" do + it_behaves_like :string_unpack_basic, 'l' + it_behaves_like :string_unpack_32bit_le, 'l' + it_behaves_like :string_unpack_32bit_le_signed, 'l' + end + + platform_is c_long_size: 32 do + describe "String#unpack with format 'L' with modifier '_'" do + it_behaves_like :string_unpack_32bit_le, 'L_' + it_behaves_like :string_unpack_32bit_le_unsigned, 'L_' + end + + describe "String#unpack with format 'L' with modifier '!'" do + it_behaves_like :string_unpack_32bit_le, 'L!' + it_behaves_like :string_unpack_32bit_le_unsigned, 'L!' + end + + describe "String#unpack with format 'l' with modifier '_'" do + it_behaves_like :string_unpack_32bit_le, 'l_' + it_behaves_like :string_unpack_32bit_le_signed, 'l' + end + + describe "String#unpack with format 'l' with modifier '!'" do + it_behaves_like :string_unpack_32bit_le, 'l!' + it_behaves_like :string_unpack_32bit_le_signed, 'l' + end + end + + platform_is c_long_size: 64 do + describe "String#unpack with format 'L' with modifier '_'" do + it_behaves_like :string_unpack_64bit_le, 'L_' + it_behaves_like :string_unpack_64bit_le_unsigned, 'L_' + end + + describe "String#unpack with format 'L' with modifier '!'" do + it_behaves_like :string_unpack_64bit_le, 'L!' + it_behaves_like :string_unpack_64bit_le_unsigned, 'L!' + end + + describe "String#unpack with format 'l' with modifier '_'" do + it_behaves_like :string_unpack_64bit_le, 'l_' + it_behaves_like :string_unpack_64bit_le_signed, 'l_' + end + + describe "String#unpack with format 'l' with modifier '!'" do + it_behaves_like :string_unpack_64bit_le, 'l!' + it_behaves_like :string_unpack_64bit_le_signed, 'l!' + end + end +end + +big_endian do + describe "String#unpack with format 'L'" do + it_behaves_like :string_unpack_basic, 'L' + it_behaves_like :string_unpack_32bit_be, 'L' + it_behaves_like :string_unpack_32bit_be_unsigned, 'L' + end + + describe "String#unpack with format 'l'" do + it_behaves_like :string_unpack_basic, 'l' + it_behaves_like :string_unpack_32bit_be, 'l' + it_behaves_like :string_unpack_32bit_be_signed, 'l' + end + + platform_is c_long_size: 32 do + describe "String#unpack with format 'L' with modifier '_'" do + it_behaves_like :string_unpack_32bit_be, 'L_' + it_behaves_like :string_unpack_32bit_be_unsigned, 'L_' + end + + describe "String#unpack with format 'L' with modifier '!'" do + it_behaves_like :string_unpack_32bit_be, 'L!' + it_behaves_like :string_unpack_32bit_be_unsigned, 'L!' + end + + describe "String#unpack with format 'l' with modifier '_'" do + it_behaves_like :string_unpack_32bit_be, 'l_' + it_behaves_like :string_unpack_32bit_be_signed, 'l' + end + + describe "String#unpack with format 'l' with modifier '!'" do + it_behaves_like :string_unpack_32bit_be, 'l!' + it_behaves_like :string_unpack_32bit_be_signed, 'l' + end + end + + platform_is c_long_size: 64 do + describe "String#unpack with format 'L' with modifier '_'" do + it_behaves_like :string_unpack_64bit_be, 'L_' + it_behaves_like :string_unpack_64bit_be_unsigned, 'L_' + end + + describe "String#unpack with format 'L' with modifier '!'" do + it_behaves_like :string_unpack_64bit_be, 'L!' + it_behaves_like :string_unpack_64bit_be_unsigned, 'L!' + end + + describe "String#unpack with format 'l' with modifier '_'" do + it_behaves_like :string_unpack_64bit_be, 'l_' + it_behaves_like :string_unpack_64bit_be_signed, 'l_' + end + + describe "String#unpack with format 'l' with modifier '!'" do + it_behaves_like :string_unpack_64bit_be, 'l!' + it_behaves_like :string_unpack_64bit_be_signed, 'l!' + end + end + +end diff --git a/spec/ruby/core/string/unpack/m_spec.rb b/spec/ruby/core/string/unpack/m_spec.rb new file mode 100644 index 0000000000..357987a053 --- /dev/null +++ b/spec/ruby/core/string/unpack/m_spec.rb @@ -0,0 +1,192 @@ +# encoding: binary +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' +require_relative 'shared/taint' + +describe "String#unpack with format 'M'" do + it_behaves_like :string_unpack_basic, 'M' + it_behaves_like :string_unpack_no_platform, 'M' + it_behaves_like :string_unpack_taint, 'M' + + it "decodes an empty string" do + "".unpack("M").should == [""] + end + + it "decodes the complete string ignoring newlines when given a single directive" do + "a=\nb=\nc=\n".unpack("M").should == ["abc"] + end + + it "appends empty string to the array for directives exceeding the input size" do + "a=\nb=\nc=\n".unpack("MMM").should == ["abc", "", ""] + end + + it "ignores the count or '*' modifier and decodes the entire string" do + [ ["a=\nb=\nc=\n", "M238", ["abc"]], + ["a=\nb=\nc=\n", "M*", ["abc"]] + ].should be_computed_by(:unpack) + end + + it "decodes the '=' character" do + "=3D=\n".unpack("M").should == ["="] + end + + it "decodes an embedded space character" do + "a b=\n".unpack("M").should == ["a b"] + end + + it "decodes a space at the end of the pre-encoded string" do + "a =\n".unpack("M").should == ["a "] + end + + it "decodes an embedded tab character" do + "a\tb=\n".unpack("M").should == ["a\tb"] + end + + it "decodes a tab character at the end of the pre-encoded string" do + "a\t=\n".unpack("M").should == ["a\t"] + end + + it "decodes an embedded newline" do + "a\nb=\n".unpack("M").should == ["a\nb"] + end + + it "decodes pre-encoded byte values 33..60" do + [ ["!\"\#$%&'()*+,-./=\n", ["!\"\#$%&'()*+,-./"]], + ["0123456789=\n", ["0123456789"]], + [":;<=\n", [":;<"]] + ].should be_computed_by(:unpack, "M") + end + + it "decodes pre-encoded byte values 62..126" do + [ [">?@=\n", [">?@"]], + ["ABCDEFGHIJKLMNOPQRSTUVWXYZ=\n", ["ABCDEFGHIJKLMNOPQRSTUVWXYZ"]], + ["[\\]^_`=\n", ["[\\]^_`"]], + ["abcdefghijklmnopqrstuvwxyz=\n", ["abcdefghijklmnopqrstuvwxyz"]], + ["{|}~=\n", ["{|}~"]] + ].should be_computed_by(:unpack, "M") + end + + it "decodes pre-encoded byte values 0..31 except tab and newline" do + [ ["=00=01=02=03=04=05=06=\n", ["\x00\x01\x02\x03\x04\x05\x06"]], + ["=07=08=0B=0C=0D=\n", ["\a\b\v\f\r"]], + ["=0E=0F=10=11=12=13=14=\n", ["\x0e\x0f\x10\x11\x12\x13\x14"]], + ["=15=16=17=18=19=1A=\n", ["\x15\x16\x17\x18\x19\x1a"]], + ["=1B=\n", ["\e"]], + ["=1C=1D=1E=1F=\n", ["\x1c\x1d\x1e\x1f"]] + ].should be_computed_by(:unpack, "M") + end + + it "decodes pre-encoded byte values 127..255" do + [ ["=7F=80=81=82=83=84=85=86=\n", ["\x7f\x80\x81\x82\x83\x84\x85\x86"]], + ["=87=88=89=8A=8B=8C=8D=8E=\n", ["\x87\x88\x89\x8a\x8b\x8c\x8d\x8e"]], + ["=8F=90=91=92=93=94=95=96=\n", ["\x8f\x90\x91\x92\x93\x94\x95\x96"]], + ["=97=98=99=9A=9B=9C=9D=9E=\n", ["\x97\x98\x99\x9a\x9b\x9c\x9d\x9e"]], + ["=9F=A0=A1=A2=A3=A4=A5=A6=\n", ["\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6"]], + ["=A7=A8=A9=AA=AB=AC=AD=AE=\n", ["\xa7\xa8\xa9\xaa\xab\xac\xad\xae"]], + ["=AF=B0=B1=B2=B3=B4=B5=B6=\n", ["\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6"]], + ["=B7=B8=B9=BA=BB=BC=BD=BE=\n", ["\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe"]], + ["=BF=C0=C1=C2=C3=C4=C5=C6=\n", ["\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6"]], + ["=C7=C8=C9=CA=CB=CC=CD=CE=\n", ["\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce"]], + ["=CF=D0=D1=D2=D3=D4=D5=D6=\n", ["\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6"]], + ["=D7=D8=D9=DA=DB=DC=DD=DE=\n", ["\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde"]], + ["=DF=E0=E1=E2=E3=E4=E5=E6=\n", ["\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6"]], + ["=E7=E8=E9=EA=EB=EC=ED=EE=\n", ["\xe7\xe8\xe9\xea\xeb\xec\xed\xee"]], + ["=EF=F0=F1=F2=F3=F4=F5=F6=\n", ["\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6"]], + ["=F7=F8=F9=FA=FB=FC=FD=FE=\n", ["\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe"]], + ["=FF=\n", ["\xff"]] + ].should be_computed_by(:unpack, "M") + end + + it "unpacks incomplete escape sequences as literal characters" do + "foo=".unpack("M").should == ["foo="] + "foo=4".unpack("M").should == ["foo=4"] + end +end + +describe "String#unpack with format 'm'" do + it_behaves_like :string_unpack_basic, 'm' + it_behaves_like :string_unpack_no_platform, 'm' + it_behaves_like :string_unpack_taint, 'm' + + it "decodes an empty string" do + "".unpack("m").should == [""] + end + + it "decodes the complete string ignoring newlines when given a single directive" do + "YWJj\nREVG\n".unpack("m").should == ["abcDEF"] + end + + it "ignores the count or '*' modifier and decodes the entire string" do + [ ["YWJj\nREVG\n", "m238", ["abcDEF"]], + ["YWJj\nREVG\n", "m*", ["abcDEF"]] + ].should be_computed_by(:unpack) + end + + it "appends empty string to the array for directives exceeding the input size" do + "YWJj\nREVG\n".unpack("mmm").should == ["abcDEF", "", ""] + end + + it "decodes all pre-encoded ascii byte values" do + [ ["AAECAwQFBg==\n", ["\x00\x01\x02\x03\x04\x05\x06"]], + ["BwgJCgsMDQ==\n", ["\a\b\t\n\v\f\r"]], + ["Dg8QERITFBUW\n", ["\x0E\x0F\x10\x11\x12\x13\x14\x15\x16"]], + ["FxgZGhscHR4f\n", ["\x17\x18\x19\x1a\e\x1c\x1d\x1e\x1f"]], + ["ISIjJCUmJygpKissLS4v\n", ["!\"\#$%&'()*+,-./"]], + ["MDEyMzQ1Njc4OQ==\n", ["0123456789"]], + ["Ojs8PT4/QA==\n", [":;<=>?@"]], + ["QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVo=\n", ["ABCDEFGHIJKLMNOPQRSTUVWXYZ"]], + ["W1xdXl9g\n", ["[\\]^_`"]], + ["YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo=\n", ["abcdefghijklmnopqrstuvwxyz"]], + ["e3x9fg==\n", ["{|}~"]], + ["f8KAwoHCgsKD\n", ["\x7f\xc2\x80\xc2\x81\xc2\x82\xc2\x83"]], + ["woTChcKGwofC\n", ["\xc2\x84\xc2\x85\xc2\x86\xc2\x87\xc2"]], + ["iMKJworCi8KM\n", ["\x88\xc2\x89\xc2\x8a\xc2\x8b\xc2\x8c"]], + ["wo3CjsKPwpDC\n", ["\xc2\x8d\xc2\x8e\xc2\x8f\xc2\x90\xc2"]], + ["kcKSwpPClMKV\n", ["\x91\xc2\x92\xc2\x93\xc2\x94\xc2\x95"]], + ["wpbCl8KYwpnC\n", ["\xc2\x96\xc2\x97\xc2\x98\xc2\x99\xc2"]], + ["msKbwpzCncKe\n", ["\x9a\xc2\x9b\xc2\x9c\xc2\x9d\xc2\x9e"]], + ["wp/CoMKhwqLC\n", ["\xc2\x9f\xc2\xa0\xc2\xa1\xc2\xa2\xc2"]], + ["o8KkwqXCpsKn\n", ["\xa3\xc2\xa4\xc2\xa5\xc2\xa6\xc2\xa7"]], + ["wqjCqcKqwqvC\n", ["\xc2\xa8\xc2\xa9\xc2\xaa\xc2\xab\xc2"]], + ["rMKtwq7Cr8Kw\n", ["\xac\xc2\xad\xc2\xae\xc2\xaf\xc2\xb0"]], + ["wrHCssKzwrTC\n", ["\xc2\xb1\xc2\xb2\xc2\xb3\xc2\xb4\xc2"]], + ["tcK2wrfCuMK5\n", ["\xb5\xc2\xb6\xc2\xb7\xc2\xb8\xc2\xb9"]], + ["wrrCu8K8wr3C\n", ["\xc2\xba\xc2\xbb\xc2\xbc\xc2\xbd\xc2"]], + ["vsK/w4DDgcOC\n", ["\xbe\xc2\xbf\xc3\x80\xc3\x81\xc3\x82"]], + ["w4PDhMOFw4bD\n", ["\xc3\x83\xc3\x84\xc3\x85\xc3\x86\xc3"]], + ["h8OIw4nDisOL\n", ["\x87\xc3\x88\xc3\x89\xc3\x8a\xc3\x8b"]], + ["w4zDjcOOw4/D\n", ["\xc3\x8c\xc3\x8d\xc3\x8e\xc3\x8f\xc3"]], + ["kMORw5LDk8OU\n", ["\x90\xc3\x91\xc3\x92\xc3\x93\xc3\x94"]], + ["w5XDlsOXw5jD\n", ["\xc3\x95\xc3\x96\xc3\x97\xc3\x98\xc3"]], + ["mcOaw5vDnMOd\n", ["\x99\xc3\x9a\xc3\x9b\xc3\x9c\xc3\x9d"]], + ["w57Dn8Ogw6HD\n", ["\xc3\x9e\xc3\x9f\xc3\xa0\xc3\xa1\xc3"]], + ["osOjw6TDpcOm\n", ["\xa2\xc3\xa3\xc3\xa4\xc3\xa5\xc3\xa6"]], + ["w6fDqMOpw6rD\n", ["\xc3\xa7\xc3\xa8\xc3\xa9\xc3\xaa\xc3"]], + ["q8Osw63DrsOv\n", ["\xab\xc3\xac\xc3\xad\xc3\xae\xc3\xaf"]], + ["w7DDscOyw7PD\n", ["\xc3\xb0\xc3\xb1\xc3\xb2\xc3\xb3\xc3"]], + ["tMO1w7bDt8O4\n", ["\xb4\xc3\xb5\xc3\xb6\xc3\xb7\xc3\xb8"]], + ["w7nDusO7w7zD\n", ["\xc3\xb9\xc3\xba\xc3\xbb\xc3\xbc\xc3"]], + ["vcO+w78=\n", ["\xbd\xc3\xbe\xc3\xbf"]] + ].should be_computed_by(:unpack, "m") + end + + it "produces binary strings" do + "".unpack("m").first.encoding.should == Encoding::BINARY + "Ojs8PT4/QA==\n".unpack("m").first.encoding.should == Encoding::BINARY + end + + it "does not raise an error for an invalid base64 character" do + "dGV%zdA==".unpack("m").should == ["test"] + end + + describe "when given count 0" do + it "decodes base64" do + "dGVzdA==".unpack("m0").should == ["test"] + end + + it "raises an ArgumentError for an invalid base64 character" do + -> { "dGV%zdA==".unpack("m0") }.should raise_error(ArgumentError) + end + end +end diff --git a/spec/ruby/core/string/unpack/n_spec.rb b/spec/ruby/core/string/unpack/n_spec.rb new file mode 100644 index 0000000000..09173f4fcb --- /dev/null +++ b/spec/ruby/core/string/unpack/n_spec.rb @@ -0,0 +1,18 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' +require_relative 'shared/integer' + +describe "String#unpack with format 'N'" do + it_behaves_like :string_unpack_basic, 'N' + it_behaves_like :string_unpack_32bit_be, 'N' + it_behaves_like :string_unpack_32bit_be_unsigned, 'N' + it_behaves_like :string_unpack_no_platform, 'N' +end + +describe "String#unpack with format 'n'" do + it_behaves_like :string_unpack_basic, 'n' + it_behaves_like :string_unpack_16bit_be, 'n' + it_behaves_like :string_unpack_16bit_be_unsigned, 'n' + it_behaves_like :string_unpack_no_platform, 'n' +end diff --git a/spec/ruby/core/string/unpack/p_spec.rb b/spec/ruby/core/string/unpack/p_spec.rb new file mode 100644 index 0000000000..cd48c0523d --- /dev/null +++ b/spec/ruby/core/string/unpack/p_spec.rb @@ -0,0 +1,44 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' +require_relative 'shared/taint' + +describe "String#unpack with format 'P'" do + it_behaves_like :string_unpack_basic, 'P' + it_behaves_like :string_unpack_taint, 'P' + + it "round-trips a string through pack and unpack" do + ["hello"].pack("P").unpack("P5").should == ["hello"] + end + + it "cannot unpack a string except from the same object that created it, or a duplicate of it" do + packed = ["hello"].pack("P") + packed.unpack("P5").should == ["hello"] + packed.dup.unpack("P5").should == ["hello"] + -> { packed.to_sym.to_s.unpack("P5") }.should raise_error(ArgumentError, /no associated pointer/) + end + + it "reads as many characters as specified" do + ["hello"].pack("P").unpack("P1").should == ["h"] + end + + it "reads only as far as a NUL character" do + ["hello"].pack("P").unpack("P10").should == ["hello"] + end +end + +describe "String#unpack with format 'p'" do + it_behaves_like :string_unpack_basic, 'p' + it_behaves_like :string_unpack_taint, 'p' + + it "round-trips a string through pack and unpack" do + ["hello"].pack("p").unpack("p").should == ["hello"] + end + + it "cannot unpack a string except from the same object that created it, or a duplicate of it" do + packed = ["hello"].pack("p") + packed.unpack("p").should == ["hello"] + packed.dup.unpack("p").should == ["hello"] + -> { packed.to_sym.to_s.unpack("p") }.should raise_error(ArgumentError, /no associated pointer/) + end +end diff --git a/spec/ruby/core/string/unpack/percent_spec.rb b/spec/ruby/core/string/unpack/percent_spec.rb new file mode 100644 index 0000000000..0e27663195 --- /dev/null +++ b/spec/ruby/core/string/unpack/percent_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../../spec_helper' + +describe "String#unpack with format '%'" do + it "raises an Argument Error" do + -> { "abc".unpack("%") }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/string/unpack/q_spec.rb b/spec/ruby/core/string/unpack/q_spec.rb new file mode 100644 index 0000000000..2f667d6c4d --- /dev/null +++ b/spec/ruby/core/string/unpack/q_spec.rb @@ -0,0 +1,64 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' +require_relative 'shared/integer' + +describe "String#unpack with format 'Q'" do + describe "with modifier '<'" do + it_behaves_like :string_unpack_64bit_le, 'Q<' + it_behaves_like :string_unpack_64bit_le_unsigned, 'Q<' + end + + describe "with modifier '>'" do + it_behaves_like :string_unpack_64bit_be, 'Q>' + it_behaves_like :string_unpack_64bit_be_unsigned, 'Q>' + end +end + +describe "String#unpack with format 'q'" do + describe "with modifier '<'" do + it_behaves_like :string_unpack_64bit_le, 'q<' + it_behaves_like :string_unpack_64bit_le_signed, 'q<' + end + + describe "with modifier '>'" do + it_behaves_like :string_unpack_64bit_be, 'q>' + it_behaves_like :string_unpack_64bit_be_signed, 'q>' + end +end + +describe "String#unpack with format 'Q'" do + it_behaves_like :string_unpack_basic, 'Q' +end + +describe "String#unpack with format 'q'" do + it_behaves_like :string_unpack_basic, 'q' +end + +little_endian do + describe "String#unpack with format 'Q'" do + it_behaves_like :string_unpack_64bit_le, 'Q' + it_behaves_like :string_unpack_64bit_le_extra, 'Q' + it_behaves_like :string_unpack_64bit_le_unsigned, 'Q' + end + + describe "String#unpack with format 'q'" do + it_behaves_like :string_unpack_64bit_le, 'q' + it_behaves_like :string_unpack_64bit_le_extra, 'q' + it_behaves_like :string_unpack_64bit_le_signed, 'q' + end +end + +big_endian do + describe "String#unpack with format 'Q'" do + it_behaves_like :string_unpack_64bit_be, 'Q' + it_behaves_like :string_unpack_64bit_be_extra, 'Q' + it_behaves_like :string_unpack_64bit_be_unsigned, 'Q' + end + + describe "String#unpack with format 'q'" do + it_behaves_like :string_unpack_64bit_be, 'q' + it_behaves_like :string_unpack_64bit_be_extra, 'q' + it_behaves_like :string_unpack_64bit_be_signed, 'q' + end +end diff --git a/spec/ruby/core/string/unpack/s_spec.rb b/spec/ruby/core/string/unpack/s_spec.rb new file mode 100644 index 0000000000..d331fd720e --- /dev/null +++ b/spec/ruby/core/string/unpack/s_spec.rb @@ -0,0 +1,152 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' +require_relative 'shared/integer' + +describe "String#unpack with format 'S'" do + describe "with modifier '<'" do + it_behaves_like :string_unpack_16bit_le, 'S<' + it_behaves_like :string_unpack_16bit_le_unsigned, 'S<' + end + + describe "with modifier '<' and '_'" do + it_behaves_like :string_unpack_16bit_le, 'S<_' + it_behaves_like :string_unpack_16bit_le, 'S_<' + it_behaves_like :string_unpack_16bit_le_unsigned, 'S_<' + it_behaves_like :string_unpack_16bit_le_unsigned, 'S<_' + end + + describe "with modifier '<' and '!'" do + it_behaves_like :string_unpack_16bit_le, 'S<!' + it_behaves_like :string_unpack_16bit_le, 'S!<' + it_behaves_like :string_unpack_16bit_le_unsigned, 'S!<' + it_behaves_like :string_unpack_16bit_le_unsigned, 'S<!' + end + + describe "with modifier '>'" do + it_behaves_like :string_unpack_16bit_be, 'S>' + it_behaves_like :string_unpack_16bit_be_unsigned, 'S>' + end + + describe "with modifier '>' and '_'" do + it_behaves_like :string_unpack_16bit_be, 'S>_' + it_behaves_like :string_unpack_16bit_be, 'S_>' + it_behaves_like :string_unpack_16bit_be_unsigned, 'S>_' + it_behaves_like :string_unpack_16bit_be_unsigned, 'S_>' + end + + describe "with modifier '>' and '!'" do + it_behaves_like :string_unpack_16bit_be, 'S>!' + it_behaves_like :string_unpack_16bit_be, 'S!>' + it_behaves_like :string_unpack_16bit_be_unsigned, 'S>!' + it_behaves_like :string_unpack_16bit_be_unsigned, 'S!>' + end +end + +describe "String#unpack with format 's'" do + describe "with modifier '<'" do + it_behaves_like :string_unpack_16bit_le, 's<' + it_behaves_like :string_unpack_16bit_le_signed, 's<' + end + + describe "with modifier '<' and '_'" do + it_behaves_like :string_unpack_16bit_le, 's<_' + it_behaves_like :string_unpack_16bit_le, 's_<' + it_behaves_like :string_unpack_16bit_le_signed, 's<_' + it_behaves_like :string_unpack_16bit_le_signed, 's_<' + end + + describe "with modifier '<' and '!'" do + it_behaves_like :string_unpack_16bit_le, 's<!' + it_behaves_like :string_unpack_16bit_le, 's!<' + it_behaves_like :string_unpack_16bit_le_signed, 's<!' + it_behaves_like :string_unpack_16bit_le_signed, 's!<' + end + + describe "with modifier '>'" do + it_behaves_like :string_unpack_16bit_be, 's>' + it_behaves_like :string_unpack_16bit_be_signed, 's>' + end + + describe "with modifier '>' and '_'" do + it_behaves_like :string_unpack_16bit_be, 's>_' + it_behaves_like :string_unpack_16bit_be, 's_>' + it_behaves_like :string_unpack_16bit_be_signed, 's>_' + it_behaves_like :string_unpack_16bit_be_signed, 's_>' + end + + describe "with modifier '>' and '!'" do + it_behaves_like :string_unpack_16bit_be, 's>!' + it_behaves_like :string_unpack_16bit_be, 's!>' + it_behaves_like :string_unpack_16bit_be_signed, 's>!' + it_behaves_like :string_unpack_16bit_be_signed, 's!>' + end +end + +little_endian do + describe "String#unpack with format 'S'" do + it_behaves_like :string_unpack_basic, 'S' + it_behaves_like :string_unpack_16bit_le, 'S' + it_behaves_like :string_unpack_16bit_le_unsigned, 'S' + end + + describe "String#unpack with format 'S' with modifier '_'" do + it_behaves_like :string_unpack_16bit_le, 'S_' + it_behaves_like :string_unpack_16bit_le_unsigned, 'S_' + end + + describe "String#unpack with format 'S' with modifier '!'" do + it_behaves_like :string_unpack_16bit_le, 'S!' + it_behaves_like :string_unpack_16bit_le_unsigned, 'S!' + end + + describe "String#unpack with format 's'" do + it_behaves_like :string_unpack_basic, 's' + it_behaves_like :string_unpack_16bit_le, 's' + it_behaves_like :string_unpack_16bit_le_signed, 's' + end + + describe "String#unpack with format 's' with modifier '_'" do + it_behaves_like :string_unpack_16bit_le, 's_' + it_behaves_like :string_unpack_16bit_le_signed, 's_' + end + + describe "String#unpack with format 's' with modifier '!'" do + it_behaves_like :string_unpack_16bit_le, 's!' + it_behaves_like :string_unpack_16bit_le_signed, 's!' + end +end + +big_endian do + describe "String#unpack with format 'S'" do + it_behaves_like :string_unpack_basic, 'S' + it_behaves_like :string_unpack_16bit_be, 'S' + it_behaves_like :string_unpack_16bit_be_unsigned, 'S' + end + + describe "String#unpack with format 'S' with modifier '_'" do + it_behaves_like :string_unpack_16bit_be, 'S_' + it_behaves_like :string_unpack_16bit_be_unsigned, 'S_' + end + + describe "String#unpack with format 'S' with modifier '!'" do + it_behaves_like :string_unpack_16bit_be, 'S!' + it_behaves_like :string_unpack_16bit_be_unsigned, 'S!' + end + + describe "String#unpack with format 's'" do + it_behaves_like :string_unpack_basic, 's' + it_behaves_like :string_unpack_16bit_be, 's' + it_behaves_like :string_unpack_16bit_be_signed, 's' + end + + describe "String#unpack with format 's' with modifier '_'" do + it_behaves_like :string_unpack_16bit_be, 's_' + it_behaves_like :string_unpack_16bit_be_signed, 's_' + end + + describe "String#unpack with format 's' with modifier '!'" do + it_behaves_like :string_unpack_16bit_be, 's!' + it_behaves_like :string_unpack_16bit_be_signed, 's!' + end +end diff --git a/spec/ruby/core/string/unpack/shared/basic.rb b/spec/ruby/core/string/unpack/shared/basic.rb new file mode 100644 index 0000000000..734630bda0 --- /dev/null +++ b/spec/ruby/core/string/unpack/shared/basic.rb @@ -0,0 +1,37 @@ +describe :string_unpack_basic, shared: true do + it "ignores whitespace in the format string" do + "abc".unpack("a \t\n\v\f\r"+unpack_format).should be_an_instance_of(Array) + end + + it "calls #to_str to coerce the directives string" do + d = mock("unpack directive") + d.should_receive(:to_str).and_return("a"+unpack_format) + "abc".unpack(d).should be_an_instance_of(Array) + end + + ruby_version_is ""..."3.3" do + it "warns about using an unknown directive" do + -> { "abcdefgh".unpack("a R" + unpack_format) }.should complain(/unknown unpack directive 'R' in 'a R#{unpack_format}'/) + -> { "abcdefgh".unpack("a 0" + unpack_format) }.should complain(/unknown unpack directive '0' in 'a 0#{unpack_format}'/) + -> { "abcdefgh".unpack("a :" + unpack_format) }.should complain(/unknown unpack directive ':' in 'a :#{unpack_format}'/) + end + end + + ruby_version_is "3.3" do + it "raises ArgumentError when a directive is unknown" do + -> { "abcdefgh".unpack("a K" + unpack_format) }.should raise_error(ArgumentError, "unknown unpack directive 'K' in 'a K#{unpack_format}'") + -> { "abcdefgh".unpack("a 0" + unpack_format) }.should raise_error(ArgumentError, "unknown unpack directive '0' in 'a 0#{unpack_format}'") + -> { "abcdefgh".unpack("a :" + unpack_format) }.should raise_error(ArgumentError, "unknown unpack directive ':' in 'a :#{unpack_format}'") + end + end +end + +describe :string_unpack_no_platform, shared: true do + it "raises an ArgumentError when the format modifier is '_'" do + -> { "abcdefgh".unpack(unpack_format("_")) }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError when the format modifier is '!'" do + -> { "abcdefgh".unpack(unpack_format("!")) }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/string/unpack/shared/float.rb b/spec/ruby/core/string/unpack/shared/float.rb new file mode 100644 index 0000000000..b31c2c8bdc --- /dev/null +++ b/spec/ruby/core/string/unpack/shared/float.rb @@ -0,0 +1,319 @@ +# encoding: binary + +describe :string_unpack_float_le, shared: true do + it "decodes one float for a single format character" do + "\x8f\xc2\xb5?".unpack(unpack_format).should == [1.4199999570846558] + end + + it "decodes a negative float" do + "\xcd\xcc\x08\xc2".unpack(unpack_format).should == [-34.200000762939453] + end + + it "decodes two floats for two format characters" do + array = "\x9a\x999@33\xb3?".unpack(unpack_format(nil, 2)) + array.should == [2.9000000953674316, 1.399999976158142] + end + + it "decodes the number of floats requested by the count modifier" do + array = "\x9a\x999@33\xb3?33\x03A".unpack(unpack_format(3)) + array.should == [2.9000000953674316, 1.399999976158142, 8.199999809265137] + end + + it "decodes the remaining floats when passed the '*' modifier" do + array = "\x9a\x999@33\xb3?33\x03A".unpack(unpack_format("*")) + array.should == [2.9000000953674316, 1.399999976158142, 8.199999809265137] + end + + it "decodes the remaining floats when passed the '*' modifier after another directive" do + array = "\x9a\x99\xa9@33\x13A".unpack(unpack_format()+unpack_format('*')) + array.should == [5.300000190734863, 9.199999809265137] + end + + it "does not decode a float when fewer bytes than a float remain and the '*' modifier is passed" do + [ ["\xff", []], + ["\xff\x00", []], + ["\xff\x00\xff", []] + ].should be_computed_by(:unpack, unpack_format("*")) + end + + it "adds nil for each element requested beyond the end of the String" do + [ ["abc", [nil, nil, nil]], + ["\x8f\xc2\xb5?abc", [1.4199999570846558, nil, nil]], + ["\x9a\x999@33\xb3?abc", [2.9000000953674316, 1.399999976158142, nil]] + ].should be_computed_by(:unpack, unpack_format(3)) + end + + it "decodes positive Infinity" do + "\x00\x00\x80\x7f".unpack(unpack_format).should == [infinity_value] + end + + it "decodes negative Infinity" do + "\x00\x00\x80\xff".unpack(unpack_format).should == [-infinity_value] + end + + it "decodes NaN" do + # mumble mumble NaN mumble https://bugs.ruby-lang.org/issues/5884 + [nan_value].pack(unpack_format).unpack(unpack_format).first.nan?.should be_true + end + + ruby_version_is ""..."3.3" do + it "ignores NULL bytes between directives" do + suppress_warning do + array = "\x9a\x999@33\xb3?".unpack(unpack_format("\000", 2)) + array.should == [2.9000000953674316, 1.399999976158142] + end + end + end + + ruby_version_is "3.3" do + it "raise ArgumentError for NULL bytes between directives" do + -> { + "\x9a\x999@33\xb3?".unpack(unpack_format("\000", 2)) + }.should raise_error(ArgumentError, /unknown unpack directive/) + end + end + + it "ignores spaces between directives" do + array = "\x9a\x999@33\xb3?".unpack(unpack_format(' ', 2)) + array.should == [2.9000000953674316, 1.399999976158142] + end +end + +describe :string_unpack_float_be, shared: true do + it "decodes one float for a single format character" do + "?\xb5\xc2\x8f".unpack(unpack_format).should == [1.4199999570846558] + end + + it "decodes a negative float" do + "\xc2\x08\xcc\xcd".unpack(unpack_format).should == [-34.200000762939453] + end + + it "decodes two floats for two format characters" do + array = "@9\x99\x9a?\xb333".unpack(unpack_format(nil, 2)) + array.should == [2.9000000953674316, 1.399999976158142] + end + + it "decodes the number of floats requested by the count modifier" do + array = "@9\x99\x9a?\xb333A\x0333".unpack(unpack_format(3)) + array.should == [2.9000000953674316, 1.399999976158142, 8.199999809265137] + end + + it "decodes the remaining floats when passed the '*' modifier" do + array = "@9\x99\x9a?\xb333A\x0333".unpack(unpack_format("*")) + array.should == [2.9000000953674316, 1.399999976158142, 8.199999809265137] + end + + it "decodes the remaining floats when passed the '*' modifier after another directive" do + array = "@\xa9\x99\x9aA\x1333".unpack(unpack_format()+unpack_format('*')) + array.should == [5.300000190734863, 9.199999809265137] + end + + it "does not decode a float when fewer bytes than a float remain and the '*' modifier is passed" do + [ ["\xff", []], + ["\xff\x00", []], + ["\xff\x00\xff", []] + ].should be_computed_by(:unpack, unpack_format("*")) + end + + it "adds nil for each element requested beyond the end of the String" do + [ ["abc", [nil, nil, nil]], + ["?\xb5\xc2\x8fabc", [1.4199999570846558, nil, nil]], + ["@9\x99\x9a?\xb333abc", [2.9000000953674316, 1.399999976158142, nil]] + ].should be_computed_by(:unpack, unpack_format(3)) + end + + it "decodes positive Infinity" do + "\x7f\x80\x00\x00".unpack(unpack_format).should == [infinity_value] + end + + it "decodes negative Infinity" do + "\xff\x80\x00\x00".unpack(unpack_format).should == [-infinity_value] + end + + it "decodes NaN" do + # mumble mumble NaN mumble https://bugs.ruby-lang.org/issues/5884 + [nan_value].pack(unpack_format).unpack(unpack_format).first.nan?.should be_true + end + + ruby_version_is ""..."3.3" do + it "ignores NULL bytes between directives" do + suppress_warning do + array = "@9\x99\x9a?\xb333".unpack(unpack_format("\000", 2)) + array.should == [2.9000000953674316, 1.399999976158142] + end + end + end + + ruby_version_is "3.3" do + it "raise ArgumentError for NULL bytes between directives" do + -> { + "@9\x99\x9a?\xb333".unpack(unpack_format("\000", 2)) + }.should raise_error(ArgumentError, /unknown unpack directive/) + end + end + + it "ignores spaces between directives" do + array = "@9\x99\x9a?\xb333".unpack(unpack_format(' ', 2)) + array.should == [2.9000000953674316, 1.399999976158142] + end +end + +describe :string_unpack_double_le, shared: true do + it "decodes one double for a single format character" do + "\xb8\x1e\x85\xebQ\xb8\xf6?".unpack(unpack_format).should == [1.42] + end + + it "decodes a negative double" do + "\x9a\x99\x99\x99\x99\x19A\xc0".unpack(unpack_format).should == [-34.2] + end + + it "decodes two doubles for two format characters" do + "333333\x07@ffffff\xf6?".unpack(unpack_format(nil, 2)).should == [2.9, 1.4] + end + + it "decodes the number of doubles requested by the count modifier" do + array = "333333\x07@ffffff\xf6?ffffff\x20@".unpack(unpack_format(3)) + array.should == [2.9, 1.4, 8.2] + end + + it "decodes the remaining doubles when passed the '*' modifier" do + array = "333333\x07@ffffff\xf6?ffffff\x20@".unpack(unpack_format("*")) + array.should == [2.9, 1.4, 8.2] + end + + it "decodes the remaining doubles when passed the '*' modifier after another directive" do + array = "333333\x15@ffffff\x22@".unpack(unpack_format()+unpack_format('*')) + array.should == [5.3, 9.2] + end + + it "does not decode a double when fewer bytes than a double remain and the '*' modifier is passed" do + [ ["\xff", []], + ["\xff\x00", []], + ["\xff\x00\xff", []], + ["\xff\x00\xff\x00", []], + ["\xff\x00\xff\x00\xff", []], + ["\xff\x00\xff\x00\xff\x00", []], + ["\xff\x00\xff\x00\xff\x00\xff", []] + ].should be_computed_by(:unpack, unpack_format("*")) + end + + it "adds nil for each element requested beyond the end of the String" do + [ ["\xff\x00\xff\x00\xff\x00\xff", [nil, nil, nil]], + ["\xb8\x1e\x85\xebQ\xb8\xf6?abc", [1.42, nil, nil]], + ["333333\x07@ffffff\xf6?abcd", [2.9, 1.4, nil]] + ].should be_computed_by(:unpack, unpack_format(3)) + end + + it "decodes positive Infinity" do + "\x00\x00\x00\x00\x00\x00\xf0\x7f".unpack(unpack_format).should == [infinity_value] + end + + it "decodes negative Infinity" do + "\x00\x00\x00\x00\x00\x00\xf0\xff".unpack(unpack_format).should == [-infinity_value] + end + + it "decodes NaN" do + # mumble mumble NaN mumble https://bugs.ruby-lang.org/issues/5884 + [nan_value].pack(unpack_format).unpack(unpack_format).first.nan?.should be_true + end + + ruby_version_is ""..."3.3" do + it "ignores NULL bytes between directives" do + suppress_warning do + "333333\x07@ffffff\xf6?".unpack(unpack_format("\000", 2)).should == [2.9, 1.4] + end + end + end + + ruby_version_is "3.3" do + it "raise ArgumentError for NULL bytes between directives" do + -> { + "333333\x07@ffffff\xf6?".unpack(unpack_format("\000", 2)) + }.should raise_error(ArgumentError, /unknown unpack directive/) + end + end + + it "ignores spaces between directives" do + "333333\x07@ffffff\xf6?".unpack(unpack_format(' ', 2)).should == [2.9, 1.4] + end +end + +describe :string_unpack_double_be, shared: true do + it "decodes one double for a single format character" do + "?\xf6\xb8Q\xeb\x85\x1e\xb8".unpack(unpack_format).should == [1.42] + end + + it "decodes a negative double" do + "\xc0A\x19\x99\x99\x99\x99\x9a".unpack(unpack_format).should == [-34.2] + end + + it "decodes two doubles for two format characters" do + "@\x07333333?\xf6ffffff".unpack(unpack_format(nil, 2)).should == [2.9, 1.4] + end + + it "decodes the number of doubles requested by the count modifier" do + array = "@\x07333333?\xf6ffffff@\x20ffffff".unpack(unpack_format(3)) + array.should == [2.9, 1.4, 8.2] + end + + it "decodes the remaining doubles when passed the '*' modifier" do + array = "@\x07333333?\xf6ffffff@\x20ffffff".unpack(unpack_format("*")) + array.should == [2.9, 1.4, 8.2] + end + + it "decodes the remaining doubles when passed the '*' modifier after another directive" do + array = "@\x15333333@\x22ffffff".unpack(unpack_format()+unpack_format('*')) + array.should == [5.3, 9.2] + end + + it "does not decode a double when fewer bytes than a double remain and the '*' modifier is passed" do + [ ["\xff", []], + ["\xff\x00", []], + ["\xff\x00\xff", []], + ["\xff\x00\xff\x00", []], + ["\xff\x00\xff\x00\xff", []], + ["\xff\x00\xff\x00\xff\x00", []], + ["\xff\x00\xff\x00\xff\x00\xff", []] + ].should be_computed_by(:unpack, unpack_format("*")) + end + + it "adds nil for each element requested beyond the end of the String" do + [ ["abcdefg", [nil, nil, nil]], + ["?\xf6\xb8Q\xeb\x85\x1e\xb8abc", [1.42, nil, nil]], + ["@\x07333333?\xf6ffffffabcd", [2.9, 1.4, nil]] + ].should be_computed_by(:unpack, unpack_format(3)) + end + + it "decodes positive Infinity" do + "\x7f\xf0\x00\x00\x00\x00\x00\x00".unpack(unpack_format).should == [infinity_value] + end + + it "decodes negative Infinity" do + "\xff\xf0\x00\x00\x00\x00\x00\x00".unpack(unpack_format).should == [-infinity_value] + end + + it "decodes NaN" do + # mumble mumble NaN mumble https://bugs.ruby-lang.org/issues/5884 + [nan_value].pack(unpack_format).unpack(unpack_format).first.nan?.should be_true + end + + ruby_version_is ""..."3.3" do + it "ignores NULL bytes between directives" do + suppress_warning do + "@\x07333333?\xf6ffffff".unpack(unpack_format("\000", 2)).should == [2.9, 1.4] + end + end + end + + ruby_version_is "3.3" do + it "raise ArgumentError for NULL bytes between directives" do + -> { + "@\x07333333?\xf6ffffff".unpack(unpack_format("\000", 2)) + }.should raise_error(ArgumentError, /unknown unpack directive/) + end + end + + it "ignores spaces between directives" do + "@\x07333333?\xf6ffffff".unpack(unpack_format(' ', 2)).should == [2.9, 1.4] + end +end diff --git a/spec/ruby/core/string/unpack/shared/integer.rb b/spec/ruby/core/string/unpack/shared/integer.rb new file mode 100644 index 0000000000..d3934753ba --- /dev/null +++ b/spec/ruby/core/string/unpack/shared/integer.rb @@ -0,0 +1,411 @@ +# encoding: binary + +describe :string_unpack_16bit_le, shared: true do + it "decodes one short for a single format character" do + "ab".unpack(unpack_format).should == [25185] + end + + it "decodes two shorts for two format characters" do + "abcd".unpack(unpack_format(nil, 2)).should == [25185, 25699] + end + + it "decodes the number of shorts requested by the count modifier" do + "abcdef".unpack(unpack_format(3)).should == [25185, 25699, 26213] + end + + it "decodes the remaining shorts when passed the '*' modifier" do + "abcd".unpack(unpack_format('*')).should == [25185, 25699] + end + + it "decodes the remaining shorts when passed the '*' modifier after another directive" do + "abcd".unpack(unpack_format()+unpack_format('*')).should == [25185, 25699] + end + + it "does not decode a short when fewer bytes than a short remain and the '*' modifier is passed" do + "\xff".unpack(unpack_format('*')).should == [] + end + + it "adds nil for each element requested beyond the end of the String" do + [ ["", [nil, nil, nil]], + ["abc", [25185, nil, nil]], + ["abcd", [25185, 25699, nil]] + ].should be_computed_by(:unpack, unpack_format(3)) + end + + ruby_version_is ""..."3.3" do + it "ignores NULL bytes between directives" do + suppress_warning do + "abcd".unpack(unpack_format("\000", 2)).should == [25185, 25699] + end + end + end + + ruby_version_is "3.3" do + it "raise ArgumentError for NULL bytes between directives" do + -> { + "abcd".unpack(unpack_format("\000", 2)) + }.should raise_error(ArgumentError, /unknown unpack directive/) + end + end + + it "ignores spaces between directives" do + "abcd".unpack(unpack_format(' ', 2)).should == [25185, 25699] + end +end + +describe :string_unpack_16bit_le_signed, shared: true do + it "decodes a short with most significant bit set as a negative number" do + "\x00\xff".unpack(unpack_format()).should == [-256] + end +end + +describe :string_unpack_16bit_le_unsigned, shared: true do + it "decodes a short with most significant bit set as a positive number" do + "\x00\xff".unpack(unpack_format()).should == [65280] + end +end + +describe :string_unpack_16bit_be, shared: true do + it "decodes one short for a single format character" do + "ba".unpack(unpack_format).should == [25185] + end + + it "decodes two shorts for two format characters" do + "badc".unpack(unpack_format(nil, 2)).should == [25185, 25699] + end + + it "decodes the number of shorts requested by the count modifier" do + "badcfe".unpack(unpack_format(3)).should == [25185, 25699, 26213] + end + + it "decodes the remaining shorts when passed the '*' modifier" do + "badc".unpack(unpack_format('*')).should == [25185, 25699] + end + + it "decodes the remaining shorts when passed the '*' modifier after another directive" do + "badc".unpack(unpack_format()+unpack_format('*')).should == [25185, 25699] + end + + it "does not decode a short when fewer bytes than a short remain and the '*' modifier is passed" do + "\xff".unpack(unpack_format('*')).should == [] + end + + it "adds nil for each element requested beyond the end of the String" do + [ ["", [nil, nil, nil]], + ["bac", [25185, nil, nil]], + ["badc", [25185, 25699, nil]] + ].should be_computed_by(:unpack, unpack_format(3)) + end + + ruby_version_is ""..."3.3" do + it "ignores NULL bytes between directives" do + suppress_warning do + "badc".unpack(unpack_format("\000", 2)).should == [25185, 25699] + end + end + end + + ruby_version_is "3.3" do + it "raise ArgumentError for NULL bytes between directives" do + -> { + "badc".unpack(unpack_format("\000", 2)) + }.should raise_error(ArgumentError, /unknown unpack directive/) + end + end + + it "ignores spaces between directives" do + "badc".unpack(unpack_format(' ', 2)).should == [25185, 25699] + end +end + +describe :string_unpack_16bit_be_signed, shared: true do + it "decodes a short with most significant bit set as a negative number" do + "\xff\x00".unpack(unpack_format()).should == [-256] + end +end + +describe :string_unpack_16bit_be_unsigned, shared: true do + it "decodes a short with most significant bit set as a positive number" do + "\xff\x00".unpack(unpack_format()).should == [65280] + end +end + +describe :string_unpack_32bit_le, shared: true do + it "decodes one int for a single format character" do + "abcd".unpack(unpack_format).should == [1684234849] + end + + it "decodes two ints for two format characters" do + "abghefcd".unpack(unpack_format(nil, 2)).should == [1751605857, 1684235877] + end + + it "decodes the number of ints requested by the count modifier" do + "abcedfgh".unpack(unpack_format(2)).should == [1701012065, 1751606884] + end + + it "decodes the remaining ints when passed the '*' modifier" do + "acbdegfh".unpack(unpack_format('*')).should == [1684169569, 1751541605] + end + + it "decodes the remaining ints when passed the '*' modifier after another directive" do + "abcdefgh".unpack(unpack_format()+unpack_format('*')).should == [1684234849, 1751606885] + end + + it "does not decode an int when fewer bytes than an int remain and the '*' modifier is passed" do + "abc".unpack(unpack_format('*')).should == [] + end + + it "adds nil for each element requested beyond the end of the String" do + [ ["", [nil, nil, nil]], + ["abcde", [1684234849, nil, nil]], + ["abcdefg", [1684234849, nil, nil]], + ["abcdefgh", [1684234849, 1751606885, nil]] + ].should be_computed_by(:unpack, unpack_format(3)) + end + + ruby_version_is ""..."3.3" do + it "ignores NULL bytes between directives" do + suppress_warning do + "abcdefgh".unpack(unpack_format("\000", 2)).should == [1684234849, 1751606885] + end + end + end + + ruby_version_is "3.3" do + it "raise ArgumentError for NULL bytes between directives" do + -> { + "abcdefgh".unpack(unpack_format("\000", 2)) + }.should raise_error(ArgumentError, /unknown unpack directive/) + end + end + + it "ignores spaces between directives" do + "abcdefgh".unpack(unpack_format(' ', 2)).should == [1684234849, 1751606885] + end +end + +describe :string_unpack_32bit_le_signed, shared: true do + it "decodes an int with most significant bit set as a negative number" do + "\x00\xaa\x00\xff".unpack(unpack_format()).should == [-16733696] + end +end + +describe :string_unpack_32bit_le_unsigned, shared: true do + it "decodes an int with most significant bit set as a positive number" do + "\x00\xaa\x00\xff".unpack(unpack_format()).should == [4278233600] + end +end + +describe :string_unpack_32bit_be, shared: true do + it "decodes one int for a single format character" do + "dcba".unpack(unpack_format).should == [1684234849] + end + + it "decodes two ints for two format characters" do + "hgbadcfe".unpack(unpack_format(nil, 2)).should == [1751605857, 1684235877] + end + + it "decodes the number of ints requested by the count modifier" do + "ecbahgfd".unpack(unpack_format(2)).should == [1701012065, 1751606884] + end + + it "decodes the remaining ints when passed the '*' modifier" do + "dbcahfge".unpack(unpack_format('*')).should == [1684169569, 1751541605] + end + + it "decodes the remaining ints when passed the '*' modifier after another directive" do + "dcbahgfe".unpack(unpack_format()+unpack_format('*')).should == [1684234849, 1751606885] + end + + it "does not decode an int when fewer bytes than an int remain and the '*' modifier is passed" do + "abc".unpack(unpack_format('*')).should == [] + end + + it "adds nil for each element requested beyond the end of the String" do + [ ["", [nil, nil, nil]], + ["dcbae", [1684234849, nil, nil]], + ["dcbaefg", [1684234849, nil, nil]], + ["dcbahgfe", [1684234849, 1751606885, nil]] + ].should be_computed_by(:unpack, unpack_format(3)) + end + + ruby_version_is ""..."3.3" do + it "ignores NULL bytes between directives" do + suppress_warning do + "dcbahgfe".unpack(unpack_format("\000", 2)).should == [1684234849, 1751606885] + end + end + end + + ruby_version_is "3.3" do + it "raise ArgumentError for NULL bytes between directives" do + -> { + "dcbahgfe".unpack(unpack_format("\000", 2)) + }.should raise_error(ArgumentError, /unknown unpack directive/) + end + end + + it "ignores spaces between directives" do + "dcbahgfe".unpack(unpack_format(' ', 2)).should == [1684234849, 1751606885] + end +end + +describe :string_unpack_32bit_be_signed, shared: true do + it "decodes an int with most significant bit set as a negative number" do + "\xff\x00\xaa\x00".unpack(unpack_format()).should == [-16733696] + end +end + +describe :string_unpack_32bit_be_unsigned, shared: true do + it "decodes an int with most significant bit set as a positive number" do + "\xff\x00\xaa\x00".unpack(unpack_format()).should == [4278233600] + end +end + +describe :string_unpack_64bit_le, shared: true do + it "decodes one long for a single format character" do + "abcdefgh".unpack(unpack_format).should == [7523094288207667809] + end + + it "decodes two longs for two format characters" do + array = "abghefcdghefabcd".unpack(unpack_format(nil, 2)) + array.should == [7233738012216484449, 7233733596956420199] + end + + it "decodes the number of longs requested by the count modifier" do + array = "abcedfghefcdghef".unpack(unpack_format(2)) + array.should == [7523094283929477729, 7378418357791581797] + end + + it "decodes the remaining longs when passed the '*' modifier" do + array = "acbdegfhdegfhacb".unpack(unpack_format('*')) + array.should == [7522813912742519649, 7089617339433837924] + end + + it "decodes the remaining longs when passed the '*' modifier after another directive" do + array = "bcahfgedhfgedbca".unpack(unpack_format()+unpack_format('*')) + array.should == [7234302065976107874, 7017560827710891624] + end + + it "does not decode a long when fewer bytes than a long remain and the '*' modifier is passed" do + "abc".unpack(unpack_format('*')).should == [] + end + + ruby_version_is ""..."3.3" do + it "ignores NULL bytes between directives" do + suppress_warning do + array = "abcdefghabghefcd".unpack(unpack_format("\000", 2)) + array.should == [7523094288207667809, 7233738012216484449] + end + end + end + + ruby_version_is "3.3" do + it "raise ArgumentError for NULL bytes between directives" do + -> { + "badc".unpack(unpack_format("\000", 2)) + }.should raise_error(ArgumentError, /unknown unpack directive/) + end + end + + it "ignores spaces between directives" do + array = "abcdefghabghefcd".unpack(unpack_format(' ', 2)) + array.should == [7523094288207667809, 7233738012216484449] + end +end + +describe :string_unpack_64bit_le_extra, shared: true do + it "adds nil for each element requested beyond the end of the String" do + [ ["", [nil, nil, nil]], + ["abcdefgh", [7523094288207667809, nil, nil]], + ["abcdefghcdefab", [7523094288207667809, nil, nil]], + ["abcdefghcdefabde", [7523094288207667809, 7306072665971057763, nil]] + ].should be_computed_by(:unpack, unpack_format(3)) + end +end + +describe :string_unpack_64bit_le_signed, shared: true do + it "decodes a long with most significant bit set as a negative number" do + "\x00\xcc\x00\xbb\x00\xaa\x00\xff".unpack(unpack_format()).should == [-71870673923814400] + end +end + +describe :string_unpack_64bit_le_unsigned, shared: true do + it "decodes a long with most significant bit set as a positive number" do + "\x00\xcc\x00\xbb\x00\xaa\x00\xff".unpack(unpack_format()).should == [18374873399785737216] + end +end + +describe :string_unpack_64bit_be, shared: true do + it "decodes one long for a single format character" do + "hgfedcba".unpack(unpack_format).should == [7523094288207667809] + end + + it "decodes two longs for two format characters" do + array = "dcfehgbadcbafehg".unpack(unpack_format(nil, 2)) + array.should == [7233738012216484449, 7233733596956420199] + end + + it "decodes the number of longs requested by the count modifier" do + array = "hgfdecbafehgdcfe".unpack(unpack_format(2)) + array.should == [7523094283929477729, 7378418357791581797] + end + + it "decodes the remaining longs when passed the '*' modifier" do + array = "hfgedbcabcahfged".unpack(unpack_format('*')) + array.should == [7522813912742519649, 7089617339433837924] + end + + it "decodes the remaining longs when passed the '*' modifier after another directive" do + array = "degfhacbacbdegfh".unpack(unpack_format()+unpack_format('*')) + array.should == [7234302065976107874, 7017560827710891624] + end + + it "does not decode a long when fewer bytes than a long remain and the '*' modifier is passed" do + "abc".unpack(unpack_format('*')).should == [] + end + + ruby_version_is ""..."3.3" do + it "ignores NULL bytes between directives" do + suppress_warning do + array = "hgfedcbadcfehgba".unpack(unpack_format("\000", 2)) + array.should == [7523094288207667809, 7233738012216484449] + end + end + end + + ruby_version_is "3.3" do + it "raise ArgumentError for NULL bytes between directives" do + -> { + "hgfedcbadcfehgba".unpack(unpack_format("\000", 2)) + }.should raise_error(ArgumentError, /unknown unpack directive/) + end + end + + it "ignores spaces between directives" do + array = "hgfedcbadcfehgba".unpack(unpack_format(' ', 2)) + array.should == [7523094288207667809, 7233738012216484449] + end +end + +describe :string_unpack_64bit_be_extra, shared: true do + it "adds nil for each element requested beyond the end of the String" do + [ ["", [nil, nil, nil]], + ["hgfedcba", [7523094288207667809, nil, nil]], + ["hgfedcbacdefab", [7523094288207667809, nil, nil]], + ["hgfedcbaedbafedc", [7523094288207667809, 7306072665971057763, nil]] + ].should be_computed_by(:unpack, unpack_format(3)) + end +end + +describe :string_unpack_64bit_be_signed, shared: true do + it "decodes a long with most significant bit set as a negative number" do + "\xff\x00\xaa\x00\xbb\x00\xcc\x00".unpack(unpack_format()).should == [-71870673923814400] + end +end + +describe :string_unpack_64bit_be_unsigned, shared: true do + it "decodes a long with most significant bit set as a positive number" do + "\xff\x00\xaa\x00\xbb\x00\xcc\x00".unpack(unpack_format()).should == [18374873399785737216] + end +end diff --git a/spec/ruby/core/string/unpack/shared/string.rb b/spec/ruby/core/string/unpack/shared/string.rb new file mode 100644 index 0000000000..9d85eedf26 --- /dev/null +++ b/spec/ruby/core/string/unpack/shared/string.rb @@ -0,0 +1,51 @@ +describe :string_unpack_string, shared: true do + it "returns an empty string if the input is empty" do + "".unpack(unpack_format).should == [""] + end + + it "returns empty strings for repeated formats if the input is empty" do + "".unpack(unpack_format(nil, 3)).should == ["", "", ""] + end + + it "returns an empty string and does not decode any bytes when the count modifier is zero" do + "abc".unpack(unpack_format(0)+unpack_format).should == ["", "a"] + end + + it "implicitly has a count of one when no count is specified" do + "abc".unpack(unpack_format).should == ["a"] + end + + it "decodes the number of bytes specified by the count modifier" do + "abc".unpack(unpack_format(3)).should == ["abc"] + end + + it "decodes the number of bytes specified by the count modifier including whitespace bytes" do + [ ["a bc", ["a b", "c"]], + ["a\fbc", ["a\fb", "c"]], + ["a\nbc", ["a\nb", "c"]], + ["a\rbc", ["a\rb", "c"]], + ["a\tbc", ["a\tb", "c"]], + ["a\vbc", ["a\vb", "c"]] + ].should be_computed_by(:unpack, unpack_format(3)+unpack_format) + end + + it "decodes past whitespace bytes when passed the '*' modifier" do + [ ["a b c", ["a b c"]], + ["a\fb c", ["a\fb c"]], + ["a\nb c", ["a\nb c"]], + ["a\rb c", ["a\rb c"]], + ["a\tb c", ["a\tb c"]], + ["a\vb c", ["a\vb c"]], + ].should be_computed_by(:unpack, unpack_format("*")) + end +end + +describe :string_unpack_Aa, shared: true do + it "decodes the number of bytes specified by the count modifier including NULL bytes" do + "a\x00bc".unpack(unpack_format(3)+unpack_format).should == ["a\x00b", "c"] + end + + it "decodes past NULL bytes when passed the '*' modifier" do + "a\x00b c".unpack(unpack_format("*")).should == ["a\x00b c"] + end +end diff --git a/spec/ruby/core/string/unpack/shared/taint.rb b/spec/ruby/core/string/unpack/shared/taint.rb new file mode 100644 index 0000000000..79c7251f01 --- /dev/null +++ b/spec/ruby/core/string/unpack/shared/taint.rb @@ -0,0 +1,2 @@ +describe :string_unpack_taint, shared: true do +end diff --git a/spec/ruby/core/string/unpack/shared/unicode.rb b/spec/ruby/core/string/unpack/shared/unicode.rb new file mode 100644 index 0000000000..9fe07f53ae --- /dev/null +++ b/spec/ruby/core/string/unpack/shared/unicode.rb @@ -0,0 +1,72 @@ +# -*- encoding: utf-8 -*- + +describe :string_unpack_unicode, shared: true do + it "decodes Unicode codepoints as ASCII values" do + [ ["\x00", [0]], + ["\x01", [1]], + ["\x08", [8]], + ["\x0f", [15]], + ["\x18", [24]], + ["\x1f", [31]], + ["\x7f", [127]], + ["\xc2\x80", [128]], + ["\xc2\x81", [129]], + ["\xc3\xbf", [255]] + ].should be_computed_by(:unpack, "U") + end + + it "decodes the number of characters specified by the count modifier" do + [ ["\xc2\x80\xc2\x81\xc2\x82\xc2\x83", "U1", [0x80]], + ["\xc2\x80\xc2\x81\xc2\x82\xc2\x83", "U2", [0x80, 0x81]], + ["\xc2\x80\xc2\x81\xc2\x82\xc2\x83", "U3", [0x80, 0x81, 0x82]] + ].should be_computed_by(:unpack) + end + + it "implicitly has a count of one when no count modifier is passed" do + "\xc2\x80\xc2\x81\xc2\x82\xc2\x83".unpack("U1").should == [0x80] + end + + it "decodes all remaining characters when passed the '*' modifier" do + "\xc2\x80\xc2\x81\xc2\x82\xc2\x83".unpack("U*").should == [0x80, 0x81, 0x82, 0x83] + end + + it "decodes UTF-8 BMP codepoints" do + [ ["\xc2\x80", [0x80]], + ["\xdf\xbf", [0x7ff]], + ["\xe0\xa0\x80", [0x800]], + ["\xef\xbf\xbf", [0xffff]] + ].should be_computed_by(:unpack, "U") + end + + it "decodes UTF-8 max codepoints" do + [ ["\xf0\x90\x80\x80", [0x10000]], + ["\xf3\xbf\xbf\xbf", [0xfffff]], + ["\xf4\x80\x80\x80", [0x100000]], + ["\xf4\x8f\xbf\xbf", [0x10ffff]] + ].should be_computed_by(:unpack, "U") + end + + it "does not decode any items for directives exceeding the input string size" do + "\xc2\x80".unpack("UUUU").should == [0x80] + end + + ruby_version_is ""..."3.3" do + it "ignores NULL bytes between directives" do + suppress_warning do + "\x01\x02".unpack("U\x00U").should == [1, 2] + end + end + end + + ruby_version_is "3.3" do + it "raise ArgumentError for NULL bytes between directives" do + -> { + "\x01\x02".unpack("U\x00U") + }.should raise_error(ArgumentError, /unknown unpack directive/) + end + end + + it "ignores spaces between directives" do + "\x01\x02".unpack("U U").should == [1, 2] + end +end diff --git a/spec/ruby/core/string/unpack/u_spec.rb b/spec/ruby/core/string/unpack/u_spec.rb new file mode 100644 index 0000000000..68c8f6f11c --- /dev/null +++ b/spec/ruby/core/string/unpack/u_spec.rb @@ -0,0 +1,97 @@ +# encoding: binary +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' +require_relative 'shared/unicode' +require_relative 'shared/taint' + +describe "String#unpack with format 'U'" do + it_behaves_like :string_unpack_basic, 'U' + it_behaves_like :string_unpack_no_platform, 'U' + it_behaves_like :string_unpack_unicode, 'U' + it_behaves_like :string_unpack_taint, 'U' + + it "raises ArgumentError on a malformed byte sequence" do + -> { "\xE3".unpack('U') }.should raise_error(ArgumentError) + end + + it "raises ArgumentError on a malformed byte sequence and doesn't continue when used with the * modifier" do + -> { "\xE3".unpack('U*') }.should raise_error(ArgumentError) + end +end + +describe "String#unpack with format 'u'" do + it_behaves_like :string_unpack_basic, 'u' + it_behaves_like :string_unpack_no_platform, 'u' + it_behaves_like :string_unpack_taint, 'u' + + it "decodes an empty string as an empty string" do + "".unpack("u").should == [""] + end + + it "decodes into raw (ascii) string values" do + str = "".unpack("u")[0] + str.encoding.should == Encoding::BINARY + + str = "1".dup.force_encoding('UTF-8').unpack("u")[0] + str.encoding.should == Encoding::BINARY + end + + it "decodes the complete string ignoring newlines when given a single directive" do + "#86)C\n#1$5&\n".unpack("u").should == ["abcDEF"] + end + + it "appends empty string to the array for directives exceeding the input size" do + "#86)C\n#1$5&\n".unpack("uuu").should == ["abcDEF", "", ""] + end + + it "ignores the count or '*' modifier and decodes the entire string" do + [ ["#86)C\n#1$5&\n", "u238", ["abcDEF"]], + ["#86)C\n#1$5&\n", "u*", ["abcDEF"]] + ].should be_computed_by(:unpack) + end + + it "decodes all ascii characters" do + [ ["'``$\"`P0%!@``\n", ["\x00\x01\x02\x03\x04\x05\x06"]], + ["'!P@)\"@L,#0``\n", ["\a\b\t\n\v\f\r"]], + [")\#@\\0$1(3%!46\n", ["\x0E\x0F\x10\x11\x12\x13\x14\x15\x16"]], + [")%Q@9&AL<'1X?\n", ["\x17\x18\x19\x1a\e\x1c\x1d\x1e\x1f"]], + ["/(2(C)\"4F)R@I*BLL+2XO\n", ["!\"\#$%&'()*+,-./"]], + ["*,\#$R,S0U-C<X.0``\n", ["0123456789"]], + ["'.CL\\/3X_0```\n", [":;<=>?@"]], + [":04)#1$5&1TA)2DM,34Y/4%%24U155E=865H`\n", ["ABCDEFGHIJKLMNOPQRSTUVWXYZ"]], + ["&6UQ=7E]@\n", ["[\\]^_`"]], + [":86)C9&5F9VAI:FML;6YO<'%R<W1U=G=X>7H`\n", ["abcdefghijklmnopqrstuvwxyz"]], + ["$>WQ]?@``\n", ["{|}~"]], + [")?\\*`PH'\"@L*#\n", ["\x7f\xc2\x80\xc2\x81\xc2\x82\xc2\x83"]], + [")PH3\"A<*&PH?\"\n", ["\xc2\x84\xc2\x85\xc2\x86\xc2\x87\xc2"]], + [")B,*)PHK\"B\\*,\n", ["\x88\xc2\x89\xc2\x8a\xc2\x8b\xc2\x8c"]], + [")PHW\"CL*/PI#\"\n", ["\xc2\x8d\xc2\x8e\xc2\x8f\xc2\x90\xc2"]], + [")D<*2PI/\"E,*5\n", ["\x91\xc2\x92\xc2\x93\xc2\x94\xc2\x95"]], + [")PI;\"E\\*8PIG\"\n", ["\xc2\x96\xc2\x97\xc2\x98\xc2\x99\xc2"]], + [")FL*;PIS\"G<*>\n", ["\x9a\xc2\x9b\xc2\x9c\xc2\x9d\xc2\x9e"]], + [")PI_\"H,*APJ+\"\n", ["\xc2\x9f\xc2\xa0\xc2\xa1\xc2\xa2\xc2"]], + [")H\\*DPJ7\"IL*G\n", ["\xa3\xc2\xa4\xc2\xa5\xc2\xa6\xc2\xa7"]], + [")PJC\"J<*JPJO\"\n", ["\xc2\xa8\xc2\xa9\xc2\xaa\xc2\xab\xc2"]], + [")K,*MPJ[\"K\\*P\n", ["\xac\xc2\xad\xc2\xae\xc2\xaf\xc2\xb0"]], + [")PK'\"LL*SPK3\"\n", ["\xc2\xb1\xc2\xb2\xc2\xb3\xc2\xb4\xc2"]], + [")M<*VPK?\"N,*Y\n", ["\xb5\xc2\xb6\xc2\xb7\xc2\xb8\xc2\xb9"]], + [")PKK\"N\\*\\PKW\"\n", ["\xc2\xba\xc2\xbb\xc2\xbc\xc2\xbd\xc2"]], + [")OL*_PX#\#@<.\"\n", ["\xbe\xc2\xbf\xc3\x80\xc3\x81\xc3\x82"]], + [")PX/#A,.%PX;#\n", ["\xc3\x83\xc3\x84\xc3\x85\xc3\x86\xc3"]], + [")A\\.(PXG#BL.+\n", ["\x87\xc3\x88\xc3\x89\xc3\x8a\xc3\x8b"]], + [")PXS#C<..PX_#\n", ["\xc3\x8c\xc3\x8d\xc3\x8e\xc3\x8f\xc3"]], + [")D,.1PY+#D\\.4\n", ["\x90\xc3\x91\xc3\x92\xc3\x93\xc3\x94"]], + [")PY7#EL.7PYC#\n", ["\xc3\x95\xc3\x96\xc3\x97\xc3\x98\xc3"]], + [")F<.:PYO#G,.=\n", ["\x99\xc3\x9a\xc3\x9b\xc3\x9c\xc3\x9d"]], + [")PY[#G\\.@PZ'#\n", ["\xc3\x9e\xc3\x9f\xc3\xa0\xc3\xa1\xc3"]], + [")HL.CPZ3#I<.F\n", ["\xa2\xc3\xa3\xc3\xa4\xc3\xa5\xc3\xa6"]], + [")PZ?#J,.IPZK#\n", ["\xc3\xa7\xc3\xa8\xc3\xa9\xc3\xaa\xc3"]], + [")J\\.LPZW#KL.O\n", ["\xab\xc3\xac\xc3\xad\xc3\xae\xc3\xaf"]], + [")P[##L<.RP[/#\n", ["\xc3\xb0\xc3\xb1\xc3\xb2\xc3\xb3\xc3"]], + [")M,.UP[;#M\\.X\n", ["\xb4\xc3\xb5\xc3\xb6\xc3\xb7\xc3\xb8"]], + [")P[G#NL.[P[S#\n", ["\xc3\xb9\xc3\xba\xc3\xbb\xc3\xbc\xc3"]], + ["%O<.^P[\\`\n", ["\xbd\xc3\xbe\xc3\xbf"]] + ].should be_computed_by(:unpack, "u") + end +end diff --git a/spec/ruby/core/string/unpack/v_spec.rb b/spec/ruby/core/string/unpack/v_spec.rb new file mode 100644 index 0000000000..929e8712cb --- /dev/null +++ b/spec/ruby/core/string/unpack/v_spec.rb @@ -0,0 +1,18 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' +require_relative 'shared/integer' + +describe "String#unpack with format 'V'" do + it_behaves_like :string_unpack_basic, 'V' + it_behaves_like :string_unpack_32bit_le, 'V' + it_behaves_like :string_unpack_32bit_le_unsigned, 'V' + it_behaves_like :string_unpack_no_platform, 'V' +end + +describe "String#unpack with format 'v'" do + it_behaves_like :string_unpack_basic, 'v' + it_behaves_like :string_unpack_16bit_le, 'v' + it_behaves_like :string_unpack_16bit_le_unsigned, 'v' + it_behaves_like :string_unpack_no_platform, 'v' +end diff --git a/spec/ruby/core/string/unpack/w_spec.rb b/spec/ruby/core/string/unpack/w_spec.rb new file mode 100644 index 0000000000..7d3533ccae --- /dev/null +++ b/spec/ruby/core/string/unpack/w_spec.rb @@ -0,0 +1,47 @@ +# encoding: binary +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' + +describe "String#unpack with directive 'w'" do + it_behaves_like :string_unpack_basic, 'w' + it_behaves_like :string_unpack_no_platform, 'w' + + it "decodes a BER-compressed integer" do + [ ["\x00", [0]], + ["\x01", [1]], + ["\xce\x0f", [9999]], + ["\x84\x80\x80\x80\x80\x80\x80\x80\x80\x00", [2**65]] + ].should be_computed_by(:unpack, "w") + end + + ruby_version_is ""..."3.3" do + it "ignores NULL bytes between directives" do + suppress_warning do + "\x01\x02\x03".unpack("w\x00w").should == [1, 2] + end + end + end + + ruby_version_is "3.3" do + it "raise ArgumentError for NULL bytes between directives" do + -> { + "\x01\x02\x03".unpack("w\x00w") + }.should raise_error(ArgumentError, /unknown unpack directive/) + end + end + + it "ignores spaces between directives" do + "\x01\x02\x03".unpack("w w").should == [1, 2] + end +end + +describe "String#unpack with directive 'w*'" do + + it "decodes BER-compressed integers" do + "\x01\x02\x03\x04".unpack("w*").should == [1, 2, 3, 4] + "\x00\xCE\x0F\x84\x80\x80\x80\x80\x80\x80\x80\x80\x00\x01\x00".unpack("w*").should == [0, 9999, 2**65, 1, 0] + "\x81\x80\x80\x80\x80\x80\x80\x80\x80\x00\x90\x80\x80\x80\x80\x80\x80\x80\x03\x01\x02".unpack("w*").should == [2**63, (2**60 + 3), 1, 2] + end + +end diff --git a/spec/ruby/core/string/unpack/x_spec.rb b/spec/ruby/core/string/unpack/x_spec.rb new file mode 100644 index 0000000000..2926ebbe0f --- /dev/null +++ b/spec/ruby/core/string/unpack/x_spec.rb @@ -0,0 +1,62 @@ +# encoding: binary +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' + +describe "String#unpack with format 'X'" do + it_behaves_like :string_unpack_basic, 'X' + it_behaves_like :string_unpack_no_platform, 'X' + + it "moves the read index back by the number of bytes specified by count" do + "\x01\x02\x03\x04".unpack("C3X2C").should == [1, 2, 3, 2] + end + + it "does not change the read index when passed a count of zero" do + "\x01\x02\x03\x04".unpack("C3X0C").should == [1, 2, 3, 4] + end + + it "implicitly has a count of one when count is not specified" do + "\x01\x02\x03\x04".unpack("C3XC").should == [1, 2, 3, 3] + end + + it "moves the read index back by the remaining bytes when passed the '*' modifier" do + "abcd".unpack("C3X*C").should == [97, 98, 99, 99] + end + + it "raises an ArgumentError when passed the '*' modifier if the remaining bytes exceed the bytes from the index to the start of the String" do + -> { "abcd".unpack("CX*C") }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError if the count exceeds the bytes from current index to the start of the String" do + -> { "\x01\x02\x03\x04".unpack("C3X4C") }.should raise_error(ArgumentError) + end +end + +describe "String#unpack with format 'x'" do + it_behaves_like :string_unpack_basic, 'x' + it_behaves_like :string_unpack_no_platform, 'x' + + it "moves the read index forward by the number of bytes specified by count" do + "\x01\x02\x03\x04".unpack("Cx2C").should == [1, 4] + end + + it "implicitly has a count of one when count is not specified" do + "\x01\x02\x03\x04".unpack("CxC").should == [1, 3] + end + + it "does not change the read index when passed a count of zero" do + "\x01\x02\x03\x04".unpack("Cx0C").should == [1, 2] + end + + it "moves the read index to the end of the string when passed the '*' modifier" do + "\x01\x02\x03\x04".unpack("Cx*C").should == [1, nil] + end + + it "positions the read index one beyond the last readable byte in the String" do + "\x01\x02\x03\x04".unpack("C2x2C").should == [1, 2, nil] + end + + it "raises an ArgumentError if the count exceeds the size of the String" do + -> { "\x01\x02\x03\x04".unpack("C2x3C") }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/string/unpack/z_spec.rb b/spec/ruby/core/string/unpack/z_spec.rb new file mode 100644 index 0000000000..1030390550 --- /dev/null +++ b/spec/ruby/core/string/unpack/z_spec.rb @@ -0,0 +1,28 @@ +# encoding: binary +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' +require_relative 'shared/basic' +require_relative 'shared/string' +require_relative 'shared/taint' + +describe "String#unpack with format 'Z'" do + it_behaves_like :string_unpack_basic, 'Z' + it_behaves_like :string_unpack_no_platform, 'Z' + it_behaves_like :string_unpack_string, 'Z' + it_behaves_like :string_unpack_taint, 'Z' + + it "stops decoding at NULL bytes when passed the '*' modifier" do + "a\x00\x00 b \x00c".unpack('Z*Z*Z*Z*').should == ["a", "", " b ", "c"] + end + + it "decodes the number of bytes specified by the count modifier and truncates the decoded string at the first NULL byte" do + [ ["a\x00 \x00b c", ["a", " "]], + ["\x00a\x00 bc \x00", ["", "c"]] + ].should be_computed_by(:unpack, "Z5Z") + end + + it "does not advance past the null byte when given a 'Z' format specifier" do + "a\x00\x0f".unpack('Zxc').should == ['a', 15] + "a\x00\x0f".unpack('Zcc').should == ['a', 0, 15] + end +end diff --git a/spec/ruby/core/string/unpack1_spec.rb b/spec/ruby/core/string/unpack1_spec.rb new file mode 100644 index 0000000000..cfb47fe695 --- /dev/null +++ b/spec/ruby/core/string/unpack1_spec.rb @@ -0,0 +1,47 @@ +require_relative '../../spec_helper' + +describe "String#unpack1" do + it "returns the first value of #unpack" do + "ABCD".unpack1('x3C').should == "ABCD".unpack('x3C')[0] + "\u{3042 3044 3046}".unpack1("U*").should == 0x3042 + "aG9nZWZ1Z2E=".unpack1("m").should == "hogefuga" + "A".unpack1("B*").should == "01000001" + end + + it "starts unpacking from the given offset" do + "ZZABCD".unpack1('x3C', offset: 2).should == "ABCD".unpack('x3C')[0] + "ZZZZaG9nZWZ1Z2E=".unpack1("m", offset: 4).should == "hogefuga" + "ZA".unpack1("B*", offset: 1).should == "01000001" + end + + it "traits offset as a bytes offset" do + "؈".unpack("CC").should == [216, 136] + "؈".unpack1("C").should == 216 + "؈".unpack1("C", offset: 1).should == 136 + end + + it "raises an ArgumentError when the offset is negative" do + -> { "a".unpack1("C", offset: -1) }.should raise_error(ArgumentError, "offset can't be negative") + end + + it "returns nil if the offset is at the end of the string" do + "a".unpack1("C", offset: 1).should == nil + end + + it "raises an ArgumentError when the offset is larger than the string bytesize" do + -> { "a".unpack1("C", offset: 2) }.should raise_error(ArgumentError, "offset outside of string") + end + + context "with format 'm0'" do + # unpack1("m0") takes a special code path that calls Pack.unpackBase46Strict instead of Pack.unpack_m, + # which is why we repeat the tests for unpack("m0") here. + + it "decodes base64" do + "dGVzdA==".unpack1("m0").should == "test" + end + + it "raises an ArgumentError for an invalid base64 character" do + -> { "dGV%zdA==".unpack1("m0") }.should raise_error(ArgumentError) + end + end +end diff --git a/spec/ruby/core/string/unpack_spec.rb b/spec/ruby/core/string/unpack_spec.rb new file mode 100644 index 0000000000..a0abf8fa99 --- /dev/null +++ b/spec/ruby/core/string/unpack_spec.rb @@ -0,0 +1,32 @@ +require_relative '../../spec_helper' + +describe "String#unpack" do + it "raises a TypeError when passed nil" do + -> { "abc".unpack(nil) }.should raise_error(TypeError) + end + + it "raises a TypeError when passed an Integer" do + -> { "abc".unpack(1) }.should raise_error(TypeError) + end + + it "starts unpacking from the given offset" do + "abc".unpack("CC", offset: 1).should == [98, 99] + end + + it "traits offset as a bytes offset" do + "؈".unpack("CC").should == [216, 136] + "؈".unpack("CC", offset: 1).should == [136, nil] + end + + it "raises an ArgumentError when the offset is negative" do + -> { "a".unpack("C", offset: -1) }.should raise_error(ArgumentError, "offset can't be negative") + end + + it "returns nil if the offset is at the end of the string" do + "a".unpack("C", offset: 1).should == [nil] + end + + it "raises an ArgumentError when the offset is larger than the string" do + -> { "a".unpack("C", offset: 2) }.should raise_error(ArgumentError, "offset outside of string") + end +end diff --git a/spec/ruby/core/string/upcase_spec.rb b/spec/ruby/core/string/upcase_spec.rb new file mode 100644 index 0000000000..652de5c2ef --- /dev/null +++ b/spec/ruby/core/string/upcase_spec.rb @@ -0,0 +1,187 @@ +# -*- encoding: utf-8 -*- +# frozen_string_literal: false +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "String#upcase" do + it "returns a copy of self with all lowercase letters upcased" do + "Hello".upcase.should == "HELLO" + "hello".upcase.should == "HELLO" + end + + it "returns a String in the same encoding as self" do + "hello".encode("US-ASCII").upcase.encoding.should == Encoding::US_ASCII + end + + describe "full Unicode case mapping" do + it "works for all of Unicode with no option" do + "äöü".upcase.should == "ÄÖÜ" + end + + it "updates string metadata" do + upcased = "aßet".upcase + + upcased.should == "ASSET" + upcased.size.should == 5 + upcased.bytesize.should == 5 + upcased.ascii_only?.should be_true + end + end + + describe "ASCII-only case mapping" do + it "does not upcase non-ASCII characters" do + "aßet".upcase(:ascii).should == "AßET" + end + + it "works with substrings" do + "prefix té"[-2..-1].upcase(:ascii).should == "Té" + end + end + + describe "full Unicode case mapping adapted for Turkic languages" do + it "upcases ASCII characters according to Turkic semantics" do + "i".upcase(:turkic).should == "İ" + end + + it "allows Lithuanian as an extra option" do + "i".upcase(:turkic, :lithuanian).should == "İ" + end + + it "does not allow any other additional option" do + -> { "i".upcase(:turkic, :ascii) }.should raise_error(ArgumentError) + end + end + + describe "full Unicode case mapping adapted for Lithuanian" do + it "currently works the same as full Unicode case mapping" do + "iß".upcase(:lithuanian).should == "ISS" + end + + it "allows Turkic as an extra option (and applies Turkic semantics)" do + "iß".upcase(:lithuanian, :turkic).should == "İSS" + end + + it "does not allow any other additional option" do + -> { "iß".upcase(:lithuanian, :ascii) }.should raise_error(ArgumentError) + end + end + + it "does not allow the :fold option for upcasing" do + -> { "abc".upcase(:fold) }.should raise_error(ArgumentError) + end + + it "does not allow invalid options" do + -> { "abc".upcase(:invalid_option) }.should raise_error(ArgumentError) + end + + it "returns a String instance for subclasses" do + StringSpecs::MyString.new("fooBAR").upcase.should be_an_instance_of(String) + end +end + +describe "String#upcase!" do + it "modifies self in place" do + a = "HeLlO" + a.upcase!.should equal(a) + a.should == "HELLO" + end + + it "modifies self in place for non-ascii-compatible encodings" do + a = "HeLlO".encode("utf-16le") + a.upcase! + a.should == "HELLO".encode("utf-16le") + end + + describe "full Unicode case mapping" do + it "modifies self in place for all of Unicode with no option" do + a = "äöü" + a.upcase! + a.should == "ÄÖÜ" + end + + it "works for non-ascii-compatible encodings" do + a = "äöü".encode("utf-16le") + a.upcase! + a.should == "ÄÖÜ".encode("utf-16le") + end + + it "updates string metadata for self" do + upcased = "aßet" + upcased.upcase! + + upcased.should == "ASSET" + upcased.size.should == 5 + upcased.bytesize.should == 5 + upcased.ascii_only?.should be_true + end + end + + describe "modifies self in place for ASCII-only case mapping" do + it "does not upcase non-ASCII characters" do + a = "aßet" + a.upcase!(:ascii) + a.should == "AßET" + end + + it "works for non-ascii-compatible encodings" do + a = "abc".encode("utf-16le") + a.upcase!(:ascii) + a.should == "ABC".encode("utf-16le") + end + end + + describe "modifies self in place for full Unicode case mapping adapted for Turkic languages" do + it "upcases ASCII characters according to Turkic semantics" do + a = "i" + a.upcase!(:turkic) + a.should == "İ" + end + + it "allows Lithuanian as an extra option" do + a = "i" + a.upcase!(:turkic, :lithuanian) + a.should == "İ" + end + + it "does not allow any other additional option" do + -> { a = "i"; a.upcase!(:turkic, :ascii) }.should raise_error(ArgumentError) + end + end + + describe "modifies self in place for full Unicode case mapping adapted for Lithuanian" do + it "currently works the same as full Unicode case mapping" do + a = "iß" + a.upcase!(:lithuanian) + a.should == "ISS" + end + + it "allows Turkic as an extra option (and applies Turkic semantics)" do + a = "iß" + a.upcase!(:lithuanian, :turkic) + a.should == "İSS" + end + + it "does not allow any other additional option" do + -> { a = "iß"; a.upcase!(:lithuanian, :ascii) }.should raise_error(ArgumentError) + end + end + + it "does not allow the :fold option for upcasing" do + -> { a = "abc"; a.upcase!(:fold) }.should raise_error(ArgumentError) + end + + it "does not allow invalid options" do + -> { a = "abc"; a.upcase!(:invalid_option) }.should raise_error(ArgumentError) + end + + it "returns nil if no modifications were made" do + a = "HELLO" + a.upcase!.should == nil + a.should == "HELLO" + end + + it "raises a FrozenError when self is frozen" do + -> { "HeLlo".freeze.upcase! }.should raise_error(FrozenError) + -> { "HELLO".freeze.upcase! }.should raise_error(FrozenError) + end +end diff --git a/spec/ruby/core/string/uplus_spec.rb b/spec/ruby/core/string/uplus_spec.rb new file mode 100644 index 0000000000..20767bcc01 --- /dev/null +++ b/spec/ruby/core/string/uplus_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: false +require_relative '../../spec_helper' + +describe 'String#+@' do + it 'returns an unfrozen copy of a frozen String' do + input = 'foo'.freeze + output = +input + + output.should_not.frozen? + output.should == 'foo' + + output << 'bar' + output.should == 'foobar' + end + + it 'returns a mutable String itself' do + input = String.new("foo") + output = +input + + output.should.equal?(input) + + input << "bar" + output.should == "foobar" + end + + context 'if file has "frozen_string_literal: true" magic comment' do + it 'returns mutable copy of a literal' do + ruby_exe(fixture(__FILE__, "freeze_magic_comment.rb")).should == 'mutable' + end + end + + context 'if file has "frozen_string_literal: false" magic comment' do + it 'returns literal string itself' do + input = 'foo' + output = +input + + output.equal?(input).should == true + end + end + + context 'if file has no frozen_string_literal magic comment' do + ruby_version_is ''...'3.4' do + it 'returns literal string itself' do + eval(<<~RUBY).should == true + s = "foo" + s.equal?(+s) + RUBY + end + end + + ruby_version_is '3.4' do + it 'returns mutable copy of a literal' do + eval(<<~RUBY).should == false + s = "foo" + s.equal?(+s) + RUBY + end + end + end +end diff --git a/spec/ruby/core/string/upto_spec.rb b/spec/ruby/core/string/upto_spec.rb new file mode 100644 index 0000000000..8bc847d5ac --- /dev/null +++ b/spec/ruby/core/string/upto_spec.rb @@ -0,0 +1,110 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "String#upto" do + it "passes successive values, starting at self and ending at other_string, to the block" do + a = [] + "*+".upto("*3") { |s| a << s } + a.should == ["*+", "*,", "*-", "*.", "*/", "*0", "*1", "*2", "*3"] + end + + it "calls the block once even when start equals stop" do + a = [] + "abc".upto("abc") { |s| a << s } + a.should == ["abc"] + end + + it "doesn't call block with self even if self is less than stop but stop length is less than self length" do + a = [] + "25".upto("5") { |s| a << s } + a.should == [] + end + + it "doesn't call block if stop is less than self and stop length is less than self length" do + a = [] + "25".upto("1") { |s| a << s } + a.should == [] + end + + it "doesn't call the block if self is greater than stop" do + a = [] + "5".upto("2") { |s| a << s } + a.should == [] + end + + it "stops iterating as soon as the current value's character count gets higher than stop's" do + a = [] + "96".upto("AA") { |s| a << s } + a.should == ["96", "97", "98", "99"] + end + + it "returns self" do + "abc".upto("abd") { }.should == "abc" + "5".upto("2") { |i| i }.should == "5" + end + + it "tries to convert other to string using to_str" do + other = mock('abd') + def other.to_str() "abd" end + + a = [] + "abc".upto(other) { |s| a << s } + a.should == ["abc", "abd"] + end + + it "raises a TypeError if other can't be converted to a string" do + -> { "abc".upto(123) { } }.should raise_error(TypeError) + -> { "abc".upto(mock('x')){ } }.should raise_error(TypeError) + end + + + it "does not work with symbols" do + -> { "a".upto(:c).to_a }.should raise_error(TypeError) + end + + it "returns non-alphabetic characters in the ASCII range for single letters" do + "9".upto("A").to_a.should == ["9", ":", ";", "<", "=", ">", "?", "@", "A"] + "Z".upto("a").to_a.should == ["Z", "[", "\\", "]", "^", "_", "`", "a"] + "z".upto("~").to_a.should == ["z", "{", "|", "}", "~"] + end + + it "stops before the last value if exclusive" do + a = [] + "a".upto("d", true) { |s| a << s} + a.should == ["a", "b", "c"] + end + + it "works with non-ASCII ranges" do + a = [] + 'Σ'.upto('Ω') { |s| a << s } + a.should == ["Σ", "Τ", "Î¥", "Φ", "Χ", "Ψ", "Ω"] + end + + it "raises Encoding::CompatibilityError when incompatible characters are given" do + char1 = 'a'.dup.force_encoding("EUC-JP") + char2 = 'b'.dup.force_encoding("ISO-2022-JP") + -> { char1.upto(char2) {} }.should raise_error(Encoding::CompatibilityError, "incompatible character encodings: EUC-JP and ISO-2022-JP") + end + + describe "on sequence of numbers" do + it "calls the block as Integer#upto" do + "8".upto("11").to_a.should == 8.upto(11).map(&:to_s) + end + end + + describe "when no block is given" do + it "returns an enumerator" do + enum = "aaa".upto("baa", true) + enum.should be_an_instance_of(Enumerator) + enum.count.should == 26**2 + end + + describe "returned Enumerator" do + describe "size" do + it "should return nil" do + "a".upto("b").size.should == nil + end + end + end + end +end diff --git a/spec/ruby/core/string/valid_encoding/utf_8_spec.rb b/spec/ruby/core/string/valid_encoding/utf_8_spec.rb new file mode 100644 index 0000000000..a14c3af830 --- /dev/null +++ b/spec/ruby/core/string/valid_encoding/utf_8_spec.rb @@ -0,0 +1,214 @@ +# -*- encoding: utf-8 -*- +require_relative '../../../spec_helper' + +describe "String#valid_encoding? and UTF-8" do + def utf8(bytes) + bytes.pack("C*").force_encoding("UTF-8") + end + + describe "1-byte character" do + it "is valid if is in format 0xxxxxxx" do + utf8([0b00000000]).valid_encoding?.should == true + utf8([0b01111111]).valid_encoding?.should == true + end + + it "is not valid if is not in format 0xxxxxxx" do + utf8([0b10000000]).valid_encoding?.should == false + utf8([0b11111111]).valid_encoding?.should == false + end + end + + describe "2-bytes character" do + it "is valid if in format [110xxxxx 10xxxxx]" do + utf8([0b11000010, 0b10000000]).valid_encoding?.should == true + utf8([0b11000010, 0b10111111]).valid_encoding?.should == true + + utf8([0b11011111, 0b10000000]).valid_encoding?.should == true + utf8([0b11011111, 0b10111111]).valid_encoding?.should == true + end + + it "is not valid if the first byte is not in format 110xxxxx" do + utf8([0b00000010, 0b10000000]).valid_encoding?.should == false + utf8([0b00100010, 0b10000000]).valid_encoding?.should == false + utf8([0b01000010, 0b10000000]).valid_encoding?.should == false + utf8([0b01100010, 0b10000000]).valid_encoding?.should == false + utf8([0b10000010, 0b10000000]).valid_encoding?.should == false + utf8([0b10100010, 0b10000000]).valid_encoding?.should == false + utf8([0b11000010, 0b10000000]).valid_encoding?.should == true # correct bytes + utf8([0b11100010, 0b10000000]).valid_encoding?.should == false + end + + it "is not valid if the second byte is not in format 10xxxxxx" do + utf8([0b11000010, 0b00000000]).valid_encoding?.should == false + utf8([0b11000010, 0b01000000]).valid_encoding?.should == false + utf8([0b11000010, 0b11000000]).valid_encoding?.should == false + end + + it "is not valid if is smaller than [xxxxxx10 xx000000] (codepoints < U+007F, that are encoded with the 1-byte format)" do + utf8([0b11000000, 0b10111111]).valid_encoding?.should == false + utf8([0b11000001, 0b10111111]).valid_encoding?.should == false + end + + it "is not valid if the first byte is missing" do + bytes = [0b11000010, 0b10000000] + utf8(bytes[1..1]).valid_encoding?.should == false + end + + it "is not valid if the second byte is missing" do + bytes = [0b11000010, 0b10000000] + utf8(bytes[0..0]).valid_encoding?.should == false + end + end + + describe "3-bytes character" do + it "is valid if in format [1110xxxx 10xxxxxx 10xxxxxx]" do + utf8([0b11100000, 0b10100000, 0b10000000]).valid_encoding?.should == true + utf8([0b11100000, 0b10100000, 0b10111111]).valid_encoding?.should == true + utf8([0b11100000, 0b10111111, 0b10111111]).valid_encoding?.should == true + utf8([0b11101111, 0b10111111, 0b10111111]).valid_encoding?.should == true + end + + it "is not valid if the first byte is not in format 1110xxxx" do + utf8([0b00000000, 0b10100000, 0b10000000]).valid_encoding?.should == false + utf8([0b00010000, 0b10100000, 0b10000000]).valid_encoding?.should == false + utf8([0b00100000, 0b10100000, 0b10000000]).valid_encoding?.should == false + utf8([0b00110000, 0b10100000, 0b10000000]).valid_encoding?.should == false + utf8([0b01000000, 0b10100000, 0b10000000]).valid_encoding?.should == false + utf8([0b01010000, 0b10100000, 0b10000000]).valid_encoding?.should == false + utf8([0b01100000, 0b10100000, 0b10000000]).valid_encoding?.should == false + utf8([0b01110000, 0b10100000, 0b10000000]).valid_encoding?.should == false + utf8([0b10000000, 0b10100000, 0b10000000]).valid_encoding?.should == false + utf8([0b10010000, 0b10100000, 0b10000000]).valid_encoding?.should == false + utf8([0b10100000, 0b10100000, 0b10000000]).valid_encoding?.should == false + utf8([0b10110000, 0b10100000, 0b10000000]).valid_encoding?.should == false + utf8([0b11000000, 0b10100000, 0b10000000]).valid_encoding?.should == false + utf8([0b11010000, 0b10100000, 0b10000000]).valid_encoding?.should == false + utf8([0b11100000, 0b10100000, 0b10000000]).valid_encoding?.should == true # correct bytes + utf8([0b11110000, 0b10100000, 0b10000000]).valid_encoding?.should == false + end + + it "is not valid if the second byte is not in format 10xxxxxx" do + utf8([0b11100000, 0b00100000, 0b10000000]).valid_encoding?.should == false + utf8([0b11100000, 0b01100000, 0b10000000]).valid_encoding?.should == false + utf8([0b11100000, 0b11100000, 0b10000000]).valid_encoding?.should == false + end + + it "is not valid if the third byte is not in format 10xxxxxx" do + utf8([0b11100000, 0b10100000, 0b00000000]).valid_encoding?.should == false + utf8([0b11100000, 0b10100000, 0b01000000]).valid_encoding?.should == false + utf8([0b11100000, 0b10100000, 0b01000000]).valid_encoding?.should == false + end + + it "is not valid if is smaller than [xxxx0000 xx100000 xx000000] (codepoints < U+07FF that are encoded with the 2-byte format)" do + utf8([0b11100000, 0b10010000, 0b10000000]).valid_encoding?.should == false + utf8([0b11100000, 0b10001000, 0b10000000]).valid_encoding?.should == false + utf8([0b11100000, 0b10000100, 0b10000000]).valid_encoding?.should == false + utf8([0b11100000, 0b10000010, 0b10000000]).valid_encoding?.should == false + utf8([0b11100000, 0b10000001, 0b10000000]).valid_encoding?.should == false + utf8([0b11100000, 0b10000000, 0b10000000]).valid_encoding?.should == false + end + + it "is not valid if in range [xxxx1101 xx100000 xx000000] - [xxxx1101 xx111111 xx111111] (codepoints U+D800 - U+DFFF)" do + utf8([0b11101101, 0b10100000, 0b10000000]).valid_encoding?.should == false + utf8([0b11101101, 0b10100000, 0b10000001]).valid_encoding?.should == false + utf8([0b11101101, 0b10111111, 0b10111111]).valid_encoding?.should == false + + utf8([0b11101101, 0b10011111, 0b10111111]).valid_encoding?.should == true # lower boundary - 1 + utf8([0b11101110, 0b10000000, 0b10000000]).valid_encoding?.should == true # upper boundary + 1 + end + + it "is not valid if the first byte is missing" do + bytes = [0b11100000, 0b10100000, 0b10000000] + utf8(bytes[2..3]).valid_encoding?.should == false + end + + it "is not valid if the second byte is missing" do + bytes = [0b11100000, 0b10100000, 0b10000000] + utf8([bytes[0], bytes[2]]).valid_encoding?.should == false + end + + it "is not valid if the second and the third bytes are missing" do + bytes = [0b11100000, 0b10100000, 0b10000000] + utf8(bytes[0..0]).valid_encoding?.should == false + end + end + + describe "4-bytes character" do + it "is valid if in format [11110xxx 10xxxxxx 10xxxxxx 10xxxxxx]" do + utf8([0b11110000, 0b10010000, 0b10000000, 0b10000000]).valid_encoding?.should == true + utf8([0b11110000, 0b10010000, 0b10000000, 0b10111111]).valid_encoding?.should == true + utf8([0b11110000, 0b10010000, 0b10111111, 0b10111111]).valid_encoding?.should == true + utf8([0b11110000, 0b10111111, 0b10111111, 0b10111111]).valid_encoding?.should == true + utf8([0b11110100, 0b10001111, 0b10111111, 0b10111111]).valid_encoding?.should == true + end + + it "is not valid if the first byte is not in format 11110xxx" do + utf8([0b11100000, 0b10010000, 0b10000000, 0b10000000]).valid_encoding?.should == false + utf8([0b11010000, 0b10010000, 0b10000000, 0b10000000]).valid_encoding?.should == false + utf8([0b10110000, 0b10010000, 0b10000000, 0b10000000]).valid_encoding?.should == false + utf8([0b01110000, 0b10010000, 0b10000000, 0b10000000]).valid_encoding?.should == false + end + + it "is not valid if the second byte is not in format 10xxxxxx" do + utf8([0b11110000, 0b00010000, 0b10000000, 0b10000000]).valid_encoding?.should == false + utf8([0b11110000, 0b01010000, 0b10000000, 0b10000000]).valid_encoding?.should == false + utf8([0b11110000, 0b10010000, 0b10000000, 0b10000000]).valid_encoding?.should == true # correct bytes + utf8([0b11110000, 0b11010000, 0b10000000, 0b10000000]).valid_encoding?.should == false + end + + it "is not valid if the third byte is not in format 10xxxxxx" do + utf8([0b11110000, 0b10010000, 0b00000000, 0b10000000]).valid_encoding?.should == false + utf8([0b11110000, 0b10010000, 0b01000000, 0b10000000]).valid_encoding?.should == false + utf8([0b11110000, 0b10010000, 0b10000000, 0b10000000]).valid_encoding?.should == true # correct bytes + utf8([0b11110000, 0b10010000, 0b11000000, 0b10000000]).valid_encoding?.should == false + end + + it "is not valid if the forth byte is not in format 10xxxxxx" do + utf8([0b11110000, 0b10010000, 0b10000000, 0b00000000]).valid_encoding?.should == false + utf8([0b11110000, 0b10010000, 0b10000000, 0b01000000]).valid_encoding?.should == false + utf8([0b11110000, 0b10010000, 0b10000000, 0b10000000]).valid_encoding?.should == true # correct bytes + utf8([0b11110000, 0b10010000, 0b10000000, 0b11000000]).valid_encoding?.should == false + end + + it "is not valid if is smaller than [xxxxx000 xx001000 xx000000 xx000000] (codepoint < U+10000)" do + utf8([0b11110000, 0b10000111, 0b10000000, 0b10000000]).valid_encoding?.should == false + utf8([0b11110000, 0b10000110, 0b10000000, 0b10000000]).valid_encoding?.should == false + utf8([0b11110000, 0b10000101, 0b10000000, 0b10000000]).valid_encoding?.should == false + utf8([0b11110000, 0b10000100, 0b10000000, 0b10000000]).valid_encoding?.should == false + utf8([0b11110000, 0b10000011, 0b10000000, 0b10000000]).valid_encoding?.should == false + utf8([0b11110000, 0b10000010, 0b10000000, 0b10000000]).valid_encoding?.should == false + utf8([0b11110000, 0b10000001, 0b10000000, 0b10000000]).valid_encoding?.should == false + utf8([0b11110000, 0b10000000, 0b10000000, 0b10000000]).valid_encoding?.should == false + end + + it "is not valid if is greater than [xxxxx100 xx001111 xx111111 xx111111] (codepoint > U+10FFFF)" do + utf8([0b11110100, 0b10010000, 0b10000000, 0b10000000]).valid_encoding?.should == false + utf8([0b11110100, 0b10100000, 0b10000000, 0b10000000]).valid_encoding?.should == false + utf8([0b11110100, 0b10110000, 0b10000000, 0b10000000]).valid_encoding?.should == false + + utf8([0b11110101, 0b10001111, 0b10111111, 0b10111111]).valid_encoding?.should == false + utf8([0b11110110, 0b10001111, 0b10111111, 0b10111111]).valid_encoding?.should == false + utf8([0b11110111, 0b10001111, 0b10111111, 0b10111111]).valid_encoding?.should == false + end + + it "is not valid if the first byte is missing" do + bytes = [0b11110000, 0b10010000, 0b10000000, 0b10000000] + utf8(bytes[1..3]).valid_encoding?.should == false + end + + it "is not valid if the second byte is missing" do + bytes = [0b11110000, 0b10010000, 0b10000000, 0b10000000] + utf8([bytes[0], bytes[2], bytes[3]]).valid_encoding?.should == false + end + + it "is not valid if the second and the third bytes are missing" do + bytes = [0b11110000, 0b10010000, 0b10000000, 0b10000000] + utf8([bytes[0], bytes[3]]).valid_encoding?.should == false + end + + it "is not valid if the second, the third and the fourth bytes are missing" do + bytes = [0b11110000, 0b10010000, 0b10000000, 0b10000000] + utf8(bytes[0..0]).valid_encoding?.should == false + end + end +end diff --git a/spec/ruby/core/string/valid_encoding_spec.rb b/spec/ruby/core/string/valid_encoding_spec.rb new file mode 100644 index 0000000000..375035cd94 --- /dev/null +++ b/spec/ruby/core/string/valid_encoding_spec.rb @@ -0,0 +1,133 @@ +require_relative '../../spec_helper' + +describe "String#valid_encoding?" do + it "returns true if the String's encoding is valid" do + "a".valid_encoding?.should be_true + "\u{8365}\u{221}".valid_encoding?.should be_true + end + + it "returns true if self is valid in the current encoding and other encodings" do + str = +"\x77" + str.force_encoding('utf-8').valid_encoding?.should be_true + str.force_encoding('binary').valid_encoding?.should be_true + end + + it "returns true for all encodings self is valid in" do + str = +"\xE6\x9D\x94" + str.force_encoding('BINARY').valid_encoding?.should be_true + str.force_encoding('UTF-8').valid_encoding?.should be_true + str.force_encoding('US-ASCII').valid_encoding?.should be_false + str.force_encoding('Big5').valid_encoding?.should be_false + str.force_encoding('CP949').valid_encoding?.should be_false + str.force_encoding('Emacs-Mule').valid_encoding?.should be_false + str.force_encoding('EUC-JP').valid_encoding?.should be_false + str.force_encoding('EUC-KR').valid_encoding?.should be_false + str.force_encoding('EUC-TW').valid_encoding?.should be_false + str.force_encoding('GB18030').valid_encoding?.should be_false + str.force_encoding('GBK').valid_encoding?.should be_false + str.force_encoding('ISO-8859-1').valid_encoding?.should be_true + str.force_encoding('ISO-8859-2').valid_encoding?.should be_true + str.force_encoding('ISO-8859-3').valid_encoding?.should be_true + str.force_encoding('ISO-8859-4').valid_encoding?.should be_true + str.force_encoding('ISO-8859-5').valid_encoding?.should be_true + str.force_encoding('ISO-8859-6').valid_encoding?.should be_true + str.force_encoding('ISO-8859-7').valid_encoding?.should be_true + str.force_encoding('ISO-8859-8').valid_encoding?.should be_true + str.force_encoding('ISO-8859-9').valid_encoding?.should be_true + str.force_encoding('ISO-8859-10').valid_encoding?.should be_true + str.force_encoding('ISO-8859-11').valid_encoding?.should be_true + str.force_encoding('ISO-8859-13').valid_encoding?.should be_true + str.force_encoding('ISO-8859-14').valid_encoding?.should be_true + str.force_encoding('ISO-8859-15').valid_encoding?.should be_true + str.force_encoding('ISO-8859-16').valid_encoding?.should be_true + str.force_encoding('KOI8-R').valid_encoding?.should be_true + str.force_encoding('KOI8-U').valid_encoding?.should be_true + str.force_encoding('Shift_JIS').valid_encoding?.should be_false + "\xD8\x00".dup.force_encoding('UTF-16BE').valid_encoding?.should be_false + "\x00\xD8".dup.force_encoding('UTF-16LE').valid_encoding?.should be_false + "\x04\x03\x02\x01".dup.force_encoding('UTF-32BE').valid_encoding?.should be_false + "\x01\x02\x03\x04".dup.force_encoding('UTF-32LE').valid_encoding?.should be_false + str.force_encoding('Windows-1251').valid_encoding?.should be_true + str.force_encoding('IBM437').valid_encoding?.should be_true + str.force_encoding('IBM737').valid_encoding?.should be_true + str.force_encoding('IBM775').valid_encoding?.should be_true + str.force_encoding('CP850').valid_encoding?.should be_true + str.force_encoding('IBM852').valid_encoding?.should be_true + str.force_encoding('CP852').valid_encoding?.should be_true + str.force_encoding('IBM855').valid_encoding?.should be_true + str.force_encoding('CP855').valid_encoding?.should be_true + str.force_encoding('IBM857').valid_encoding?.should be_true + str.force_encoding('IBM860').valid_encoding?.should be_true + str.force_encoding('IBM861').valid_encoding?.should be_true + str.force_encoding('IBM862').valid_encoding?.should be_true + str.force_encoding('IBM863').valid_encoding?.should be_true + str.force_encoding('IBM864').valid_encoding?.should be_true + str.force_encoding('IBM865').valid_encoding?.should be_true + str.force_encoding('IBM866').valid_encoding?.should be_true + str.force_encoding('IBM869').valid_encoding?.should be_true + str.force_encoding('Windows-1258').valid_encoding?.should be_true + str.force_encoding('GB1988').valid_encoding?.should be_true + str.force_encoding('macCentEuro').valid_encoding?.should be_true + str.force_encoding('macCroatian').valid_encoding?.should be_true + str.force_encoding('macCyrillic').valid_encoding?.should be_true + str.force_encoding('macGreek').valid_encoding?.should be_true + str.force_encoding('macIceland').valid_encoding?.should be_true + str.force_encoding('macRoman').valid_encoding?.should be_true + str.force_encoding('macRomania').valid_encoding?.should be_true + str.force_encoding('macThai').valid_encoding?.should be_true + str.force_encoding('macTurkish').valid_encoding?.should be_true + str.force_encoding('macUkraine').valid_encoding?.should be_true + str.force_encoding('stateless-ISO-2022-JP').valid_encoding?.should be_false + str.force_encoding('eucJP-ms').valid_encoding?.should be_false + str.force_encoding('CP51932').valid_encoding?.should be_false + str.force_encoding('GB2312').valid_encoding?.should be_false + str.force_encoding('GB12345').valid_encoding?.should be_false + str.force_encoding('ISO-2022-JP').valid_encoding?.should be_true + str.force_encoding('ISO-2022-JP-2').valid_encoding?.should be_true + str.force_encoding('CP50221').valid_encoding?.should be_true + str.force_encoding('Windows-1252').valid_encoding?.should be_true + str.force_encoding('Windows-1250').valid_encoding?.should be_true + str.force_encoding('Windows-1256').valid_encoding?.should be_true + str.force_encoding('Windows-1253').valid_encoding?.should be_true + str.force_encoding('Windows-1255').valid_encoding?.should be_true + str.force_encoding('Windows-1254').valid_encoding?.should be_true + str.force_encoding('TIS-620').valid_encoding?.should be_true + str.force_encoding('Windows-874').valid_encoding?.should be_true + str.force_encoding('Windows-1257').valid_encoding?.should be_true + str.force_encoding('Windows-31J').valid_encoding?.should be_false + str.force_encoding('MacJapanese').valid_encoding?.should be_false + str.force_encoding('UTF-7').valid_encoding?.should be_true + str.force_encoding('UTF8-MAC').valid_encoding?.should be_true + end + + it "returns true for IBM720 encoding self is valid in" do + str = +"\xE6\x9D\x94" + str.force_encoding('IBM720').valid_encoding?.should be_true + str.force_encoding('CP720').valid_encoding?.should be_true + end + + it "returns false if self is valid in one encoding, but invalid in the one it's tagged with" do + str = +"\u{8765}" + str.valid_encoding?.should be_true + str.force_encoding('ascii') + str.valid_encoding?.should be_false + end + + it "returns false if self contains a character invalid in the associated encoding" do + "abc#{[0x80].pack('C')}".dup.force_encoding('ascii').valid_encoding?.should be_false + end + + it "returns false if a valid String had an invalid character appended to it" do + str = +"a" + str.valid_encoding?.should be_true + str << [0xDD].pack('C').force_encoding('utf-8') + str.valid_encoding?.should be_false + end + + it "returns true if an invalid string is appended another invalid one but both make a valid string" do + str = [0xD0].pack('C').force_encoding('utf-8') + str.valid_encoding?.should be_false + str << [0xBF].pack('C').force_encoding('utf-8') + str.valid_encoding?.should be_true + end +end diff --git a/spec/ruby/core/struct/clone_spec.rb b/spec/ruby/core/struct/clone_spec.rb new file mode 100644 index 0000000000..40c4d52d57 --- /dev/null +++ b/spec/ruby/core/struct/clone_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/dup' + +describe "Struct-based class#clone" do + it_behaves_like :struct_dup, :clone +end diff --git a/spec/ruby/core/struct/constants_spec.rb b/spec/ruby/core/struct/constants_spec.rb new file mode 100644 index 0000000000..7e8af1a211 --- /dev/null +++ b/spec/ruby/core/struct/constants_spec.rb @@ -0,0 +1,13 @@ +require_relative '../../spec_helper' + +describe "Struct::Group" do + it "is no longer defined" do + Struct.should_not.const_defined?(:Group) + end +end + +describe "Struct::Passwd" do + it "is no longer defined" do + Struct.should_not.const_defined?(:Passwd) + end +end diff --git a/spec/ruby/core/struct/deconstruct_keys_spec.rb b/spec/ruby/core/struct/deconstruct_keys_spec.rb new file mode 100644 index 0000000000..e16b50f930 --- /dev/null +++ b/spec/ruby/core/struct/deconstruct_keys_spec.rb @@ -0,0 +1,130 @@ +require_relative '../../spec_helper' + +describe "Struct#deconstruct_keys" do + it "returns a hash of attributes" do + struct = Struct.new(:x, :y) + s = struct.new(1, 2) + + s.deconstruct_keys([:x, :y]).should == {x: 1, y: 2} + end + + it "requires one argument" do + struct = Struct.new(:x) + obj = struct.new(1) + + -> { + obj.deconstruct_keys + }.should raise_error(ArgumentError, /wrong number of arguments \(given 0, expected 1\)/) + end + + it "returns only specified keys" do + struct = Struct.new(:x, :y, :z) + s = struct.new(1, 2, 3) + + s.deconstruct_keys([:x, :y]).should == {x: 1, y: 2} + s.deconstruct_keys([:x] ).should == {x: 1} + s.deconstruct_keys([] ).should == {} + end + + it "accepts string attribute names" do + struct = Struct.new(:x, :y) + s = struct.new(1, 2) + + s.deconstruct_keys(['x', 'y']).should == {'x' => 1, 'y' => 2} + end + + it "accepts argument position number as well but returns them as keys" do + struct = Struct.new(:x, :y, :z) + s = struct.new(10, 20, 30) + + s.deconstruct_keys([0, 1, 2]).should == {0 => 10, 1 => 20, 2 => 30} + s.deconstruct_keys([0, 1] ).should == {0 => 10, 1 => 20} + s.deconstruct_keys([0] ).should == {0 => 10} + s.deconstruct_keys([-1] ).should == {-1 => 30} + end + + it "ignores incorrect position numbers" do + struct = Struct.new(:x, :y, :z) + s = struct.new(10, 20, 30) + + s.deconstruct_keys([0, 3]).should == {0 => 10} + end + + it "support mixing attribute names and argument position numbers" do + struct = Struct.new(:x, :y) + s = struct.new(1, 2) + + s.deconstruct_keys([0, :x]).should == {0 => 1, :x => 1} + end + + it "returns an empty hash when there are more keys than attributes" do + struct = Struct.new(:x, :y) + s = struct.new(1, 2) + + s.deconstruct_keys([:x, :y, :a]).should == {} + end + + it "returns at first not existing attribute name" do + struct = Struct.new(:x, :y) + s = struct.new(1, 2) + + s.deconstruct_keys([:a, :x]).should == {} + s.deconstruct_keys([:x, :a]).should == {x: 1} + end + + it "returns at first not existing argument position number" do + struct = Struct.new(:x, :y) + s = struct.new(1, 2) + + s.deconstruct_keys([3, 0]).should == {} + s.deconstruct_keys([0, 3]).should == {0 => 1} + end + + it "accepts nil argument and return all the attributes" do + struct = Struct.new(:x, :y) + obj = struct.new(1, 2) + + obj.deconstruct_keys(nil).should == {x: 1, y: 2} + end + + it "tries to convert a key with #to_int if index is not a String nor a Symbol, but responds to #to_int" do + struct = Struct.new(:x, :y) + s = struct.new(1, 2) + + key = mock("to_int") + key.should_receive(:to_int).and_return(1) + + s.deconstruct_keys([key]).should == { key => 2 } + end + + it "raises a TypeError if the conversion with #to_int does not return an Integer" do + struct = Struct.new(:x, :y) + s = struct.new(1, 2) + + key = mock("to_int") + key.should_receive(:to_int).and_return("not an Integer") + + -> { + s.deconstruct_keys([key]) + }.should raise_error(TypeError, /can't convert MockObject to Integer/) + end + + it "raises TypeError if index is not a String, a Symbol and not convertible to Integer" do + struct = Struct.new(:x, :y) + s = struct.new(1, 2) + + -> { + s.deconstruct_keys([0, []]) + }.should raise_error(TypeError, "no implicit conversion of Array into Integer") + end + + it "raise TypeError if passed anything except nil or array" do + struct = Struct.new(:x, :y) + s = struct.new(1, 2) + + -> { s.deconstruct_keys('x') }.should raise_error(TypeError, /expected Array or nil/) + -> { s.deconstruct_keys(1) }.should raise_error(TypeError, /expected Array or nil/) + -> { s.deconstruct_keys(:x) }.should raise_error(TypeError, /expected Array or nil/) + -> { s.deconstruct_keys({}) }.should raise_error(TypeError, /expected Array or nil/) + end +end diff --git a/spec/ruby/core/struct/deconstruct_spec.rb b/spec/ruby/core/struct/deconstruct_spec.rb new file mode 100644 index 0000000000..32d4f6bac4 --- /dev/null +++ b/spec/ruby/core/struct/deconstruct_spec.rb @@ -0,0 +1,10 @@ +require_relative '../../spec_helper' + +describe "Struct#deconstruct" do + it "returns an array of attribute values" do + struct = Struct.new(:x, :y) + s = struct.new(1, 2) + + s.deconstruct.should == [1, 2] + end +end diff --git a/spec/ruby/core/struct/dig_spec.rb b/spec/ruby/core/struct/dig_spec.rb new file mode 100644 index 0000000000..93a52dbbe1 --- /dev/null +++ b/spec/ruby/core/struct/dig_spec.rb @@ -0,0 +1,52 @@ +require_relative '../../spec_helper' + +describe "Struct#dig" do + before(:each) do + @klass = Struct.new(:a) + @instance = @klass.new(@klass.new({ b: [1, 2, 3] })) + end + + it "returns the nested value specified by the sequence of keys" do + @instance.dig(:a, :a).should == { b: [1, 2, 3] } + end + + it "accepts String keys" do + @instance.dig('a', 'a').should == { b: [1, 2, 3] } + end + + it "returns the value by the index" do + instance = Struct.new(:a, :b).new(:one, :two) + instance.dig(0).should == :one + instance.dig(1).should == :two + end + + it "returns the nested value specified if the sequence includes an index" do + @instance.dig(:a, :a, :b, 0).should == 1 + end + + it "returns nil if any intermediate step is nil" do + @instance.dig(:b, 0).should == nil + end + + it "raises a TypeError if any intermediate step does not respond to #dig" do + instance = @klass.new(1) + -> { + instance.dig(:a, 3) + }.should raise_error(TypeError) + end + + it "raises an ArgumentError if no arguments provided" do + -> { @instance.dig }.should raise_error(ArgumentError) + end + + it "calls #dig on any intermediate step with the rest of the sequence as arguments" do + obj = Object.new + instance = @klass.new(obj) + + def obj.dig(*args) + {dug: args} + end + + instance.dig(:a, :bar, :baz).should == { dug: [:bar, :baz] } + end +end diff --git a/spec/ruby/core/struct/dup_spec.rb b/spec/ruby/core/struct/dup_spec.rb new file mode 100644 index 0000000000..8b50c39014 --- /dev/null +++ b/spec/ruby/core/struct/dup_spec.rb @@ -0,0 +1,23 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/dup' + +describe "Struct-based class#dup" do + + it_behaves_like :struct_dup, :dup + + # From https://github.com/jruby/jruby/issues/3686 + it "retains an included module in the ancestor chain for the struct's singleton class" do + klass = Struct.new(:foo) + mod = Module.new do + def hello + "hello" + end + end + + klass.extend(mod) + klass_dup = klass.dup + klass_dup.hello.should == "hello" + end + +end diff --git a/spec/ruby/core/struct/each_pair_spec.rb b/spec/ruby/core/struct/each_pair_spec.rb new file mode 100644 index 0000000000..1230ca9026 --- /dev/null +++ b/spec/ruby/core/struct/each_pair_spec.rb @@ -0,0 +1,33 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/accessor' +require_relative '../enumerable/shared/enumeratorized' + +describe "Struct#each_pair" do + before :each do + @car = StructClasses::Car.new('Ford', 'Ranger', 2001) + end + + it "passes each key value pair to the given block" do + @car.each_pair do |key, value| + value.should == @car[key] + end + end + + context "with a block variable" do + it "passes an array to the given block" do + @car.each_pair.map { |var| var }.should == StructClasses::Car.members.zip(@car.values) + end + end + + it "returns self if passed a block" do + @car.each_pair {}.should equal(@car) + end + + it "returns an Enumerator if not passed a block" do + @car.each_pair.should be_an_instance_of(Enumerator) + end + + it_behaves_like :struct_accessor, :each_pair + it_behaves_like :enumeratorized_with_origin_size, :each_pair, StructClasses::Car.new('Ford', 'Ranger') +end diff --git a/spec/ruby/core/struct/each_spec.rb b/spec/ruby/core/struct/each_spec.rb new file mode 100644 index 0000000000..41c0fbd4d0 --- /dev/null +++ b/spec/ruby/core/struct/each_spec.rb @@ -0,0 +1,27 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/accessor' +require_relative '../enumerable/shared/enumeratorized' + +describe "Struct#each" do + it "passes each value to the given block" do + car = StructClasses::Car.new('Ford', 'Ranger') + i = -1 + car.each do |value| + value.should == car[i += 1] + end + end + + it "returns self if passed a block" do + car = StructClasses::Car.new('Ford', 'Ranger') + car.each {}.should == car + end + + it "returns an Enumerator if not passed a block" do + car = StructClasses::Car.new('Ford', 'Ranger') + car.each.should be_an_instance_of(Enumerator) + end + + it_behaves_like :struct_accessor, :each + it_behaves_like :enumeratorized_with_origin_size, :each, StructClasses::Car.new('Ford', 'Ranger') +end diff --git a/spec/ruby/core/struct/element_reference_spec.rb b/spec/ruby/core/struct/element_reference_spec.rb new file mode 100644 index 0000000000..0f6d547f66 --- /dev/null +++ b/spec/ruby/core/struct/element_reference_spec.rb @@ -0,0 +1,52 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Struct[]" do + it "is a synonym for new" do + StructClasses::Ruby['2.0', 'i686'].should be_kind_of(StructClasses::Ruby) + end +end + +describe "Struct#[]" do + it "returns the attribute referenced" do + car = StructClasses::Car.new('Ford', 'Ranger', 1983) + car['make'].should == 'Ford' + car['model'].should == 'Ranger' + car['year'].should == 1983 + car[:make].should == 'Ford' + car[:model].should == 'Ranger' + car[:year].should == 1983 + car[0].should == 'Ford' + car[1].should == 'Ranger' + car[2].should == 1983 + car[-3].should == 'Ford' + car[-2].should == 'Ranger' + car[-1].should == 1983 + end + + it "fails when it does not know about the requested attribute" do + car = StructClasses::Car.new('Ford', 'Ranger') + -> { car[3] }.should raise_error(IndexError) + -> { car[-4] }.should raise_error(IndexError) + -> { car[:body] }.should raise_error(NameError) + -> { car['wheels'] }.should raise_error(NameError) + end + + it "fails if passed too many arguments" do + car = StructClasses::Car.new('Ford', 'Ranger') + -> { car[:make, :model] }.should raise_error(ArgumentError) + end + + it "fails if not passed a string, symbol, or integer" do + car = StructClasses::Car.new('Ford', 'Ranger') + -> { car[Object.new] }.should raise_error(TypeError) + end + + it "returns attribute names that contain hyphens" do + klass = Struct.new(:'current-state') + tuple = klass.new(0) + tuple['current-state'].should == 0 + tuple[:'current-state'].should == 0 + tuple[0].should == 0 + end +end diff --git a/spec/ruby/core/struct/element_set_spec.rb b/spec/ruby/core/struct/element_set_spec.rb new file mode 100644 index 0000000000..0a0e34a5ee --- /dev/null +++ b/spec/ruby/core/struct/element_set_spec.rb @@ -0,0 +1,36 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Struct#[]=" do + it "assigns the passed value" do + car = StructClasses::Car.new('Ford', 'Ranger') + + car[:model] = 'Escape' + car[:model].should == 'Escape' + + car['model'] = 'Fusion' + car[:model].should == 'Fusion' + + car[1] = 'Excursion' + car[:model].should == 'Excursion' + + car[-1] = '2000-2005' + car[:year].should == '2000-2005' + end + + it "fails when trying to assign attributes which don't exist" do + car = StructClasses::Car.new('Ford', 'Ranger') + + -> { car[:something] = true }.should raise_error(NameError) + -> { car[3] = true }.should raise_error(IndexError) + -> { car[-4] = true }.should raise_error(IndexError) + -> { car[Object.new] = true }.should raise_error(TypeError) + end + + it "raises a FrozenError on a frozen struct" do + car = StructClasses::Car.new('Ford', 'Ranger') + car.freeze + + -> { car[:model] = 'Escape' }.should raise_error(FrozenError) + end +end diff --git a/spec/ruby/core/struct/eql_spec.rb b/spec/ruby/core/struct/eql_spec.rb new file mode 100644 index 0000000000..c864b2b943 --- /dev/null +++ b/spec/ruby/core/struct/eql_spec.rb @@ -0,0 +1,13 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/equal_value' + +describe "Struct#eql?" do + it_behaves_like :struct_equal_value, :eql? + + it "returns false if any corresponding elements are not #eql?" do + car = StructClasses::Car.new("Honda", "Accord", 1998) + similar_car = StructClasses::Car.new("Honda", "Accord", 1998.0) + car.should_not eql(similar_car) + end +end diff --git a/spec/ruby/core/struct/equal_value_spec.rb b/spec/ruby/core/struct/equal_value_spec.rb new file mode 100644 index 0000000000..0c0f7ba570 --- /dev/null +++ b/spec/ruby/core/struct/equal_value_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/equal_value' + +describe "Struct#==" do + it_behaves_like :struct_equal_value, :== +end diff --git a/spec/ruby/core/struct/filter_spec.rb b/spec/ruby/core/struct/filter_spec.rb new file mode 100644 index 0000000000..0ccd8ad6b2 --- /dev/null +++ b/spec/ruby/core/struct/filter_spec.rb @@ -0,0 +1,10 @@ +require_relative '../../spec_helper' +require_relative 'shared/select' +require_relative 'shared/accessor' +require_relative '../enumerable/shared/enumeratorized' + +describe "Struct#filter" do + it_behaves_like :struct_select, :filter + it_behaves_like :struct_accessor, :filter + it_behaves_like :enumeratorized_with_origin_size, :filter, Struct.new(:foo).new +end diff --git a/spec/ruby/core/struct/fixtures/classes.rb b/spec/ruby/core/struct/fixtures/classes.rb new file mode 100644 index 0000000000..7b80b814ef --- /dev/null +++ b/spec/ruby/core/struct/fixtures/classes.rb @@ -0,0 +1,34 @@ +module StructClasses + + class Apple < Struct; end + + Ruby = Struct.new(:version, :platform) + + Car = Struct.new(:make, :model, :year) + + class Honda < Car + def initialize(*args) + self.make = "Honda" + super(*args) + end + end + + class StructWithOverriddenName < Struct.new(:a) + def self.name + "A" + end + end + + class SubclassX < Struct + end + + class SubclassX + attr_reader :key + def initialize(*) + @key = :value + super + end + end + + class StructSubclass < Struct; end +end diff --git a/spec/ruby/core/struct/hash_spec.rb b/spec/ruby/core/struct/hash_spec.rb new file mode 100644 index 0000000000..53361eb7a9 --- /dev/null +++ b/spec/ruby/core/struct/hash_spec.rb @@ -0,0 +1,64 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/accessor' + +describe "Struct#hash" do + + it "returns the same integer for structs with the same content" do + [StructClasses::Ruby.new("1.8.6", "PPC"), + StructClasses::Car.new("Hugo", "Foo", "1972")].each do |stc| + stc.hash.should == stc.dup.hash + stc.hash.should be_kind_of(Integer) + end + end + + it "returns the same value if structs are #eql?" do + car = StructClasses::Car.new("Honda", "Accord", "1998") + similar_car = StructClasses::Car.new("Honda", "Accord", "1998") + car.should eql(similar_car) + car.hash.should == similar_car.hash + end + + it "returns different hashes for structs with different values" do + s1 = StructClasses::Ruby.new('2.7.0', 'linux') + s2 = StructClasses::Ruby.new('2.7.0', 'macos') + s1.hash.should_not == s2.hash + end + + it "returns different hashes for structs with different values when using keyword_init: true" do + key = :"1 non symbol member" + struct_class = Struct.new(key, keyword_init: true) + t1 = struct_class.new(key => 1) + t2 = struct_class.new(key => 2) + t1.hash.should_not == t2.hash + end + + it "allows for overriding methods in an included module" do + mod = Module.new do + def hash + "different" + end + end + s = Struct.new(:arg) do + include mod + end + s.new.hash.should == "different" + end + + it "returns the same hash for recursive structs" do + car = StructClasses::Car.new("Honda", "Accord", "1998") + similar_car = StructClasses::Car.new("Honda", "Accord", "1998") + car[:make] = car + similar_car[:make] = car + car.hash.should == similar_car.hash + # This is because car.eql?(similar_car). + # Objects that are eql? must return the same hash. + # See the Struct#eql? specs + end + + it "returns different hashes for different struct classes" do + Struct.new(:x).new(1).hash.should != Struct.new(:y).new(1).hash + end + + it_behaves_like :struct_accessor, :hash +end diff --git a/spec/ruby/core/struct/initialize_spec.rb b/spec/ruby/core/struct/initialize_spec.rb new file mode 100644 index 0000000000..06055594d5 --- /dev/null +++ b/spec/ruby/core/struct/initialize_spec.rb @@ -0,0 +1,51 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Struct#initialize" do + + it "is private" do + StructClasses::Car.should have_private_instance_method(:initialize) + end + + it 'allows valid Ruby method names for members' do + valid_method_names = [ + :method1, + :method_1, + :method_1?, + :method_1!, + :a_method + ] + valid_method_names.each do |method_name| + klass = Struct.new(method_name) + instance = klass.new(:value) + instance.send(method_name).should == :value + writer_method = "#{method_name}=".to_sym + result = instance.send(writer_method, :new_value) + result.should == :new_value + instance.send(method_name).should == :new_value + end + end + + it "does nothing when passed a set of fields equal to self" do + car = same_car = StructClasses::Car.new("Honda", "Accord", "1998") + car.instance_eval { initialize("Honda", "Accord", "1998") } + car.should == same_car + end + + it "explicitly sets instance variables to nil when args not provided to initialize" do + car = StructClasses::Honda.new + car.make.should == nil # still nil despite override in Honda#initialize b/c of super order + end + + it "can be overridden" do + StructClasses::SubclassX.new(:y).new.key.should == :value + end + + it "can be initialized with keyword arguments" do + positional_args = StructClasses::Ruby.new("3.2", "OS") + keyword_args = StructClasses::Ruby.new(version: "3.2", platform: "OS") + + positional_args.version.should == keyword_args.version + positional_args.platform.should == keyword_args.platform + end +end diff --git a/spec/ruby/core/struct/inspect_spec.rb b/spec/ruby/core/struct/inspect_spec.rb new file mode 100644 index 0000000000..657b06abc1 --- /dev/null +++ b/spec/ruby/core/struct/inspect_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/inspect' + +describe "Struct#inspect" do + it_behaves_like :struct_inspect, :inspect +end diff --git a/spec/ruby/core/struct/instance_variable_get_spec.rb b/spec/ruby/core/struct/instance_variable_get_spec.rb new file mode 100644 index 0000000000..e4a3ea87dc --- /dev/null +++ b/spec/ruby/core/struct/instance_variable_get_spec.rb @@ -0,0 +1,16 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Struct#instance_variable_get" do + it "returns nil for attributes" do + car = StructClasses::Car.new("Hugo", "Foo", "1972") + car.instance_variable_get(:@make).should be_nil + end + + it "returns a user value for variables with the same name as attributes" do + car = StructClasses::Car.new("Hugo", "Foo", "1972") + car.instance_variable_set :@make, "explicit" + car.instance_variable_get(:@make).should == "explicit" + car.make.should == "Hugo" + end +end diff --git a/spec/ruby/core/struct/instance_variables_spec.rb b/spec/ruby/core/struct/instance_variables_spec.rb new file mode 100644 index 0000000000..f6d30ea97e --- /dev/null +++ b/spec/ruby/core/struct/instance_variables_spec.rb @@ -0,0 +1,16 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Struct#instance_variables" do + it "returns an empty array if only attributes are defined" do + car = StructClasses::Car.new("Hugo", "Foo", "1972") + car.instance_variables.should == [] + end + + it "returns an array with one name if an instance variable is added" do + car = StructClasses::Car.new("Hugo", "Foo", "1972") + car.instance_variables.should == [] + car.instance_variable_set("@test", 1) + car.instance_variables.should == [:@test] + end +end diff --git a/spec/ruby/core/struct/keyword_init_spec.rb b/spec/ruby/core/struct/keyword_init_spec.rb new file mode 100644 index 0000000000..536b82041a --- /dev/null +++ b/spec/ruby/core/struct/keyword_init_spec.rb @@ -0,0 +1,45 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +# See https://bugs.ruby-lang.org/issues/18008 +describe "StructClass#keyword_init?" do + it "returns true for a struct that accepts keyword arguments to initialize" do + struct = Struct.new(:arg, keyword_init: true) + struct.keyword_init?.should be_true + end + + it "returns false for a struct that does not accept keyword arguments to initialize" do + struct = Struct.new(:arg, keyword_init: false) + struct.keyword_init?.should be_false + end + + it "returns nil for a struct that did not explicitly specify keyword_init" do + struct = Struct.new(:arg) + struct.keyword_init?.should be_nil + end + + it "returns nil for a struct that does specify keyword_init to be nil" do + struct = Struct.new(:arg, keyword_init: nil) + struct.keyword_init?.should be_nil + end + + it "returns true for any truthy value, not just for true" do + struct = Struct.new(:arg, keyword_init: 1) + struct.keyword_init?.should be_true + + struct = Struct.new(:arg, keyword_init: "") + struct.keyword_init?.should be_true + + struct = Struct.new(:arg, keyword_init: []) + struct.keyword_init?.should be_true + + struct = Struct.new(:arg, keyword_init: {}) + struct.keyword_init?.should be_true + end + + context "class inheriting Struct" do + it "isn't available in a subclass" do + StructClasses::StructSubclass.should_not.respond_to?(:keyword_init?) + end + end +end diff --git a/spec/ruby/core/struct/length_spec.rb b/spec/ruby/core/struct/length_spec.rb new file mode 100644 index 0000000000..1143676122 --- /dev/null +++ b/spec/ruby/core/struct/length_spec.rb @@ -0,0 +1,12 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/accessor' + +describe "Struct#length" do + it "returns the number of attributes" do + StructClasses::Car.new('Cadillac', 'DeVille').length.should == 3 + StructClasses::Car.new.length.should == 3 + end + + it_behaves_like :struct_accessor, :length +end diff --git a/spec/ruby/core/struct/members_spec.rb b/spec/ruby/core/struct/members_spec.rb new file mode 100644 index 0000000000..1ff7b9387a --- /dev/null +++ b/spec/ruby/core/struct/members_spec.rb @@ -0,0 +1,25 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/accessor' + +describe "Struct#members" do + it "returns an array of attribute names" do + StructClasses::Car.new.members.should == [:make, :model, :year] + StructClasses::Car.new('Cadillac').members.should == [:make, :model, :year] + StructClasses::Ruby.members.should == [:version, :platform] + end + + it_behaves_like :struct_accessor, :members +end + +describe "StructClass#members" do + it "returns an array of attribute names" do + StructClasses::Car.members.should == [:make, :model, :year] + end + + context "class inheriting Struct" do + it "isn't available in a subclass" do + StructClasses::StructSubclass.should_not.respond_to?(:members) + end + end +end diff --git a/spec/ruby/core/struct/new_spec.rb b/spec/ruby/core/struct/new_spec.rb new file mode 100644 index 0000000000..1d35de7b87 --- /dev/null +++ b/spec/ruby/core/struct/new_spec.rb @@ -0,0 +1,263 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Struct.new" do + it "creates a constant in Struct namespace with string as first argument" do + struct = Struct.new('Animal', :name, :legs, :eyeballs) + struct.should == Struct::Animal + struct.name.should == "Struct::Animal" + ensure + Struct.send(:remove_const, :Animal) + end + + it "overwrites previously defined constants with string as first argument" do + first = Struct.new('Person', :height, :weight) + first.should == Struct::Person + + second = nil + -> { + second = Struct.new('Person', :hair, :sex) + }.should complain(/constant/) + second.should == Struct::Person + + first.members.should_not == second.members + ensure + Struct.send(:remove_const, :Person) + end + + it "calls to_str on its first argument (constant name)" do + obj = mock('Foo') + def obj.to_str() "Foo" end + struct = Struct.new(obj) + struct.should == Struct::Foo + struct.name.should == "Struct::Foo" + ensure + Struct.send(:remove_const, :Foo) + end + + it "creates a new anonymous class with nil first argument" do + struct = Struct.new(nil, :foo) + struct.new("bar").foo.should == "bar" + struct.should be_kind_of(Class) + struct.name.should be_nil + end + + it "creates a new anonymous class with symbol arguments" do + struct = Struct.new(:make, :model) + struct.should be_kind_of(Class) + struct.name.should == nil + end + + it "does not create a constant with symbol as first argument" do + Struct.new(:Animal2, :name, :legs, :eyeballs) + Struct.const_defined?("Animal2").should be_false + end + + it "allows non-ASCII member name" do + name = "r\xe9sum\xe9".dup.force_encoding(Encoding::ISO_8859_1).to_sym + struct = Struct.new(name) + struct.new("foo").send(name).should == "foo" + end + + it "fails with invalid constant name as first argument" do + -> { Struct.new('animal', :name, :legs, :eyeballs) }.should raise_error(NameError) + end + + it "raises a TypeError if object doesn't respond to to_sym" do + -> { Struct.new(:animal, mock('giraffe')) }.should raise_error(TypeError) + -> { Struct.new(:animal, 1.0) }.should raise_error(TypeError) + -> { Struct.new(:animal, Time.now) }.should raise_error(TypeError) + -> { Struct.new(:animal, Class) }.should raise_error(TypeError) + -> { Struct.new(:animal, nil) }.should raise_error(TypeError) + -> { Struct.new(:animal, true) }.should raise_error(TypeError) + -> { Struct.new(:animal, ['chris', 'evan']) }.should raise_error(TypeError) + end + + it "raises a TypeError if passed a Hash with an unknown key" do + -> { Struct.new(:animal, { name: 'chris' }) }.should raise_error(TypeError) + end + + ruby_version_is ""..."3.3" do + it "raises ArgumentError if not provided any arguments" do + -> { Struct.new }.should raise_error(ArgumentError) + end + end + + ruby_version_is "3.3" do + it "works when not provided any arguments" do + c = Struct.new + c.should be_kind_of(Class) + c.superclass.should == Struct + end + end + + it "raises ArgumentError when there is a duplicate member" do + -> { Struct.new(:foo, :foo) }.should raise_error(ArgumentError, "duplicate member: foo") + end + + it "raises a TypeError if object is not a Symbol" do + obj = mock(':ruby') + def obj.to_sym() :ruby end + -> { Struct.new(:animal, obj) }.should raise_error(TypeError) + end + + it "processes passed block with instance_eval" do + klass = Struct.new(:something) { @something_else = 'something else entirely!' } + klass.instance_variables.should include(:@something_else) + end + + context "with a block" do + it "allows class to be modified via the block" do + klass = Struct.new(:version) do + def platform + :ruby + end + end + instance = klass.new('2.2') + + instance.version.should == '2.2' + instance.platform.should == :ruby + end + + it "passes same struct class to the block" do + given = nil + klass = Struct.new(:attr) do |block_parameter| + given = block_parameter + end + klass.should equal(given) + end + end + + context "on subclasses" do + it "creates a constant in subclass' namespace" do + struct = StructClasses::Apple.new('Computer', :size) + struct.should == StructClasses::Apple::Computer + ensure + StructClasses::Apple.send(:remove_const, :Computer) + end + + it "creates an instance" do + StructClasses::Ruby.new.kind_of?(StructClasses::Ruby).should == true + end + + it "creates reader methods" do + StructClasses::Ruby.new.should have_method(:version) + StructClasses::Ruby.new.should have_method(:platform) + end + + it "creates writer methods" do + StructClasses::Ruby.new.should have_method(:version=) + StructClasses::Ruby.new.should have_method(:platform=) + end + + it "fails with too many arguments" do + -> { StructClasses::Ruby.new('2.0', 'i686', true) }.should raise_error(ArgumentError) + end + + it "accepts keyword arguments to initialize" do + type = Struct.new(:args) + + obj = type.new(args: 42) + obj2 = type.new(42) + + obj.should == obj2 + obj.args.should == 42 + obj2.args.should == 42 + end + + context "given positional and keyword arguments" do + it "treats keyword arguments as a positional parameter" do + type = Struct.new(:a, :b) + s = type.new("a", b: "b") + s.a.should == "a" + s.b.should == {b: "b"} + + type = Struct.new(:a, :b, :c) + s = type.new("a", b: "b", c: "c") + s.a.should == "a" + s.b.should == {b: "b", c: "c"} + s.c.should == nil + end + + it "ignores empty keyword arguments" do + type = Struct.new(:a, :b) + h = {} + s = type.new("a", **h) + + s.a.should == "a" + s.b.should == nil + end + + it "raises ArgumentError when all struct attribute values are specified" do + type = Struct.new(:a, :b) + -> { type.new("a", "b", c: "c") }.should raise_error(ArgumentError, "struct size differs") + end + end + end + + context "keyword_init: true option" do + before :all do + @struct_with_kwa = Struct.new(:name, :legs, keyword_init: true) + end + + it "creates a class that accepts keyword arguments to initialize" do + obj = @struct_with_kwa.new(name: "elefant", legs: 4) + obj.name.should == "elefant" + obj.legs.should == 4 + end + + it "raises when there is a duplicate member" do + -> { Struct.new(:foo, :foo, keyword_init: true) }.should raise_error(ArgumentError, "duplicate member: foo") + end + + describe "new class instantiation" do + it "accepts arguments as hash as well" do + obj = @struct_with_kwa.new({name: "elefant", legs: 4}) + obj.name.should == "elefant" + obj.legs.should == 4 + end + + it "allows missing arguments" do + obj = @struct_with_kwa.new(name: "elefant") + obj.name.should == "elefant" + obj.legs.should be_nil + end + + it "allows no arguments" do + obj = @struct_with_kwa.new + obj.name.should be_nil + obj.legs.should be_nil + end + + it "raises ArgumentError when passed not declared keyword argument" do + -> { + @struct_with_kwa.new(name: "elefant", legs: 4, foo: "bar") + }.should raise_error(ArgumentError, /unknown keywords: foo/) + end + + it "raises ArgumentError when passed a list of arguments" do + -> { + @struct_with_kwa.new("elefant", 4) + }.should raise_error(ArgumentError, /wrong number of arguments/) + end + + it "raises ArgumentError when passed a single non-hash argument" do + -> { + @struct_with_kwa.new("elefant") + }.should raise_error(ArgumentError, /wrong number of arguments/) + end + end + end + + context "keyword_init: false option" do + before :all do + @struct_without_kwa = Struct.new(:name, :legs, keyword_init: false) + end + + it "behaves like it does without :keyword_init option" do + obj = @struct_without_kwa.new("elefant", 4) + obj.name.should == "elefant" + obj.legs.should == 4 + end + end +end diff --git a/spec/ruby/core/struct/select_spec.rb b/spec/ruby/core/struct/select_spec.rb new file mode 100644 index 0000000000..ee846ec45f --- /dev/null +++ b/spec/ruby/core/struct/select_spec.rb @@ -0,0 +1,10 @@ +require_relative '../../spec_helper' +require_relative 'shared/select' +require_relative 'shared/accessor' +require_relative '../enumerable/shared/enumeratorized' + +describe "Struct#select" do + it_behaves_like :struct_select, :select + it_behaves_like :struct_accessor, :select + it_behaves_like :enumeratorized_with_origin_size, :select, Struct.new(:foo).new +end diff --git a/spec/ruby/core/struct/shared/accessor.rb b/spec/ruby/core/struct/shared/accessor.rb new file mode 100644 index 0000000000..dbf5e78f43 --- /dev/null +++ b/spec/ruby/core/struct/shared/accessor.rb @@ -0,0 +1,7 @@ +describe :struct_accessor, shared: true do + it "does not override the instance accessor method" do + struct = Struct.new(@method.to_sym) + instance = struct.new 42 + instance.send(@method).should == 42 + end +end diff --git a/spec/ruby/core/struct/shared/dup.rb b/spec/ruby/core/struct/shared/dup.rb new file mode 100644 index 0000000000..994f3f443e --- /dev/null +++ b/spec/ruby/core/struct/shared/dup.rb @@ -0,0 +1,9 @@ +describe :struct_dup, shared: true do + it "duplicates members" do + klass = Struct.new(:foo, :bar) + instance = klass.new(14, 2) + duped = instance.send(@method) + duped.foo.should == 14 + duped.bar.should == 2 + end +end diff --git a/spec/ruby/core/struct/shared/equal_value.rb b/spec/ruby/core/struct/shared/equal_value.rb new file mode 100644 index 0000000000..a7e0856df5 --- /dev/null +++ b/spec/ruby/core/struct/shared/equal_value.rb @@ -0,0 +1,37 @@ +describe :struct_equal_value, shared: true do + it "returns true if the other is the same object" do + car = same_car = StructClasses::Car.new("Honda", "Accord", "1998") + car.send(@method, same_car).should == true + end + + it "returns true if the other has all the same fields" do + car = StructClasses::Car.new("Honda", "Accord", "1998") + similar_car = StructClasses::Car.new("Honda", "Accord", "1998") + car.send(@method, similar_car).should == true + end + + it "returns false if the other is a different object or has different fields" do + car = StructClasses::Car.new("Honda", "Accord", "1998") + different_car = StructClasses::Car.new("Honda", "Accord", "1995") + car.send(@method, different_car).should == false + end + + it "returns false if other is of a different class" do + car = StructClasses::Car.new("Honda", "Accord", "1998") + klass = Struct.new(:make, :model, :year) + clone = klass.new("Honda", "Accord", "1998") + car.send(@method, clone).should == false + end + + it "handles recursive structures by returning false if a difference can be found" do + x = StructClasses::Car.new("Honda", "Accord", "1998") + x[:make] = x + stepping = StructClasses::Car.new("Honda", "Accord", "1998") + stone = StructClasses::Car.new(stepping, "Accord", "1998") + stepping[:make] = stone + x.send(@method, stepping).should == true + + stone[:year] = "1999" # introduce a difference + x.send(@method, stepping).should == false + end +end diff --git a/spec/ruby/core/struct/shared/inspect.rb b/spec/ruby/core/struct/shared/inspect.rb new file mode 100644 index 0000000000..1a0fb6a6b2 --- /dev/null +++ b/spec/ruby/core/struct/shared/inspect.rb @@ -0,0 +1,40 @@ +describe :struct_inspect, shared: true do + it "returns a string representation showing members and values" do + car = StructClasses::Car.new('Ford', 'Ranger') + car.send(@method).should == '#<struct StructClasses::Car make="Ford", model="Ranger", year=nil>' + end + + it "returns a string representation without the class name for anonymous structs" do + Struct.new(:a).new("").send(@method).should == '#<struct a="">' + end + + it "returns a string representation without the class name for structs nested in anonymous classes" do + c = Class.new + c.class_eval <<~DOC + class Foo < Struct.new(:a); end + DOC + + c::Foo.new("").send(@method).should == '#<struct a="">' + end + + it "returns a string representation without the class name for structs nested in anonymous modules" do + m = Module.new + m.module_eval <<~DOC + class Foo < Struct.new(:a); end + DOC + + m::Foo.new("").send(@method).should == '#<struct a="">' + end + + it "does not call #name method" do + struct = StructClasses::StructWithOverriddenName.new("") + struct.send(@method).should == '#<struct StructClasses::StructWithOverriddenName a="">' + end + + it "does not call #name method when struct is anonymous" do + struct = Struct.new(:a) + def struct.name; "A"; end + + struct.new("").send(@method).should == '#<struct a="">' + end +end diff --git a/spec/ruby/core/struct/shared/select.rb b/spec/ruby/core/struct/shared/select.rb new file mode 100644 index 0000000000..35abee461b --- /dev/null +++ b/spec/ruby/core/struct/shared/select.rb @@ -0,0 +1,26 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' + +describe :struct_select, shared: true do + it "raises an ArgumentError if given any non-block arguments" do + struct = StructClasses::Car.new + -> { struct.send(@method, 1) { } }.should raise_error(ArgumentError) + end + + it "returns a new array of elements for which block is true" do + struct = StructClasses::Car.new("Toyota", "Tercel", "2000") + struct.send(@method) { |i| i == "2000" }.should == [ "2000" ] + end + + it "returns an instance of Array" do + struct = StructClasses::Car.new("Ford", "Escort", "1995") + struct.send(@method) { true }.should be_an_instance_of(Array) + end + + describe "without block" do + it "returns an instance of Enumerator" do + struct = Struct.new(:foo).new + struct.send(@method).should be_an_instance_of(Enumerator) + end + end +end diff --git a/spec/ruby/core/struct/size_spec.rb b/spec/ruby/core/struct/size_spec.rb new file mode 100644 index 0000000000..09f260cf20 --- /dev/null +++ b/spec/ruby/core/struct/size_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/accessor' + +describe "Struct#size" do + it "is a synonym for length" do + StructClasses::Car.new.size.should == StructClasses::Car.new.length + end + + it_behaves_like :struct_accessor, :size +end diff --git a/spec/ruby/core/struct/struct_spec.rb b/spec/ruby/core/struct/struct_spec.rb new file mode 100644 index 0000000000..1b6a4488ce --- /dev/null +++ b/spec/ruby/core/struct/struct_spec.rb @@ -0,0 +1,50 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Struct" do + it "includes Enumerable" do + Struct.include?(Enumerable).should == true + end +end + +describe "Struct anonymous class instance methods" do + it "includes Enumerable" do + StructClasses::Car.include?(Enumerable).should == true + end + + it "reader method should be a synonym for []" do + klass = Struct.new(:clock, :radio) + alarm = klass.new(true) + alarm.clock.should == alarm[:clock] + alarm.radio.should == alarm['radio'] + end + + it "reader method should not interfere with undefined methods" do + car = StructClasses::Car.new('Ford', 'Ranger') + -> { car.something_weird }.should raise_error(NoMethodError) + end + + it "writer method be a synonym for []=" do + car = StructClasses::Car.new('Ford', 'Ranger') + car.model.should == 'Ranger' + car.model = 'F150' + car.model.should == 'F150' + car[:model].should == 'F150' + car['model'].should == 'F150' + car[1].should == 'F150' + end + + it "writer methods raise a FrozenError on a frozen struct" do + car = StructClasses::Car.new('Ford', 'Ranger') + car.freeze + + -> { car.model = 'Escape' }.should raise_error(FrozenError) + end +end + +describe "Struct subclasses" do + it "can be subclassed" do + compact = Class.new StructClasses::Car + compact.new.class.should == compact + end +end diff --git a/spec/ruby/core/struct/to_a_spec.rb b/spec/ruby/core/struct/to_a_spec.rb new file mode 100644 index 0000000000..cb61dc45cc --- /dev/null +++ b/spec/ruby/core/struct/to_a_spec.rb @@ -0,0 +1,12 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/accessor' + +describe "Struct#to_a" do + it "returns the values for this instance as an array" do + StructClasses::Car.new('Geo', 'Metro', 1995).to_a.should == ['Geo', 'Metro', 1995] + StructClasses::Car.new('Ford').to_a.should == ['Ford', nil, nil] + end + + it_behaves_like :struct_accessor, :to_a +end diff --git a/spec/ruby/core/struct/to_h_spec.rb b/spec/ruby/core/struct/to_h_spec.rb new file mode 100644 index 0000000000..861ce3f49d --- /dev/null +++ b/spec/ruby/core/struct/to_h_spec.rb @@ -0,0 +1,68 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Struct#to_h" do + it "returns a Hash with members as keys" do + car = StructClasses::Car.new('Ford', 'Ranger') + car.to_h.should == {make: "Ford", model: "Ranger", year: nil} + end + + it "returns a Hash that is independent from the struct" do + car = StructClasses::Car.new('Ford', 'Ranger') + car.to_h[:make] = 'Suzuki' + car.make.should == 'Ford' + end + + context "with block" do + it "converts [key, value] pairs returned by the block to a hash" do + car = StructClasses::Car.new('Ford', 'Ranger') + + h = car.to_h { |k, v| [k.to_s, "#{v}".downcase] } + h.should == { "make" => "ford", "model" => "ranger", "year" => "" } + end + + it "passes to a block each pair's key and value as separate arguments" do + s = StructClasses::Ruby.new('3.2.4', 'macos') + + ScratchPad.record [] + s.to_h { |k, v| ScratchPad << [k, v]; [k, v] } + ScratchPad.recorded.sort.should == [[:platform, 'macos'], [:version, '3.2.4']] + + ScratchPad.record [] + s.to_h { |*args| ScratchPad << args; [args[0], args[1]] } + ScratchPad.recorded.sort.should == [[:platform, 'macos'], [:version, '3.2.4']] + end + + it "raises ArgumentError if block returns longer or shorter array" do + -> do + StructClasses::Car.new.to_h { |k, v| [k.to_s, "#{v}".downcase, 1] } + end.should raise_error(ArgumentError, /element has wrong array length/) + + -> do + StructClasses::Car.new.to_h { |k, v| [k] } + end.should raise_error(ArgumentError, /element has wrong array length/) + end + + it "raises TypeError if block returns something other than Array" do + -> do + StructClasses::Car.new.to_h { |k, v| "not-array" } + end.should raise_error(TypeError, /wrong element type String/) + end + + it "coerces returned pair to Array with #to_ary" do + x = mock('x') + x.stub!(:to_ary).and_return([:b, 'b']) + + StructClasses::Car.new.to_h { |k| x }.should == { :b => 'b' } + end + + it "does not coerce returned pair to Array with #to_a" do + x = mock('x') + x.stub!(:to_a).and_return([:b, 'b']) + + -> do + StructClasses::Car.new.to_h { |k| x } + end.should raise_error(TypeError, /wrong element type MockObject/) + end + end +end diff --git a/spec/ruby/core/struct/to_s_spec.rb b/spec/ruby/core/struct/to_s_spec.rb new file mode 100644 index 0000000000..94c672d3d5 --- /dev/null +++ b/spec/ruby/core/struct/to_s_spec.rb @@ -0,0 +1,12 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/inspect' + +describe "Struct#to_s" do + it "is a synonym for inspect" do + car = StructClasses::Car.new('Ford', 'Ranger') + car.inspect.should == car.to_s + end + + it_behaves_like :struct_inspect, :to_s +end diff --git a/spec/ruby/core/struct/values_at_spec.rb b/spec/ruby/core/struct/values_at_spec.rb new file mode 100644 index 0000000000..5e5a496600 --- /dev/null +++ b/spec/ruby/core/struct/values_at_spec.rb @@ -0,0 +1,59 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +# Should be synchronized with core/array/values_at_spec.rb +describe "Struct#values_at" do + before do + clazz = Struct.new(:name, :director, :year) + @movie = clazz.new('Sympathy for Mr. Vengeance', 'Chan-wook Park', 2002) + end + + context "when passed a list of Integers" do + it "returns an array containing each value given by one of integers" do + @movie.values_at(0, 1).should == ['Sympathy for Mr. Vengeance', 'Chan-wook Park'] + end + + it "raises IndexError if any of integers is out of range" do + -> { @movie.values_at(3) }.should raise_error(IndexError, "offset 3 too large for struct(size:3)") + -> { @movie.values_at(-4) }.should raise_error(IndexError, "offset -4 too small for struct(size:3)") + end + end + + context "when passed an integer Range" do + it "returns an array containing each value given by the elements of the range" do + @movie.values_at(0..2).should == ['Sympathy for Mr. Vengeance', 'Chan-wook Park', 2002] + end + + it "fills with nil values for range elements larger than the structure" do + @movie.values_at(0..3).should == ['Sympathy for Mr. Vengeance', 'Chan-wook Park', 2002, nil] + end + + it "raises RangeError if any element of the range is negative and out of range" do + -> { @movie.values_at(-4..3) }.should raise_error(RangeError, "-4..3 out of range") + end + + it "supports endless Range" do + @movie.values_at(0..).should == ["Sympathy for Mr. Vengeance", "Chan-wook Park", 2002] + end + + it "supports beginningless Range" do + @movie.values_at(..2).should == ["Sympathy for Mr. Vengeance", "Chan-wook Park", 2002] + end + end + + it "supports multiple integer Ranges" do + @movie.values_at(0..2, 1..2).should == ['Sympathy for Mr. Vengeance', 'Chan-wook Park', 2002, 'Chan-wook Park', 2002] + end + + it "supports mixing integer Ranges and Integers" do + @movie.values_at(0..2, 2).should == ['Sympathy for Mr. Vengeance', 'Chan-wook Park', 2002, 2002] + end + + it "returns a new empty Array if no arguments given" do + @movie.values_at().should == [] + end + + it "fails when passed unsupported types" do + -> { @movie.values_at('make') }.should raise_error(TypeError, "no implicit conversion of String into Integer") + end +end diff --git a/spec/ruby/core/struct/values_spec.rb b/spec/ruby/core/struct/values_spec.rb new file mode 100644 index 0000000000..b2d11725b9 --- /dev/null +++ b/spec/ruby/core/struct/values_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Struct#values" do + it "is a synonym for to_a" do + car = StructClasses::Car.new('Nissan', 'Maxima') + car.values.should == car.to_a + + StructClasses::Car.new.values.should == StructClasses::Car.new.to_a + end +end diff --git a/spec/ruby/core/symbol/all_symbols_spec.rb b/spec/ruby/core/symbol/all_symbols_spec.rb new file mode 100644 index 0000000000..1e21809093 --- /dev/null +++ b/spec/ruby/core/symbol/all_symbols_spec.rb @@ -0,0 +1,19 @@ +require_relative '../../spec_helper' + +describe "Symbol.all_symbols" do + it "returns an array of Symbols" do + all_symbols = Symbol.all_symbols + all_symbols.should be_an_instance_of(Array) + all_symbols.each { |s| s.should be_an_instance_of(Symbol) } + end + + it "includes symbols that are strongly referenced" do + symbol = "symbol_specs_#{rand(5_000_000)}".to_sym + Symbol.all_symbols.should include(symbol) + end + + it "includes symbols that are referenced in source code but not yet executed" do + Symbol.all_symbols.any? { |s| s.to_s == 'symbol_specs_referenced_in_source_code' }.should be_true + :symbol_specs_referenced_in_source_code + end +end diff --git a/spec/ruby/core/symbol/capitalize_spec.rb b/spec/ruby/core/symbol/capitalize_spec.rb new file mode 100644 index 0000000000..a84bcf280a --- /dev/null +++ b/spec/ruby/core/symbol/capitalize_spec.rb @@ -0,0 +1,41 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' + +describe "Symbol#capitalize" do + it "returns a Symbol" do + :glark.capitalize.should be_an_instance_of(Symbol) + end + + it "converts the first character to uppercase if it is ASCII" do + :lower.capitalize.should == :Lower + end + + it "leaves the first character alone if it is not an alphabetical character" do + :"£1.20".capitalize.should == :"£1.20" + end + + it "capitalizes the first character if it is Unicode" do + :"äöü".capitalize.should == :"Äöü" + :"aou".capitalize.should == :"Aou" + end + + it "converts subsequent uppercase ASCII characters to their lowercase equivalents" do + :lOWER.capitalize.should == :Lower + end + + it "leaves ASCII characters already in the correct case as they were" do + :Title.capitalize.should == :Title + end + + it "works with both upper- and lowercase ASCII characters in the same Symbol" do + :mIxEd.capitalize.should == :Mixed + end + + it "leaves lowercase Unicode characters (except in first position) as they were" do + "a\u{00DF}C".to_sym.capitalize.should == :"Aßc" + end + + it "leaves non-alphabetic ASCII characters as they were" do + "Glark?!?".to_sym.capitalize.should == :"Glark?!?" + end +end diff --git a/spec/ruby/core/symbol/case_compare_spec.rb b/spec/ruby/core/symbol/case_compare_spec.rb new file mode 100644 index 0000000000..0c6bc1eda5 --- /dev/null +++ b/spec/ruby/core/symbol/case_compare_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' + +describe "Symbol#===" do + it "returns true when the argument is a Symbol" do + (Symbol === :ruby).should == true + end + + it "returns false when the argument is a String" do + (Symbol === 'ruby').should == false + end +end diff --git a/spec/ruby/core/symbol/casecmp_spec.rb b/spec/ruby/core/symbol/casecmp_spec.rb new file mode 100644 index 0000000000..662a29a284 --- /dev/null +++ b/spec/ruby/core/symbol/casecmp_spec.rb @@ -0,0 +1,152 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' + +describe "Symbol#casecmp with Symbol" do + it "compares symbols without regard to case" do + :abcdef.casecmp(:abcde).should == 1 + :aBcDeF.casecmp(:abcdef).should == 0 + :abcdef.casecmp(:abcdefg).should == -1 + :abcdef.casecmp(:ABCDEF).should == 0 + end + + it "doesn't consider non-ascii characters equal that aren't" do + # -- Latin-1 -- + upper_a_tilde = "\xC3".b.to_sym + upper_a_umlaut = "\xC4".b.to_sym + lower_a_tilde = "\xE3".b.to_sym + lower_a_umlaut = "\xE4".b.to_sym + + lower_a_tilde.casecmp(lower_a_umlaut).should_not == 0 + lower_a_umlaut.casecmp(lower_a_tilde).should_not == 0 + upper_a_tilde.casecmp(upper_a_umlaut).should_not == 0 + upper_a_umlaut.casecmp(upper_a_tilde).should_not == 0 + + # -- UTF-8 -- + upper_a_tilde = :"Ã" + lower_a_tilde = :"ã" + upper_a_umlaut = :"Ä" + lower_a_umlaut = :"ä" + + lower_a_tilde.casecmp(lower_a_umlaut).should_not == 0 + lower_a_umlaut.casecmp(lower_a_tilde).should_not == 0 + upper_a_tilde.casecmp(upper_a_umlaut).should_not == 0 + upper_a_umlaut.casecmp(upper_a_tilde).should_not == 0 + end + + it "doesn't do case mapping for non-ascii characters" do + # -- Latin-1 -- + upper_a_tilde = "\xC3".b.to_sym + upper_a_umlaut = "\xC4".b.to_sym + lower_a_tilde = "\xE3".b.to_sym + lower_a_umlaut = "\xE4".b.to_sym + + upper_a_tilde.casecmp(lower_a_tilde).should == -1 + upper_a_umlaut.casecmp(lower_a_umlaut).should == -1 + lower_a_tilde.casecmp(upper_a_tilde).should == 1 + lower_a_umlaut.casecmp(upper_a_umlaut).should == 1 + + # -- UTF-8 -- + upper_a_tilde = :"Ã" + lower_a_tilde = :"ã" + upper_a_umlaut = :"Ä" + lower_a_umlaut = :"ä" + + upper_a_tilde.casecmp(lower_a_tilde).should == -1 + upper_a_umlaut.casecmp(lower_a_umlaut).should == -1 + lower_a_tilde.casecmp(upper_a_tilde).should == 1 + lower_a_umlaut.casecmp(upper_a_umlaut).should == 1 + end + + it "returns 0 for empty strings in different encodings" do + ''.to_sym.casecmp(''.encode("UTF-32LE").to_sym).should == 0 + end +end + +describe "Symbol#casecmp" do + it "returns nil if other is a String" do + :abc.casecmp("abc").should be_nil + end + + it "returns nil if other is an Integer" do + :abc.casecmp(1).should be_nil + end + + it "returns nil if other is an object" do + obj = mock("string <=>") + :abc.casecmp(obj).should be_nil + end +end + +describe 'Symbol#casecmp?' do + it "compares symbols without regard to case" do + :abcdef.casecmp?(:abcde).should == false + :aBcDeF.casecmp?(:abcdef).should == true + :abcdef.casecmp?(:abcdefg).should == false + :abcdef.casecmp?(:ABCDEF).should == true + end + + it "doesn't consider non-ascii characters equal that aren't" do + # -- Latin-1 -- + upper_a_tilde = "\xC3".b.to_sym + upper_a_umlaut = "\xC4".b.to_sym + lower_a_tilde = "\xE3".b.to_sym + lower_a_umlaut = "\xE4".b.to_sym + + lower_a_tilde.casecmp?(lower_a_umlaut).should_not == true + lower_a_umlaut.casecmp?(lower_a_tilde).should_not == true + upper_a_tilde.casecmp?(upper_a_umlaut).should_not == true + upper_a_umlaut.casecmp?(upper_a_tilde).should_not == true + + # -- UTF-8 -- + upper_a_tilde = :"Ã" + lower_a_tilde = :"ã" + upper_a_umlaut = :"Ä" + lower_a_umlaut = :"ä" + + lower_a_tilde.casecmp?(lower_a_umlaut).should_not == true + lower_a_umlaut.casecmp?(lower_a_tilde).should_not == true + upper_a_tilde.casecmp?(upper_a_umlaut).should_not == true + upper_a_umlaut.casecmp?(upper_a_tilde).should_not == true + end + + it "doesn't do case mapping for non-ascii and non-unicode characters" do + # -- Latin-1 -- + upper_a_tilde = "\xC3".b.to_sym + upper_a_umlaut = "\xC4".b.to_sym + lower_a_tilde = "\xE3".b.to_sym + lower_a_umlaut = "\xE4".b.to_sym + + upper_a_tilde.casecmp?(lower_a_tilde).should == false + upper_a_umlaut.casecmp?(lower_a_umlaut).should == false + lower_a_tilde.casecmp?(upper_a_tilde).should == false + lower_a_umlaut.casecmp?(upper_a_umlaut).should == false + end + + it 'does case mapping for unicode characters' do + # -- UTF-8 -- + upper_a_tilde = :"Ã" + lower_a_tilde = :"ã" + upper_a_umlaut = :"Ä" + lower_a_umlaut = :"ä" + + upper_a_tilde.casecmp?(lower_a_tilde).should == true + upper_a_umlaut.casecmp?(lower_a_umlaut).should == true + lower_a_tilde.casecmp?(upper_a_tilde).should == true + lower_a_umlaut.casecmp?(upper_a_umlaut).should == true + end + + it 'returns nil when comparing characters with different encodings' do + # -- Latin-1 -- + upper_a_tilde = "\xC3".b.to_sym + + # -- UTF-8 -- + lower_a_tilde = :"ã" + + upper_a_tilde.casecmp?(lower_a_tilde).should == nil + lower_a_tilde.casecmp?(upper_a_tilde).should == nil + end + + it "returns true for empty symbols in different encodings" do + ''.to_sym.should.casecmp?(''.encode("UTF-32LE").to_sym) + end +end diff --git a/spec/ruby/core/symbol/comparison_spec.rb b/spec/ruby/core/symbol/comparison_spec.rb new file mode 100644 index 0000000000..613177be05 --- /dev/null +++ b/spec/ruby/core/symbol/comparison_spec.rb @@ -0,0 +1,51 @@ +require_relative '../../spec_helper' + +describe "Symbol#<=> with Symbol" do + it "compares individual characters based on their ascii value" do + ascii_order = Array.new(256) { |x| x.chr.to_sym } + sort_order = ascii_order.sort + sort_order.should == ascii_order + end + + it "returns -1 when self is less than other" do + (:this <=> :those).should == -1 + end + + it "returns 0 when self is equal to other" do + (:yep <=> :yep).should == 0 + end + + it "returns 1 when self is greater than other" do + (:yoddle <=> :griddle).should == 1 + end + + it "considers symbol that comes lexicographically first to be less if the symbols have same size" do + (:aba <=> :abc).should == -1 + (:abc <=> :aba).should == 1 + end + + it "doesn't consider shorter string to be less if longer string starts with shorter one" do + (:abc <=> :abcd).should == -1 + (:abcd <=> :abc).should == 1 + end + + it "compares shorter string with corresponding number of first chars of longer string" do + (:abx <=> :abcd).should == 1 + (:abcd <=> :abx).should == -1 + end +end + +describe "Symbol#<=>" do + it "returns nil if other is a String" do + (:abc <=> "abc").should be_nil + end + + it "returns nil if other is an Integer" do + (:abc <=> 1).should be_nil + end + + it "returns nil if other is an object" do + obj = mock("string <=>") + (:abc <=> obj).should be_nil + end +end diff --git a/spec/ruby/core/symbol/downcase_spec.rb b/spec/ruby/core/symbol/downcase_spec.rb new file mode 100644 index 0000000000..7e94c669cc --- /dev/null +++ b/spec/ruby/core/symbol/downcase_spec.rb @@ -0,0 +1,25 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' + +describe "Symbol#downcase" do + it "returns a Symbol" do + :glark.downcase.should be_an_instance_of(Symbol) + end + + it "converts uppercase ASCII characters to their lowercase equivalents" do + :lOwEr.downcase.should == :lower + end + + it "leaves lowercase Unicode characters as they were" do + "\u{E0}Bc".to_sym.downcase.should == :"à bc" + end + + it "uncapitalizes all Unicode characters" do + "ÄÖÜ".to_sym.downcase.should == :"äöü" + "AOU".to_sym.downcase.should == :"aou" + end + + it "leaves non-alphabetic ASCII characters as they were" do + "Glark?!?".to_sym.downcase.should == :"glark?!?" + end +end diff --git a/spec/ruby/core/symbol/dup_spec.rb b/spec/ruby/core/symbol/dup_spec.rb new file mode 100644 index 0000000000..8b35917c27 --- /dev/null +++ b/spec/ruby/core/symbol/dup_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' + +describe "Symbol#dup" do + it "returns self" do + :a_symbol.dup.should equal(:a_symbol) + end +end diff --git a/spec/ruby/core/symbol/element_reference_spec.rb b/spec/ruby/core/symbol/element_reference_spec.rb new file mode 100644 index 0000000000..df6bc15ddb --- /dev/null +++ b/spec/ruby/core/symbol/element_reference_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/slice' + +describe "Symbol#[]" do + it_behaves_like :symbol_slice, :[] +end diff --git a/spec/ruby/core/symbol/empty_spec.rb b/spec/ruby/core/symbol/empty_spec.rb new file mode 100644 index 0000000000..19c23cfe5f --- /dev/null +++ b/spec/ruby/core/symbol/empty_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' + +describe "Symbol#empty?" do + it "returns true if self is empty" do + :"".empty?.should be_true + end + + it "returns false if self is non-empty" do + :"a".empty?.should be_false + end +end diff --git a/spec/ruby/core/symbol/encoding_spec.rb b/spec/ruby/core/symbol/encoding_spec.rb new file mode 100644 index 0000000000..732fd62e26 --- /dev/null +++ b/spec/ruby/core/symbol/encoding_spec.rb @@ -0,0 +1,23 @@ +# encoding: utf-8 + +require_relative '../../spec_helper' + +describe "Symbol#encoding for ASCII symbols" do + it "is US-ASCII" do + :foo.encoding.name.should == "US-ASCII" + end + + it "is US-ASCII after converting to string" do + :foo.to_s.encoding.name.should == "US-ASCII" + end +end + +describe "Symbol#encoding for UTF-8 symbols" do + it "is UTF-8" do + :åäö.encoding.name.should == "UTF-8" + end + + it "is UTF-8 after converting to string" do + :åäö.to_s.encoding.name.should == "UTF-8" + end +end diff --git a/spec/ruby/core/symbol/end_with_spec.rb b/spec/ruby/core/symbol/end_with_spec.rb new file mode 100644 index 0000000000..4b9f5a4996 --- /dev/null +++ b/spec/ruby/core/symbol/end_with_spec.rb @@ -0,0 +1,8 @@ +# -*- encoding: utf-8 -*- + +require_relative '../../spec_helper' +require_relative '../../shared/string/end_with' + +describe "Symbol#end_with?" do + it_behaves_like :end_with, :to_sym +end diff --git a/spec/ruby/core/symbol/equal_value_spec.rb b/spec/ruby/core/symbol/equal_value_spec.rb new file mode 100644 index 0000000000..3fe997d02a --- /dev/null +++ b/spec/ruby/core/symbol/equal_value_spec.rb @@ -0,0 +1,14 @@ +require_relative '../../spec_helper' + +describe "Symbol#==" do + it "only returns true when the other is exactly the same symbol" do + (:ruby == :ruby).should == true + (:ruby == :"ruby").should == true + (:ruby == :'ruby').should == true + (:@ruby == :@ruby).should == true + + (:ruby == :@ruby).should == false + (:foo == :bar).should == false + (:ruby == 'ruby').should == false + end +end diff --git a/spec/ruby/core/symbol/fixtures/classes.rb b/spec/ruby/core/symbol/fixtures/classes.rb new file mode 100644 index 0000000000..6552f6ee38 --- /dev/null +++ b/spec/ruby/core/symbol/fixtures/classes.rb @@ -0,0 +1,3 @@ +module SymbolSpecs + class MyRange < Range; end +end diff --git a/spec/ruby/core/symbol/id2name_spec.rb b/spec/ruby/core/symbol/id2name_spec.rb new file mode 100644 index 0000000000..2caa89fc37 --- /dev/null +++ b/spec/ruby/core/symbol/id2name_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/id2name' + +describe "Symbol#id2name" do + it_behaves_like :symbol_id2name, :id2name +end diff --git a/spec/ruby/core/symbol/inspect_spec.rb b/spec/ruby/core/symbol/inspect_spec.rb new file mode 100644 index 0000000000..df4566c48e --- /dev/null +++ b/spec/ruby/core/symbol/inspect_spec.rb @@ -0,0 +1,112 @@ +require_relative '../../spec_helper' + +describe "Symbol#inspect" do + symbols = { + fred: ":fred", + :fred? => ":fred?", + :fred! => ":fred!", + :BAD! => ":BAD!", + :_BAD! => ":_BAD!", + :$ruby => ":$ruby", + :@ruby => ":@ruby", + :@@ruby => ":@@ruby", + :"$ruby!" => ":\"$ruby!\"", + :"$ruby?" => ":\"$ruby?\"", + :"@ruby!" => ":\"@ruby!\"", + :"@ruby?" => ":\"@ruby?\"", + :"@@ruby!" => ":\"@@ruby!\"", + :"@@ruby?" => ":\"@@ruby?\"", + + :$-w => ":$-w", + :"$-ww" => ":\"$-ww\"", + :"$+" => ":$+", + :"$~" => ":$~", + :"$:" => ":$:", + :"$?" => ":$?", + :"$<" => ":$<", + :"$_" => ":$_", + :"$/" => ":$/", + :"$'" => ":$'", + :"$\"" => ":$\"", + :"$$" => ":$$", + :"$." => ":$.", + :"$," => ":$,", + :"$`" => ":$`", + :"$!" => ":$!", + :"$;" => ":$;", + :"$\\" => ":$\\", + :"$=" => ":$=", + :"$*" => ":$*", + :"$>" => ":$>", + :"$&" => ":$&", + :"$@" => ":$@", + :"$1234" => ":$1234", + + :-@ => ":-@", + :+@ => ":+@", + :% => ":%", + :& => ":&", + :* => ":*", + :** => ":**", + :"/" => ":/", # lhs quoted for emacs happiness + :< => ":<", + :<= => ":<=", + :<=> => ":<=>", + :== => ":==", + :=== => ":===", + :=~ => ":=~", + :> => ":>", + :>= => ":>=", + :>> => ":>>", + :[] => ":[]", + :[]= => ":[]=", + :"\<\<" => ":\<\<", + :^ => ":^", + :"`" => ":`", # for emacs, and justice! + :~ => ":~", + :| => ":|", + + :"!" => ":!", + :"!=" => ":!=", + :"!~" => ":!~", + :"\$" => ":\"$\"", # for justice! + :"&&" => ":\"&&\"", + :"'" => ":\"\'\"", + :"," => ":\",\"", + :"." => ":\".\"", + :".." => ":\"..\"", + :"..." => ":\"...\"", + :":" => ":\":\"", + :"::" => ":\"::\"", + :";" => ":\";\"", + :"=" => ":\"=\"", + :"=>" => ":\"=>\"", + :"\?" => ":\"?\"", # rawr! + :"@" => ":\"@\"", + :"||" => ":\"||\"", + :"|||" => ":\"|||\"", + :"++" => ":\"++\"", + + :"\"" => ":\"\\\"\"", + :"\"\"" => ":\"\\\"\\\"\"", + + :"9" => ":\"9\"", + :"foo bar" => ":\"foo bar\"", + :"*foo" => ":\"*foo\"", + :"foo " => ":\"foo \"", + :" foo" => ":\" foo\"", + :" " => ":\" \"", + + :"ê" => [":ê", ":\"\\u00EA\""], + :"测" => [":测", ":\"\\u6D4B\""], + :"🦊" => [":🦊", ":\"\\u{1F98A}\""], + } + + expected_by_encoding = Encoding::default_external == Encoding::UTF_8 ? 0 : 1 + symbols.each do |input, expected| + expected = expected[expected_by_encoding] if expected.is_a?(Array) + it "returns self as a symbol literal for #{expected}" do + input.inspect.should == expected + end + end +end diff --git a/spec/ruby/core/symbol/intern_spec.rb b/spec/ruby/core/symbol/intern_spec.rb new file mode 100644 index 0000000000..ea04b87e8a --- /dev/null +++ b/spec/ruby/core/symbol/intern_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' + +describe "Symbol#intern" do + it "returns self" do + :foo.intern.should == :foo + end + + it "returns a Symbol" do + :foo.intern.should be_kind_of(Symbol) + end +end diff --git a/spec/ruby/core/symbol/length_spec.rb b/spec/ruby/core/symbol/length_spec.rb new file mode 100644 index 0000000000..27bee575ef --- /dev/null +++ b/spec/ruby/core/symbol/length_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/length' + +describe "Symbol#length" do + it_behaves_like :symbol_length, :length +end diff --git a/spec/ruby/core/symbol/match_spec.rb b/spec/ruby/core/symbol/match_spec.rb new file mode 100644 index 0000000000..41e058f977 --- /dev/null +++ b/spec/ruby/core/symbol/match_spec.rb @@ -0,0 +1,77 @@ +require_relative '../../spec_helper' + +describe :symbol_match, shared: true do + it "returns the index of the beginning of the match" do + :abc.send(@method, /b/).should == 1 + end + + it "returns nil if there is no match" do + :a.send(@method, /b/).should be_nil + end + + it "sets the last match pseudo-variables" do + :a.send(@method, /(.)/).should == 0 + $1.should == "a" + end +end + +describe "Symbol#=~" do + it_behaves_like :symbol_match, :=~ +end + +describe "Symbol#match" do + it "returns the MatchData" do + result = :abc.match(/b/) + result.should be_kind_of(MatchData) + result[0].should == 'b' + end + + it "returns nil if there is no match" do + :a.match(/b/).should be_nil + end + + it "sets the last match pseudo-variables" do + :a.match(/(.)/)[0].should == 'a' + $1.should == "a" + end + + describe "when passed a block" do + it "yields the MatchData" do + :abc.match(/./) {|m| ScratchPad.record m } + ScratchPad.recorded.should be_kind_of(MatchData) + end + + it "returns the block result" do + :abc.match(/./) { :result }.should == :result + end + + it "does not yield if there is no match" do + ScratchPad.record [] + :b.match(/a/) {|m| ScratchPad << m } + ScratchPad.recorded.should == [] + end + end +end + +describe "Symbol#match?" do + before :each do + # Resetting Regexp.last_match + /DONTMATCH/.match '' + end + + context "when matches the given regex" do + it "returns true but does not set Regexp.last_match" do + :string.match?(/string/i).should be_true + Regexp.last_match.should be_nil + end + end + + it "returns false when does not match the given regex" do + :string.match?(/STRING/).should be_false + end + + it "takes matching position as the 2nd argument" do + :string.match?(/str/i, 0).should be_true + :string.match?(/str/i, 1).should be_false + end +end diff --git a/spec/ruby/core/symbol/name_spec.rb b/spec/ruby/core/symbol/name_spec.rb new file mode 100644 index 0000000000..f9b631266c --- /dev/null +++ b/spec/ruby/core/symbol/name_spec.rb @@ -0,0 +1,17 @@ +require_relative '../../spec_helper' + +describe "Symbol#name" do + it "returns string" do + :ruby.name.should == "ruby" + :ルビー.name.should == "ルビー" + end + + it "returns same string instance" do + :"ruby_3".name.should.equal?(:ruby_3.name) + :"ruby_#{1+2}".name.should.equal?(:ruby_3.name) + end + + it "returns frozen string" do + :symbol.name.should.frozen? + end +end diff --git a/spec/ruby/core/symbol/next_spec.rb b/spec/ruby/core/symbol/next_spec.rb new file mode 100644 index 0000000000..97fe913739 --- /dev/null +++ b/spec/ruby/core/symbol/next_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/succ' + +describe "Symbol#next" do + it_behaves_like :symbol_succ, :next +end diff --git a/spec/ruby/core/symbol/shared/id2name.rb b/spec/ruby/core/symbol/shared/id2name.rb new file mode 100644 index 0000000000..00a9c7d7dc --- /dev/null +++ b/spec/ruby/core/symbol/shared/id2name.rb @@ -0,0 +1,30 @@ +describe :symbol_id2name, shared: true do + it "returns the string corresponding to self" do + :rubinius.send(@method).should == "rubinius" + :squash.send(@method).should == "squash" + :[].send(@method).should == "[]" + :@ruby.send(@method).should == "@ruby" + :@@ruby.send(@method).should == "@@ruby" + end + + it "returns a String in the same encoding as self" do + string = "ruby".encode("US-ASCII") + symbol = string.to_sym + + symbol.send(@method).encoding.should == Encoding::US_ASCII + end + + ruby_version_is "3.4" do + it "warns about mutating returned string" do + -> { :bad!.send(@method).upcase! }.should complain(/warning: string returned by :bad!.to_s will be frozen in the future/) + end + + it "does not warn about mutation when Warning[:deprecated] is false" do + deprecated = Warning[:deprecated] + Warning[:deprecated] = false + -> { :bad!.send(@method).upcase! }.should_not complain + ensure + Warning[:deprecated] = deprecated + end + end +end diff --git a/spec/ruby/core/symbol/shared/length.rb b/spec/ruby/core/symbol/shared/length.rb new file mode 100644 index 0000000000..692e8c57e3 --- /dev/null +++ b/spec/ruby/core/symbol/shared/length.rb @@ -0,0 +1,23 @@ +# -*- encoding: utf-8 -*- + +describe :symbol_length, shared: true do + it "returns 0 for empty name" do + :''.send(@method).should == 0 + end + + it "returns 1 for name formed by a NUL character" do + :"\x00".send(@method).should == 1 + end + + it "returns 3 for name formed by 3 ASCII characters" do + :one.send(@method).should == 3 + end + + it "returns 4 for name formed by 4 ASCII characters" do + :four.send(@method).should == 4 + end + + it "returns 4 for name formed by 1 multibyte and 3 ASCII characters" do + :"\xC3\x9Cber".send(@method).should == 4 + end +end diff --git a/spec/ruby/core/symbol/shared/slice.rb b/spec/ruby/core/symbol/shared/slice.rb new file mode 100644 index 0000000000..d3d4aad617 --- /dev/null +++ b/spec/ruby/core/symbol/shared/slice.rb @@ -0,0 +1,262 @@ +require_relative '../fixtures/classes' + +describe :symbol_slice, shared: true do + describe "with an Integer index" do + it "returns the character code of the element at the index" do + :symbol.send(@method, 1).should == ?y + end + + it "returns nil if the index starts from the end and is greater than the length" do + :symbol.send(@method, -10).should be_nil + end + + it "returns nil if the index is greater than the length" do + :symbol.send(@method, 42).should be_nil + end + end + + describe "with an Integer index and length" do + describe "and a positive index and length" do + it "returns a slice" do + :symbol.send(@method, 1,3).should == "ymb" + end + + it "returns a blank slice if the length is 0" do + :symbol.send(@method, 0,0).should == "" + :symbol.send(@method, 1,0).should == "" + end + + it "returns a slice of all remaining characters if the given length is greater than the actual length" do + :symbol.send(@method, 1,100).should == "ymbol" + end + + it "returns nil if the index is greater than the length" do + :symbol.send(@method, 10,1).should be_nil + end + end + + describe "and a positive index and negative length" do + it "returns nil" do + :symbol.send(@method, 0,-1).should be_nil + :symbol.send(@method, 1,-1).should be_nil + end + end + + describe "and a negative index and positive length" do + it "returns a slice starting from the end upto the length" do + :symbol.send(@method, -3,2).should == "bo" + end + + it "returns a blank slice if the length is 0" do + :symbol.send(@method, -1,0).should == "" + end + + it "returns a slice of all remaining characters if the given length is larger than the actual length" do + :symbol.send(@method, -4,100).should == "mbol" + end + + it "returns nil if the index is past the start" do + :symbol.send(@method, -10,1).should be_nil + end + end + + describe "and a negative index and negative length" do + it "returns nil" do + :symbol.send(@method, -1,-1).should be_nil + end + end + + describe "and a Float length" do + it "converts the length to an Integer" do + :symbol.send(@method, 2,2.5).should == "mb" + end + end + + describe "and a nil length" do + it "raises a TypeError" do + -> { :symbol.send(@method, 1,nil) }.should raise_error(TypeError) + end + end + + describe "and a length that cannot be converted into an Integer" do + it "raises a TypeError when given an Array" do + -> { :symbol.send(@method, 1,Array.new) }.should raise_error(TypeError) + end + + it "raises a TypeError when given an Hash" do + -> { :symbol.send(@method, 1,Hash.new) }.should raise_error(TypeError) + end + + it "raises a TypeError when given an Object" do + -> { :symbol.send(@method, 1,Object.new) }.should raise_error(TypeError) + end + end + end + + describe "with a Float index" do + it "converts the index to an Integer" do + :symbol.send(@method, 1.5).should == ?y + end + end + + describe "with a nil index" do + it "raises a TypeError" do + -> { :symbol.send(@method, nil) }.should raise_error(TypeError) + end + end + + describe "with an index that cannot be converted into an Integer" do + it "raises a TypeError when given an Array" do + -> { :symbol.send(@method, Array.new) }.should raise_error(TypeError) + end + + it "raises a TypeError when given an Hash" do + -> { :symbol.send(@method, Hash.new) }.should raise_error(TypeError) + end + + it "raises a TypeError when given an Object" do + -> { :symbol.send(@method, Object.new) }.should raise_error(TypeError) + end + end + + describe "with a Range slice" do + describe "that is within bounds" do + it "returns a slice if both range values begin at the start and are within bounds" do + :symbol.send(@method, 1..4).should == "ymbo" + end + + it "returns a slice if the first range value begins at the start and the last begins at the end" do + :symbol.send(@method, 1..-1).should == "ymbol" + end + + it "returns a slice if the first range value begins at the end and the last begins at the end" do + :symbol.send(@method, -4..-1).should == "mbol" + end + end + + describe "that is out of bounds" do + it "returns nil if the first range value begins past the end" do + :symbol.send(@method, 10..12).should be_nil + end + + it "returns a blank string if the first range value is within bounds and the last range value is not" do + :symbol.send(@method, -2..-10).should == "" + :symbol.send(@method, 2..-10).should == "" + end + + it "returns nil if the first range value starts from the end and is within bounds and the last value starts from the end and is greater than the length" do + :symbol.send(@method, -10..-12).should be_nil + end + + it "returns nil if the first range value starts from the end and is out of bounds and the last value starts from the end and is less than the length" do + :symbol.send(@method, -10..-2).should be_nil + end + end + + describe "with Float values" do + it "converts the first value to an Integer" do + :symbol.send(@method, 0.5..2).should == "sym" + end + + it "converts the last value to an Integer" do + :symbol.send(@method, 0..2.5).should == "sym" + end + end + end + + describe "with a Range subclass slice" do + it "returns a slice" do + range = SymbolSpecs::MyRange.new(1, 4) + :symbol.send(@method, range).should == "ymbo" + end + end + + describe "with a Regex slice" do + describe "without a capture index" do + it "returns a string of the match" do + :symbol.send(@method, /[^bol]+/).should == "sym" + end + + it "returns nil if the expression does not match" do + :symbol.send(@method, /0-9/).should be_nil + end + + it "sets $~ to the MatchData if there is a match" do + :symbol.send(@method, /[^bol]+/) + $~[0].should == "sym" + end + + it "does not set $~ if there if there is not a match" do + :symbol.send(@method, /[0-9]+/) + $~.should be_nil + end + end + + describe "with a capture index" do + it "returns a string of the complete match if the capture index is 0" do + :symbol.send(@method, /(sy)(mb)(ol)/, 0).should == "symbol" + end + + it "returns a string for the matched capture at the given index" do + :symbol.send(@method, /(sy)(mb)(ol)/, 1).should == "sy" + :symbol.send(@method, /(sy)(mb)(ol)/, -1).should == "ol" + end + + it "returns nil if there is no capture for the index" do + :symbol.send(@method, /(sy)(mb)(ol)/, 4).should be_nil + :symbol.send(@method, /(sy)(mb)(ol)/, -4).should be_nil + end + + it "converts the index to an Integer" do + :symbol.send(@method, /(sy)(mb)(ol)/, 1.5).should == "sy" + end + + describe "and an index that cannot be converted to an Integer" do + it "raises a TypeError when given an Hash" do + -> { :symbol.send(@method, /(sy)(mb)(ol)/, Hash.new) }.should raise_error(TypeError) + end + + it "raises a TypeError when given an Array" do + -> { :symbol.send(@method, /(sy)(mb)(ol)/, Array.new) }.should raise_error(TypeError) + end + + it "raises a TypeError when given an Object" do + -> { :symbol.send(@method, /(sy)(mb)(ol)/, Object.new) }.should raise_error(TypeError) + end + end + + it "raises a TypeError if the index is nil" do + -> { :symbol.send(@method, /(sy)(mb)(ol)/, nil) }.should raise_error(TypeError) + end + + it "sets $~ to the MatchData if there is a match" do + :symbol.send(@method, /(sy)(mb)(ol)/, 0) + $~[0].should == "symbol" + $~[1].should == "sy" + $~[2].should == "mb" + $~[3].should == "ol" + end + + it "does not set $~ to the MatchData if there is not a match" do + :symbol.send(@method, /0-9/, 0) + $~.should be_nil + end + end + end + + describe "with a String slice" do + it "does not set $~" do + $~ = nil + :symbol.send(@method, "sym") + $~.should be_nil + end + + it "returns a string if there is match" do + :symbol.send(@method, "ymb").should == "ymb" + end + + it "returns nil if there is not a match" do + :symbol.send(@method, "foo").should be_nil + end + end +end diff --git a/spec/ruby/core/symbol/shared/succ.rb b/spec/ruby/core/symbol/shared/succ.rb new file mode 100644 index 0000000000..dde298207e --- /dev/null +++ b/spec/ruby/core/symbol/shared/succ.rb @@ -0,0 +1,18 @@ +require_relative '../../../spec_helper' + +describe :symbol_succ, shared: true do + it "returns a successor" do + :abcd.send(@method).should == :abce + :THX1138.send(@method).should == :THX1139 + end + + it "propagates a 'carry'" do + :"1999zzz".send(@method).should == :"2000aaa" + :ZZZ9999.send(@method).should == :AAAA0000 + end + + it "increments non-alphanumeric characters when no alphanumeric characters are present" do + :"<<koala>>".send(@method).should == :"<<koalb>>" + :"***".send(@method).should == :"**+" + end +end diff --git a/spec/ruby/core/symbol/size_spec.rb b/spec/ruby/core/symbol/size_spec.rb new file mode 100644 index 0000000000..5e2aa8d4d2 --- /dev/null +++ b/spec/ruby/core/symbol/size_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/length' + +describe "Symbol#size" do + it_behaves_like :symbol_length, :size +end diff --git a/spec/ruby/core/symbol/slice_spec.rb b/spec/ruby/core/symbol/slice_spec.rb new file mode 100644 index 0000000000..d2421c474c --- /dev/null +++ b/spec/ruby/core/symbol/slice_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/slice' + +describe "Symbol#slice" do + it_behaves_like :symbol_slice, :slice +end diff --git a/spec/ruby/core/symbol/start_with_spec.rb b/spec/ruby/core/symbol/start_with_spec.rb new file mode 100644 index 0000000000..cd43279003 --- /dev/null +++ b/spec/ruby/core/symbol/start_with_spec.rb @@ -0,0 +1,8 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative '../../shared/string/start_with' + +describe "Symbol#start_with?" do + it_behaves_like :start_with, :to_sym +end diff --git a/spec/ruby/core/symbol/succ_spec.rb b/spec/ruby/core/symbol/succ_spec.rb new file mode 100644 index 0000000000..694bfff862 --- /dev/null +++ b/spec/ruby/core/symbol/succ_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/succ' + +describe "Symbol#succ" do + it_behaves_like :symbol_succ, :succ +end diff --git a/spec/ruby/core/symbol/swapcase_spec.rb b/spec/ruby/core/symbol/swapcase_spec.rb new file mode 100644 index 0000000000..24709cac30 --- /dev/null +++ b/spec/ruby/core/symbol/swapcase_spec.rb @@ -0,0 +1,29 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' + +describe "Symbol#swapcase" do + it "returns a Symbol" do + :glark.swapcase.should be_an_instance_of(Symbol) + end + + it "converts lowercase ASCII characters to their uppercase equivalents" do + :lower.swapcase.should == :LOWER + end + + it "converts uppercase ASCII characters to their lowercase equivalents" do + :UPPER.swapcase.should == :upper + end + + it "works with both upper- and lowercase ASCII characters in the same Symbol" do + :mIxEd.swapcase.should == :MiXeD + end + + it "swaps the case for Unicode characters" do + "äÖü".to_sym.swapcase.should == :"ÄöÜ" + "aOu".to_sym.swapcase.should == :"AoU" + end + + it "leaves non-alphabetic ASCII characters as they were" do + "Glark?!?".to_sym.swapcase.should == :"gLARK?!?" + end +end diff --git a/spec/ruby/core/symbol/symbol_spec.rb b/spec/ruby/core/symbol/symbol_spec.rb new file mode 100644 index 0000000000..cefe70bc99 --- /dev/null +++ b/spec/ruby/core/symbol/symbol_spec.rb @@ -0,0 +1,19 @@ +require_relative '../../spec_helper' + +describe "Symbol" do + it "includes Comparable" do + Symbol.include?(Comparable).should == true + end + + it ".allocate raises a TypeError" do + -> do + Symbol.allocate + end.should raise_error(TypeError) + end + + it ".new is undefined" do + -> do + Symbol.new + end.should raise_error(NoMethodError) + end +end diff --git a/spec/ruby/core/symbol/to_proc_spec.rb b/spec/ruby/core/symbol/to_proc_spec.rb new file mode 100644 index 0000000000..def5d6d344 --- /dev/null +++ b/spec/ruby/core/symbol/to_proc_spec.rb @@ -0,0 +1,78 @@ +require_relative '../../spec_helper' + +describe "Symbol#to_proc" do + it "returns a new Proc" do + proc = :to_s.to_proc + proc.should be_kind_of(Proc) + end + + it "sends self to arguments passed when calling #call on the Proc" do + obj = mock("Receiving #to_s") + obj.should_receive(:to_s).and_return("Received #to_s") + :to_s.to_proc.call(obj).should == "Received #to_s" + end + + it "returns a Proc with #lambda? true" do + pr = :to_s.to_proc + pr.should.lambda? + end + + it "produces a Proc with arity -2" do + pr = :to_s.to_proc + pr.arity.should == -2 + end + + it "produces a Proc that always returns [[:req], [:rest]] for #parameters" do + pr = :to_s.to_proc + pr.parameters.should == [[:req], [:rest]] + end + + it "only calls public methods" do + body = proc do + public def pub; @a << :pub end + protected def pro; @a << :pro end + private def pri; @a << :pri end + attr_reader :a + end + + @a = [] + singleton_class.class_eval(&body) + tap(&:pub) + proc{tap(&:pro)}.should raise_error(NoMethodError, /protected method [`']pro' called/) + proc{tap(&:pri)}.should raise_error(NoMethodError, /private method [`']pri' called/) + @a.should == [:pub] + + @a = [] + c = Class.new(&body) + o = c.new + o.instance_variable_set(:@a, []) + o.tap(&:pub) + proc{tap(&:pro)}.should raise_error(NoMethodError, /protected method [`']pro' called/) + proc{o.tap(&:pri)}.should raise_error(NoMethodError, /private method [`']pri' called/) + o.a.should == [:pub] + end + + it "raises an ArgumentError when calling #call on the Proc without receiver" do + -> { + :object_id.to_proc.call + }.should raise_error(ArgumentError, /no receiver given|wrong number of arguments \(given 0, expected 1\+\)/) + end + + it "passes along the block passed to Proc#call" do + klass = Class.new do + def m + yield + end + + def to_proc + :m.to_proc.call(self) { :value } + end + end + klass.new.to_proc.should == :value + end + + it "produces a proc with source location nil" do + pr = :to_s.to_proc + pr.source_location.should == nil + end +end diff --git a/spec/ruby/core/symbol/to_s_spec.rb b/spec/ruby/core/symbol/to_s_spec.rb new file mode 100644 index 0000000000..cd963faa28 --- /dev/null +++ b/spec/ruby/core/symbol/to_s_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/id2name' + +describe "Symbol#to_s" do + it_behaves_like :symbol_id2name, :to_s +end diff --git a/spec/ruby/core/symbol/to_sym_spec.rb b/spec/ruby/core/symbol/to_sym_spec.rb new file mode 100644 index 0000000000..e75f3d48a8 --- /dev/null +++ b/spec/ruby/core/symbol/to_sym_spec.rb @@ -0,0 +1,9 @@ +require_relative '../../spec_helper' + +describe "Symbol#to_sym" do + it "returns self" do + [:rubinius, :squash, :[], :@ruby, :@@ruby].each do |sym| + sym.to_sym.should == sym + end + end +end diff --git a/spec/ruby/core/symbol/upcase_spec.rb b/spec/ruby/core/symbol/upcase_spec.rb new file mode 100644 index 0000000000..f704bdcbf3 --- /dev/null +++ b/spec/ruby/core/symbol/upcase_spec.rb @@ -0,0 +1,21 @@ +# -*- encoding: utf-8 -*- +require_relative '../../spec_helper' + +describe "Symbol#upcase" do + it "returns a Symbol" do + :glark.upcase.should be_an_instance_of(Symbol) + end + + it "converts lowercase ASCII characters to their uppercase equivalents" do + :lOwEr.upcase.should == :LOWER + end + + it "capitalizes all Unicode characters" do + "äöü".to_sym.upcase.should == :"ÄÖÜ" + "aou".to_sym.upcase.should == :"AOU" + end + + it "leaves non-alphabetic ASCII characters as they were" do + "Glark?!?".to_sym.upcase.should == :"GLARK?!?" + end +end diff --git a/spec/ruby/core/systemexit/initialize_spec.rb b/spec/ruby/core/systemexit/initialize_spec.rb new file mode 100644 index 0000000000..2cebaa7993 --- /dev/null +++ b/spec/ruby/core/systemexit/initialize_spec.rb @@ -0,0 +1,26 @@ +require_relative '../../spec_helper' + +describe "SystemExit#initialize" do + it "accepts a status" do + s = SystemExit.new 1 + s.status.should == 1 + s.message.should == 'SystemExit' + end + + it "accepts a message" do + s = SystemExit.new 'message' + s.status.should == 0 + s.message.should == 'message' + end + + it "accepts a status and message" do + s = SystemExit.new 10, 'message' + s.status.should == 10 + s.message.should == 'message' + end + + it "sets the status to 0 by default" do + s = SystemExit.new + s.status.should == 0 + end +end diff --git a/spec/ruby/core/systemexit/success_spec.rb b/spec/ruby/core/systemexit/success_spec.rb new file mode 100644 index 0000000000..ba2fd22ded --- /dev/null +++ b/spec/ruby/core/systemexit/success_spec.rb @@ -0,0 +1,13 @@ +require_relative '../../spec_helper' + +describe "SystemExit#success?" do + it "returns true when the status is 0" do + s = SystemExit.new 0 + s.should.success? + end + + it "returns false when the status is not 0" do + s = SystemExit.new 1 + s.should_not.success? + end +end diff --git a/spec/ruby/core/thread/abort_on_exception_spec.rb b/spec/ruby/core/thread/abort_on_exception_spec.rb new file mode 100644 index 0000000000..49be84ea9f --- /dev/null +++ b/spec/ruby/core/thread/abort_on_exception_spec.rb @@ -0,0 +1,106 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Thread#abort_on_exception" do + before do + ThreadSpecs.clear_state + @thread = Thread.new { Thread.pass until ThreadSpecs.state == :exit } + end + + after do + ThreadSpecs.state = :exit + @thread.join + end + + it "is false by default" do + @thread.abort_on_exception.should be_false + end + + it "returns true when #abort_on_exception= is passed true" do + @thread.abort_on_exception = true + @thread.abort_on_exception.should be_true + end +end + +describe :thread_abort_on_exception, shared: true do + before do + @thread = Thread.new do + Thread.pass until ThreadSpecs.state == :run + raise RuntimeError, "Thread#abort_on_exception= specs" + end + end + + it "causes the main thread to raise the exception raised in the thread" do + begin + ScratchPad << :before + + @thread.abort_on_exception = true if @object + -> do + ThreadSpecs.state = :run + # Wait for the main thread to be interrupted + sleep + end.should raise_error(RuntimeError, "Thread#abort_on_exception= specs") + + ScratchPad << :after + rescue Exception => e + ScratchPad << [:rescue, e] + end + + ScratchPad.recorded.should == [:before, :after] + end +end + +describe "Thread#abort_on_exception=" do + describe "when enabled and the thread dies due to an exception" do + before do + ScratchPad.record [] + ThreadSpecs.clear_state + @stderr, $stderr = $stderr, IOStub.new + end + + after do + $stderr = @stderr + end + + it_behaves_like :thread_abort_on_exception, nil, true + end +end + +describe "Thread.abort_on_exception" do + before do + @abort_on_exception = Thread.abort_on_exception + end + + after do + Thread.abort_on_exception = @abort_on_exception + end + + it "is false by default" do + Thread.abort_on_exception.should == false + end + + it "returns true when .abort_on_exception= is passed true" do + Thread.abort_on_exception = true + Thread.abort_on_exception.should be_true + end +end + +describe "Thread.abort_on_exception=" do + describe "when enabled and a non-main thread dies due to an exception" do + before :each do + ScratchPad.record [] + ThreadSpecs.clear_state + @stderr, $stderr = $stderr, IOStub.new + + @abort_on_exception = Thread.abort_on_exception + Thread.abort_on_exception = true + end + + after :each do + Thread.abort_on_exception = @abort_on_exception + $stderr = @stderr + end + + it_behaves_like :thread_abort_on_exception, nil, false + end +end diff --git a/spec/ruby/core/thread/add_trace_func_spec.rb b/spec/ruby/core/thread/add_trace_func_spec.rb new file mode 100644 index 0000000000..0abae81a78 --- /dev/null +++ b/spec/ruby/core/thread/add_trace_func_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../spec_helper' + +describe "Thread#add_trace_func" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/thread/alive_spec.rb b/spec/ruby/core/thread/alive_spec.rb new file mode 100644 index 0000000000..c2f5f5371d --- /dev/null +++ b/spec/ruby/core/thread/alive_spec.rb @@ -0,0 +1,58 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Thread#alive?" do + it "can check it's own status" do + ThreadSpecs.status_of_current_thread.should.alive? + end + + it "describes a running thread" do + ThreadSpecs.status_of_running_thread.should.alive? + end + + it "describes a sleeping thread" do + ThreadSpecs.status_of_sleeping_thread.should.alive? + end + + it "describes a blocked thread" do + ThreadSpecs.status_of_blocked_thread.should.alive? + end + + it "describes a completed thread" do + ThreadSpecs.status_of_completed_thread.should_not.alive? + end + + it "describes a killed thread" do + ThreadSpecs.status_of_killed_thread.should_not.alive? + end + + it "describes a thread with an uncaught exception" do + ThreadSpecs.status_of_thread_with_uncaught_exception.should_not.alive? + end + + it "describes a dying running thread" do + ThreadSpecs.status_of_dying_running_thread.should.alive? + end + + it "describes a dying sleeping thread" do + ThreadSpecs.status_of_dying_sleeping_thread.should.alive? + end + + it "returns true for a killed but still running thread" do + exit = false + t = Thread.new do + begin + sleep + ensure + Thread.pass until exit + end + end + + ThreadSpecs.spin_until_sleeping(t) + + t.kill + t.should.alive? + exit = true + t.join + end +end diff --git a/spec/ruby/core/thread/allocate_spec.rb b/spec/ruby/core/thread/allocate_spec.rb new file mode 100644 index 0000000000..cfd556812f --- /dev/null +++ b/spec/ruby/core/thread/allocate_spec.rb @@ -0,0 +1,9 @@ +require_relative '../../spec_helper' + +describe "Thread.allocate" do + it "raises a TypeError" do + -> { + Thread.allocate + }.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/thread/backtrace/limit_spec.rb b/spec/ruby/core/thread/backtrace/limit_spec.rb new file mode 100644 index 0000000000..b55ca67ea0 --- /dev/null +++ b/spec/ruby/core/thread/backtrace/limit_spec.rb @@ -0,0 +1,13 @@ +require_relative '../../../spec_helper' + +describe "Thread::Backtrace.limit" do + it "returns maximum backtrace length set by --backtrace-limit command-line option" do + out = ruby_exe("print Thread::Backtrace.limit", options: "--backtrace-limit=2") + out.should == "2" + end + + it "returns -1 when --backtrace-limit command-line option is not set" do + out = ruby_exe("print Thread::Backtrace.limit") + out.should == "-1" + end +end diff --git a/spec/ruby/core/thread/backtrace/location/absolute_path_spec.rb b/spec/ruby/core/thread/backtrace/location/absolute_path_spec.rb new file mode 100644 index 0000000000..68a69049d9 --- /dev/null +++ b/spec/ruby/core/thread/backtrace/location/absolute_path_spec.rb @@ -0,0 +1,93 @@ +require_relative '../../../../spec_helper' +require_relative 'fixtures/classes' + +describe 'Thread::Backtrace::Location#absolute_path' do + before :each do + @frame = ThreadBacktraceLocationSpecs.locations[0] + end + + it 'returns the absolute path of the call frame' do + @frame.absolute_path.should == File.realpath(__FILE__) + end + + it 'returns an absolute path when using a relative main script path' do + script = fixture(__FILE__, 'absolute_path_main.rb') + Dir.chdir(File.dirname(script)) do + ruby_exe('absolute_path_main.rb').should == "absolute_path_main.rb\n#{script}\n" + end + end + + it 'returns the correct absolute path when using a relative main script path and changing CWD' do + script = fixture(__FILE__, 'subdir/absolute_path_main_chdir.rb') + sibling = fixture(__FILE__, 'subdir/sibling.rb') + subdir = File.dirname script + Dir.chdir(fixture(__FILE__)) do + ruby_exe('subdir/absolute_path_main_chdir.rb').should == "subdir/absolute_path_main_chdir.rb\n#{subdir}\n#{subdir}\n#{script}\n#{sibling}\n" + end + end + + context "when used in eval with a given filename" do + it "returns nil with absolute_path" do + code = "caller_locations(0)[0].absolute_path" + + eval(code, nil, "foo.rb").should == nil + eval(code, nil, "foo/bar.rb").should == nil + end + end + + context "when used in #method_added" do + it "returns the user filename that defined the method" do + path = fixture(__FILE__, "absolute_path_method_added.rb") + load path + locations = ScratchPad.recorded + locations[0].absolute_path.should == path + # Make sure it's from the class body, not from the file top-level + locations[0].label.should include 'MethodAddedAbsolutePath' + end + end + + context "when used in a core method" do + it "returns nil" do + location = nil + tap { location = caller_locations(1, 1)[0] } + location.label.should =~ /\A(?:Kernel#)?tap\z/ + if location.path.start_with?("<internal:") + location.absolute_path.should == nil + else + location.absolute_path.should == File.realpath(__FILE__) + end + end + end + + context "canonicalization" do + platform_is_not :windows do + before :each do + @file = fixture(__FILE__, "absolute_path.rb") + @symlink = tmp("symlink.rb") + File.symlink(@file, @symlink) + ScratchPad.record [] + end + + after :each do + rm_r @symlink + end + + it "returns a canonical path without symlinks, even when __FILE__ does not" do + realpath = File.realpath(@symlink) + realpath.should_not == @symlink + + load @symlink + ScratchPad.recorded.should == [@symlink, realpath] + end + + it "returns a canonical path without symlinks, even when __FILE__ is removed" do + realpath = File.realpath(@symlink) + realpath.should_not == @symlink + + ScratchPad << -> { rm_r(@symlink) } + load @symlink + ScratchPad.recorded.should == [@symlink, realpath] + end + end + end +end diff --git a/spec/ruby/core/thread/backtrace/location/base_label_spec.rb b/spec/ruby/core/thread/backtrace/location/base_label_spec.rb new file mode 100644 index 0000000000..739f62f42f --- /dev/null +++ b/spec/ruby/core/thread/backtrace/location/base_label_spec.rb @@ -0,0 +1,49 @@ +require_relative '../../../../spec_helper' +require_relative 'fixtures/classes' + +describe 'Thread::Backtrace::Location#base_label' do + before :each do + @frame = ThreadBacktraceLocationSpecs.locations[0] + end + + it 'returns the base label of the call frame' do + @frame.base_label.should == '<top (required)>' + end + + describe 'when call frame is inside a block' do + before :each do + @frame = ThreadBacktraceLocationSpecs.block_location[0] + end + + it 'returns the name of the method that contains the block' do + @frame.base_label.should == 'block_location' + end + end + + it "is <module:A> for a module body" do + module ThreadBacktraceLocationSpecs + module ModuleLabel + ScratchPad.record caller_locations(0, 1)[0].base_label + end + end + ScratchPad.recorded.should == '<module:ModuleLabel>' + end + + it "is <class:A> for a class body" do + module ThreadBacktraceLocationSpecs + class ClassLabel + ScratchPad.record caller_locations(0, 1)[0].base_label + end + end + ScratchPad.recorded.should == '<class:ClassLabel>' + end + + it "is 'singleton class' for a singleton class body" do + module ThreadBacktraceLocationSpecs + class << Object.new + ScratchPad.record caller_locations(0, 1)[0].base_label + end + end + ScratchPad.recorded.should =~ /\A(singleton class|<singleton class>)\z/ + end +end diff --git a/spec/ruby/core/thread/backtrace/location/fixtures/absolute_path.rb b/spec/ruby/core/thread/backtrace/location/fixtures/absolute_path.rb new file mode 100644 index 0000000000..875e97ffac --- /dev/null +++ b/spec/ruby/core/thread/backtrace/location/fixtures/absolute_path.rb @@ -0,0 +1,4 @@ +action = ScratchPad.recorded.pop +ScratchPad << __FILE__ +action.call if action +ScratchPad << caller_locations(0)[0].absolute_path diff --git a/spec/ruby/core/thread/backtrace/location/fixtures/absolute_path_main.rb b/spec/ruby/core/thread/backtrace/location/fixtures/absolute_path_main.rb new file mode 100644 index 0000000000..d2b23393d4 --- /dev/null +++ b/spec/ruby/core/thread/backtrace/location/fixtures/absolute_path_main.rb @@ -0,0 +1,2 @@ +puts __FILE__ +puts caller_locations(0)[0].absolute_path diff --git a/spec/ruby/core/thread/backtrace/location/fixtures/absolute_path_method_added.rb b/spec/ruby/core/thread/backtrace/location/fixtures/absolute_path_method_added.rb new file mode 100644 index 0000000000..26d6298a19 --- /dev/null +++ b/spec/ruby/core/thread/backtrace/location/fixtures/absolute_path_method_added.rb @@ -0,0 +1,10 @@ +module ThreadBacktraceLocationSpecs + class MethodAddedAbsolutePath + def self.method_added(name) + ScratchPad.record caller_locations + end + + def foo + end + end +end diff --git a/spec/ruby/core/thread/backtrace/location/fixtures/classes.rb b/spec/ruby/core/thread/backtrace/location/fixtures/classes.rb new file mode 100644 index 0000000000..e903c3e450 --- /dev/null +++ b/spec/ruby/core/thread/backtrace/location/fixtures/classes.rb @@ -0,0 +1,35 @@ +module ThreadBacktraceLocationSpecs + MODULE_LOCATION = caller_locations(0) rescue nil + + def self.locations + caller_locations + end + + def self.method_location + caller_locations(0) + end + + def self.block_location + 1.times do + return caller_locations(0) + end + end + + def self.locations_inside_nested_blocks + first_level_location = nil + second_level_location = nil + third_level_location = nil + + 1.times do + first_level_location = locations[0] + 1.times do + second_level_location = locations[0] + 1.times do + third_level_location = locations[0] + end + end + end + + [first_level_location, second_level_location, third_level_location] + end +end diff --git a/spec/ruby/core/thread/backtrace/location/fixtures/locations_in_main.rb b/spec/ruby/core/thread/backtrace/location/fixtures/locations_in_main.rb new file mode 100644 index 0000000000..b124c8161c --- /dev/null +++ b/spec/ruby/core/thread/backtrace/location/fixtures/locations_in_main.rb @@ -0,0 +1,5 @@ +1.times do + puts Thread.current.backtrace_locations(1..1)[0].label +end + +require_relative 'locations_in_required' diff --git a/spec/ruby/core/thread/backtrace/location/fixtures/locations_in_required.rb b/spec/ruby/core/thread/backtrace/location/fixtures/locations_in_required.rb new file mode 100644 index 0000000000..5f5ed89e98 --- /dev/null +++ b/spec/ruby/core/thread/backtrace/location/fixtures/locations_in_required.rb @@ -0,0 +1,3 @@ +1.times do + puts Thread.current.backtrace_locations(1..1)[0].label +end diff --git a/spec/ruby/core/thread/backtrace/location/fixtures/main.rb b/spec/ruby/core/thread/backtrace/location/fixtures/main.rb new file mode 100644 index 0000000000..bde208a059 --- /dev/null +++ b/spec/ruby/core/thread/backtrace/location/fixtures/main.rb @@ -0,0 +1,5 @@ +def backtrace_location_example + caller_locations[0].path +end + +print backtrace_location_example diff --git a/spec/ruby/core/thread/backtrace/location/fixtures/path.rb b/spec/ruby/core/thread/backtrace/location/fixtures/path.rb new file mode 100644 index 0000000000..fba34cb0bc --- /dev/null +++ b/spec/ruby/core/thread/backtrace/location/fixtures/path.rb @@ -0,0 +1,2 @@ +ScratchPad << __FILE__ +ScratchPad << caller_locations(0)[0].path diff --git a/spec/ruby/core/thread/backtrace/location/fixtures/subdir/absolute_path_main_chdir.rb b/spec/ruby/core/thread/backtrace/location/fixtures/subdir/absolute_path_main_chdir.rb new file mode 100644 index 0000000000..33c8fb36ef --- /dev/null +++ b/spec/ruby/core/thread/backtrace/location/fixtures/subdir/absolute_path_main_chdir.rb @@ -0,0 +1,11 @@ +puts __FILE__ +puts __dir__ +Dir.chdir __dir__ + +# Check __dir__ is still correct after chdir +puts __dir__ + +puts caller_locations(0)[0].absolute_path + +# require_relative also needs to know the absolute path of the current file so we test it here too +require_relative 'sibling' diff --git a/spec/ruby/core/thread/backtrace/location/fixtures/subdir/sibling.rb b/spec/ruby/core/thread/backtrace/location/fixtures/subdir/sibling.rb new file mode 100644 index 0000000000..2a854ddccd --- /dev/null +++ b/spec/ruby/core/thread/backtrace/location/fixtures/subdir/sibling.rb @@ -0,0 +1 @@ +puts __FILE__ diff --git a/spec/ruby/core/thread/backtrace/location/inspect_spec.rb b/spec/ruby/core/thread/backtrace/location/inspect_spec.rb new file mode 100644 index 0000000000..20e477a5a6 --- /dev/null +++ b/spec/ruby/core/thread/backtrace/location/inspect_spec.rb @@ -0,0 +1,13 @@ +require_relative '../../../../spec_helper' +require_relative 'fixtures/classes' + +describe 'Thread::Backtrace::Location#inspect' do + before :each do + @frame = ThreadBacktraceLocationSpecs.locations[0] + @line = __LINE__ - 1 + end + + it 'converts the call frame to a String' do + @frame.inspect.should include("#{__FILE__}:#{@line}:in ") + end +end diff --git a/spec/ruby/core/thread/backtrace/location/label_spec.rb b/spec/ruby/core/thread/backtrace/location/label_spec.rb new file mode 100644 index 0000000000..85ddccc8e3 --- /dev/null +++ b/spec/ruby/core/thread/backtrace/location/label_spec.rb @@ -0,0 +1,37 @@ +require_relative '../../../../spec_helper' +require_relative 'fixtures/classes' + +describe 'Thread::Backtrace::Location#label' do + it 'returns the base label of the call frame' do + ThreadBacktraceLocationSpecs.locations[0].label.should include('<top (required)>') + end + + it 'returns the method name for a method location' do + ThreadBacktraceLocationSpecs.method_location[0].label.should =~ /\A(?:ThreadBacktraceLocationSpecs\.)?method_location\z/ + end + + it 'returns the block name for a block location' do + ThreadBacktraceLocationSpecs.block_location[0].label.should =~ /\Ablock in (?:ThreadBacktraceLocationSpecs\.)?block_location\z/ + end + + it 'returns the module name for a module location' do + ThreadBacktraceLocationSpecs::MODULE_LOCATION[0].label.should include "ThreadBacktraceLocationSpecs" + end + + it 'includes the nesting level of a block as part of the location label' do + first_level_location, second_level_location, third_level_location = + ThreadBacktraceLocationSpecs.locations_inside_nested_blocks + + first_level_location.label.should =~ /\Ablock in (?:ThreadBacktraceLocationSpecs\.)?locations_inside_nested_blocks\z/ + second_level_location.label.should =~ /\Ablock \(2 levels\) in (?:ThreadBacktraceLocationSpecs\.)?locations_inside_nested_blocks\z/ + third_level_location.label.should =~ /\Ablock \(3 levels\) in (?:ThreadBacktraceLocationSpecs\.)?locations_inside_nested_blocks\z/ + end + + it 'sets the location label for a top-level block differently depending on it being in the main file or a required file' do + path = fixture(__FILE__, "locations_in_main.rb") + main_label, required_label = ruby_exe(path).lines + + main_label.should == "block in <main>\n" + required_label.should == "block in <top (required)>\n" + end +end diff --git a/spec/ruby/core/thread/backtrace/location/lineno_spec.rb b/spec/ruby/core/thread/backtrace/location/lineno_spec.rb new file mode 100644 index 0000000000..10457f80f0 --- /dev/null +++ b/spec/ruby/core/thread/backtrace/location/lineno_spec.rb @@ -0,0 +1,23 @@ +require_relative '../../../../spec_helper' +require_relative 'fixtures/classes' + +describe 'Thread::Backtrace::Location#lineno' do + before :each do + @frame = ThreadBacktraceLocationSpecs.locations[0] + @line = __LINE__ - 1 + end + + it 'returns the line number of the call frame' do + @frame.lineno.should == @line + end + + it 'should be the same line number as in #to_s, including for core methods' do + # Get the caller_locations from a call made into a core library method + locations = [:non_empty].map { caller_locations }[0] + + locations.each do |location| + line_number = location.to_s[/:(\d+):/, 1] + location.lineno.should == Integer(line_number) + end + end +end diff --git a/spec/ruby/core/thread/backtrace/location/path_spec.rb b/spec/ruby/core/thread/backtrace/location/path_spec.rb new file mode 100644 index 0000000000..75f76833a9 --- /dev/null +++ b/spec/ruby/core/thread/backtrace/location/path_spec.rb @@ -0,0 +1,124 @@ +require_relative '../../../../spec_helper' +require_relative 'fixtures/classes' + +describe 'Thread::Backtrace::Location#path' do + context 'outside a main script' do + it 'returns an absolute path' do + frame = ThreadBacktraceLocationSpecs.locations[0] + + frame.path.should == __FILE__ + end + end + + context 'in a main script' do + before do + @script = fixture(__FILE__, 'main.rb') + end + + context 'when the script is in the working directory' do + before do + @directory = File.dirname(@script) + end + + context 'when using a relative script path' do + it 'returns a path relative to the working directory' do + Dir.chdir(@directory) { + ruby_exe('main.rb') + }.should == 'main.rb' + end + end + + context 'when using an absolute script path' do + it 'returns an absolute path' do + Dir.chdir(@directory) { + ruby_exe(@script) + }.should == @script + end + end + end + + context 'when the script is in a sub directory of the working directory' do + context 'when using a relative script path' do + it 'returns a path relative to the working directory' do + path = 'fixtures/main.rb' + directory = __dir__ + Dir.chdir(directory) { + ruby_exe(path) + }.should == path + end + end + + context 'when using an absolute script path' do + it 'returns an absolute path' do + ruby_exe(@script).should == @script + end + end + end + + context 'when the script is outside of the working directory' do + before :each do + @parent_dir = tmp('path_outside_pwd') + @sub_dir = File.join(@parent_dir, 'sub') + @script = File.join(@parent_dir, 'main.rb') + source = fixture(__FILE__, 'main.rb') + + mkdir_p(@sub_dir) + + cp(source, @script) + end + + after :each do + rm_r(@parent_dir) + end + + context 'when using a relative script path' do + it 'returns a path relative to the working directory' do + Dir.chdir(@sub_dir) { + ruby_exe('../main.rb') + }.should == '../main.rb' + end + end + + context 'when using an absolute path' do + it 'returns an absolute path' do + ruby_exe(@script).should == @script + end + end + end + end + + it 'should be the same path as in #to_s, including for core methods' do + # Get the caller_locations from a call made into a core library method + locations = [:non_empty].map { caller_locations }[0] + + locations.each do |location| + filename = location.to_s[/^(.+):\d+:/, 1] + path = location.path + + path.should == filename + end + end + + context "canonicalization" do + platform_is_not :windows do + before :each do + @file = fixture(__FILE__, "path.rb") + @symlink = tmp("symlink.rb") + File.symlink(@file, @symlink) + ScratchPad.record [] + end + + after :each do + rm_r @symlink + end + + it "returns a non-canonical path with symlinks, the same as __FILE__" do + realpath = File.realpath(@symlink) + realpath.should_not == @symlink + + load @symlink + ScratchPad.recorded.should == [@symlink, @symlink] + end + end + end +end diff --git a/spec/ruby/core/thread/backtrace/location/to_s_spec.rb b/spec/ruby/core/thread/backtrace/location/to_s_spec.rb new file mode 100644 index 0000000000..5911cdced0 --- /dev/null +++ b/spec/ruby/core/thread/backtrace/location/to_s_spec.rb @@ -0,0 +1,13 @@ +require_relative '../../../../spec_helper' +require_relative 'fixtures/classes' + +describe 'Thread::Backtrace::Location#to_s' do + before :each do + @frame = ThreadBacktraceLocationSpecs.locations[0] + @line = __LINE__ - 1 + end + + it 'converts the call frame to a String' do + @frame.to_s.should include("#{__FILE__}:#{@line}:in ") + end +end diff --git a/spec/ruby/core/thread/backtrace_locations_spec.rb b/spec/ruby/core/thread/backtrace_locations_spec.rb new file mode 100644 index 0000000000..09fe622e0d --- /dev/null +++ b/spec/ruby/core/thread/backtrace_locations_spec.rb @@ -0,0 +1,79 @@ +require_relative '../../spec_helper' + +describe "Thread#backtrace_locations" do + it "returns an Array" do + locations = Thread.current.backtrace_locations + locations.should be_an_instance_of(Array) + locations.should_not be_empty + end + + it "sets each element to a Thread::Backtrace::Location" do + locations = Thread.current.backtrace_locations + locations.each { |loc| loc.should be_an_instance_of(Thread::Backtrace::Location) } + end + + it "can be called on any Thread" do + locations = Thread.new { Thread.current.backtrace_locations }.value + locations.should be_an_instance_of(Array) + locations.should_not be_empty + locations.each { |loc| loc.should be_an_instance_of(Thread::Backtrace::Location) } + end + + it "can be called with a number of locations to omit" do + locations1 = Thread.current.backtrace_locations + locations2 = Thread.current.backtrace_locations(2) + locations2.length.should == locations1[2..-1].length + locations2.map(&:to_s).should == locations1[2..-1].map(&:to_s) + end + + it "can be called with a maximum number of locations to return as second parameter" do + locations1 = Thread.current.backtrace_locations + locations2 = Thread.current.backtrace_locations(2, 3) + locations2.map(&:to_s).should == locations1[2..4].map(&:to_s) + end + + it "can be called with a range" do + locations1 = Thread.current.backtrace_locations + locations2 = Thread.current.backtrace_locations(2..4) + locations2.map(&:to_s).should == locations1[2..4].map(&:to_s) + end + + it "can be called with a range whose end is negative" do + Thread.current.backtrace_locations(2..-1).map(&:to_s).should == Thread.current.backtrace_locations[2..-1].map(&:to_s) + Thread.current.backtrace_locations(2..-2).map(&:to_s).should == Thread.current.backtrace_locations[2..-2].map(&:to_s) + end + + it "can be called with an endless range" do + locations1 = Thread.current.backtrace_locations(0) + locations2 = Thread.current.backtrace_locations(eval("(2..)")) + locations2.map(&:to_s).should == locations1[2..-1].map(&:to_s) + end + + it "can be called with an beginless range" do + locations1 = Thread.current.backtrace_locations(0) + locations2 = Thread.current.backtrace_locations((..5)) + locations2.map(&:to_s)[eval("(2..)")].should == locations1[(..5)].map(&:to_s)[eval("(2..)")] + end + + it "returns nil if omitting more locations than available" do + Thread.current.backtrace_locations(100).should == nil + Thread.current.backtrace_locations(100..-1).should == nil + end + + it "returns [] if omitting exactly the number of locations available" do + omit = Thread.current.backtrace_locations.length + Thread.current.backtrace_locations(omit).should == [] + end + + it "without argument is the same as showing all locations with 0..-1" do + Thread.current.backtrace_locations.map(&:to_s).should == Thread.current.backtrace_locations(0..-1).map(&:to_s) + end + + it "the first location reports the call to #backtrace_locations" do + Thread.current.backtrace_locations(0..0)[0].to_s.should =~ /\A#{__FILE__ }:#{__LINE__ }:in [`'](?:Thread#)?backtrace_locations'\z/ + end + + it "[1..-1] is the same as #caller_locations(0..-1) for Thread.current" do + Thread.current.backtrace_locations(1..-1).map(&:to_s).should == caller_locations(0..-1).map(&:to_s) + end +end diff --git a/spec/ruby/core/thread/backtrace_spec.rb b/spec/ruby/core/thread/backtrace_spec.rb new file mode 100644 index 0000000000..15bb29a349 --- /dev/null +++ b/spec/ruby/core/thread/backtrace_spec.rb @@ -0,0 +1,69 @@ +require_relative '../../spec_helper' + +describe "Thread#backtrace" do + it "returns the current backtrace of a thread" do + t = Thread.new do + begin + sleep + rescue + end + end + + Thread.pass while t.status && t.status != 'sleep' + + backtrace = t.backtrace + backtrace.should be_kind_of(Array) + backtrace.first.should =~ /[`'](?:Kernel#)?sleep'/ + + t.raise 'finish the thread' + t.join + end + + it "returns nil for dead thread" do + t = Thread.new {} + t.join + t.backtrace.should == nil + end + + it "returns an array (which may be empty) immediately after the thread is created" do + t = Thread.new { sleep } + backtrace = t.backtrace + t.kill + t.join + backtrace.should be_kind_of(Array) + end + + it "can be called with a number of locations to omit" do + locations1 = Thread.current.backtrace + locations2 = Thread.current.backtrace(2) + locations1[2..-1].length.should == locations2.length + locations1[2..-1].map(&:to_s).should == locations2.map(&:to_s) + end + + it "can be called with a maximum number of locations to return as second parameter" do + locations1 = Thread.current.backtrace + locations2 = Thread.current.backtrace(2, 3) + locations1[2..4].map(&:to_s).should == locations2.map(&:to_s) + end + + it "can be called with a range" do + locations1 = Thread.current.backtrace + locations2 = Thread.current.backtrace(2..4) + locations1[2..4].map(&:to_s).should == locations2.map(&:to_s) + end + + it "can be called with a range whose end is negative" do + Thread.current.backtrace(2..-1).should == Thread.current.backtrace[2..-1] + Thread.current.backtrace(2..-2).should == Thread.current.backtrace[2..-2] + end + + it "returns nil if omitting more locations than available" do + Thread.current.backtrace(100).should == nil + Thread.current.backtrace(100..-1).should == nil + end + + it "returns [] if omitting exactly the number of locations available" do + omit = Thread.current.backtrace.length + Thread.current.backtrace(omit).should == [] + end +end diff --git a/spec/ruby/core/thread/current_spec.rb b/spec/ruby/core/thread/current_spec.rb new file mode 100644 index 0000000000..f5ed1d95cd --- /dev/null +++ b/spec/ruby/core/thread/current_spec.rb @@ -0,0 +1,31 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Thread.current" do + it "returns a thread" do + current = Thread.current + current.should be_kind_of(Thread) + end + + it "returns the current thread" do + t = Thread.new { Thread.current } + t.value.should equal(t) + Thread.current.should_not equal(t.value) + end + + it "returns the correct thread in a Fiber" do + # This catches a bug where Fibers are running on a thread-pool + # and Fibers from a different Ruby Thread reuse the same native thread. + # Caching the Ruby Thread based on the native thread is not correct in that case. + 2.times do + t = Thread.new { + cur = Thread.current + Fiber.new { + Thread.current + }.resume.should equal cur + cur + } + t.value.should equal t + end + end +end diff --git a/spec/ruby/core/thread/each_caller_location_spec.rb b/spec/ruby/core/thread/each_caller_location_spec.rb new file mode 100644 index 0000000000..aa7423675b --- /dev/null +++ b/spec/ruby/core/thread/each_caller_location_spec.rb @@ -0,0 +1,47 @@ +require_relative '../../spec_helper' + +describe "Thread.each_caller_location" do + it "iterates through the current execution stack and matches caller_locations content and type" do + ScratchPad.record [] + Thread.each_caller_location { |l| ScratchPad << l; } + + ScratchPad.recorded.map(&:to_s).should == caller_locations.map(&:to_s) + ScratchPad.recorded[0].should be_kind_of(Thread::Backtrace::Location) + end + + it "returns subset of 'Thread.to_enum(:each_caller_location)' locations" do + ar = [] + ecl = Thread.each_caller_location { |x| ar << x } + + (ar.map(&:to_s) - Thread.to_enum(:each_caller_location).to_a.map(&:to_s)).should.empty? + end + + it "stops the backtrace iteration if 'break' occurs" do + i = 0 + ar = [] + ecl = Thread.each_caller_location do |x| + ar << x + i += 1 + break x if i == 2 + end + + ar.map(&:to_s).should == caller_locations(1, 2).map(&:to_s) + ecl.should be_kind_of(Thread::Backtrace::Location) + end + + it "returns nil" do + Thread.each_caller_location {}.should == nil + end + + it "raises LocalJumpError when called without a block" do + -> { + Thread.each_caller_location + }.should raise_error(LocalJumpError, "no block given") + end + + it "doesn't accept keyword arguments" do + -> { + Thread.each_caller_location(12, foo: 10) {} + }.should raise_error(ArgumentError); + end +end diff --git a/spec/ruby/core/thread/element_reference_spec.rb b/spec/ruby/core/thread/element_reference_spec.rb new file mode 100644 index 0000000000..fde9d1f440 --- /dev/null +++ b/spec/ruby/core/thread/element_reference_spec.rb @@ -0,0 +1,55 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Thread#[]" do + it "gives access to thread local values" do + th = Thread.new do + Thread.current[:value] = 5 + end + th.join + th[:value].should == 5 + Thread.current[:value].should == nil + end + + it "is not shared across threads" do + t1 = Thread.new do + Thread.current[:value] = 1 + end + t2 = Thread.new do + Thread.current[:value] = 2 + end + [t1,t2].each {|x| x.join} + t1[:value].should == 1 + t2[:value].should == 2 + end + + it "is accessible using strings or symbols" do + t1 = Thread.new do + Thread.current[:value] = 1 + end + t2 = Thread.new do + Thread.current["value"] = 2 + end + [t1,t2].each {|x| x.join} + t1[:value].should == 1 + t1["value"].should == 1 + t2[:value].should == 2 + t2["value"].should == 2 + end + + it "converts a key that is neither String nor Symbol with #to_str" do + key = mock('value') + key.should_receive(:to_str).and_return('value') + + th = Thread.new do + Thread.current[:value] = 1 + end.join + + th[key].should == 1 + end + + it "raises exceptions on the wrong type of keys" do + -> { Thread.current[nil] }.should raise_error(TypeError) + -> { Thread.current[5] }.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/thread/element_set_spec.rb b/spec/ruby/core/thread/element_set_spec.rb new file mode 100644 index 0000000000..f205177304 --- /dev/null +++ b/spec/ruby/core/thread/element_set_spec.rb @@ -0,0 +1,74 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Thread#[]=" do + after :each do + Thread.current[:value] = nil + end + + it "raises a FrozenError if the thread is frozen" do + Thread.new do + th = Thread.current + th.freeze + -> { + th[:foo] = "bar" + }.should raise_error(FrozenError, "can't modify frozen thread locals") + end.join + end + + it "accepts Strings and Symbols" do + t1 = Thread.new do + Thread.current[:value] = 1 + end.join + t2 = Thread.new do + Thread.current["value"] = 2 + end.join + + t1[:value].should == 1 + t2[:value].should == 2 + end + + it "converts a key that is neither String nor Symbol with #to_str" do + key = mock('value') + key.should_receive(:to_str).and_return('value') + + th = Thread.new do + Thread.current[key] = 1 + end.join + + th[:value].should == 1 + end + + it "raises exceptions on the wrong type of keys" do + -> { Thread.current[nil] = true }.should raise_error(TypeError) + -> { Thread.current[5] = true }.should raise_error(TypeError) + end + + it "is not shared across fibers" do + fib = Fiber.new do + Thread.current[:value] = 1 + Fiber.yield + Thread.current[:value].should == 1 + end + fib.resume + Thread.current[:value].should be_nil + Thread.current[:value] = 2 + fib.resume + Thread.current[:value] = 2 + end + + it "stores a local in another thread when in a fiber" do + fib = Fiber.new do + t = Thread.new do + sleep + Thread.current[:value].should == 1 + end + + Thread.pass while t.status and t.status != "sleep" + t[:value] = 1 + t.wakeup + t.join + end + fib.resume + end +end diff --git a/spec/ruby/core/thread/exit_spec.rb b/spec/ruby/core/thread/exit_spec.rb new file mode 100644 index 0000000000..c3f710920e --- /dev/null +++ b/spec/ruby/core/thread/exit_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/exit' + +describe "Thread#exit!" do + it "needs to be reviewed for spec completeness" +end + +describe "Thread.exit" do + it "causes the current thread to exit" do + thread = Thread.new { Thread.exit; sleep } + thread.join + thread.status.should be_false + end +end diff --git a/spec/ruby/core/thread/fetch_spec.rb b/spec/ruby/core/thread/fetch_spec.rb new file mode 100644 index 0000000000..85ffb71874 --- /dev/null +++ b/spec/ruby/core/thread/fetch_spec.rb @@ -0,0 +1,66 @@ +require_relative '../../spec_helper' + +describe 'Thread#fetch' do + describe 'with 2 arguments' do + it 'returns the value of the fiber-local variable if value has been assigned' do + th = Thread.new { Thread.current[:cat] = 'meow' } + th.join + th.fetch(:cat, true).should == 'meow' + end + + it "returns the default value if fiber-local variable hasn't been assigned" do + th = Thread.new {} + th.join + th.fetch(:cat, true).should == true + end + end + + describe 'with 1 argument' do + it 'raises a KeyError when the Thread does not have a fiber-local variable of the same name' do + th = Thread.new {} + th.join + -> { th.fetch(:cat) }.should raise_error(KeyError) + end + + it 'returns the value of the fiber-local variable if value has been assigned' do + th = Thread.new { Thread.current[:cat] = 'meow' } + th.join + th.fetch(:cat).should == 'meow' + end + end + + describe 'with a block' do + it 'returns the value of the fiber-local variable if value has been assigned' do + th = Thread.new { Thread.current[:cat] = 'meow' } + th.join + th.fetch(:cat) { true }.should == 'meow' + end + + it "returns the block value if fiber-local variable hasn't been assigned" do + th = Thread.new {} + th.join + th.fetch(:cat) { true }.should == true + end + + it "does not call the block if value has been assigned" do + th = Thread.new { Thread.current[:cat] = 'meow' } + th.join + var = :not_updated + th.fetch(:cat) { var = :updated }.should == 'meow' + var.should == :not_updated + end + + it "uses the block if a default is given and warns about it" do + th = Thread.new {} + th.join + -> { + th.fetch(:cat, false) { true }.should == true + }.should complain(/warning: block supersedes default value argument/) + end + end + + it 'raises an ArgumentError when not passed one or two arguments' do + -> { Thread.current.fetch() }.should raise_error(ArgumentError) + -> { Thread.current.fetch(1, 2, 3) }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/thread/fixtures/classes.rb b/spec/ruby/core/thread/fixtures/classes.rb new file mode 100644 index 0000000000..7c485660a8 --- /dev/null +++ b/spec/ruby/core/thread/fixtures/classes.rb @@ -0,0 +1,322 @@ +module ThreadSpecs + + class SubThread < Thread + def initialize(*args) + super { args.first << 1 } + end + end + + class NewThreadToRaise + def self.raise(*args, **kwargs, &block) + thread = Thread.new do + Thread.current.report_on_exception = false + + if block_given? + block.call do + sleep + end + else + sleep + end + end + + Thread.pass until thread.stop? + + thread.raise(*args, **kwargs) + + thread.join + ensure + thread.kill if thread.alive? + Thread.pass while thread.alive? # Thread#kill may not terminate a thread immediately so it may be detected as a leaked one + end + end + + class Status + attr_reader :thread, :inspect, :status, :to_s + def initialize(thread) + @thread = thread + @alive = thread.alive? + @inspect = thread.inspect + @to_s = thread.to_s + @status = thread.status + @stop = thread.stop? + end + + def alive? + @alive + end + + def stop? + @stop + end + end + + # TODO: In the great Thread spec rewrite, abstract this + class << self + attr_accessor :state + end + + def self.clear_state + @state = nil + end + + def self.spin_until_sleeping(t) + Thread.pass while t.status and t.status != "sleep" + end + + def self.sleeping_thread + Thread.new do + begin + sleep + ScratchPad.record :woken + rescue Object => e + ScratchPad.record e + end + end + end + + def self.running_thread + Thread.new do + begin + ThreadSpecs.state = :running + loop { Thread.pass } + ScratchPad.record :woken + rescue Object => e + ScratchPad.record e + end + end + end + + def self.completed_thread + Thread.new {} + end + + def self.status_of_current_thread + Thread.new { Status.new(Thread.current) }.value + end + + def self.status_of_running_thread + t = running_thread + Thread.pass while t.status and t.status != "run" + status = Status.new t + t.kill + t.join + status + end + + def self.status_of_completed_thread + t = completed_thread + t.join + Status.new t + end + + def self.status_of_sleeping_thread + t = sleeping_thread + Thread.pass while t.status and t.status != 'sleep' + status = Status.new t + t.run + t.join + status + end + + def self.status_of_blocked_thread + m = Mutex.new + m.lock + t = Thread.new { m.lock } + Thread.pass while t.status and t.status != 'sleep' + status = Status.new t + m.unlock + t.join + status + end + + def self.status_of_killed_thread + t = Thread.new { sleep } + Thread.pass while t.status and t.status != 'sleep' + t.kill + t.join + Status.new t + end + + def self.status_of_thread_with_uncaught_exception + t = Thread.new { + Thread.current.report_on_exception = false + raise "error" + } + begin + t.join + rescue RuntimeError + end + Status.new t + end + + def self.status_of_dying_running_thread + status = nil + t = dying_thread_ensures { status = Status.new Thread.current } + t.join + status + end + + def self.status_of_dying_sleeping_thread + t = dying_thread_ensures { Thread.stop; } + Thread.pass while t.status and t.status != 'sleep' + status = Status.new t + t.wakeup + t.join + status + end + + def self.status_of_dying_thread_after_sleep + status = nil + t = dying_thread_ensures { + Thread.stop + status = Status.new(Thread.current) + } + Thread.pass while t.status and t.status != 'sleep' + t.wakeup + Thread.pass while t.status and t.status == 'sleep' + t.join + status + end + + def self.dying_thread_ensures(kill_method_name=:kill) + Thread.new do + Thread.current.report_on_exception = false + begin + Thread.current.send(kill_method_name) + ensure + yield + end + end + end + + def self.dying_thread_with_outer_ensure(kill_method_name=:kill) + Thread.new do + Thread.current.report_on_exception = false + begin + begin + Thread.current.send(kill_method_name) + ensure + raise "In dying thread" + end + ensure + yield + end + end + end + + def self.join_dying_thread_with_outer_ensure(kill_method_name=:kill) + t = dying_thread_with_outer_ensure(kill_method_name) { yield } + -> { t.join }.should raise_error(RuntimeError, "In dying thread") + return t + end + + def self.wakeup_dying_sleeping_thread(kill_method_name=:kill) + t = ThreadSpecs.dying_thread_ensures(kill_method_name) { yield } + Thread.pass while t.status and t.status != 'sleep' + t.wakeup + t.join + end + + def self.critical_is_reset + # Create another thread to verify that it can call Thread.critical= + t = Thread.new do + initial_critical = Thread.critical + Thread.critical = true + Thread.critical = false + initial_critical == false && Thread.critical == false + end + v = t.value + t.join + v + end + + def self.counter + @@counter + end + + def self.counter= c + @@counter = c + end + + def self.increment_counter(incr) + incr.times do + begin + Thread.critical = true + @@counter += 1 + ensure + Thread.critical = false + end + end + end + + def self.critical_thread1 + Thread.critical = true + Thread.current.key?(:thread_specs).should == false + end + + def self.critical_thread2(is_thread_stop) + Thread.current[:thread_specs].should == 101 + Thread.critical.should == !is_thread_stop + unless is_thread_stop + Thread.critical = false + end + end + + def self.main_thread1(critical_thread, is_thread_sleep, is_thread_stop) + # Thread.stop resets Thread.critical. Also, with native threads, the Thread.Stop may not have executed yet + # since the main thread will race with the critical thread + unless is_thread_stop + Thread.critical.should == true + end + critical_thread[:thread_specs] = 101 + if is_thread_sleep or is_thread_stop + # Thread#wakeup calls are not queued up. So we need to ensure that the thread is sleeping before calling wakeup + Thread.pass while critical_thread.status and critical_thread.status != "sleep" + critical_thread.wakeup + end + end + + def self.main_thread2(critical_thread) + Thread.pass # The join below seems to cause a deadlock with CRuby unless Thread.pass is called first + critical_thread.join + Thread.critical.should == false + end + + def self.critical_thread_yields_to_main_thread(is_thread_sleep=false, is_thread_stop=false) + @@after_first_sleep = false + + critical_thread = Thread.new do + Thread.pass while Thread.main.status and Thread.main.status != "sleep" + critical_thread1() + Thread.main.wakeup + yield + Thread.pass while @@after_first_sleep != true # Need to ensure that the next statement does not see the first sleep itself + Thread.pass while Thread.main.status and Thread.main.status != "sleep" + critical_thread2(is_thread_stop) + Thread.main.wakeup + end + + sleep 5 + @@after_first_sleep = true + main_thread1(critical_thread, is_thread_sleep, is_thread_stop) + sleep 5 + main_thread2(critical_thread) + end + + def self.create_critical_thread + Thread.new do + Thread.critical = true + yield + Thread.critical = false + end + end + + def self.create_and_kill_critical_thread(pass_after_kill=false) + ThreadSpecs.create_critical_thread do + Thread.current.kill + Thread.pass if pass_after_kill + ScratchPad.record("status=" + Thread.current.status) + end + end +end diff --git a/spec/ruby/core/thread/fork_spec.rb b/spec/ruby/core/thread/fork_spec.rb new file mode 100644 index 0000000000..a2f4181298 --- /dev/null +++ b/spec/ruby/core/thread/fork_spec.rb @@ -0,0 +1,9 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/start' + +describe "Thread.fork" do + describe "Thread.start" do + it_behaves_like :thread_start, :fork + end +end diff --git a/spec/ruby/core/thread/group_spec.rb b/spec/ruby/core/thread/group_spec.rb new file mode 100644 index 0000000000..d0d4704b66 --- /dev/null +++ b/spec/ruby/core/thread/group_spec.rb @@ -0,0 +1,16 @@ +require_relative '../../spec_helper' + +describe "Thread#group" do + it "returns the default thread group for the main thread" do + Thread.main.group.should == ThreadGroup::Default + end + + it "returns the thread group explicitly set for this thread" do + thread = Thread.new { nil } + thread_group = ThreadGroup.new + thread_group.add(thread) + thread.group.should == thread_group + ensure + thread.join if thread + end +end diff --git a/spec/ruby/core/thread/handle_interrupt_spec.rb b/spec/ruby/core/thread/handle_interrupt_spec.rb new file mode 100644 index 0000000000..ea7e81cb98 --- /dev/null +++ b/spec/ruby/core/thread/handle_interrupt_spec.rb @@ -0,0 +1,125 @@ +require_relative '../../spec_helper' + +describe "Thread.handle_interrupt" do + def make_handle_interrupt_thread(interrupt_config, blocking = true) + interrupt_class = Class.new(RuntimeError) + + ScratchPad.record [] + + in_handle_interrupt = Queue.new + can_continue = Queue.new + + thread = Thread.new do + begin + Thread.handle_interrupt(interrupt_config) do + begin + in_handle_interrupt << true + if blocking + Thread.pass # Make it clearer the other thread needs to wait for this one to be in #pop + can_continue.pop + else + begin + can_continue.pop(true) + rescue ThreadError + Thread.pass + retry + end + end + rescue interrupt_class + ScratchPad << :interrupted + end + end + rescue interrupt_class + ScratchPad << :deferred + end + end + + in_handle_interrupt.pop + if blocking + # Ensure the thread is inside Thread#pop, as if thread.raise is done before it would be deferred + Thread.pass until thread.stop? + end + thread.raise interrupt_class, "interrupt" + can_continue << true + thread.join + + ScratchPad.recorded + end + + before :each do + Thread.pending_interrupt?.should == false # sanity check + end + + it "with :never defers interrupts until exiting the handle_interrupt block" do + make_handle_interrupt_thread(RuntimeError => :never).should == [:deferred] + end + + it "with :on_blocking defers interrupts until the next blocking call" do + make_handle_interrupt_thread(RuntimeError => :on_blocking).should == [:interrupted] + make_handle_interrupt_thread({ RuntimeError => :on_blocking }, false).should == [:deferred] + end + + it "with :immediate handles interrupts immediately" do + make_handle_interrupt_thread(RuntimeError => :immediate).should == [:interrupted] + end + + it "with :immediate immediately runs pending interrupts, before the block" do + Thread.handle_interrupt(RuntimeError => :never) do + current = Thread.current + Thread.new { + current.raise "interrupt immediate" + }.join + + Thread.pending_interrupt?.should == true + -> { + Thread.handle_interrupt(RuntimeError => :immediate) { + flunk "not reached" + } + }.should raise_error(RuntimeError, "interrupt immediate") + Thread.pending_interrupt?.should == false + end + end + + it "also works with suspended Fibers and does not duplicate interrupts" do + fiber = Fiber.new { Fiber.yield } + fiber.resume + + Thread.handle_interrupt(RuntimeError => :never) do + current = Thread.current + Thread.new { + current.raise "interrupt with fibers" + }.join + + Thread.pending_interrupt?.should == true + -> { + Thread.handle_interrupt(RuntimeError => :immediate) { + flunk "not reached" + } + }.should raise_error(RuntimeError, "interrupt with fibers") + Thread.pending_interrupt?.should == false + end + + fiber.resume + end + + it "runs pending interrupts at the end of the block, even if there was an exception raised in the block" do + executed = false + -> { + Thread.handle_interrupt(RuntimeError => :never) do + current = Thread.current + Thread.new { + current.raise "interrupt exception" + }.join + + Thread.pending_interrupt?.should == true + executed = true + raise "regular exception" + end + }.should raise_error(RuntimeError, "interrupt exception") + executed.should == true + end + + it "supports multiple pairs in the Hash" do + make_handle_interrupt_thread(ArgumentError => :never, RuntimeError => :never).should == [:deferred] + end +end diff --git a/spec/ruby/core/thread/ignore_deadlock_spec.rb b/spec/ruby/core/thread/ignore_deadlock_spec.rb new file mode 100644 index 0000000000..b48bc9f9b0 --- /dev/null +++ b/spec/ruby/core/thread/ignore_deadlock_spec.rb @@ -0,0 +1,19 @@ +require_relative '../../spec_helper' + +describe "Thread.ignore_deadlock" do + it "returns false by default" do + Thread.ignore_deadlock.should == false + end +end + +describe "Thread.ignore_deadlock=" do + it "changes the value of Thread.ignore_deadlock" do + ignore_deadlock = Thread.ignore_deadlock + Thread.ignore_deadlock = true + begin + Thread.ignore_deadlock.should == true + ensure + Thread.ignore_deadlock = ignore_deadlock + end + end +end diff --git a/spec/ruby/core/thread/initialize_spec.rb b/spec/ruby/core/thread/initialize_spec.rb new file mode 100644 index 0000000000..4fca900cd8 --- /dev/null +++ b/spec/ruby/core/thread/initialize_spec.rb @@ -0,0 +1,27 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Thread#initialize" do + + describe "already initialized" do + + before do + @t = Thread.new { sleep } + end + + after do + @t.kill + @t.join + end + + it "raises a ThreadError" do + -> { + @t.instance_eval do + initialize {} + end + }.should raise_error(ThreadError) + end + + end + +end diff --git a/spec/ruby/core/thread/inspect_spec.rb b/spec/ruby/core/thread/inspect_spec.rb new file mode 100644 index 0000000000..bd6e0c31fc --- /dev/null +++ b/spec/ruby/core/thread/inspect_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/to_s' + +describe "Thread#inspect" do + it_behaves_like :thread_to_s, :inspect +end diff --git a/spec/ruby/core/thread/join_spec.rb b/spec/ruby/core/thread/join_spec.rb new file mode 100644 index 0000000000..213fe2e505 --- /dev/null +++ b/spec/ruby/core/thread/join_spec.rb @@ -0,0 +1,70 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Thread#join" do + it "returns the thread when it is finished" do + t = Thread.new {} + t.join.should equal(t) + end + + it "returns the thread when it is finished when given a timeout" do + t = Thread.new {} + t.join + t.join(0).should equal(t) + end + + it "coerces timeout to a Float if it is not nil" do + t = Thread.new {} + t.join + t.join(0).should equal(t) + t.join(0.0).should equal(t) + t.join(nil).should equal(t) + end + + it "raises TypeError if the argument is not a valid timeout" do + t = Thread.new { } + t.join + -> { t.join(:foo) }.should raise_error TypeError + -> { t.join("bar") }.should raise_error TypeError + end + + it "returns nil if it is not finished when given a timeout" do + q = Queue.new + t = Thread.new { q.pop } + begin + t.join(0).should == nil + ensure + q << true + end + t.join.should == t + end + + it "accepts a floating point timeout length" do + q = Queue.new + t = Thread.new { q.pop } + begin + t.join(0.01).should == nil + ensure + q << true + end + t.join.should == t + end + + it "raises any exceptions encountered in the thread body" do + t = Thread.new { + Thread.current.report_on_exception = false + raise NotImplementedError.new("Just kidding") + } + -> { t.join }.should raise_error(NotImplementedError) + end + + it "returns the dead thread" do + t = Thread.new { Thread.current.kill } + t.join.should equal(t) + end + + it "raises any uncaught exception encountered in ensure block" do + t = ThreadSpecs.dying_thread_ensures { raise NotImplementedError.new("Just kidding") } + -> { t.join }.should raise_error(NotImplementedError) + end +end diff --git a/spec/ruby/core/thread/key_spec.rb b/spec/ruby/core/thread/key_spec.rb new file mode 100644 index 0000000000..339fa98f53 --- /dev/null +++ b/spec/ruby/core/thread/key_spec.rb @@ -0,0 +1,60 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Thread#key?" do + before :each do + @th = Thread.new do + Thread.current[:oliver] = "a" + end + @th.join + end + + it "tests for existence of thread local variables using symbols or strings" do + @th.key?(:oliver).should == true + @th.key?("oliver").should == true + @th.key?(:stanley).should == false + @th.key?(:stanley.to_s).should == false + end + + it "converts a key that is neither String nor Symbol with #to_str" do + key = mock('key') + key.should_receive(:to_str).and_return('oliver') + + @th.key?(key).should == true + end + + it "raises exceptions on the wrong type of keys" do + -> { Thread.current.key? nil }.should raise_error(TypeError) + -> { Thread.current.key? 5 }.should raise_error(TypeError) + end + + it "is not shared across fibers" do + fib = Fiber.new do + Thread.current[:val1] = 1 + Fiber.yield + Thread.current.key?(:val1).should be_true + Thread.current.key?(:val2).should be_false + end + Thread.current.key?(:val1).should_not be_true + fib.resume + Thread.current[:val2] = 2 + fib.resume + Thread.current.key?(:val1).should be_false + Thread.current.key?(:val2).should be_true + end + + it "stores a local in another thread when in a fiber" do + fib = Fiber.new do + t = Thread.new do + sleep + Thread.current.key?(:value).should be_true + end + + Thread.pass while t.status and t.status != "sleep" + t[:value] = 1 + t.wakeup + t.join + end + fib.resume + end +end diff --git a/spec/ruby/core/thread/keys_spec.rb b/spec/ruby/core/thread/keys_spec.rb new file mode 100644 index 0000000000..15efda51d6 --- /dev/null +++ b/spec/ruby/core/thread/keys_spec.rb @@ -0,0 +1,44 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Thread#keys" do + it "returns an array of the names of the thread-local variables as symbols" do + th = Thread.new do + Thread.current["cat"] = 'woof' + Thread.current[:cat] = 'meow' + Thread.current[:dog] = 'woof' + end + th.join + th.keys.sort_by {|x| x.to_s}.should == [:cat,:dog] + end + + it "is not shared across fibers" do + fib = Fiber.new do + Thread.current[:val1] = 1 + Fiber.yield + Thread.current.keys.should include(:val1) + Thread.current.keys.should_not include(:val2) + end + Thread.current.keys.should_not include(:val1) + fib.resume + Thread.current[:val2] = 2 + fib.resume + Thread.current.keys.should include(:val2) + Thread.current.keys.should_not include(:val1) + end + + it "stores a local in another thread when in a fiber" do + fib = Fiber.new do + t = Thread.new do + sleep + Thread.current.keys.should include(:value) + end + + Thread.pass while t.status and t.status != "sleep" + t[:value] = 1 + t.wakeup + t.join + end + fib.resume + end +end diff --git a/spec/ruby/core/thread/kill_spec.rb b/spec/ruby/core/thread/kill_spec.rb new file mode 100644 index 0000000000..4b62c686c7 --- /dev/null +++ b/spec/ruby/core/thread/kill_spec.rb @@ -0,0 +1,21 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/exit' + +# This spec randomly kills mspec worker like: https://ci.appveyor.com/project/ruby/ruby/builds/19473223/job/f69derxnlo09xhuj +# TODO: Investigate the cause or at least print helpful logs, and remove this `platform_is_not` guard. +platform_is_not :mingw do + describe "Thread#kill" do + it_behaves_like :thread_exit, :kill + end + + describe "Thread.kill" do + it "causes the given thread to exit" do + thread = Thread.new { sleep } + Thread.pass while thread.status and thread.status != "sleep" + Thread.kill(thread).should == thread + thread.join + thread.status.should be_false + end + end +end diff --git a/spec/ruby/core/thread/list_spec.rb b/spec/ruby/core/thread/list_spec.rb new file mode 100644 index 0000000000..3c6f70c13e --- /dev/null +++ b/spec/ruby/core/thread/list_spec.rb @@ -0,0 +1,55 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Thread.list" do + it "includes the current and main thread" do + Thread.list.should include(Thread.current) + Thread.list.should include(Thread.main) + end + + it "includes threads of non-default thread groups" do + t = Thread.new { sleep } + begin + ThreadGroup.new.add(t) + Thread.list.should include(t) + ensure + t.kill + t.join + end + end + + it "does not include deceased threads" do + t = Thread.new { 1; } + t.join + Thread.list.should_not include(t) + end + + it "includes waiting threads" do + q = Queue.new + t = Thread.new { q.pop } + begin + Thread.pass while t.status and t.status != 'sleep' + Thread.list.should include(t) + ensure + q << nil + t.join + end + end + + it "returns instances of Thread and not null or nil values" do + spawner = Thread.new do + Array.new(100) do + Thread.new {} + end + end + + begin + Thread.list.each { |th| + th.should be_kind_of(Thread) + } + end while spawner.alive? + + threads = spawner.value + threads.each(&:join) + end +end diff --git a/spec/ruby/core/thread/main_spec.rb b/spec/ruby/core/thread/main_spec.rb new file mode 100644 index 0000000000..ec91709576 --- /dev/null +++ b/spec/ruby/core/thread/main_spec.rb @@ -0,0 +1,10 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Thread.main" do + it "returns the main thread" do + Thread.new { @main = Thread.main ; @current = Thread.current}.join + @main.should_not == @current + @main.should == Thread.current + end +end diff --git a/spec/ruby/core/thread/name_spec.rb b/spec/ruby/core/thread/name_spec.rb new file mode 100644 index 0000000000..9b3d2f4b09 --- /dev/null +++ b/spec/ruby/core/thread/name_spec.rb @@ -0,0 +1,54 @@ +require_relative '../../spec_helper' + +describe "Thread#name" do + before :each do + @thread = Thread.new {} + end + + after :each do + @thread.join + end + + it "is nil initially" do + @thread.name.should == nil + end + + it "returns the thread name" do + @thread.name = "thread_name" + @thread.name.should == "thread_name" + end +end + +describe "Thread#name=" do + before :each do + @thread = Thread.new {} + end + + after :each do + @thread.join + end + + it "can be set to a String" do + @thread.name = "new thread name" + @thread.name.should == "new thread name" + end + + it "raises an ArgumentError if the name includes a null byte" do + -> { + @thread.name = "new thread\0name" + }.should raise_error(ArgumentError) + end + + it "can be reset to nil" do + @thread.name = nil + @thread.name.should == nil + end + + it "calls #to_str to convert name to String" do + name = mock("Thread#name") + name.should_receive(:to_str).and_return("a thread name") + + @thread.name = name + @thread.name.should == "a thread name" + end +end diff --git a/spec/ruby/core/thread/native_thread_id_spec.rb b/spec/ruby/core/thread/native_thread_id_spec.rb new file mode 100644 index 0000000000..374cc59279 --- /dev/null +++ b/spec/ruby/core/thread/native_thread_id_spec.rb @@ -0,0 +1,35 @@ +require_relative '../../spec_helper' + +platform_is :linux, :darwin, :windows, :freebsd do + describe "Thread#native_thread_id" do + it "returns an integer when the thread is alive" do + Thread.current.native_thread_id.should be_kind_of(Integer) + end + + it "returns nil when the thread is not running" do + t = Thread.new {} + t.join + t.native_thread_id.should == nil + end + + it "each thread has different native thread id" do + t = Thread.new { sleep } + Thread.pass until t.stop? + main_thread_id = Thread.current.native_thread_id + t_thread_id = t.native_thread_id + + if ruby_version_is "3.3" + # native_thread_id can be nil on a M:N scheduler + t_thread_id.should be_kind_of(Integer) if t_thread_id != nil + else + t_thread_id.should be_kind_of(Integer) + end + + main_thread_id.should_not == t_thread_id + + t.run + t.join + t.native_thread_id.should == nil + end + end +end diff --git a/spec/ruby/core/thread/new_spec.rb b/spec/ruby/core/thread/new_spec.rb new file mode 100644 index 0000000000..47a836201c --- /dev/null +++ b/spec/ruby/core/thread/new_spec.rb @@ -0,0 +1,83 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Thread.new" do + it "creates a thread executing the given block" do + q = Queue.new + Thread.new { q << true }.join + q << false + q.pop.should == true + end + + it "can pass arguments to the thread block" do + arr = [] + a, b, c = 1, 2, 3 + t = Thread.new(a,b,c) {|d,e,f| arr << d << e << f } + t.join + arr.should == [a,b,c] + end + + it "raises an exception when not given a block" do + -> { Thread.new }.should raise_error(ThreadError) + end + + it "creates a subclass of thread calls super with a block in initialize" do + arr = [] + t = ThreadSpecs::SubThread.new(arr) + t.join + arr.should == [1] + end + + it "calls #initialize and raises an error if super not used" do + c = Class.new(Thread) do + def initialize + end + end + + -> { + c.new + }.should raise_error(ThreadError) + end + + it "calls and respects #initialize for the block to use" do + c = Class.new(Thread) do + def initialize + ScratchPad.record [:good] + super { ScratchPad << :in_thread } + end + end + + t = c.new + t.join + + ScratchPad.recorded.should == [:good, :in_thread] + end + + it "releases Mutexes held by the Thread when the Thread finishes" do + m1 = Mutex.new + m2 = Mutex.new + t = Thread.new { + m1.lock + m1.should.locked? + m2.lock + m2.should.locked? + } + t.join + m1.should_not.locked? + m2.should_not.locked? + end + + it "releases Mutexes held by the Thread when the Thread finishes, also with Mutex#synchronize" do + m = Mutex.new + t = Thread.new { + m.synchronize { + m.unlock + m.lock + } + m.lock + m.should.locked? + } + t.join + m.should_not.locked? + end +end diff --git a/spec/ruby/core/thread/pass_spec.rb b/spec/ruby/core/thread/pass_spec.rb new file mode 100644 index 0000000000..a5ac11a58c --- /dev/null +++ b/spec/ruby/core/thread/pass_spec.rb @@ -0,0 +1,8 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Thread.pass" do + it "returns nil" do + Thread.pass.should == nil + end +end diff --git a/spec/ruby/core/thread/pending_interrupt_spec.rb b/spec/ruby/core/thread/pending_interrupt_spec.rb new file mode 100644 index 0000000000..cd565d92a4 --- /dev/null +++ b/spec/ruby/core/thread/pending_interrupt_spec.rb @@ -0,0 +1,32 @@ +require_relative '../../spec_helper' + +describe "Thread.pending_interrupt?" do + it "returns false if there are no pending interrupts, e.g., outside any Thread.handle_interrupt block" do + Thread.pending_interrupt?.should == false + end + + it "returns true if there are pending interrupts, e.g., Thread#raise inside Thread.handle_interrupt" do + executed = false + -> { + Thread.handle_interrupt(RuntimeError => :never) do + Thread.pending_interrupt?.should == false + + current = Thread.current + Thread.new { + current.raise "interrupt" + }.join + + Thread.pending_interrupt?.should == true + executed = true + end + }.should raise_error(RuntimeError, "interrupt") + executed.should == true + Thread.pending_interrupt?.should == false + end +end + +describe "Thread#pending_interrupt?" do + it "returns whether the given threads has pending interrupts" do + Thread.current.pending_interrupt?.should == false + end +end diff --git a/spec/ruby/core/thread/priority_spec.rb b/spec/ruby/core/thread/priority_spec.rb new file mode 100644 index 0000000000..e13ad478b5 --- /dev/null +++ b/spec/ruby/core/thread/priority_spec.rb @@ -0,0 +1,72 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Thread#priority" do + before :each do + @current_priority = Thread.current.priority + ThreadSpecs.clear_state + @thread = Thread.new { Thread.pass until ThreadSpecs.state == :exit } + Thread.pass until @thread.alive? + end + + after :each do + ThreadSpecs.state = :exit + @thread.join + end + + it "inherits the priority of the current thread while running" do + @thread.alive?.should be_true + @thread.priority.should == @current_priority + end + + it "maintain the priority of the current thread after death" do + ThreadSpecs.state = :exit + @thread.join + @thread.alive?.should be_false + @thread.priority.should == @current_priority + end + + it "returns an integer" do + @thread.priority.should be_kind_of(Integer) + end +end + +describe "Thread#priority=" do + before :each do + ThreadSpecs.clear_state + @thread = Thread.new { Thread.pass until ThreadSpecs.state == :exit } + Thread.pass until @thread.alive? + end + + after :each do + ThreadSpecs.state = :exit + @thread.join + end + + describe "when set with an integer" do + it "returns an integer" do + value = (@thread.priority = 3) + value.should == 3 + end + + it "clamps the priority to -3..3" do + @thread.priority = 42 + @thread.priority.should == 3 + @thread.priority = -42 + @thread.priority.should == -3 + end + end + + describe "when set with a non-integer" do + it "raises a type error" do + ->{ @thread.priority = Object.new }.should raise_error(TypeError) + end + end + + it "sets priority even when the thread has died" do + thread = Thread.new {} + thread.join + thread.priority = 3 + thread.priority.should == 3 + end +end diff --git a/spec/ruby/core/thread/raise_spec.rb b/spec/ruby/core/thread/raise_spec.rb new file mode 100644 index 0000000000..b473eabd42 --- /dev/null +++ b/spec/ruby/core/thread/raise_spec.rb @@ -0,0 +1,235 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative '../../shared/kernel/raise' + +describe "Thread#raise" do + it_behaves_like :kernel_raise, :raise, ThreadSpecs::NewThreadToRaise + it_behaves_like :kernel_raise_across_contexts, :raise, ThreadSpecs::NewThreadToRaise + + it "ignores dead threads and returns nil" do + t = Thread.new { :dead } + Thread.pass while t.alive? + t.raise("Kill the thread").should == nil + t.join + end +end + +describe "Thread#raise on a sleeping thread" do + before :each do + ScratchPad.clear + @thr = ThreadSpecs.sleeping_thread + Thread.pass while @thr.status and @thr.status != "sleep" + end + + after :each do + @thr.kill + @thr.join + end + + it "raises a RuntimeError if no exception class is given" do + @thr.raise + Thread.pass while @thr.status + ScratchPad.recorded.should be_kind_of(RuntimeError) + end + + it "raises the given exception" do + @thr.raise Exception + Thread.pass while @thr.status + ScratchPad.recorded.should be_kind_of(Exception) + end + + it "raises the given exception with the given message" do + @thr.raise Exception, "get to work" + Thread.pass while @thr.status + ScratchPad.recorded.should be_kind_of(Exception) + ScratchPad.recorded.message.should == "get to work" + end + + it "raises the given exception and the backtrace is the one of the interrupted thread" do + @thr.raise Exception + Thread.pass while @thr.status + ScratchPad.recorded.should be_kind_of(Exception) + ScratchPad.recorded.backtrace[0].should include("sleep") + end + + it "is captured and raised by Thread#value" do + t = Thread.new do + Thread.current.report_on_exception = false + sleep + end + + ThreadSpecs.spin_until_sleeping(t) + + t.raise + -> { t.value }.should raise_error(RuntimeError) + end + + it "raises a RuntimeError when called with no arguments inside rescue" do + t = Thread.new do + Thread.current.report_on_exception = false + begin + 1/0 + rescue ZeroDivisionError + sleep + end + end + begin + raise RangeError + rescue + ThreadSpecs.spin_until_sleeping(t) + t.raise + end + -> { t.value }.should raise_error(RuntimeError) + end + + it "re-raises a previously rescued exception without overwriting the backtrace" do + t = Thread.new do + -> { # To make sure there is at least one entry in the call stack + begin + sleep + rescue => e + e + end + }.call + end + + ThreadSpecs.spin_until_sleeping(t) + + begin + initial_raise_line = __LINE__; raise 'raised' + rescue => raised + raise_again_line = __LINE__; t.raise raised + raised_again = t.value + + raised_again.backtrace.first.should include("#{__FILE__}:#{initial_raise_line}:") + raised_again.backtrace.first.should_not include("#{__FILE__}:#{raise_again_line}:") + end + end + + it "calls #exception in both the caller and in the target thread" do + cls = Class.new(Exception) do + attr_accessor :log + def initialize(*args) + @log = [] # This is shared because the super #exception uses a shallow clone + super + end + + def exception(*args) + @log << [self, Thread.current, args] + super + end + end + exc = cls.new + + @thr.raise exc, "Thread#raise #exception spec" + @thr.join + ScratchPad.recorded.should.is_a?(cls) + exc.log.should == [ + [exc, Thread.current, ["Thread#raise #exception spec"]], + [ScratchPad.recorded, @thr, []] + ] + end +end + +describe "Thread#raise on a running thread" do + before :each do + ScratchPad.clear + ThreadSpecs.clear_state + + @thr = ThreadSpecs.running_thread + Thread.pass until ThreadSpecs.state == :running + end + + after :each do + @thr.kill + @thr.join + end + + it "raises a RuntimeError if no exception class is given" do + @thr.raise + Thread.pass while @thr.status + ScratchPad.recorded.should be_kind_of(RuntimeError) + end + + it "raises the given exception" do + @thr.raise Exception + Thread.pass while @thr.status + ScratchPad.recorded.should be_kind_of(Exception) + end + + it "raises the given exception with the given message" do + @thr.raise Exception, "get to work" + Thread.pass while @thr.status + ScratchPad.recorded.should be_kind_of(Exception) + ScratchPad.recorded.message.should == "get to work" + end + + it "can go unhandled" do + q = Queue.new + t = Thread.new do + Thread.current.report_on_exception = false + q << true + loop { Thread.pass } + end + + q.pop # wait for `report_on_exception = false`. + t.raise + -> { t.value }.should raise_error(RuntimeError) + end + + it "raises the given argument even when there is an active exception" do + raised = false + t = Thread.new do + Thread.current.report_on_exception = false + begin + 1/0 + rescue ZeroDivisionError + raised = true + loop { Thread.pass } + end + end + begin + raise "Create an active exception for the current thread too" + rescue + Thread.pass until raised + t.raise RangeError + -> { t.value }.should raise_error(RangeError) + end + end + + it "raises a RuntimeError when called with no arguments inside rescue" do + raised = false + t = Thread.new do + Thread.current.report_on_exception = false + begin + 1/0 + rescue ZeroDivisionError + raised = true + loop { Thread.pass } + end + end + begin + raise RangeError + rescue + Thread.pass until raised + t.raise + end + -> { t.value }.should raise_error(RuntimeError) + end +end + +describe "Thread#raise on same thread" do + it_behaves_like :kernel_raise, :raise, Thread.current + + it "raises a RuntimeError when called with no arguments inside rescue" do + t = Thread.new do + Thread.current.report_on_exception = false + begin + 1/0 + rescue ZeroDivisionError + Thread.current.raise + end + end + -> { t.value }.should raise_error(RuntimeError, '') + end +end diff --git a/spec/ruby/core/thread/report_on_exception_spec.rb b/spec/ruby/core/thread/report_on_exception_spec.rb new file mode 100644 index 0000000000..d9daa041cd --- /dev/null +++ b/spec/ruby/core/thread/report_on_exception_spec.rb @@ -0,0 +1,155 @@ +require_relative '../../spec_helper' + +describe "Thread.report_on_exception" do + it "defaults to true" do + ruby_exe("p Thread.report_on_exception").should == "true\n" + end +end + +describe "Thread.report_on_exception=" do + before :each do + @report_on_exception = Thread.report_on_exception + end + + after :each do + Thread.report_on_exception = @report_on_exception + end + + it "changes the default value for new threads" do + Thread.report_on_exception = true + Thread.report_on_exception.should == true + t = Thread.new {} + t.join + t.report_on_exception.should == true + end +end + +describe "Thread#report_on_exception" do + it "returns true for the main Thread" do + Thread.current.report_on_exception.should == true + end + + it "returns true for new Threads" do + Thread.new { Thread.current.report_on_exception }.value.should == true + end + + it "returns whether the Thread will print a backtrace if it exits with an exception" do + t = Thread.new { Thread.current.report_on_exception = true } + t.join + t.report_on_exception.should == true + + t = Thread.new { Thread.current.report_on_exception = false } + t.join + t.report_on_exception.should == false + end +end + +describe "Thread#report_on_exception=" do + describe "when set to true" do + it "prints a backtrace on $stderr if it terminates with an exception" do + t = nil + -> { + t = Thread.new { + Thread.current.report_on_exception = true + raise RuntimeError, "Thread#report_on_exception specs" + } + Thread.pass while t.alive? + }.should output("", /Thread.+terminated with exception.+Thread#report_on_exception specs/m) + + -> { + t.join + }.should raise_error(RuntimeError, "Thread#report_on_exception specs") + end + + it "prints a backtrace on $stderr in the regular backtrace order" do + line_raise = __LINE__ + 2 + def foo + raise RuntimeError, "Thread#report_on_exception specs backtrace order" + end + + line_call_foo = __LINE__ + 5 + go = false + t = Thread.new { + Thread.current.report_on_exception = true + Thread.pass until go + foo + } + + -> { + go = true + Thread.pass while t.alive? + }.should output("", /\A +#{Regexp.quote(t.inspect)}\sterminated\swith\sexception\s\(report_on_exception\sis\strue\):\n +#{Regexp.quote(__FILE__)}:#{line_raise}:in\s[`']foo':\sThread\#report_on_exception\sspecs\sbacktrace\sorder\s\(RuntimeError\)\n +\tfrom\s#{Regexp.quote(__FILE__)}:#{line_call_foo}:in\s[`']block\s\(4\slevels\)\sin\s<top\s\(required\)>'\n +\z/x) + + -> { + t.join + }.should raise_error(RuntimeError, "Thread#report_on_exception specs backtrace order") + end + + it "prints the backtrace even if the thread was killed just after Thread#raise" do + t = nil + ready = false + -> { + t = Thread.new { + Thread.current.report_on_exception = true + ready = true + sleep + } + + Thread.pass until ready and t.stop? + t.raise RuntimeError, "Thread#report_on_exception before kill spec" + t.kill + Thread.pass while t.alive? + }.should output("", /Thread.+terminated with exception.+Thread#report_on_exception before kill spec/m) + + -> { + t.join + }.should raise_error(RuntimeError, "Thread#report_on_exception before kill spec") + end + end + + describe "when set to false" do + it "lets the thread terminates silently with an exception" do + t = nil + -> { + t = Thread.new { + Thread.current.report_on_exception = false + raise RuntimeError, "Thread#report_on_exception specs" + } + Thread.pass while t.alive? + }.should output("", "") + + -> { + t.join + }.should raise_error(RuntimeError, "Thread#report_on_exception specs") + end + end + + describe "when used in conjunction with Thread#abort_on_exception" do + it "first reports then send the exception back to the main Thread" do + t = nil + mutex = Mutex.new + mutex.lock + -> { + t = Thread.new { + Thread.current.abort_on_exception = true + Thread.current.report_on_exception = true + mutex.lock + mutex.unlock + raise RuntimeError, "Thread#report_on_exception specs" + } + + -> { + mutex.sleep(5) + }.should raise_error(RuntimeError, "Thread#report_on_exception specs") + }.should output("", /Thread.+terminated with exception.+Thread#report_on_exception specs/m) + + -> { + t.join + }.should raise_error(RuntimeError, "Thread#report_on_exception specs") + end + end +end diff --git a/spec/ruby/core/thread/run_spec.rb b/spec/ruby/core/thread/run_spec.rb new file mode 100644 index 0000000000..f86f793489 --- /dev/null +++ b/spec/ruby/core/thread/run_spec.rb @@ -0,0 +1,8 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +require_relative 'shared/wakeup' + +describe "Thread#run" do + it_behaves_like :thread_wakeup, :run +end diff --git a/spec/ruby/core/thread/set_trace_func_spec.rb b/spec/ruby/core/thread/set_trace_func_spec.rb new file mode 100644 index 0000000000..e5d8298ae0 --- /dev/null +++ b/spec/ruby/core/thread/set_trace_func_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../spec_helper' + +describe "Thread#set_trace_func" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/thread/shared/exit.rb b/spec/ruby/core/thread/shared/exit.rb new file mode 100644 index 0000000000..13e8832684 --- /dev/null +++ b/spec/ruby/core/thread/shared/exit.rb @@ -0,0 +1,219 @@ +describe :thread_exit, shared: true do + before :each do + ScratchPad.clear + end + + # This spec randomly kills mspec worker like: https://ci.appveyor.com/project/ruby/ruby/builds/19390874/job/wv1bsm8skd4e1pxl + # TODO: Investigate the cause or at least print helpful logs, and remove this `platform_is_not` guard. + platform_is_not :mingw do + + it "kills sleeping thread" do + sleeping_thread = Thread.new do + sleep + ScratchPad.record :after_sleep + end + Thread.pass while sleeping_thread.status and sleeping_thread.status != "sleep" + sleeping_thread.send(@method) + sleeping_thread.join + ScratchPad.recorded.should == nil + end + + it "kills current thread" do + thread = Thread.new do + Thread.current.send(@method) + ScratchPad.record :after_sleep + end + thread.join + ScratchPad.recorded.should == nil + end + + it "runs ensure clause" do + thread = ThreadSpecs.dying_thread_ensures(@method) { ScratchPad.record :in_ensure_clause } + thread.join + ScratchPad.recorded.should == :in_ensure_clause + end + + it "runs nested ensure clauses" do + ScratchPad.record [] + @outer = Thread.new do + begin + @inner = Thread.new do + begin + sleep + ensure + ScratchPad << :inner_ensure_clause + end + end + sleep + ensure + ScratchPad << :outer_ensure_clause + @inner.send(@method) + @inner.join + end + end + Thread.pass while @outer.status and @outer.status != "sleep" + Thread.pass until @inner + Thread.pass while @inner.status and @inner.status != "sleep" + @outer.send(@method) + @outer.join + ScratchPad.recorded.should include(:inner_ensure_clause) + ScratchPad.recorded.should include(:outer_ensure_clause) + end + + it "does not set $!" do + thread = ThreadSpecs.dying_thread_ensures(@method) { ScratchPad.record $! } + thread.join + ScratchPad.recorded.should == nil + end + + it "does not reset $!" do + ScratchPad.record [] + + exc = RuntimeError.new("foo") + thread = Thread.new do + begin + raise exc + ensure + ScratchPad << $! + begin + Thread.current.send(@method) + ensure + ScratchPad << $! + end + end + end + thread.join + ScratchPad.recorded.should == [exc, exc] + end + + it "cannot be rescued" do + thread = Thread.new do + begin + Thread.current.send(@method) + rescue Exception + ScratchPad.record :in_rescue + end + ScratchPad.record :end_of_thread_block + end + + thread.join + ScratchPad.recorded.should == nil + end + + it "kills the entire thread when a fiber is active" do + t = Thread.new do + Fiber.new do + sleep + end.resume + ScratchPad.record :fiber_resumed + end + Thread.pass while t.status and t.status != "sleep" + t.send(@method) + t.join + ScratchPad.recorded.should == nil + end + + it "kills other fibers of that thread without running their ensure clauses" do + t = Thread.new do + f = Fiber.new do + ScratchPad.record :fiber_resumed + begin + Fiber.yield + ensure + ScratchPad.record :fiber_ensure + end + end + f.resume + sleep + end + Thread.pass until t.stop? + t.send(@method) + t.join + ScratchPad.recorded.should == :fiber_resumed + end + + # This spec is a mess. It fails randomly, it hangs on MRI, it needs to be removed + quarantine! do + it "killing dying running does nothing" do + in_ensure_clause = false + exit_loop = true + t = ThreadSpecs.dying_thread_ensures do + in_ensure_clause = true + loop { if exit_loop then break end } + ScratchPad.record :after_stop + end + + Thread.pass until in_ensure_clause == true + 10.times { t.send(@method); Thread.pass } + exit_loop = true + t.join + ScratchPad.recorded.should == :after_stop + end + end + + quarantine! do + + it "propagates inner exception to Thread.join if there is an outer ensure clause" do + thread = ThreadSpecs.dying_thread_with_outer_ensure(@method) { } + -> { thread.join }.should raise_error(RuntimeError, "In dying thread") + end + + it "runs all outer ensure clauses even if inner ensure clause raises exception" do + ThreadSpecs.join_dying_thread_with_outer_ensure(@method) { ScratchPad.record :in_outer_ensure_clause } + ScratchPad.recorded.should == :in_outer_ensure_clause + end + + it "sets $! in outer ensure clause if inner ensure clause raises exception" do + ThreadSpecs.join_dying_thread_with_outer_ensure(@method) { ScratchPad.record $! } + ScratchPad.recorded.to_s.should == "In dying thread" + end + end + + it "can be rescued by outer rescue clause when inner ensure clause raises exception" do + thread = Thread.new do + begin + begin + Thread.current.send(@method) + ensure + raise "In dying thread" + end + rescue Exception + ScratchPad.record $! + end + :end_of_thread_block + end + + thread.value.should == :end_of_thread_block + ScratchPad.recorded.to_s.should == "In dying thread" + end + + it "is deferred if ensure clause does Thread.stop" do + ThreadSpecs.wakeup_dying_sleeping_thread(@method) { Thread.stop; ScratchPad.record :after_sleep } + ScratchPad.recorded.should == :after_sleep + end + + # Hangs on 1.8.6.114 OS X, possibly also on Linux + quarantine! do + it "is deferred if ensure clause sleeps" do + ThreadSpecs.wakeup_dying_sleeping_thread(@method) { sleep; ScratchPad.record :after_sleep } + ScratchPad.recorded.should == :after_sleep + end + end + + # This case occurred in JRuby where native threads are used to provide + # the same behavior as MRI green threads. Key to this issue was the fact + # that the thread which called #exit in its block was also being explicitly + # sent #join from outside the thread. The 100.times provides a certain + # probability that the deadlock will occur. It was sufficient to reliably + # reproduce the deadlock in JRuby. + it "does not deadlock when called from within the thread while being joined from without" do + 100.times do + t = Thread.new { Thread.stop; Thread.current.send(@method) } + Thread.pass while t.status and t.status != "sleep" + t.wakeup.should == t + t.join.should == t + end + end + + end # platform_is_not :mingw +end diff --git a/spec/ruby/core/thread/shared/start.rb b/spec/ruby/core/thread/shared/start.rb new file mode 100644 index 0000000000..2ba926bf00 --- /dev/null +++ b/spec/ruby/core/thread/shared/start.rb @@ -0,0 +1,41 @@ +describe :thread_start, shared: true do + before :each do + ScratchPad.clear + end + + it "raises an ArgumentError if not passed a block" do + -> { + Thread.send(@method) + }.should raise_error(ArgumentError) + end + + it "spawns a new Thread running the block" do + run = false + t = Thread.send(@method) { run = true } + t.should be_kind_of(Thread) + t.join + + run.should be_true + end + + it "respects Thread subclasses" do + c = Class.new(Thread) + t = c.send(@method) { } + t.should be_kind_of(c) + + t.join + end + + it "does not call #initialize" do + c = Class.new(Thread) do + def initialize + ScratchPad.record :bad + end + end + + t = c.send(@method) { } + t.join + + ScratchPad.recorded.should == nil + end +end diff --git a/spec/ruby/core/thread/shared/to_s.rb b/spec/ruby/core/thread/shared/to_s.rb new file mode 100644 index 0000000000..43640deb33 --- /dev/null +++ b/spec/ruby/core/thread/shared/to_s.rb @@ -0,0 +1,53 @@ +require_relative '../fixtures/classes' + +describe :thread_to_s, shared: true do + it "returns a description including file and line number" do + thread, line = Thread.new { "hello" }, __LINE__ + thread.join + thread.send(@method).should =~ /^#<Thread:([^ ]*?) #{Regexp.escape __FILE__}:#{line} \w+>$/ + end + + it "has a binary encoding" do + ThreadSpecs.status_of_current_thread.send(@method).encoding.should == Encoding::BINARY + end + + it "can check it's own status" do + ThreadSpecs.status_of_current_thread.send(@method).should include('run') + end + + it "describes a running thread" do + ThreadSpecs.status_of_running_thread.send(@method).should include('run') + end + + it "describes a sleeping thread" do + ThreadSpecs.status_of_sleeping_thread.send(@method).should include('sleep') + end + + it "describes a blocked thread" do + ThreadSpecs.status_of_blocked_thread.send(@method).should include('sleep') + end + + it "describes a completed thread" do + ThreadSpecs.status_of_completed_thread.send(@method).should include('dead') + end + + it "describes a killed thread" do + ThreadSpecs.status_of_killed_thread.send(@method).should include('dead') + end + + it "describes a thread with an uncaught exception" do + ThreadSpecs.status_of_thread_with_uncaught_exception.send(@method).should include('dead') + end + + it "describes a dying sleeping thread" do + ThreadSpecs.status_of_dying_sleeping_thread.send(@method).should include('sleep') + end + + it "reports aborting on a killed thread" do + ThreadSpecs.status_of_dying_running_thread.send(@method).should include('aborting') + end + + it "reports aborting on a killed thread after sleep" do + ThreadSpecs.status_of_dying_thread_after_sleep.send(@method).should include('aborting') + end +end diff --git a/spec/ruby/core/thread/shared/wakeup.rb b/spec/ruby/core/thread/shared/wakeup.rb new file mode 100644 index 0000000000..6f010fea25 --- /dev/null +++ b/spec/ruby/core/thread/shared/wakeup.rb @@ -0,0 +1,62 @@ +describe :thread_wakeup, shared: true do + it "can interrupt Kernel#sleep" do + exit_loop = false + after_sleep1 = false + after_sleep2 = false + + t = Thread.new do + while true + break if exit_loop == true + Thread.pass + end + + sleep + after_sleep1 = true + + sleep + after_sleep2 = true + end + + 10.times { t.send(@method); Thread.pass } + t.status.should_not == "sleep" + + exit_loop = true + + 10.times { sleep 0.1 if t.status and t.status != "sleep" } + after_sleep1.should == false # t should be blocked on the first sleep + t.send(@method) + + 10.times { sleep 0.1 if after_sleep1 != true } + 10.times { sleep 0.1 if t.status and t.status != "sleep" } + after_sleep2.should == false # t should be blocked on the second sleep + t.send(@method) + + t.join + end + + it "does not result in a deadlock" do + t = Thread.new do + 10.times { Thread.stop } + end + + while t.status + begin + t.send(@method) + rescue ThreadError + # The thread might die right after. + t.status.should == false + end + Thread.pass + sleep 0.001 + end + + t.status.should == false + t.join + end + + it "raises a ThreadError when trying to wake up a dead thread" do + t = Thread.new { 1 } + t.join + -> { t.send @method }.should raise_error(ThreadError) + end +end diff --git a/spec/ruby/core/thread/start_spec.rb b/spec/ruby/core/thread/start_spec.rb new file mode 100644 index 0000000000..3dd040f98b --- /dev/null +++ b/spec/ruby/core/thread/start_spec.rb @@ -0,0 +1,9 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/start' + +describe "Thread.start" do + describe "Thread.start" do + it_behaves_like :thread_start, :start + end +end diff --git a/spec/ruby/core/thread/status_spec.rb b/spec/ruby/core/thread/status_spec.rb new file mode 100644 index 0000000000..4fde663c91 --- /dev/null +++ b/spec/ruby/core/thread/status_spec.rb @@ -0,0 +1,60 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Thread#status" do + it "can check it's own status" do + ThreadSpecs.status_of_current_thread.status.should == 'run' + end + + it "describes a running thread" do + ThreadSpecs.status_of_running_thread.status.should == 'run' + end + + it "describes a sleeping thread" do + ThreadSpecs.status_of_sleeping_thread.status.should == 'sleep' + end + + it "describes a blocked thread" do + ThreadSpecs.status_of_blocked_thread.status.should == 'sleep' + end + + it "describes a completed thread" do + ThreadSpecs.status_of_completed_thread.status.should == false + end + + it "describes a killed thread" do + ThreadSpecs.status_of_killed_thread.status.should == false + end + + it "describes a thread with an uncaught exception" do + ThreadSpecs.status_of_thread_with_uncaught_exception.status.should == nil + end + + it "describes a dying sleeping thread" do + ThreadSpecs.status_of_dying_sleeping_thread.status.should == 'sleep' + end + + it "reports aborting on a killed thread" do + ThreadSpecs.status_of_dying_running_thread.status.should == 'aborting' + end + + it "reports aborting on a killed thread after sleep" do + ThreadSpecs.status_of_dying_thread_after_sleep.status.should == 'aborting' + end + + it "reports aborting on an externally killed thread that sleeps" do + q = Queue.new + t = Thread.new do + begin + q.push nil + sleep + ensure + q.push Thread.current.status + end + end + q.pop + t.kill + t.join + q.pop.should == 'aborting' + end +end diff --git a/spec/ruby/core/thread/stop_spec.rb b/spec/ruby/core/thread/stop_spec.rb new file mode 100644 index 0000000000..084ab46ef6 --- /dev/null +++ b/spec/ruby/core/thread/stop_spec.rb @@ -0,0 +1,54 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Thread.stop" do + it "causes the current thread to sleep indefinitely" do + t = Thread.new { Thread.stop; 5 } + Thread.pass while t.status and t.status != 'sleep' + t.status.should == 'sleep' + t.run + t.value.should == 5 + end +end + +describe "Thread#stop?" do + it "can check it's own status" do + ThreadSpecs.status_of_current_thread.should_not.stop? + end + + it "describes a running thread" do + ThreadSpecs.status_of_running_thread.should_not.stop? + end + + it "describes a sleeping thread" do + ThreadSpecs.status_of_sleeping_thread.should.stop? + end + + it "describes a blocked thread" do + ThreadSpecs.status_of_blocked_thread.should.stop? + end + + it "describes a completed thread" do + ThreadSpecs.status_of_completed_thread.should.stop? + end + + it "describes a killed thread" do + ThreadSpecs.status_of_killed_thread.should.stop? + end + + it "describes a thread with an uncaught exception" do + ThreadSpecs.status_of_thread_with_uncaught_exception.should.stop? + end + + it "describes a dying running thread" do + ThreadSpecs.status_of_dying_running_thread.should_not.stop? + end + + it "describes a dying sleeping thread" do + ThreadSpecs.status_of_dying_sleeping_thread.should.stop? + end + + it "describes a dying thread after sleep" do + ThreadSpecs.status_of_dying_thread_after_sleep.should_not.stop? + end +end diff --git a/spec/ruby/core/thread/terminate_spec.rb b/spec/ruby/core/thread/terminate_spec.rb new file mode 100644 index 0000000000..cf6cab472b --- /dev/null +++ b/spec/ruby/core/thread/terminate_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/exit' + +describe "Thread#terminate" do + it_behaves_like :thread_exit, :terminate +end diff --git a/spec/ruby/core/thread/thread_variable_get_spec.rb b/spec/ruby/core/thread/thread_variable_get_spec.rb new file mode 100644 index 0000000000..1ea34cf2b3 --- /dev/null +++ b/spec/ruby/core/thread/thread_variable_get_spec.rb @@ -0,0 +1,60 @@ +require_relative '../../spec_helper' + +describe "Thread#thread_variable_get" do + before :each do + @t = Thread.new { } + end + + after :each do + @t.join + end + + it "returns nil if the variable is not set" do + @t.thread_variable_get(:a).should be_nil + end + + it "returns the value previously set by #thread_variable_set" do + @t.thread_variable_set(:a, 49) + @t.thread_variable_get(:a).should == 49 + end + + it "returns a value private to self" do + @t.thread_variable_set(:thread_variable_get_spec, 82) + Thread.current.thread_variable_get(:thread_variable_get_spec).should be_nil + end + + it "accepts String and Symbol keys interchangeably" do + @t.thread_variable_set("a", 49) + @t.thread_variable_get("a").should == 49 + @t.thread_variable_get(:a).should == 49 + end + + it "converts a key that is neither String nor Symbol with #to_str" do + key = mock('key') + key.should_receive(:to_str).and_return('a') + @t.thread_variable_set(:a, 49) + @t.thread_variable_get(key).should == 49 + end + + it "does not raise FrozenError if the thread is frozen" do + @t.freeze + @t.thread_variable_get(:a).should be_nil + end + + it "raises a TypeError if the key is neither Symbol nor String when thread variables are already set" do + @t.thread_variable_set(:a, 49) + -> { @t.thread_variable_get(123) }.should raise_error(TypeError, /123 is not a symbol/) + end + + ruby_version_is '3.4' do + it "raises a TypeError if the key is neither Symbol nor String when no thread variables are set" do + -> { @t.thread_variable_get(123) }.should raise_error(TypeError, /123 is not a symbol/) + end + + it "raises a TypeError if the key is neither Symbol nor String without calling #to_sym" do + key = mock('key') + key.should_not_receive(:to_sym) + -> { @t.thread_variable_get(key) }.should raise_error(TypeError, /#{Regexp.escape(key.inspect)} is not a symbol/) + end + end +end diff --git a/spec/ruby/core/thread/thread_variable_set_spec.rb b/spec/ruby/core/thread/thread_variable_set_spec.rb new file mode 100644 index 0000000000..eadee76afb --- /dev/null +++ b/spec/ruby/core/thread/thread_variable_set_spec.rb @@ -0,0 +1,62 @@ +require_relative '../../spec_helper' + +describe "Thread#thread_variable_set" do + before :each do + @t = Thread.new { } + end + + after :each do + @t.join + end + + it "returns the value set" do + @t.thread_variable_set(:a, 2).should == 2 + end + + it "sets a value that will be returned by #thread_variable_get" do + @t.thread_variable_set(:a, 49) + @t.thread_variable_get(:a).should == 49 + end + + it "sets a value private to self" do + @t.thread_variable_set(:thread_variable_get_spec, 82) + @t.thread_variable_get(:thread_variable_get_spec).should == 82 + Thread.current.thread_variable_get(:thread_variable_get_spec).should be_nil + end + + it "accepts String and Symbol keys interchangeably" do + @t.thread_variable_set('a', 49) + @t.thread_variable_get('a').should == 49 + + @t.thread_variable_set(:a, 50) + @t.thread_variable_get('a').should == 50 + end + + it "converts a key that is neither String nor Symbol with #to_str" do + key = mock('key') + key.should_receive(:to_str).and_return('a') + @t.thread_variable_set(key, 49) + @t.thread_variable_get(:a).should == 49 + end + + it "removes a key if the value is nil" do + @t.thread_variable_set(:a, 52) + @t.thread_variable_set(:a, nil) + @t.thread_variable?(:a).should be_false + end + + it "raises a FrozenError if the thread is frozen" do + @t.freeze + -> { @t.thread_variable_set(:a, 1) }.should raise_error(FrozenError, "can't modify frozen thread locals") + end + + it "raises a TypeError if the key is neither Symbol nor String, nor responds to #to_str" do + -> { @t.thread_variable_set(123, 1) }.should raise_error(TypeError, /123 is not a symbol/) + end + + it "does not try to convert the key with #to_sym" do + key = mock('key') + key.should_not_receive(:to_sym) + -> { @t.thread_variable_set(key, 42) }.should raise_error(TypeError, /#{Regexp.quote(key.inspect)} is not a symbol/) + end +end diff --git a/spec/ruby/core/thread/thread_variable_spec.rb b/spec/ruby/core/thread/thread_variable_spec.rb new file mode 100644 index 0000000000..1b021e9404 --- /dev/null +++ b/spec/ruby/core/thread/thread_variable_spec.rb @@ -0,0 +1,60 @@ +require_relative '../../spec_helper' + +describe "Thread#thread_variable?" do + before :each do + @t = Thread.new { } + end + + after :each do + @t.join + end + + it "returns false if the thread variables do not contain 'key'" do + @t.thread_variable_set(:a, 2) + @t.thread_variable?(:b).should be_false + end + + it "returns true if the thread variables contain 'key'" do + @t.thread_variable_set(:a, 2) + @t.thread_variable?(:a).should be_true + end + + it "accepts String and Symbol keys interchangeably" do + @t.thread_variable?('a').should be_false + @t.thread_variable?(:a).should be_false + + @t.thread_variable_set(:a, 49) + + @t.thread_variable?('a').should be_true + @t.thread_variable?(:a).should be_true + end + + it "converts a key that is neither String nor Symbol with #to_str" do + key = mock('key') + key.should_receive(:to_str).and_return('a') + @t.thread_variable_set(:a, 49) + @t.thread_variable?(key).should be_true + end + + it "does not raise FrozenError if the thread is frozen" do + @t.freeze + @t.thread_variable?(:a).should be_false + end + + it "raises a TypeError if the key is neither Symbol nor String when thread variables are already set" do + @t.thread_variable_set(:a, 49) + -> { @t.thread_variable?(123) }.should raise_error(TypeError, /123 is not a symbol/) + end + + ruby_version_is '3.4' do + it "raises a TypeError if the key is neither Symbol nor String when no thread variables are set" do + -> { @t.thread_variable?(123) }.should raise_error(TypeError, /123 is not a symbol/) + end + + it "raises a TypeError if the key is neither Symbol nor String without calling #to_sym" do + key = mock('key') + key.should_not_receive(:to_sym) + -> { @t.thread_variable?(key) }.should raise_error(TypeError, /#{Regexp.escape(key.inspect)} is not a symbol/) + end + end +end diff --git a/spec/ruby/core/thread/thread_variables_spec.rb b/spec/ruby/core/thread/thread_variables_spec.rb new file mode 100644 index 0000000000..51ceef3376 --- /dev/null +++ b/spec/ruby/core/thread/thread_variables_spec.rb @@ -0,0 +1,39 @@ +require_relative '../../spec_helper' + +describe "Thread#thread_variables" do + before :each do + @t = Thread.new { } + end + + after :each do + @t.join + end + + it "returns the keys of all the values set" do + @t.thread_variable_set(:a, 2) + @t.thread_variable_set(:b, 4) + @t.thread_variable_set(:c, 6) + @t.thread_variables.sort.should == [:a, :b, :c] + end + + it "returns the keys private to self" do + @t.thread_variable_set(:a, 82) + @t.thread_variable_set(:b, 82) + Thread.current.thread_variables.should_not include(:a, :b) + end + + it "only contains user thread variables and is empty initially" do + Thread.current.thread_variables.should == [] + @t.thread_variables.should == [] + end + + it "returns keys as Symbols" do + key = mock('key') + key.should_receive(:to_str).and_return('a') + + @t.thread_variable_set(key, 49) + @t.thread_variable_set('b', 50) + @t.thread_variable_set(:c, 51) + @t.thread_variables.sort.should == [:a, :b, :c] + end +end diff --git a/spec/ruby/core/thread/to_s_spec.rb b/spec/ruby/core/thread/to_s_spec.rb new file mode 100644 index 0000000000..cb182a017f --- /dev/null +++ b/spec/ruby/core/thread/to_s_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/to_s' + +describe "Thread#to_s" do + it_behaves_like :thread_to_s, :to_s +end diff --git a/spec/ruby/core/thread/value_spec.rb b/spec/ruby/core/thread/value_spec.rb new file mode 100644 index 0000000000..30e43abd1a --- /dev/null +++ b/spec/ruby/core/thread/value_spec.rb @@ -0,0 +1,31 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Thread#value" do + it "returns the result of the block" do + Thread.new { 3 }.value.should == 3 + end + + it "re-raises an error for an uncaught exception" do + t = Thread.new { + Thread.current.report_on_exception = false + raise "Hello" + } + -> { t.value }.should raise_error(RuntimeError, "Hello") + end + + it "is nil for a killed thread" do + t = Thread.new { Thread.current.exit } + t.value.should == nil + end + + it "returns when the thread finished" do + q = Queue.new + t = Thread.new { + q.pop + } + -> { t.value }.should block_caller + q.push :result + t.value.should == :result + end +end diff --git a/spec/ruby/core/thread/wakeup_spec.rb b/spec/ruby/core/thread/wakeup_spec.rb new file mode 100644 index 0000000000..da5dfea377 --- /dev/null +++ b/spec/ruby/core/thread/wakeup_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/wakeup' + +describe "Thread#wakeup" do + it_behaves_like :thread_wakeup, :wakeup +end diff --git a/spec/ruby/core/threadgroup/add_spec.rb b/spec/ruby/core/threadgroup/add_spec.rb new file mode 100644 index 0000000000..2921c1ab22 --- /dev/null +++ b/spec/ruby/core/threadgroup/add_spec.rb @@ -0,0 +1,39 @@ +require_relative '../../spec_helper' + +describe "ThreadGroup#add" do + before :each do + @q1, @q2 = Queue.new, Queue.new + @thread = Thread.new { @q1 << :go; @q2.pop } + @q1.pop + end + + after :each do + @q2 << :done + @thread.join + end + + # This spec randomly kills mspec worker like: https://ci.appveyor.com/project/ruby/ruby/build/9806/job/37tx2atojy96227m + # TODO: Investigate the cause or at least print helpful logs, and remove this `platform_is_not` guard. + platform_is_not :mingw do + it "adds the given thread to a group and returns self" do + @thread.group.should_not == nil + + tg = ThreadGroup.new + tg.add(@thread).should == tg + @thread.group.should == tg + tg.list.include?(@thread).should == true + end + + it "removes itself from any other threadgroup" do + tg1 = ThreadGroup.new + tg2 = ThreadGroup.new + + tg1.add(@thread) + @thread.group.should == tg1 + tg2.add(@thread) + @thread.group.should == tg2 + tg2.list.include?(@thread).should == true + tg1.list.include?(@thread).should == false + end + end +end diff --git a/spec/ruby/core/threadgroup/default_spec.rb b/spec/ruby/core/threadgroup/default_spec.rb new file mode 100644 index 0000000000..d7d4726cc2 --- /dev/null +++ b/spec/ruby/core/threadgroup/default_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' + +describe "ThreadGroup::Default" do + it "is a ThreadGroup instance" do + ThreadGroup::Default.should be_kind_of(ThreadGroup) + end + + it "is the ThreadGroup of the main thread" do + ThreadGroup::Default.should == Thread.main.group + end +end diff --git a/spec/ruby/core/threadgroup/enclose_spec.rb b/spec/ruby/core/threadgroup/enclose_spec.rb new file mode 100644 index 0000000000..dd9a7a362d --- /dev/null +++ b/spec/ruby/core/threadgroup/enclose_spec.rb @@ -0,0 +1,24 @@ +require_relative '../../spec_helper' + +describe "ThreadGroup#enclose" do + before :each do + @q1, @q2 = Queue.new, Queue.new + @thread = Thread.new { @q1 << :go; @q2.pop } + @q1.pop + end + + after :each do + @q2 << :done + @thread.join + end + + it "raises a ThreadError if attempting to move a Thread from an enclosed ThreadGroup" do + thread_group = ThreadGroup.new + default_group = @thread.group + thread_group.add(@thread) + thread_group.enclose + -> do + default_group.add(@thread) + end.should raise_error(ThreadError) + end +end diff --git a/spec/ruby/core/threadgroup/enclosed_spec.rb b/spec/ruby/core/threadgroup/enclosed_spec.rb new file mode 100644 index 0000000000..a734256a64 --- /dev/null +++ b/spec/ruby/core/threadgroup/enclosed_spec.rb @@ -0,0 +1,14 @@ +require_relative '../../spec_helper' + +describe "ThreadGroup#enclosed?" do + it "returns false when a ThreadGroup has not been enclosed (default state)" do + thread_group = ThreadGroup.new + thread_group.enclosed?.should be_false + end + + it "returns true when a ThreadGroup is enclosed" do + thread_group = ThreadGroup.new + thread_group.enclose + thread_group.enclosed?.should be_true + end +end diff --git a/spec/ruby/core/threadgroup/list_spec.rb b/spec/ruby/core/threadgroup/list_spec.rb new file mode 100644 index 0000000000..b2ac64324a --- /dev/null +++ b/spec/ruby/core/threadgroup/list_spec.rb @@ -0,0 +1,23 @@ +require_relative '../../spec_helper' + +describe "ThreadGroup#list" do + it "returns the list of threads in the group" do + q = Queue.new + th1 = Thread.new { q << :go; sleep } + q.pop.should == :go + tg = ThreadGroup.new + tg.add(th1) + tg.list.should include(th1) + + th2 = Thread.new { q << :go; sleep } + q.pop.should == :go + + tg.add(th2) + (tg.list & [th1, th2]).should include(th1, th2) + + Thread.pass while th1.status and th1.status != 'sleep' + Thread.pass while th2.status and th2.status != 'sleep' + th1.run; th1.join + th2.run; th2.join + end +end diff --git a/spec/ruby/core/time/_dump_spec.rb b/spec/ruby/core/time/_dump_spec.rb new file mode 100644 index 0000000000..852f9a07ab --- /dev/null +++ b/spec/ruby/core/time/_dump_spec.rb @@ -0,0 +1,55 @@ +# encoding: binary +require_relative '../../spec_helper' + +describe "Time#_dump" do + before :each do + @local = Time.at(946812800) + @t = Time.at(946812800) + @t = @t.gmtime + @s = @t.send(:_dump) + end + + it "is a private method" do + Time.should have_private_instance_method(:_dump, false) + end + + # http://redmine.ruby-lang.org/issues/show/627 + it "preserves the GMT flag" do + @t.should.gmt? + dump = @t.send(:_dump).unpack("VV").first + ((dump >> 30) & 0x1).should == 1 + + @local.should_not.gmt? + dump = @local.send(:_dump).unpack("VV").first + ((dump >> 30) & 0x1).should == 0 + end + + it "dumps a Time object to a bytestring" do + @s.should be_an_instance_of(String) + @s.should == [3222863947, 2235564032].pack("VV") + end + + it "dumps an array with a date as first element" do + high = 1 << 31 | + (@t.gmt? ? 1 : 0) << 30 | + (@t.year - 1900) << 14 | + (@t.mon - 1) << 10 | + @t.mday << 5 | + @t.hour + + high.should == @s.unpack("VV").first + end + + it "dumps an array with a time as second element" do + low = @t.min << 26 | + @t.sec << 20 | + @t.usec + low.should == @s.unpack("VV").last + end + + it "dumps like MRI's marshaled time format" do + t = Time.utc(2000, 1, 15, 20, 1, 1, 203).localtime + + t.send(:_dump).should == "\364\001\031\200\313\000\020\004" + end +end diff --git a/spec/ruby/core/time/_load_spec.rb b/spec/ruby/core/time/_load_spec.rb new file mode 100644 index 0000000000..30899de262 --- /dev/null +++ b/spec/ruby/core/time/_load_spec.rb @@ -0,0 +1,51 @@ +# encoding: binary +require_relative '../../spec_helper' + +describe "Time._load" do + it "is a private method" do + Time.should have_private_method(:_load, false) + end + + # http://redmine.ruby-lang.org/issues/show/627 + it "loads a time object in the new format" do + t = Time.local(2000, 1, 15, 20, 1, 1) + t = t.gmtime + + high = 1 << 31 | + (t.gmt? ? 1 : 0) << 30 | + (t.year - 1900) << 14 | + (t.mon - 1) << 10 | + t.mday << 5 | + t.hour + + low = t.min << 26 | + t.sec << 20 | + t.usec + + Time.send(:_load, [high, low].pack("VV")).should == t + end + + it "loads a time object in the old UNIX timestamp based format" do + t = Time.local(2000, 1, 15, 20, 1, 1, 203) + timestamp = t.to_i + + high = timestamp & ((1 << 31) - 1) + + low = t.usec + + Time.send(:_load, [high, low].pack("VV")).should == t + end + + it "loads MRI's marshaled time format" do + t = Marshal.load("\004\bu:\tTime\r\320\246\e\200\320\001\r\347") + t.utc + + t.to_s.should == "2010-10-22 16:57:48 UTC" + end + + it "treats the data as binary data" do + data = "\x04\bu:\tTime\r\fM\x1C\xC0\x00\x00\xD0\xBE".dup.force_encoding Encoding::UTF_8 + t = Marshal.load(data) + t.to_s.should == "2013-04-08 12:47:45 UTC" + end +end diff --git a/spec/ruby/core/time/asctime_spec.rb b/spec/ruby/core/time/asctime_spec.rb new file mode 100644 index 0000000000..a41ee531e4 --- /dev/null +++ b/spec/ruby/core/time/asctime_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/asctime' + +describe "Time#asctime" do + it_behaves_like :time_asctime, :asctime +end diff --git a/spec/ruby/core/time/at_spec.rb b/spec/ruby/core/time/at_spec.rb new file mode 100644 index 0000000000..97906b8c8c --- /dev/null +++ b/spec/ruby/core/time/at_spec.rb @@ -0,0 +1,316 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Time.at" do + describe "passed Numeric" do + it "returns a Time object representing the given number of Integer seconds since 1970-01-01 00:00:00 UTC" do + Time.at(1184027924).getgm.asctime.should == "Tue Jul 10 00:38:44 2007" + end + + it "returns a Time object representing the given number of Float seconds since 1970-01-01 00:00:00 UTC" do + t = Time.at(10.5) + t.usec.should == 500000.0 + t.should_not == Time.at(10) + end + + it "returns a non-UTC Time" do + Time.at(1184027924).should_not.utc? + end + + it "returns a subclass instance on a Time subclass" do + c = Class.new(Time) + t = c.at(0) + t.should be_an_instance_of(c) + end + + it "roundtrips a Rational produced by #to_r" do + t = Time.now() + t2 = Time.at(t.to_r) + + t2.should == t + t2.usec.should == t.usec + t2.nsec.should == t.nsec + end + + describe "passed Rational" do + it "returns Time with correct microseconds" do + t = Time.at(Rational(1_486_570_508_539_759, 1_000_000)) + t.usec.should == 539_759 + t.nsec.should == 539_759_000 + end + + it "returns Time with correct nanoseconds" do + t = Time.at(Rational(1_486_570_508_539_759_123, 1_000_000_000)) + t.usec.should == 539_759 + t.nsec.should == 539_759_123 + end + end + end + + describe "passed Time" do + it "creates a new time object with the value given by time" do + t = Time.now + Time.at(t).inspect.should == t.inspect + end + + it "creates a dup time object with the value given by time" do + t1 = Time.new + t2 = Time.at(t1) + t1.should_not equal t2 + end + + it "returns a UTC time if the argument is UTC" do + t = Time.now.getgm + Time.at(t).should.utc? + end + + it "returns a non-UTC time if the argument is non-UTC" do + t = Time.now + Time.at(t).should_not.utc? + end + + it "returns a subclass instance" do + c = Class.new(Time) + t = c.at(Time.now) + t.should be_an_instance_of(c) + end + end + + describe "passed non-Time, non-Numeric" do + it "raises a TypeError with a String argument" do + -> { Time.at("0") }.should raise_error(TypeError) + end + + it "raises a TypeError with a nil argument" do + -> { Time.at(nil) }.should raise_error(TypeError) + end + + describe "with an argument that responds to #to_int" do + it "coerces using #to_int" do + o = mock('integer') + o.should_receive(:to_int).and_return(0) + Time.at(o).should == Time.at(0) + end + end + + describe "with an argument that responds to #to_r" do + it "coerces using #to_r" do + o = mock_numeric('rational') + o.should_receive(:to_r).and_return(Rational(5, 2)) + Time.at(o).should == Time.at(Rational(5, 2)) + end + + it "needs for the argument to respond to #to_int too" do + o = mock('rational-but-no-to_int') + def o.to_r; Rational(5, 2) end + -> { Time.at(o) }.should raise_error(TypeError, "can't convert MockObject into an exact number") + end + end + end + + describe "passed [Integer, Numeric]" do + it "returns a Time object representing the given number of seconds and Integer microseconds since 1970-01-01 00:00:00 UTC" do + t = Time.at(10, 500000) + t.tv_sec.should == 10 + t.tv_usec.should == 500000 + end + + it "returns a Time object representing the given number of seconds and Float microseconds since 1970-01-01 00:00:00 UTC" do + t = Time.at(10, 500.500) + t.tv_sec.should == 10 + t.tv_nsec.should == 500500 + end + end + + describe "with a second argument that responds to #to_int" do + it "coerces using #to_int" do + o = mock('integer') + o.should_receive(:to_int).and_return(10) + Time.at(0, o).should == Time.at(0, 10) + end + end + + describe "with a second argument that responds to #to_r" do + it "coerces using #to_r" do + o = mock_numeric('rational') + o.should_receive(:to_r).and_return(Rational(5, 2)) + Time.at(0, o).should == Time.at(0, Rational(5, 2)) + end + end + + describe "passed [Integer, nil]" do + it "raises a TypeError" do + -> { Time.at(0, nil) }.should raise_error(TypeError) + end + end + + describe "passed [Integer, String]" do + it "raises a TypeError" do + -> { Time.at(0, "0") }.should raise_error(TypeError) + end + end + + describe "passed [Time, Integer]" do + # #8173 + it "raises a TypeError" do + -> { Time.at(Time.now, 500000) }.should raise_error(TypeError) + end + end + + describe "passed [Time, Numeric, format]" do + context ":nanosecond format" do + it "treats second argument as nanoseconds" do + Time.at(0, 123456789, :nanosecond).nsec.should == 123456789 + end + end + + context ":nsec format" do + it "treats second argument as nanoseconds" do + Time.at(0, 123456789, :nsec).nsec.should == 123456789 + end + end + + context ":microsecond format" do + it "treats second argument as microseconds" do + Time.at(0, 123456, :microsecond).nsec.should == 123456000 + end + end + + context ":usec format" do + it "treats second argument as microseconds" do + Time.at(0, 123456, :usec).nsec.should == 123456000 + end + end + + context ":millisecond format" do + it "treats second argument as milliseconds" do + Time.at(0, 123, :millisecond).nsec.should == 123000000 + end + end + + context "not supported format" do + it "raises ArgumentError" do + -> { Time.at(0, 123456, 2) }.should raise_error(ArgumentError) + -> { Time.at(0, 123456, nil) }.should raise_error(ArgumentError) + -> { Time.at(0, 123456, :invalid) }.should raise_error(ArgumentError) + end + + it "does not try to convert format to Symbol with #to_sym" do + format = +"usec" + format.should_not_receive(:to_sym) + -> { Time.at(0, 123456, format) }.should raise_error(ArgumentError) + end + end + + it "supports Float second argument" do + Time.at(0, 123456789.500, :nanosecond).nsec.should == 123456789 + Time.at(0, 123456789.500, :nsec).nsec.should == 123456789 + Time.at(0, 123456.500, :microsecond).nsec.should == 123456500 + Time.at(0, 123456.500, :usec).nsec.should == 123456500 + Time.at(0, 123.500, :millisecond).nsec.should == 123500000 + end + end + + describe ":in keyword argument" do + before do + @epoch_time = Time.now.to_i + end + + it "could be UTC offset as a String in '+HH:MM or '-HH:MM' format" do + time = Time.at(@epoch_time, in: "+05:00") + + time.utc_offset.should == 5*60*60 + time.zone.should == nil + time.to_i.should == @epoch_time + + time = Time.at(@epoch_time, in: "-09:00") + + time.utc_offset.should == -9*60*60 + time.zone.should == nil + time.to_i.should == @epoch_time + + time = Time.at(@epoch_time, in: "-09:00:01") + + time.utc_offset.should == -(9*60*60 + 1) + time.zone.should == nil + time.to_i.should == @epoch_time + end + + it "could be UTC offset as a number of seconds" do + time = Time.at(@epoch_time, in: 5*60*60) + + time.utc_offset.should == 5*60*60 + time.zone.should == nil + time.to_i.should == @epoch_time + + time = Time.at(@epoch_time, in: -9*60*60) + + time.utc_offset.should == -9*60*60 + time.zone.should == nil + time.to_i.should == @epoch_time + end + + it "could be UTC offset as a 'UTC' String" do + time = Time.at(@epoch_time, in: "UTC") + + time.utc_offset.should == 0 + time.zone.should == "UTC" + time.to_i.should == @epoch_time + end + + it "could be UTC offset as a military zone A-Z" do + time = Time.at(@epoch_time, in: "B") + + time.utc_offset.should == 3600 * 2 + time.zone.should == nil + time.to_i.should == @epoch_time + end + + it "could be a timezone object" do + zone = TimeSpecs::TimezoneWithName.new(name: "Asia/Colombo") + time = Time.at(@epoch_time, in: zone) + + time.utc_offset.should == 5*3600+30*60 + time.zone.should == zone + time.to_i.should == @epoch_time + + zone = TimeSpecs::TimezoneWithName.new(name: "PST") + time = Time.at(@epoch_time, in: zone) + + time.utc_offset.should == -9*60*60 + time.zone.should == zone + time.to_i.should == @epoch_time + end + + it "raises ArgumentError if format is invalid" do + -> { Time.at(@epoch_time, in: "+09:99") }.should raise_error(ArgumentError) + -> { Time.at(@epoch_time, in: "ABC") }.should raise_error(ArgumentError) + end + + it "raises ArgumentError if hours greater than 23" do # TODO + -> { Time.at(@epoch_time, in: "+24:00") }.should raise_error(ArgumentError, "utc_offset out of range") + -> { Time.at(@epoch_time, in: "+2400") }.should raise_error(ArgumentError, "utc_offset out of range") + + -> { Time.at(@epoch_time, in: "+99:00") }.should raise_error(ArgumentError, "utc_offset out of range") + -> { Time.at(@epoch_time, in: "+9900") }.should raise_error(ArgumentError, "utc_offset out of range") + end + + it "raises ArgumentError if minutes greater than 59" do # TODO + -> { Time.at(@epoch_time, in: "+00:60") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:60') + -> { Time.at(@epoch_time, in: "+0060") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +0060') + + -> { Time.at(@epoch_time, in: "+00:99") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:99') + -> { Time.at(@epoch_time, in: "+0099") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +0099') + end + + ruby_bug '#20797', ''...'3.4' do + it "raises ArgumentError if seconds greater than 59" do + -> { Time.at(@epoch_time, in: "+00:00:60") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:00:60') + -> { Time.at(@epoch_time, in: "+000060") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +000060') + + -> { Time.at(@epoch_time, in: "+00:00:99") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:00:99') + -> { Time.at(@epoch_time, in: "+000099") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +000099') + end + end + end +end diff --git a/spec/ruby/core/time/ceil_spec.rb b/spec/ruby/core/time/ceil_spec.rb new file mode 100644 index 0000000000..9d624a1ed0 --- /dev/null +++ b/spec/ruby/core/time/ceil_spec.rb @@ -0,0 +1,44 @@ +require_relative '../../spec_helper' + +describe "Time#ceil" do + before do + @time = Time.utc(2010, 3, 30, 5, 43, "25.0123456789".to_r) + end + + it "defaults to ceiling to 0 places" do + @time.ceil.should == Time.utc(2010, 3, 30, 5, 43, 26.to_r) + end + + it "ceils to 0 decimal places with an explicit argument" do + @time.ceil(0).should == Time.utc(2010, 3, 30, 5, 43, 26.to_r) + end + + it "ceils to 2 decimal places with an explicit argument" do + @time.ceil(2).should == Time.utc(2010, 3, 30, 5, 43, "25.02".to_r) + end + + it "ceils to 4 decimal places with an explicit argument" do + @time.ceil(4).should == Time.utc(2010, 3, 30, 5, 43, "25.0124".to_r) + end + + it "ceils to 7 decimal places with an explicit argument" do + @time.ceil(7).should == Time.utc(2010, 3, 30, 5, 43, "25.0123457".to_r) + end + + it "returns an instance of Time, even if #ceil is called on a subclass" do + subclass = Class.new(Time) + instance = subclass.at(0) + instance.class.should equal subclass + instance.ceil.should be_an_instance_of(Time) + end + + it "copies own timezone to the returning value" do + @time.zone.should == @time.ceil.zone + + time = with_timezone "JST-9" do + Time.at 0, 1 + end + + time.zone.should == time.ceil.zone + end +end diff --git a/spec/ruby/core/time/comparison_spec.rb b/spec/ruby/core/time/comparison_spec.rb new file mode 100644 index 0000000000..866fbea72e --- /dev/null +++ b/spec/ruby/core/time/comparison_spec.rb @@ -0,0 +1,130 @@ +require_relative '../../spec_helper' + +describe "Time#<=>" do + it "returns 1 if the first argument is a point in time after the second argument" do + (Time.now <=> Time.at(0)).should == 1 + end + + it "returns 1 if the first argument is a point in time after the second argument (down to a millisecond)" do + (Time.at(0, 1000) <=> Time.at(0, 0)).should == 1 + (Time.at(1202778512, 1000) <=> Time.at(1202778512, 999)).should == 1 + end + + it "returns 1 if the first argument is a point in time after the second argument (down to a microsecond)" do + (Time.at(0, 100) <=> Time.at(0, 0)).should == 1 + (Time.at(1202778512, 100) <=> Time.at(1202778512, 99)).should == 1 + end + + it "returns 0 if time is the same as other" do + (Time.at(1202778513) <=> Time.at(1202778513)).should == 0 + (Time.at(100, 100) <=> Time.at(100, 100)).should == 0 + end + + it "returns -1 if the first argument is a point in time before the second argument" do + (Time.at(0) <=> Time.now).should == -1 + (Time.at(100, 100) <=> Time.at(101, 100)).should == -1 + end + + it "returns -1 if the first argument is a point in time before the second argument (down to a millisecond)" do + (Time.at(0, 0) <=> Time.at(0, 1000)).should == -1 + end + + it "returns -1 if the first argument is a point in time before the second argument (down to a microsecond)" do + (Time.at(0, 0) <=> Time.at(0, 100)).should == -1 + end + + it "returns 1 if the first argument is a fraction of a microsecond after the second argument" do + (Time.at(100, Rational(1,1000)) <=> Time.at(100, 0)).should == 1 + end + + it "returns 0 if time is the same as other, including fractional microseconds" do + (Time.at(100, Rational(1,1000)) <=> Time.at(100, Rational(1,1000))).should == 0 + end + + it "returns -1 if the first argument is a fraction of a microsecond before the second argument" do + (Time.at(100, 0) <=> Time.at(100, Rational(1,1000))).should == -1 + end + + it "returns nil when compared to an Integer because Time does not respond to #coerce" do + time = Time.at(1) + time.respond_to?(:coerce).should == false + time.should_receive(:respond_to?).exactly(2).and_return(false) + -> { + (time <=> 2).should == nil + (2 <=> time).should == nil + }.should_not complain + end + + context "given different timezones" do + it "returns 0 if time is the same as other" do + # three timezones, all at the same timestamp + time_utc = Time.new(2000, 1, 1, 0, 0, 0, 0) + time_cet = Time.new(2000, 1, 1, 1, 0, 0, '+01:00') + time_brt = Time.new(1999, 12, 31, 21, 0, 0, '-03:00') + (time_utc <=> time_cet).should == 0 + (time_utc <=> time_brt).should == 0 + (time_cet <=> time_brt).should == 0 + end + + it "returns -1 if the first argument is before the second argument" do + # time_brt is later, even though the date is earlier + time_utc = Time.new(2000, 1, 1, 0, 0, 0, 0) + time_brt = Time.new(1999, 12, 31, 23, 0, 0, '-03:00') + (time_utc <=> time_brt).should == -1 + end + + it "returns 1 if the first argument is before the second argument" do + # time_brt is later, even though the date is earlier + time_utc = Time.new(2000, 1, 1, 0, 0, 0, 0) + time_brt = Time.new(1999, 12, 31, 23, 0, 0, '-03:00') + (time_brt <=> time_utc).should == 1 + end + end + + describe "given a non-Time argument" do + it "returns nil if argument <=> self returns nil" do + t = Time.now + obj = mock('time') + obj.should_receive(:<=>).with(t).and_return(nil) + (t <=> obj).should == nil + end + + it "returns -1 if argument <=> self is greater than 0" do + t = Time.now + r = mock('r') + r.should_receive(:>).with(0).and_return(true) + obj = mock('time') + obj.should_receive(:<=>).with(t).and_return(r) + (t <=> obj).should == -1 + end + + it "returns 1 if argument <=> self is not greater than 0 and is less than 0" do + t = Time.now + r = mock('r') + r.should_receive(:>).with(0).and_return(false) + r.should_receive(:<).with(0).and_return(true) + obj = mock('time') + obj.should_receive(:<=>).with(t).and_return(r) + (t <=> obj).should == 1 + end + + it "returns 0 if argument <=> self is neither greater than 0 nor less than 0" do + t = Time.now + r = mock('r') + r.should_receive(:>).with(0).and_return(false) + r.should_receive(:<).with(0).and_return(false) + obj = mock('time') + obj.should_receive(:<=>).with(t).and_return(r) + (t <=> obj).should == 0 + end + + it "returns nil if argument also uses an inverse comparison for <=>" do + t = Time.now + r = mock('r') + def r.<=>(other); other <=> self; end + r.should_receive(:<=>).once + + (t <=> r).should be_nil + end + end +end diff --git a/spec/ruby/core/time/ctime_spec.rb b/spec/ruby/core/time/ctime_spec.rb new file mode 100644 index 0000000000..57e7cfd9ce --- /dev/null +++ b/spec/ruby/core/time/ctime_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/asctime' + +describe "Time#ctime" do + it_behaves_like :time_asctime, :ctime +end diff --git a/spec/ruby/core/time/day_spec.rb b/spec/ruby/core/time/day_spec.rb new file mode 100644 index 0000000000..895bcd7a86 --- /dev/null +++ b/spec/ruby/core/time/day_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/day' + +describe "Time#day" do + it_behaves_like :time_day, :day +end diff --git a/spec/ruby/core/time/deconstruct_keys_spec.rb b/spec/ruby/core/time/deconstruct_keys_spec.rb new file mode 100644 index 0000000000..b5cfdaa93f --- /dev/null +++ b/spec/ruby/core/time/deconstruct_keys_spec.rb @@ -0,0 +1,43 @@ +require_relative '../../spec_helper' + +describe "Time#deconstruct_keys" do + it "returns whole hash for nil as an argument" do + d = Time.utc(2022, 10, 5, 13, 30) + res = { year: 2022, month: 10, day: 5, yday: 278, wday: 3, hour: 13, + min: 30, sec: 0, subsec: 0, dst: false, zone: "UTC" } + d.deconstruct_keys(nil).should == res + end + + it "returns only specified keys" do + d = Time.utc(2022, 10, 5, 13, 39) + d.deconstruct_keys([:zone, :subsec]).should == { zone: "UTC", subsec: 0 } + end + + it "requires one argument" do + -> { + Time.new(2022, 10, 5, 13, 30).deconstruct_keys + }.should raise_error(ArgumentError) + end + + it "it raises error when argument is neither nil nor array" do + d = Time.new(2022, 10, 5, 13, 30) + + -> { d.deconstruct_keys(1) }.should raise_error(TypeError, "wrong argument type Integer (expected Array or nil)") + -> { d.deconstruct_keys("asd") }.should raise_error(TypeError, "wrong argument type String (expected Array or nil)") + -> { d.deconstruct_keys(:x) }.should raise_error(TypeError, "wrong argument type Symbol (expected Array or nil)") + -> { d.deconstruct_keys({}) }.should raise_error(TypeError, "wrong argument type Hash (expected Array or nil)") + end + + it "returns {} when passed []" do + Time.new(2022, 10, 5, 13, 30).deconstruct_keys([]).should == {} + end + + it "ignores non-Symbol keys" do + Time.new(2022, 10, 5, 13, 30).deconstruct_keys(['year', []]).should == {} + end + + it "ignores not existing Symbol keys and processes keys after the first non-existing one" do + d = Time.utc(2022, 10, 5, 13, 30) + d.deconstruct_keys([:year, :a, :month, :b, :day]).should == { year: 2022, month: 10, day: 5 } + end +end diff --git a/spec/ruby/core/time/dst_spec.rb b/spec/ruby/core/time/dst_spec.rb new file mode 100644 index 0000000000..436240aae5 --- /dev/null +++ b/spec/ruby/core/time/dst_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/isdst' + +describe "Time#dst?" do + it_behaves_like :time_isdst, :dst? +end diff --git a/spec/ruby/core/time/dup_spec.rb b/spec/ruby/core/time/dup_spec.rb new file mode 100644 index 0000000000..5d6621beaa --- /dev/null +++ b/spec/ruby/core/time/dup_spec.rb @@ -0,0 +1,46 @@ +require_relative '../../spec_helper' + +describe "Time#dup" do + it "returns a Time object that represents the same time" do + t = Time.at(100) + t.dup.tv_sec.should == t.tv_sec + end + + it "copies the gmt state flag" do + Time.now.gmtime.dup.should.gmt? + end + + it "returns an independent Time object" do + t = Time.now + t2 = t.dup + t.gmtime + + t2.should_not.gmt? + end + + it "returns a subclass instance" do + c = Class.new(Time) + t = c.now + + t.should be_an_instance_of(c) + t.dup.should be_an_instance_of(c) + end + + it "returns a clone of Time instance" do + c = Time.dup + t = c.now + + t.should be_an_instance_of(c) + t.should_not be_an_instance_of(Time) + + t.dup.should be_an_instance_of(c) + t.dup.should_not be_an_instance_of(Time) + end + + it "does not copy frozen status from the original" do + t = Time.now + t.freeze + t2 = t.dup + t2.frozen?.should be_false + end +end diff --git a/spec/ruby/core/time/eql_spec.rb b/spec/ruby/core/time/eql_spec.rb new file mode 100644 index 0000000000..2ffb4eec96 --- /dev/null +++ b/spec/ruby/core/time/eql_spec.rb @@ -0,0 +1,29 @@ +require_relative '../../spec_helper' + +describe "Time#eql?" do + it "returns true if self and other have the same whole number of seconds" do + Time.at(100).should eql(Time.at(100)) + end + + it "returns false if self and other have differing whole numbers of seconds" do + Time.at(100).should_not eql(Time.at(99)) + end + + it "returns true if self and other have the same number of microseconds" do + Time.at(100, 100).should eql(Time.at(100, 100)) + end + + it "returns false if self and other have differing numbers of microseconds" do + Time.at(100, 100).should_not eql(Time.at(100, 99)) + end + + it "returns false if self and other have differing fractional microseconds" do + Time.at(100, Rational(100,1000)).should_not eql(Time.at(100, Rational(99,1000))) + end + + it "returns false when given a non-time value" do + Time.at(100, 100).should_not eql("100") + Time.at(100, 100).should_not eql(100) + Time.at(100, 100).should_not eql(100.1) + end +end diff --git a/spec/ruby/core/time/fixtures/classes.rb b/spec/ruby/core/time/fixtures/classes.rb new file mode 100644 index 0000000000..21c4e1effb --- /dev/null +++ b/spec/ruby/core/time/fixtures/classes.rb @@ -0,0 +1,105 @@ +module TimeSpecs + + class SubTime < Time; end + + class MethodHolder + class << self + define_method(:now, &Time.method(:now)) + define_method(:new, &Time.method(:new)) + end + end + + class Timezone + def initialize(options) + @offset = options[:offset] + end + + def local_to_utc(t) + t - @offset + end + + def utc_to_local(t) + t + @offset + end + end + + class TimezoneMethodCallRecorder < Timezone + def initialize(options, &blk) + super(options) + @blk = blk + end + + def local_to_utc(t) + @blk.call(t) + super + end + + def utc_to_local(t) + @blk.call(t) + super + end + end + + class TimeLikeArgumentRecorder + def self.result + arguments = [] + + zone = TimeSpecs::TimezoneMethodCallRecorder.new(offset: 0) do |obj| + arguments << obj + end + + # ensure timezone's methods are called at least once + Time.new(2000, 1, 1, 12, 0, 0, zone) + + return arguments[0] + end + end + + Z = Struct.new(:offset, :abbr) + Zone = Struct.new(:std, :dst, :dst_range) + Zones = { + "Asia/Colombo" => Zone[Z[5*3600+30*60, "MMT"], nil, nil], + "PST" => Zone[Z[(-9*60*60), "PST"], nil, nil], + } + + class TimezoneWithName < Timezone + attr_reader :name + + def initialize(options) + @name = options[:name] + @std, @dst, @dst_range = *Zones[@name] + end + + def dst?(t) + @dst_range&.cover?(t.mon) + end + + def zone(t) + (dst?(t) ? @dst : @std) + end + + def utc_offset(t) + zone(t)&.offset || 0 + end + + def abbr(t) + zone(t)&.abbr + end + + def local_to_utc(t) + t - utc_offset(t) + end + + def utc_to_local(t) + t + utc_offset(t) + end + end + + class TimeWithFindTimezone < Time + def self.find_timezone(name) + TimezoneWithName.new(name: name.to_s) + end + end + + TimezoneWithAbbr = TimezoneWithName +end diff --git a/spec/ruby/core/time/floor_spec.rb b/spec/ruby/core/time/floor_spec.rb new file mode 100644 index 0000000000..b0003469c9 --- /dev/null +++ b/spec/ruby/core/time/floor_spec.rb @@ -0,0 +1,36 @@ +require_relative '../../spec_helper' + +describe "Time#floor" do + before do + @time = Time.utc(2010, 3, 30, 5, 43, "25.123456789".to_r) + end + + it "defaults to flooring to 0 places" do + @time.floor.should == Time.utc(2010, 3, 30, 5, 43, 25.to_r) + end + + it "floors to 0 decimal places with an explicit argument" do + @time.floor(0).should == Time.utc(2010, 3, 30, 5, 43, 25.to_r) + end + + it "floors to 7 decimal places with an explicit argument" do + @time.floor(7).should == Time.utc(2010, 3, 30, 5, 43, "25.1234567".to_r) + end + + it "returns an instance of Time, even if #floor is called on a subclass" do + subclass = Class.new(Time) + instance = subclass.at(0) + instance.class.should equal subclass + instance.floor.should be_an_instance_of(Time) + end + + it "copies own timezone to the returning value" do + @time.zone.should == @time.floor.zone + + time = with_timezone "JST-9" do + Time.at 0, 1 + end + + time.zone.should == time.floor.zone + end +end diff --git a/spec/ruby/core/time/friday_spec.rb b/spec/ruby/core/time/friday_spec.rb new file mode 100644 index 0000000000..8bee7f7558 --- /dev/null +++ b/spec/ruby/core/time/friday_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' + +describe "Time#friday?" do + it "returns true if time represents Friday" do + Time.local(2000, 1, 7).should.friday? + end + + it "returns false if time doesn't represent Friday" do + Time.local(2000, 1, 1).should_not.friday? + end +end diff --git a/spec/ruby/core/time/getgm_spec.rb b/spec/ruby/core/time/getgm_spec.rb new file mode 100644 index 0000000000..b5d45b1d9f --- /dev/null +++ b/spec/ruby/core/time/getgm_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/getgm' + +describe "Time#getgm" do + it_behaves_like :time_getgm, :getgm +end diff --git a/spec/ruby/core/time/getlocal_spec.rb b/spec/ruby/core/time/getlocal_spec.rb new file mode 100644 index 0000000000..398596f400 --- /dev/null +++ b/spec/ruby/core/time/getlocal_spec.rb @@ -0,0 +1,206 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Time#getlocal" do + it "returns a new time which is the local representation of time" do + # Testing with America/Regina here because it doesn't have DST. + with_timezone("CST", -6) do + t = Time.gm(2007, 1, 9, 12, 0, 0) + t.localtime.should == Time.local(2007, 1, 9, 6, 0, 0) + end + end + + it "returns a Time with UTC offset specified as an Integer number of seconds" do + t = Time.gm(2007, 1, 9, 12, 0, 0).getlocal(3630) + t.should == Time.new(2007, 1, 9, 13, 0, 30, 3630) + t.utc_offset.should == 3630 + t.zone.should be_nil + end + + platform_is_not :windows do + it "returns a new time with the correct utc_offset according to the set timezone" do + t = Time.new(2005, 2, 27, 22, 50, 0, -3600) + t.utc_offset.should == -3600 + + with_timezone("America/New_York") do + t.getlocal.utc_offset.should == -18000 + end + end + end + + describe "with an argument that responds to #to_int" do + it "coerces using #to_int" do + o = mock('integer') + o.should_receive(:to_int).and_return(3630) + t = Time.gm(2007, 1, 9, 12, 0, 0).getlocal(o) + t.should == Time.new(2007, 1, 9, 13, 0, 30, 3630) + t.utc_offset.should == 3630 + end + end + + it "returns a Time with a UTC offset of the specified number of Rational seconds" do + t = Time.gm(2007, 1, 9, 12, 0, 0).getlocal(Rational(7201, 2)) + t.should == Time.new(2007, 1, 9, 13, 0, Rational(1, 2), Rational(7201, 2)) + t.utc_offset.should eql(Rational(7201, 2)) + end + + describe "with an argument that responds to #to_r" do + it "coerces using #to_r" do + o = mock_numeric('rational') + o.should_receive(:to_r).and_return(Rational(7201, 2)) + t = Time.gm(2007, 1, 9, 12, 0, 0).getlocal(o) + t.should == Time.new(2007, 1, 9, 13, 0, Rational(1, 2), Rational(7201, 2)) + t.utc_offset.should eql(Rational(7201, 2)) + end + end + + it "returns a Time with a UTC offset specified as +HH:MM" do + t = Time.gm(2007, 1, 9, 12, 0, 0).getlocal("+01:00") + t.should == Time.new(2007, 1, 9, 13, 0, 0, 3600) + t.utc_offset.should == 3600 + end + + it "returns a Time with a UTC offset specified as +HH:MM:SS" do + t = Time.gm(2007, 1, 9, 12, 0, 0).getlocal("+01:00:01") + t.should == Time.new(2007, 1, 9, 13, 0, 1, 3601) + t.utc_offset.should == 3601 + end + + it "returns a Time with a UTC offset specified as -HH:MM" do + t = Time.gm(2007, 1, 9, 12, 0, 0).getlocal("-01:00") + t.should == Time.new(2007, 1, 9, 11, 0, 0, -3600) + t.utc_offset.should == -3600 + end + + it "returns a Time with a UTC offset specified as -HH:MM:SS" do + t = Time.gm(2007, 1, 9, 12, 0, 0).getlocal("-01:00:01") + t.should == Time.new(2007, 1, 9, 10, 59, 59, -3601) + t.utc_offset.should == -3601 + end + + describe "with an argument that responds to #to_str" do + it "coerces using #to_str" do + o = mock('string') + o.should_receive(:to_str).and_return("+01:00") + t = Time.gm(2007, 1, 9, 12, 0, 0).getlocal(o) + t.should == Time.new(2007, 1, 9, 13, 0, 0, 3600) + t.utc_offset.should == 3600 + end + end + + it "raises ArgumentError if the String argument is not of the form (+|-)HH:MM" do + t = Time.now + -> { t.getlocal("3600") }.should raise_error(ArgumentError) + end + + it "raises ArgumentError if the String argument is not in an ASCII-compatible encoding" do + t = Time.now + -> { t.getlocal("-01:00".encode("UTF-16LE")) }.should raise_error(ArgumentError) + end + + it "raises ArgumentError if the argument represents a value less than or equal to -86400 seconds" do + t = Time.new + t.getlocal(-86400 + 1).utc_offset.should == (-86400 + 1) + -> { t.getlocal(-86400) }.should raise_error(ArgumentError) + end + + it "raises ArgumentError if the argument represents a value greater than or equal to 86400 seconds" do + t = Time.new + t.getlocal(86400 - 1).utc_offset.should == (86400 - 1) + -> { t.getlocal(86400) }.should raise_error(ArgumentError) + end + + it "raises ArgumentError if String argument and hours greater than 23" do + -> { Time.now.getlocal("+24:00") }.should raise_error(ArgumentError, "utc_offset out of range") + -> { Time.now.getlocal("+2400") }.should raise_error(ArgumentError, "utc_offset out of range") + + -> { Time.now.getlocal("+99:00") }.should raise_error(ArgumentError, "utc_offset out of range") + -> { Time.now.getlocal("+9900") }.should raise_error(ArgumentError, "utc_offset out of range") + end + + it "raises ArgumentError if String argument and minutes greater than 59" do + -> { Time.now.getlocal("+00:60") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:60') + -> { Time.now.getlocal("+0060") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +0060') + + -> { Time.now.getlocal("+00:99") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:99') + -> { Time.now.getlocal("+0099") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +0099') + end + + ruby_bug '#20797', ''...'3.4' do + it "raises ArgumentError if String argument and seconds greater than 59" do + -> { Time.now.getlocal("+00:00:60") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:00:60') + -> { Time.now.getlocal("+000060") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +000060') + + -> { Time.now.getlocal("+00:00:99") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:00:99') + -> { Time.now.getlocal("+000099") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +000099') + end + end + + describe "with a timezone argument" do + it "returns a Time in the timezone" do + zone = TimeSpecs::Timezone.new(offset: (5*3600+30*60)) + time = Time.utc(2000, 1, 1, 12, 0, 0).getlocal(zone) + + time.zone.should == zone + time.utc_offset.should == 5*3600+30*60 + end + + it "accepts timezone argument that must have #local_to_utc and #utc_to_local methods" do + zone = Object.new + def zone.utc_to_local(time) + time + end + def zone.local_to_utc(time) + time + end + + -> { + Time.utc(2000, 1, 1, 12, 0, 0).getlocal(zone).should be_kind_of(Time) + }.should_not raise_error + end + + it "raises TypeError if timezone does not implement #utc_to_local method" do + zone = Object.new + def zone.local_to_utc(time) + time + end + + -> { + Time.utc(2000, 1, 1, 12, 0, 0).getlocal(zone) + }.should raise_error(TypeError, /can't convert \w+ into an exact number/) + end + + it "does not raise exception if timezone does not implement #local_to_utc method" do + zone = Object.new + def zone.utc_to_local(time) + time + end + + -> { + Time.utc(2000, 1, 1, 12, 0, 0).getlocal(zone).should be_kind_of(Time) + }.should_not raise_error + end + + context "subject's class implements .find_timezone method" do + it "calls .find_timezone to build a time object if passed zone name as a timezone argument" do + time = TimeSpecs::TimeWithFindTimezone.utc(2000, 1, 1, 12, 0, 0).getlocal("Asia/Colombo") + time.zone.should be_kind_of TimeSpecs::TimezoneWithName + time.zone.name.should == "Asia/Colombo" + + time = TimeSpecs::TimeWithFindTimezone.utc(2000, 1, 1, 12, 0, 0).getlocal("some invalid zone name") + time.zone.should be_kind_of TimeSpecs::TimezoneWithName + time.zone.name.should == "some invalid zone name" + end + + it "does not call .find_timezone if passed any not string/numeric/timezone timezone argument" do + [Object.new, [], {}, :"some zone"].each do |zone| + time = TimeSpecs::TimeWithFindTimezone.utc(2000, 1, 1, 12, 0, 0) + + -> { + time.getlocal(zone) + }.should raise_error(TypeError, /can't convert \w+ into an exact number/) + end + end + end + end +end diff --git a/spec/ruby/core/time/getutc_spec.rb b/spec/ruby/core/time/getutc_spec.rb new file mode 100644 index 0000000000..0cd9c17b00 --- /dev/null +++ b/spec/ruby/core/time/getutc_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/getgm' + +describe "Time#getutc" do + it_behaves_like :time_getgm, :getutc +end diff --git a/spec/ruby/core/time/gm_spec.rb b/spec/ruby/core/time/gm_spec.rb new file mode 100644 index 0000000000..26dffbcedc --- /dev/null +++ b/spec/ruby/core/time/gm_spec.rb @@ -0,0 +1,10 @@ +require_relative '../../spec_helper' +require_relative 'shared/gm' +require_relative 'shared/time_params' + +describe "Time.gm" do + it_behaves_like :time_gm, :gm + it_behaves_like :time_params, :gm + it_behaves_like :time_params_10_arg, :gm + it_behaves_like :time_params_microseconds, :gm +end diff --git a/spec/ruby/core/time/gmt_offset_spec.rb b/spec/ruby/core/time/gmt_offset_spec.rb new file mode 100644 index 0000000000..df417e6e4e --- /dev/null +++ b/spec/ruby/core/time/gmt_offset_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/gmt_offset' + +describe "Time#gmt_offset" do + it_behaves_like :time_gmt_offset, :gmt_offset +end diff --git a/spec/ruby/core/time/gmt_spec.rb b/spec/ruby/core/time/gmt_spec.rb new file mode 100644 index 0000000000..840f59e0e8 --- /dev/null +++ b/spec/ruby/core/time/gmt_spec.rb @@ -0,0 +1,8 @@ +require_relative '../../spec_helper' + +describe "Time#gmt?" do + it "returns true if time represents a time in UTC (GMT)" do + Time.now.should_not.gmt? + Time.now.gmtime.should.gmt? + end +end diff --git a/spec/ruby/core/time/gmtime_spec.rb b/spec/ruby/core/time/gmtime_spec.rb new file mode 100644 index 0000000000..d965cd541d --- /dev/null +++ b/spec/ruby/core/time/gmtime_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/gmtime' + +describe "Time#gmtime" do + it_behaves_like :time_gmtime, :gmtime +end diff --git a/spec/ruby/core/time/gmtoff_spec.rb b/spec/ruby/core/time/gmtoff_spec.rb new file mode 100644 index 0000000000..fa28520e9e --- /dev/null +++ b/spec/ruby/core/time/gmtoff_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/gmt_offset' + +describe "Time#gmtoff" do + it_behaves_like :time_gmt_offset, :gmtoff +end diff --git a/spec/ruby/core/time/hash_spec.rb b/spec/ruby/core/time/hash_spec.rb new file mode 100644 index 0000000000..4f4d11a2cd --- /dev/null +++ b/spec/ruby/core/time/hash_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' + +describe "Time#hash" do + it "returns an Integer" do + Time.at(100).hash.should be_an_instance_of(Integer) + end + + it "is stable" do + Time.at(1234).hash.should == Time.at(1234).hash + end +end diff --git a/spec/ruby/core/time/hour_spec.rb b/spec/ruby/core/time/hour_spec.rb new file mode 100644 index 0000000000..ca69c25adb --- /dev/null +++ b/spec/ruby/core/time/hour_spec.rb @@ -0,0 +1,17 @@ +require_relative '../../spec_helper' + +describe "Time#hour" do + it "returns the hour of the day (0..23) for a local Time" do + with_timezone("CET", 1) do + Time.local(1970, 1, 1, 1).hour.should == 1 + end + end + + it "returns the hour of the day for a UTC Time" do + Time.utc(1970, 1, 1, 0).hour.should == 0 + end + + it "returns the hour of the day for a Time with a fixed offset" do + Time.new(2012, 1, 1, 0, 0, 0, -3600).hour.should == 0 + end +end diff --git a/spec/ruby/core/time/inspect_spec.rb b/spec/ruby/core/time/inspect_spec.rb new file mode 100644 index 0000000000..c3a4519a24 --- /dev/null +++ b/spec/ruby/core/time/inspect_spec.rb @@ -0,0 +1,33 @@ +require_relative '../../spec_helper' +require_relative 'shared/inspect' + +describe "Time#inspect" do + it_behaves_like :inspect, :inspect + + it "preserves microseconds" do + t = Time.utc(2007, 11, 1, 15, 25, 0, 123456) + t.inspect.should == "2007-11-01 15:25:00.123456 UTC" + end + + it "omits trailing zeros from microseconds" do + t = Time.utc(2007, 11, 1, 15, 25, 0, 100000) + t.inspect.should == "2007-11-01 15:25:00.1 UTC" + end + + it "uses the correct time zone without microseconds" do + t = Time.utc(2000, 1, 1) + t = t.localtime(9*3600) + t.inspect.should == "2000-01-01 09:00:00 +0900" + end + + it "uses the correct time zone with microseconds" do + t = Time.utc(2000, 1, 1, 0, 0, 0, 123456) + t = t.localtime(9*3600) + t.inspect.should == "2000-01-01 09:00:00.123456 +0900" + end + + it "preserves nanoseconds" do + t = Time.utc(2007, 11, 1, 15, 25, 0, 123456.789r) + t.inspect.should == "2007-11-01 15:25:00.123456789 UTC" + end +end diff --git a/spec/ruby/core/time/isdst_spec.rb b/spec/ruby/core/time/isdst_spec.rb new file mode 100644 index 0000000000..173230ca07 --- /dev/null +++ b/spec/ruby/core/time/isdst_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/isdst' + +describe "Time#isdst" do + it_behaves_like :time_isdst, :isdst +end diff --git a/spec/ruby/core/time/iso8601_spec.rb b/spec/ruby/core/time/iso8601_spec.rb new file mode 100644 index 0000000000..ad60c3bb32 --- /dev/null +++ b/spec/ruby/core/time/iso8601_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/xmlschema' + +describe "Time#iso8601" do + it_behaves_like :time_xmlschema, :iso8601 +end diff --git a/spec/ruby/core/time/local_spec.rb b/spec/ruby/core/time/local_spec.rb new file mode 100644 index 0000000000..581ed171d5 --- /dev/null +++ b/spec/ruby/core/time/local_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' +require_relative 'shared/local' +require_relative 'shared/time_params' + +describe "Time.local" do + it_behaves_like :time_local, :local + it_behaves_like :time_local_10_arg, :local + it_behaves_like :time_params, :local + it_behaves_like :time_params_10_arg, :local + it_behaves_like :time_params_microseconds, :local +end diff --git a/spec/ruby/core/time/localtime_spec.rb b/spec/ruby/core/time/localtime_spec.rb new file mode 100644 index 0000000000..71c0dfebde --- /dev/null +++ b/spec/ruby/core/time/localtime_spec.rb @@ -0,0 +1,203 @@ +require_relative '../../spec_helper' + +describe "Time#localtime" do + it "converts self to local time, modifying the receiver" do + # Testing with America/Regina here because it doesn't have DST. + with_timezone("CST", -6) do + t = Time.gm(2007, 1, 9, 12, 0, 0) + t.localtime + t.should == Time.local(2007, 1, 9, 6, 0, 0) + end + end + + it "returns self" do + t = Time.gm(2007, 1, 9, 12, 0, 0) + t.localtime.should equal(t) + end + + it "converts time to the UTC offset specified as an Integer number of seconds" do + t = Time.gm(2007, 1, 9, 12, 0, 0) + t.localtime(3630) + t.should == Time.new(2007, 1, 9, 13, 0, 30, 3630) + t.utc_offset.should == 3630 + end + + describe "on a frozen time" do + it "does not raise an error if already in the right time zone" do + time = Time.now + time.freeze + time.localtime.should equal(time) + end + + it "raises a FrozenError if the time has a different time zone" do + time = Time.gm(2007, 1, 9, 12, 0, 0) + time.freeze + -> { time.localtime }.should raise_error(FrozenError) + end + end + + describe "with an argument that responds to #to_int" do + it "coerces using #to_int" do + o = mock('integer') + o.should_receive(:to_int).and_return(3630) + t = Time.gm(2007, 1, 9, 12, 0, 0) + t.localtime(o) + t.should == Time.new(2007, 1, 9, 13, 0, 30, 3630) + t.utc_offset.should == 3630 + end + end + + it "returns a Time with a UTC offset of the specified number of Rational seconds" do + t = Time.gm(2007, 1, 9, 12, 0, 0) + t.localtime(Rational(7201, 2)) + t.should == Time.new(2007, 1, 9, 13, 0, Rational(1, 2), Rational(7201, 2)) + t.utc_offset.should eql(Rational(7201, 2)) + end + + describe "with an argument that responds to #to_r" do + it "coerces using #to_r" do + o = mock_numeric('rational') + o.should_receive(:to_r).and_return(Rational(7201, 2)) + t = Time.gm(2007, 1, 9, 12, 0, 0) + t.localtime(o) + t.should == Time.new(2007, 1, 9, 13, 0, Rational(1, 2), Rational(7201, 2)) + t.utc_offset.should eql(Rational(7201, 2)) + end + end + + it "returns a Time with a UTC offset specified as +HH:MM" do + t = Time.gm(2007, 1, 9, 12, 0, 0) + t.localtime("+01:00") + t.should == Time.new(2007, 1, 9, 13, 0, 0, 3600) + t.utc_offset.should == 3600 + end + + it "returns a Time with a UTC offset specified as +HH:MM:SS" do + t = Time.gm(2007, 1, 9, 12, 0, 0) + t.localtime("+01:00:01") + t.should == Time.new(2007, 1, 9, 13, 0, 1, 3601) + t.utc_offset.should == 3601 + end + + it "returns a Time with a UTC offset specified as -HH:MM" do + t = Time.gm(2007, 1, 9, 12, 0, 0) + t.localtime("-01:00") + t.should == Time.new(2007, 1, 9, 11, 0, 0, -3600) + t.utc_offset.should == -3600 + end + + it "returns a Time with a UTC offset specified as -HH:MM:SS" do + t = Time.gm(2007, 1, 9, 12, 0, 0) + t.localtime("-01:00:01") + t.should == Time.new(2007, 1, 9, 10, 59, 59, -3601) + t.utc_offset.should == -3601 + end + + it "returns a Time with a UTC offset specified as UTC" do + t = Time.new(2007, 1, 9, 12, 0, 0, 3600) + t.localtime("UTC") + t.utc_offset.should == 0 + end + + it "returns a Time with a UTC offset specified as A-Z military zone" do + t = Time.new(2007, 1, 9, 12, 0, 0, 3600) + t.localtime("B") + t.utc_offset.should == 3600 * 2 + end + + it "raises ArgumentError if String argument and hours greater than 23" do + -> { Time.now.localtime("+24:00") }.should raise_error(ArgumentError, "utc_offset out of range") + -> { Time.now.localtime("+2400") }.should raise_error(ArgumentError, "utc_offset out of range") + + -> { Time.now.localtime("+99:00") }.should raise_error(ArgumentError, "utc_offset out of range") + -> { Time.now.localtime("+9900") }.should raise_error(ArgumentError, "utc_offset out of range") + end + + it "raises ArgumentError if String argument and minutes greater than 59" do + -> { Time.now.localtime("+00:60") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:60') + -> { Time.now.localtime("+0060") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +0060') + + -> { Time.now.localtime("+00:99") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:99') + -> { Time.now.localtime("+0099") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +0099') + end + + ruby_bug '#20797', ''...'3.4' do + it "raises ArgumentError if String argument and seconds greater than 59" do + -> { Time.now.localtime("+00:00:60") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:00:60') + -> { Time.now.localtime("+000060") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +000060') + + -> { Time.now.localtime("+00:00:99") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:00:99') + -> { Time.now.localtime("+000099") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +000099') + end + end + + platform_is_not :windows do + it "changes the timezone according to the set one" do + t = Time.new(2005, 2, 27, 22, 50, 0, -3600) + t.utc_offset.should == -3600 + + with_timezone("America/New_York") do + t.localtime + end + + t.utc_offset.should == -18000 + end + + it "does nothing if already in a local time zone" do + time = with_timezone("America/New_York") do + Time.new(2005, 2, 27, 22, 50, 0) + end + zone = time.zone + + with_timezone("Europe/Amsterdam") do + time.localtime + end + + time.zone.should == zone + end + end + + describe "with an argument that responds to #to_str" do + it "coerces using #to_str" do + o = mock('string') + o.should_receive(:to_str).and_return("+01:00") + t = Time.gm(2007, 1, 9, 12, 0, 0) + t.localtime(o) + t.should == Time.new(2007, 1, 9, 13, 0, 0, 3600) + t.utc_offset.should == 3600 + end + end + + describe "with an argument that responds to #utc_to_local" do + it "coerces using #utc_to_local" do + o = mock('string') + o.should_receive(:utc_to_local).and_return(Time.new(2007, 1, 9, 13, 0, 0, 3600)) + t = Time.gm(2007, 1, 9, 12, 0, 0) + t.localtime(o) + t.should == Time.new(2007, 1, 9, 13, 0, 0, 3600) + t.utc_offset.should == 3600 + end + end + + it "raises ArgumentError if the String argument is not of the form (+|-)HH:MM" do + t = Time.now + -> { t.localtime("3600") }.should raise_error(ArgumentError) + end + + it "raises ArgumentError if the String argument is not in an ASCII-compatible encoding" do + t = Time.now + -> { t.localtime("-01:00".encode("UTF-16LE")) }.should raise_error(ArgumentError) + end + + it "raises ArgumentError if the argument represents a value less than or equal to -86400 seconds" do + t = Time.new + t.localtime(-86400 + 1).utc_offset.should == (-86400 + 1) + -> { t.localtime(-86400) }.should raise_error(ArgumentError) + end + + it "raises ArgumentError if the argument represents a value greater than or equal to 86400 seconds" do + t = Time.new + t.localtime(86400 - 1).utc_offset.should == (86400 - 1) + -> { t.localtime(86400) }.should raise_error(ArgumentError) + end +end diff --git a/spec/ruby/core/time/mday_spec.rb b/spec/ruby/core/time/mday_spec.rb new file mode 100644 index 0000000000..3c21939890 --- /dev/null +++ b/spec/ruby/core/time/mday_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/day' + +describe "Time#mday" do + it_behaves_like :time_day, :mday +end diff --git a/spec/ruby/core/time/min_spec.rb b/spec/ruby/core/time/min_spec.rb new file mode 100644 index 0000000000..7d087d4046 --- /dev/null +++ b/spec/ruby/core/time/min_spec.rb @@ -0,0 +1,17 @@ +require_relative '../../spec_helper' + +describe "Time#min" do + it "returns the minute of the hour (0..59) for a local Time" do + with_timezone("CET", 1) do + Time.local(1970, 1, 1, 0, 0).min.should == 0 + end + end + + it "returns the minute of the hour for a UTC Time" do + Time.utc(1970, 1, 1, 0, 0).min.should == 0 + end + + it "returns the minute of the hour for a Time with a fixed offset" do + Time.new(2012, 1, 1, 0, 0, 0, -3600).min.should == 0 + end +end diff --git a/spec/ruby/core/time/minus_spec.rb b/spec/ruby/core/time/minus_spec.rb new file mode 100644 index 0000000000..9182d99652 --- /dev/null +++ b/spec/ruby/core/time/minus_spec.rb @@ -0,0 +1,121 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Time#-" do + it "decrements the time by the specified amount" do + (Time.at(100) - 100).should == Time.at(0) + (Time.at(100) - Time.at(99)).should == 1.0 + end + + it "understands negative subtractions" do + t = Time.at(100) - -1.3 + t.usec.should == 300000 + t.to_i.should == 101 + end + + #see [ruby-dev:38446] + it "accepts arguments that can be coerced into Rational" do + (obj = mock_numeric('10')).should_receive(:to_r).and_return(Rational(10)) + (Time.at(100) - obj).should == Time.at(90) + end + + it "raises a TypeError if given argument is a coercible String" do + -> { Time.now - "1" }.should raise_error(TypeError) + -> { Time.now - "0.1" }.should raise_error(TypeError) + -> { Time.now - "1/3" }.should raise_error(TypeError) + end + + it "raises TypeError on argument that can't be coerced" do + -> { Time.now - Object.new }.should raise_error(TypeError) + -> { Time.now - "stuff" }.should raise_error(TypeError) + end + + it "raises TypeError on nil argument" do + -> { Time.now - nil }.should raise_error(TypeError) + end + + it "tracks microseconds" do + time = Time.at(0.777777) + time -= 0.654321 + time.usec.should == 123456 + time -= 1 + time.usec.should == 123456 + end + + it "tracks microseconds from a Rational" do + time = Time.at(Rational(777_777, 1_000_000)) + time -= Rational(654_321, 1_000_000) + time.usec.should == 123_456 + time -= Rational(123_456, 1_000_000) + time.usec.should == 0 + end + + it "tracks nanoseconds" do + time = Time.at(Rational(999_999_999, 1_000_000_000)) + time -= Rational(876_543_210, 1_000_000_000) + time.nsec.should == 123_456_789 + time -= Rational(123_456_789, 1_000_000_000) + time.nsec.should == 0 + end + + it "maintains precision" do + time = Time.at(10) - Rational(1_000_000_000_000_001, 1_000_000_000_000_000) + time.should_not == Time.at(9) + end + + it "maintains microseconds precision" do + time = Time.at(10) - Rational(1, 1_000_000) + time.usec.should == 999_999 + end + + it "maintains nanoseconds precision" do + time = Time.at(10) - Rational(1, 1_000_000_000) + time.nsec.should == 999_999_999 + end + + it "maintains subseconds precision" do + time = Time.at(0) - Rational(1_000_000_000_000_001, 1_000_000_000_000_000) + time.subsec.should == Rational(999_999_999_999_999, 1_000_000_000_000_000) + end + + it "returns a UTC time if self is UTC" do + (Time.utc(2012) - 10).should.utc? + end + + it "returns a non-UTC time if self is non-UTC" do + (Time.local(2012) - 10).should_not.utc? + end + + it "returns a time with the same fixed offset as self" do + (Time.new(2012, 1, 1, 0, 0, 0, 3600) - 10).utc_offset.should == 3600 + end + + it "preserves time zone" do + time_with_zone = Time.now.utc + time_with_zone.zone.should == (time_with_zone - 1).zone + + time_with_zone = Time.now + time_with_zone.zone.should == (time_with_zone - 1).zone + end + + context "zone is a timezone object" do + it "preserves time zone" do + zone = TimeSpecs::Timezone.new(offset: (5*3600+30*60)) + time = Time.new(2012, 1, 1, 12, 0, 0, zone) - 1 + + time.zone.should == zone + end + end + + it "does not return a subclass instance" do + c = Class.new(Time) + x = c.now - 1 + x.should be_an_instance_of(Time) + end + + it "returns a time with nanoseconds precision between two time objects" do + time1 = Time.utc(2000, 1, 2, 23, 59, 59, Rational(999999999, 1000)) + time2 = Time.utc(2000, 1, 2, 0, 0, 0, Rational(1, 1000)) + (time1 - time2).should == 86_399.999999998 + end +end diff --git a/spec/ruby/core/time/mktime_spec.rb b/spec/ruby/core/time/mktime_spec.rb new file mode 100644 index 0000000000..78a6a6e772 --- /dev/null +++ b/spec/ruby/core/time/mktime_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' +require_relative 'shared/local' +require_relative 'shared/time_params' + +describe "Time.mktime" do + it_behaves_like :time_local, :mktime + it_behaves_like :time_local_10_arg, :mktime + it_behaves_like :time_params, :mktime + it_behaves_like :time_params_10_arg, :mktime + it_behaves_like :time_params_microseconds, :mktime +end diff --git a/spec/ruby/core/time/mon_spec.rb b/spec/ruby/core/time/mon_spec.rb new file mode 100644 index 0000000000..f41b39648b --- /dev/null +++ b/spec/ruby/core/time/mon_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/month' + +describe "Time#mon" do + it_behaves_like :time_month, :mon +end diff --git a/spec/ruby/core/time/monday_spec.rb b/spec/ruby/core/time/monday_spec.rb new file mode 100644 index 0000000000..47ecaeb1db --- /dev/null +++ b/spec/ruby/core/time/monday_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' + +describe "Time#monday?" do + it "returns true if time represents Monday" do + Time.local(2000, 1, 3).should.monday? + end + + it "returns false if time doesn't represent Monday" do + Time.local(2000, 1, 1).should_not.monday? + end +end diff --git a/spec/ruby/core/time/month_spec.rb b/spec/ruby/core/time/month_spec.rb new file mode 100644 index 0000000000..81e20384ab --- /dev/null +++ b/spec/ruby/core/time/month_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/month' + +describe "Time#month" do + it_behaves_like :time_month, :month +end diff --git a/spec/ruby/core/time/new_spec.rb b/spec/ruby/core/time/new_spec.rb new file mode 100644 index 0000000000..dc3ccbdc00 --- /dev/null +++ b/spec/ruby/core/time/new_spec.rb @@ -0,0 +1,753 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/now' +require_relative 'shared/local' +require_relative 'shared/time_params' + +describe "Time.new" do + it_behaves_like :time_now, :new +end + +describe "Time.new" do + it_behaves_like :time_local, :new + it_behaves_like :time_params, :new +end + +describe "Time.new with a utc_offset argument" do + it "returns a non-UTC time" do + Time.new(2000, 1, 1, 0, 0, 0, 0).should_not.utc? + end + + it "returns a Time with a UTC offset of the specified number of Integer seconds" do + Time.new(2000, 1, 1, 0, 0, 0, 123).utc_offset.should == 123 + end + + describe "with an argument that responds to #to_int" do + it "coerces using #to_int" do + o = mock('integer') + o.should_receive(:to_int).and_return(123) + Time.new(2000, 1, 1, 0, 0, 0, o).utc_offset.should == 123 + end + end + + it "returns a Time with a UTC offset of the specified number of Rational seconds" do + Time.new(2000, 1, 1, 0, 0, 0, Rational(5, 2)).utc_offset.should eql(Rational(5, 2)) + end + + describe "with an argument that responds to #to_r" do + it "coerces using #to_r" do + o = mock_numeric('rational') + o.should_receive(:to_r).and_return(Rational(5, 2)) + Time.new(2000, 1, 1, 0, 0, 0, o).utc_offset.should eql(Rational(5, 2)) + end + end + + it "returns a Time with a UTC offset specified as +HH:MM" do + Time.new(2000, 1, 1, 0, 0, 0, "+05:30").utc_offset.should == 19800 + end + + it "returns a Time with a UTC offset specified as -HH:MM" do + Time.new(2000, 1, 1, 0, 0, 0, "-04:10").utc_offset.should == -15000 + end + + it "returns a Time with a UTC offset specified as +HH:MM:SS" do + Time.new(2000, 1, 1, 0, 0, 0, "+05:30:37").utc_offset.should == 19837 + end + + it "returns a Time with a UTC offset specified as -HH:MM" do + Time.new(2000, 1, 1, 0, 0, 0, "-04:10:43").utc_offset.should == -15043 + end + + it "returns a Time with a UTC offset specified as +HH" do + Time.new(2000, 1, 1, 0, 0, 0, "+05").utc_offset.should == 3600 * 5 + end + + it "returns a Time with a UTC offset specified as -HH" do + Time.new(2000, 1, 1, 0, 0, 0, "-05").utc_offset.should == -3600 * 5 + end + + it "returns a Time with a UTC offset specified as +HHMM" do + Time.new(2000, 1, 1, 0, 0, 0, "+0530").utc_offset.should == 19800 + end + + it "returns a Time with a UTC offset specified as -HHMM" do + Time.new(2000, 1, 1, 0, 0, 0, "-0530").utc_offset.should == -19800 + end + + it "returns a Time with a UTC offset specified as +HHMMSS" do + Time.new(2000, 1, 1, 0, 0, 0, "+053037").utc_offset.should == 19837 + end + + it "returns a Time with a UTC offset specified as -HHMMSS" do + Time.new(2000, 1, 1, 0, 0, 0, "-053037").utc_offset.should == -19837 + end + + describe "with an argument that responds to #to_str" do + it "coerces using #to_str" do + o = mock('string') + o.should_receive(:to_str).and_return("+05:30") + Time.new(2000, 1, 1, 0, 0, 0, o).utc_offset.should == 19800 + end + end + + it "returns a Time with UTC offset specified as UTC" do + Time.new(2000, 1, 1, 0, 0, 0, "UTC").utc_offset.should == 0 + end + + it "returns a Time with UTC offset specified as a single letter military timezone" do + [ + ["A", 3600], + ["B", 3600 * 2], + ["C", 3600 * 3], + ["D", 3600 * 4], + ["E", 3600 * 5], + ["F", 3600 * 6], + ["G", 3600 * 7], + ["H", 3600 * 8], + ["I", 3600 * 9], + # J is not supported + ["K", 3600 * 10], + ["L", 3600 * 11], + ["M", 3600 * 12], + ["N", 3600 * -1], + ["O", 3600 * -2], + ["P", 3600 * -3], + ["Q", 3600 * -4], + ["R", 3600 * -5], + ["S", 3600 * -6], + ["T", 3600 * -7], + ["U", 3600 * -8], + ["V", 3600 * -9], + ["W", 3600 * -10], + ["X", 3600 * -11], + ["Y", 3600 * -12], + ["Z", 0] + ].each do |letter, offset| + Time.new(2000, 1, 1, 0, 0, 0, letter).utc_offset.should == offset + end + end + + it "raises ArgumentError if the string argument is J" do + message = '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: J' + -> { Time.new(2000, 1, 1, 0, 0, 0, "J") }.should raise_error(ArgumentError, message) + end + + it "returns a local Time if the argument is nil" do + with_timezone("PST", -8) do + t = Time.new(2000, 1, 1, 0, 0, 0, nil) + t.utc_offset.should == -28800 + t.zone.should == "PST" + end + end + + # [Bug #8679], r47676 + it "disallows a value for minutes greater than 59" do + -> { + Time.new(2000, 1, 1, 0, 0, 0, "+01:60") + }.should raise_error(ArgumentError) + -> { + Time.new(2000, 1, 1, 0, 0, 0, "+01:99") + }.should raise_error(ArgumentError) + end + + it "raises ArgumentError if the String argument is not of the form (+|-)HH:MM" do + -> { Time.new(2000, 1, 1, 0, 0, 0, "3600") }.should raise_error(ArgumentError) + end + + it "raises ArgumentError if the hour value is greater than 23" do + -> { Time.new(2000, 1, 1, 0, 0, 0, "+24:00") }.should raise_error(ArgumentError) + end + + it "raises ArgumentError if the String argument is not in an ASCII-compatible encoding" do + # Don't check exception message - it was changed in previous CRuby versions: + # - "string contains null byte" + # - '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset' + -> { + Time.new(2000, 1, 1, 0, 0, 0, "-04:10".encode("UTF-16LE")) + }.should raise_error(ArgumentError) + end + + it "raises ArgumentError if the argument represents a value less than or equal to -86400 seconds" do + Time.new(2000, 1, 1, 0, 0, 0, -86400 + 1).utc_offset.should == (-86400 + 1) + -> { Time.new(2000, 1, 1, 0, 0, 0, -86400) }.should raise_error(ArgumentError) + end + + it "raises ArgumentError if the argument represents a value greater than or equal to 86400 seconds" do + Time.new(2000, 1, 1, 0, 0, 0, 86400 - 1).utc_offset.should == (86400 - 1) + -> { Time.new(2000, 1, 1, 0, 0, 0, 86400) }.should raise_error(ArgumentError) + end + + it "raises ArgumentError if the utc_offset argument is greater than or equal to 10e9" do + -> { Time.new(2000, 1, 1, 0, 0, 0, 1000000000) }.should raise_error(ArgumentError) + end +end + +# The method #local_to_utc is tested only here because Time.new is the only method that calls #local_to_utc. +describe "Time.new with a timezone argument" do + it "returns a Time in the timezone" do + zone = TimeSpecs::Timezone.new(offset: (5*3600+30*60)) + time = Time.new(2000, 1, 1, 12, 0, 0, zone) + + time.zone.should == zone + time.utc_offset.should == 5*3600+30*60 + time.wday.should == 6 + time.yday.should == 1 + end + + it "accepts timezone argument that must have #local_to_utc and #utc_to_local methods" do + zone = Object.new + def zone.utc_to_local(time) + time + end + def zone.local_to_utc(time) + time + end + + Time.new(2000, 1, 1, 12, 0, 0, zone).should be_kind_of(Time) + end + + it "raises TypeError if timezone does not implement #local_to_utc method" do + zone = Object.new + def zone.utc_to_local(time) + time + end + + -> { + Time.new(2000, 1, 1, 12, 0, 0, zone) + }.should raise_error(TypeError, /can't convert Object into an exact number/) + end + + it "does not raise exception if timezone does not implement #utc_to_local method" do + zone = Object.new + def zone.local_to_utc(time) + time + end + + Time.new(2000, 1, 1, 12, 0, 0, zone).should be_kind_of(Time) + end + + # The result also should be a Time or Time-like object (not necessary to be the same class) + # or respond to #to_int method. The zone of the result is just ignored. + describe "returned value by #utc_to_local and #local_to_utc methods" do + it "could be Time instance" do + zone = Object.new + def zone.local_to_utc(t) + time = Time.utc(t.year, t.mon, t.day, t.hour, t.min, t.sec) + time - 60 * 60 # - 1 hour + end + + Time.new(2000, 1, 1, 12, 0, 0, zone).should be_kind_of(Time) + Time.new(2000, 1, 1, 12, 0, 0, zone).utc_offset.should == 60*60 + end + + it "could be Time subclass instance" do + zone = Object.new + def zone.local_to_utc(t) + time = Time.utc(t.year, t.mon, t.day, t.hour, t.min, t.sec) + time -= 60 * 60 # - 1 hour + Class.new(Time).utc(time.year, time.mon, time.day, time.hour, t.min, t.sec) + end + + Time.new(2000, 1, 1, 12, 0, 0, zone).should be_kind_of(Time) + Time.new(2000, 1, 1, 12, 0, 0, zone).utc_offset.should == 60*60 + end + + it "could be any object with #to_i method" do + zone = Object.new + def zone.local_to_utc(time) + obj = Object.new + obj.singleton_class.define_method(:to_i) { time.to_i - 60*60 } + obj + end + + Time.new(2000, 1, 1, 12, 0, 0, zone).should be_kind_of(Time) + Time.new(2000, 1, 1, 12, 0, 0, zone).utc_offset.should == 60*60 + end + + it "could have any #zone and #utc_offset because they are ignored if it isn't an instance of Time" do + zone = Object.new + def zone.local_to_utc(time) + Struct.new(:to_i, :zone, :utc_offset).new(time.to_i, 'America/New_York', -5*60*60) + end + Time.new(2000, 1, 1, 12, 0, 0, zone).utc_offset.should == 0 + + zone = Object.new + def zone.local_to_utc(time) + Struct.new(:to_i, :zone, :utc_offset).new(time.to_i, 'Asia/Tokyo', 9*60*60) + end + Time.new(2000, 1, 1, 12, 0, 0, zone).utc_offset.should == 0 + end + + it "cannot have arbitrary #utc_offset if it is an instance of Time" do + zone = Object.new + def zone.local_to_utc(t) + Time.new(t.year, t.mon, t.mday, t.hour, t.min, t.sec, 9*60*60) + end + Time.new(2000, 1, 1, 12, 0, 0, zone).utc_offset.should == 9*60*60 + end + + it "raises ArgumentError if difference between argument and result is too large" do + zone = Object.new + def zone.local_to_utc(t) + Time.utc(t.year, t.mon, t.day + 1, t.hour, t.min, t.sec) + end + + -> { + Time.new(2000, 1, 1, 12, 0, 0, zone) + }.should raise_error(ArgumentError, "utc_offset out of range") + end + end + + # https://github.com/ruby/ruby/blob/v2_6_0/time.c#L5330 + # + # Time-like argument to these methods is similar to a Time object in UTC without sub-second; + # it has attribute readers for the parts, e.g. year, month, and so on, and epoch time readers, to_i + # + # The sub-second attributes are fixed as 0, and utc_offset, zone, isdst, and their aliases are same as a Time object in UTC + describe "Time-like argument of #utc_to_local and #local_to_utc methods" do + before do + @obj = TimeSpecs::TimeLikeArgumentRecorder.result + @obj.should_not == nil + end + + it "implements subset of Time methods" do + # List only methods that are explicitly documented. + [ + :year, :mon, :mday, :hour, :min, :sec, :to_i, :isdst + ].each do |name| + @obj.respond_to?(name).should == true + end + end + + it "has attribute values the same as a Time object in UTC" do + @obj.usec.should == 0 + @obj.nsec.should == 0 + @obj.subsec.should == 0 + @obj.tv_usec.should == 0 + @obj.tv_nsec.should == 0 + + @obj.utc_offset.should == 0 + @obj.zone.should == "UTC" + @obj.isdst.should == Time.new.utc.isdst + end + end + + context "#name method" do + it "uses the optional #name method for marshaling" do + zone = TimeSpecs::TimezoneWithName.new(name: "Asia/Colombo") + time = Time.new(2000, 1, 1, 12, 0, 0, zone) + time_loaded = Marshal.load(Marshal.dump(time)) + + time_loaded.zone.should == "Asia/Colombo" + time_loaded.utc_offset.should == 5*3600+30*60 + end + + it "cannot marshal Time if #name method isn't implemented" do + zone = TimeSpecs::Timezone.new(offset: (5*3600+30*60)) + time = Time.new(2000, 1, 1, 12, 0, 0, zone) + + -> { + Marshal.dump(time) + }.should raise_error(NoMethodError, /undefined method [`']name' for/) + end + end + + it "the #abbr method is used by '%Z' in #strftime" do + zone = TimeSpecs::TimezoneWithAbbr.new(name: "Asia/Colombo") + time = Time.new(2000, 1, 1, 12, 0, 0, zone) + + time.strftime("%Z").should == "MMT" + end + + # At loading marshaled data, a timezone name will be converted to a timezone object + # by find_timezone class method, if the method is defined. + # Similarly, that class method will be called when a timezone argument does not have + # the necessary methods mentioned above. + context "subject's class implements .find_timezone method" do + it "calls .find_timezone to build a time object at loading marshaled data" do + zone = TimeSpecs::TimezoneWithName.new(name: "Asia/Colombo") + time = TimeSpecs::TimeWithFindTimezone.new(2000, 1, 1, 12, 0, 0, zone) + time_loaded = Marshal.load(Marshal.dump(time)) + + time_loaded.zone.should be_kind_of TimeSpecs::TimezoneWithName + time_loaded.zone.name.should == "Asia/Colombo" + time_loaded.utc_offset.should == 5*3600+30*60 + end + + it "calls .find_timezone to build a time object if passed zone name as a timezone argument" do + time = TimeSpecs::TimeWithFindTimezone.new(2000, 1, 1, 12, 0, 0, "Asia/Colombo") + time.zone.should be_kind_of TimeSpecs::TimezoneWithName + time.zone.name.should == "Asia/Colombo" + + time = TimeSpecs::TimeWithFindTimezone.new(2000, 1, 1, 12, 0, 0, "some invalid zone name") + time.zone.should be_kind_of TimeSpecs::TimezoneWithName + time.zone.name.should == "some invalid zone name" + end + + it "does not call .find_timezone if passed any not string/numeric/timezone timezone argument" do + [Object.new, [], {}, :"some zone"].each do |zone| + -> { + TimeSpecs::TimeWithFindTimezone.new(2000, 1, 1, 12, 0, 0, zone) + }.should raise_error(TypeError, /can't convert \w+ into an exact number/) + end + end + end + + describe ":in keyword argument" do + it "could be UTC offset as a String in '+HH:MM or '-HH:MM' format" do + time = Time.new(2000, 1, 1, 12, 0, 0, in: "+05:00") + + time.utc_offset.should == 5*60*60 + time.zone.should == nil + + time = Time.new(2000, 1, 1, 12, 0, 0, in: "-09:00") + + time.utc_offset.should == -9*60*60 + time.zone.should == nil + + time = Time.new(2000, 1, 1, 12, 0, 0, in: "-09:00:01") + + time.utc_offset.should == -(9*60*60 + 1) + time.zone.should == nil + end + + it "could be UTC offset as a number of seconds" do + time = Time.new(2000, 1, 1, 12, 0, 0, in: 5*60*60) + + time.utc_offset.should == 5*60*60 + time.zone.should == nil + + time = Time.new(2000, 1, 1, 12, 0, 0, in: -9*60*60) + + time.utc_offset.should == -9*60*60 + time.zone.should == nil + end + + it "returns a Time with UTC offset specified as a single letter military timezone" do + Time.new(2000, 1, 1, 0, 0, 0, in: "W").utc_offset.should == 3600 * -10 + end + + it "could be a timezone object" do + zone = TimeSpecs::TimezoneWithName.new(name: "Asia/Colombo") + time = Time.new(2000, 1, 1, 12, 0, 0, in: zone) + + time.utc_offset.should == 5*3600+30*60 + time.zone.should == zone + + zone = TimeSpecs::TimezoneWithName.new(name: "PST") + time = Time.new(2000, 1, 1, 12, 0, 0, in: zone) + + time.utc_offset.should == -9*60*60 + time.zone.should == zone + end + + it "allows omitting minor arguments" do + Time.new(2000, 1, 1, 12, 1, 1, in: "+05:00").should == Time.new(2000, 1, 1, 12, 1, 1, "+05:00") + Time.new(2000, 1, 1, 12, 1, in: "+05:00").should == Time.new(2000, 1, 1, 12, 1, 0, "+05:00") + Time.new(2000, 1, 1, 12, in: "+05:00").should == Time.new(2000, 1, 1, 12, 0, 0, "+05:00") + Time.new(2000, 1, 1, in: "+05:00").should == Time.new(2000, 1, 1, 0, 0, 0, "+05:00") + Time.new(2000, 1, in: "+05:00").should == Time.new(2000, 1, 1, 0, 0, 0, "+05:00") + Time.new(2000, in: "+05:00").should == Time.new(2000, 1, 1, 0, 0, 0, "+05:00") + Time.new(in: "+05:00").should be_close(Time.now.getlocal("+05:00"), TIME_TOLERANCE) + end + + it "converts to a provided timezone if all the positional arguments are omitted" do + Time.new(in: "+05:00").utc_offset.should == 5*3600 + end + + it "raises ArgumentError if format is invalid" do + -> { Time.new(2000, 1, 1, 12, 0, 0, in: "+09:99") }.should raise_error(ArgumentError) + -> { Time.new(2000, 1, 1, 12, 0, 0, in: "ABC") }.should raise_error(ArgumentError) + end + + it "raises ArgumentError if two offset arguments are given" do + -> { + Time.new(2000, 1, 1, 12, 0, 0, "+05:00", in: "+05:00") + }.should raise_error(ArgumentError, "timezone argument given as positional and keyword arguments") + end + end + + describe "Time.new with a String argument" do + it "parses an ISO-8601 like format" do + t = Time.utc(2020, 12, 24, 15, 56, 17) + + Time.new("2020-12-24T15:56:17Z").should == t + Time.new("2020-12-25 00:56:17 +09:00").should == t + Time.new("2020-12-25 00:57:47 +09:01:30").should == t + Time.new("2020-12-25 00:56:17 +0900").should == t + Time.new("2020-12-25 00:57:47 +090130").should == t + Time.new("2020-12-25T00:56:17+09:00").should == t + + Time.new("2020-12-25T00:56:17.123456+09:00").should == Time.utc(2020, 12, 24, 15, 56, 17, 123456) + end + + it "accepts precision keyword argument and truncates specified digits of sub-second part" do + Time.new("2021-12-25 00:00:00.123456789876 +09:00").subsec.should == 0.123456789r + Time.new("2021-12-25 00:00:00.123456789876 +09:00", precision: nil).subsec.should == 0.123456789876r + Time.new("2021-12-25 00:00:00 +09:00", precision: 0).subsec.should == 0 + Time.new("2021-12-25 00:00:00.123456789876 +09:00", precision: -1).subsec.should == 0.123456789876r + end + + it "returns Time in local timezone if not provided in the String argument" do + Time.new("2021-12-25 00:00:00").zone.should == Time.new(2021, 12, 25).zone + Time.new("2021-12-25 00:00:00").utc_offset.should == Time.new(2021, 12, 25).utc_offset + end + + it "returns Time in timezone specified in the String argument" do + Time.new("2021-12-25 00:00:00 +05:00").to_s.should == "2021-12-25 00:00:00 +0500" + end + + it "returns Time in timezone specified in the String argument even if the in keyword argument provided" do + Time.new("2021-12-25 00:00:00 +09:00", in: "-01:00").to_s.should == "2021-12-25 00:00:00 +0900" + end + + it "returns Time in timezone specified with in keyword argument if timezone isn't provided in the String argument" do + Time.new("2021-12-25 00:00:00", in: "-01:00").to_s.should == "2021-12-25 00:00:00 -0100" + end + + it "returns Time of Jan 1 for string with just year" do + Time.new("2021").should == Time.new(2021, 1, 1) + Time.new("2021").zone.should == Time.new(2021, 1, 1).zone + Time.new("2021").utc_offset.should == Time.new(2021, 1, 1).utc_offset + end + + it "returns Time of Jan 1 for string with just year in timezone specified with in keyword argument" do + Time.new("2021", in: "+17:00").to_s.should == "2021-01-01 00:00:00 +1700" + end + + it "converts precision keyword argument into Integer if is not nil" do + obj = Object.new + def obj.to_int; 3; end + + Time.new("2021-12-25 00:00:00.123456789876 +09:00", precision: 1.2).subsec.should == 0.1r + Time.new("2021-12-25 00:00:00.123456789876 +09:00", precision: obj).subsec.should == 0.123r + Time.new("2021-12-25 00:00:00.123456789876 +09:00", precision: 3r).subsec.should == 0.123r + end + + it "returns Time with correct subseconds when given seconds fraction is shorted than 6 digits" do + Time.new("2020-12-25T00:56:17.123 +09:00").nsec.should == 123000000 + Time.new("2020-12-25T00:56:17.123 +09:00").usec.should == 123000 + Time.new("2020-12-25T00:56:17.123 +09:00").subsec.should == 0.123 + end + + it "returns Time with correct subseconds when given seconds fraction is milliseconds" do + Time.new("2020-12-25T00:56:17.123456 +09:00").nsec.should == 123456000 + Time.new("2020-12-25T00:56:17.123456 +09:00").usec.should == 123456 + Time.new("2020-12-25T00:56:17.123456 +09:00").subsec.should == 0.123456 + end + + it "returns Time with correct subseconds when given seconds fraction is longer that 6 digits but shorted than 9 digits" do + Time.new("2020-12-25T00:56:17.12345678 +09:00").nsec.should == 123456780 + Time.new("2020-12-25T00:56:17.12345678 +09:00").usec.should == 123456 + Time.new("2020-12-25T00:56:17.12345678 +09:00").subsec.should == 0.12345678 + end + + it "returns Time with correct subseconds when given seconds fraction is nanoseconds" do + Time.new("2020-12-25T00:56:17.123456789 +09:00").nsec.should == 123456789 + Time.new("2020-12-25T00:56:17.123456789 +09:00").usec.should == 123456 + Time.new("2020-12-25T00:56:17.123456789 +09:00").subsec.should == 0.123456789 + end + + it "returns Time with correct subseconds when given seconds fraction is longer than 9 digits" do + Time.new("2020-12-25T00:56:17.123456789876 +09:00").nsec.should == 123456789 + Time.new("2020-12-25T00:56:17.123456789876 +09:00").usec.should == 123456 + Time.new("2020-12-25T00:56:17.123456789876 +09:00").subsec.should == 0.123456789 + end + + ruby_version_is ""..."3.3" do + it "raise TypeError is can't convert precision keyword argument into Integer" do + -> { + Time.new("2021-12-25 00:00:00.123456789876 +09:00", precision: "") + }.should raise_error(TypeError, "no implicit conversion from string") + end + end + + ruby_version_is "3.3" do + it "raise TypeError is can't convert precision keyword argument into Integer" do + -> { + Time.new("2021-12-25 00:00:00.123456789876 +09:00", precision: "") + }.should raise_error(TypeError, "no implicit conversion of String into Integer") + end + end + + it "raises ArgumentError if part of time string is missing" do + -> { + Time.new("2020-12-25 00:56 +09:00") + }.should raise_error(ArgumentError, /missing sec part: 00:56 |can't parse:/) + + -> { + Time.new("2020-12-25 00 +09:00") + }.should raise_error(ArgumentError, /missing min part: 00 |can't parse:/) + end + + ruby_version_is "3.2.3" do + it "raises ArgumentError if the time part is missing" do + -> { + Time.new("2020-12-25") + }.should raise_error(ArgumentError, /no time information|can't parse:/) + end + + it "raises ArgumentError if day is missing" do + -> { + Time.new("2020-12") + }.should raise_error(ArgumentError, /no time information|can't parse:/) + end + end + + it "raises ArgumentError if subsecond is missing after dot" do + -> { + Time.new("2020-12-25 00:56:17. +0900") + }.should raise_error(ArgumentError, /subsecond expected after dot: 00:56:17. |can't parse:/) + end + + it "raises ArgumentError if String argument is not in the supported format" do + -> { + Time.new("021-12-25 00:00:00.123456 +09:00") + }.should raise_error(ArgumentError, /year must be 4 or more digits: 021|can't parse:/) + + -> { + Time.new("2020-012-25 00:56:17 +0900") + }.should raise_error(ArgumentError, /\Atwo digits mon is expected after [`']-': -012-25 00:\z|can't parse:/) + + -> { + Time.new("2020-2-25 00:56:17 +0900") + }.should raise_error(ArgumentError, /\Atwo digits mon is expected after [`']-': -2-25 00:56\z|can't parse:/) + + -> { + Time.new("2020-12-215 00:56:17 +0900") + }.should raise_error(ArgumentError, /\Atwo digits mday is expected after [`']-': -215 00:56:\z|can't parse:/) + + -> { + Time.new("2020-12-25 000:56:17 +0900") + }.should raise_error(ArgumentError, /two digits hour is expected: 000:56:17 |can't parse:/) + + -> { + Time.new("2020-12-25 0:56:17 +0900") + }.should raise_error(ArgumentError, /two digits hour is expected: 0:56:17 \+0|can't parse:/) + + -> { + Time.new("2020-12-25 00:516:17 +0900") + }.should raise_error(ArgumentError, /\Atwo digits min is expected after [`']:': :516:17 \+09\z|can't parse:/) + + -> { + Time.new("2020-12-25 00:6:17 +0900") + }.should raise_error(ArgumentError, /\Atwo digits min is expected after [`']:': :6:17 \+0900\z|can't parse:/) + + -> { + Time.new("2020-12-25 00:56:137 +0900") + }.should raise_error(ArgumentError, /\Atwo digits sec is expected after [`']:': :137 \+0900\z|can't parse:/) + + -> { + Time.new("2020-12-25 00:56:7 +0900") + }.should raise_error(ArgumentError, /\Atwo digits sec is expected after [`']:': :7 \+0900\z|can't parse:/) + + -> { + Time.new("2020-12-25 00:56. +0900") + }.should raise_error(ArgumentError, /fraction min is not supported: 00:56\.|can't parse:/) + + -> { + Time.new("2020-12-25 00. +0900") + }.should raise_error(ArgumentError, /fraction hour is not supported: 00\.|can't parse:/) + end + + it "raises ArgumentError if date/time parts values are not valid" do + -> { + Time.new("2020-13-25 00:56:17 +09:00") + }.should raise_error(ArgumentError, /(mon|argument) out of range/) + + -> { + Time.new("2020-12-32 00:56:17 +09:00") + }.should raise_error(ArgumentError, /(mday|argument) out of range/) + + -> { + Time.new("2020-12-25 25:56:17 +09:00") + }.should raise_error(ArgumentError, /(hour|argument) out of range/) + + -> { + Time.new("2020-12-25 00:61:17 +09:00") + }.should raise_error(ArgumentError, /(min|argument) out of range/) + + -> { + Time.new("2020-12-25 00:56:61 +09:00") + }.should raise_error(ArgumentError, /(sec|argument) out of range/) + + -> { + Time.new("2020-12-25 00:56:17 +23:59:60") + }.should raise_error(ArgumentError, /utc_offset|argument out of range/) + + -> { + Time.new("2020-12-25 00:56:17 +24:00") + }.should raise_error(ArgumentError, /(utc_offset|argument) out of range/) + + -> { + Time.new("2020-12-25 00:56:17 +23:61") + }.should raise_error(ArgumentError, /utc_offset/) + + ruby_bug '#20797', ''...'3.4' do + -> { + Time.new("2020-12-25 00:56:17 +00:23:61") + }.should raise_error(ArgumentError, /utc_offset/) + end + end + + it "raises ArgumentError if utc offset parts are not valid" do + -> { Time.new("2020-12-25 00:56:17 +24:00") }.should raise_error(ArgumentError, "utc_offset out of range") + -> { Time.new("2020-12-25 00:56:17 +2400") }.should raise_error(ArgumentError, "utc_offset out of range") + + -> { Time.new("2020-12-25 00:56:17 +99:00") }.should raise_error(ArgumentError, "utc_offset out of range") + -> { Time.new("2020-12-25 00:56:17 +9900") }.should raise_error(ArgumentError, "utc_offset out of range") + + -> { Time.new("2020-12-25 00:56:17 +00:60") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:60') + -> { Time.new("2020-12-25 00:56:17 +0060") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +0060') + + -> { Time.new("2020-12-25 00:56:17 +00:99") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:99') + -> { Time.new("2020-12-25 00:56:17 +0099") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +0099') + + ruby_bug '#20797', ''...'3.4' do + -> { Time.new("2020-12-25 00:56:17 +00:00:60") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:00:60') + -> { Time.new("2020-12-25 00:56:17 +000060") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +000060') + + -> { Time.new("2020-12-25 00:56:17 +00:00:99") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:00:99') + -> { Time.new("2020-12-25 00:56:17 +000099") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +000099') + end + end + + it "raises ArgumentError if string has not ascii-compatible encoding" do + -> { + Time.new("2021-11-31 00:00:60 +09:00".encode("utf-32le")) + }.should raise_error(ArgumentError, "time string should have ASCII compatible encoding") + end + + it "raises ArgumentError if string doesn't start with year" do + -> { + Time.new("a\nb") + }.should raise_error(ArgumentError, "can't parse: \"a\\nb\"") + end + + it "raises ArgumentError if string has extra characters after offset" do + -> { + Time.new("2021-11-31 00:00:59 +09:00 abc") + }.should raise_error(ArgumentError, /can't parse.+ abc/) + end + + ruby_version_is "3.2.3" do + it "raises ArgumentError when there are leading space characters" do + -> { Time.new(" 2020-12-02 00:00:00") }.should raise_error(ArgumentError, /can't parse/) + -> { Time.new("\t2020-12-02 00:00:00") }.should raise_error(ArgumentError, /can't parse/) + -> { Time.new("\n2020-12-02 00:00:00") }.should raise_error(ArgumentError, /can't parse/) + -> { Time.new("\v2020-12-02 00:00:00") }.should raise_error(ArgumentError, /can't parse/) + -> { Time.new("\f2020-12-02 00:00:00") }.should raise_error(ArgumentError, /can't parse/) + -> { Time.new("\r2020-12-02 00:00:00") }.should raise_error(ArgumentError, /can't parse/) + end + + it "raises ArgumentError when there are trailing whitespaces" do + -> { Time.new("2020-12-02 00:00:00 ") }.should raise_error(ArgumentError, /can't parse/) + -> { Time.new("2020-12-02 00:00:00\t") }.should raise_error(ArgumentError, /can't parse/) + -> { Time.new("2020-12-02 00:00:00\n") }.should raise_error(ArgumentError, /can't parse/) + -> { Time.new("2020-12-02 00:00:00\v") }.should raise_error(ArgumentError, /can't parse/) + -> { Time.new("2020-12-02 00:00:00\f") }.should raise_error(ArgumentError, /can't parse/) + -> { Time.new("2020-12-02 00:00:00\r") }.should raise_error(ArgumentError, /can't parse/) + end + end + end +end diff --git a/spec/ruby/core/time/now_spec.rb b/spec/ruby/core/time/now_spec.rb new file mode 100644 index 0000000000..e3fe6edad6 --- /dev/null +++ b/spec/ruby/core/time/now_spec.rb @@ -0,0 +1,181 @@ +require_relative '../../spec_helper' +require_relative 'shared/now' + +describe "Time.now" do + it_behaves_like :time_now, :now + + describe ":in keyword argument" do + it "could be UTC offset as a String in '+HH:MM or '-HH:MM' format" do + time = Time.now(in: "+05:00") + + time.utc_offset.should == 5*60*60 + time.zone.should == nil + + time = Time.now(in: "-09:00") + + time.utc_offset.should == -9*60*60 + time.zone.should == nil + + time = Time.now(in: "-09:00:01") + + time.utc_offset.should == -(9*60*60 + 1) + time.zone.should == nil + end + + it "could be UTC offset as a number of seconds" do + time = Time.now(in: 5*60*60) + + time.utc_offset.should == 5*60*60 + time.zone.should == nil + + time = Time.now(in: -9*60*60) + + time.utc_offset.should == -9*60*60 + time.zone.should == nil + end + + it "returns a Time with UTC offset specified as a single letter military timezone" do + Time.now(in: "W").utc_offset.should == 3600 * -10 + end + + it "could be a timezone object" do + zone = TimeSpecs::TimezoneWithName.new(name: "Asia/Colombo") + time = Time.now(in: zone) + + time.utc_offset.should == 5*3600+30*60 + time.zone.should == zone + + zone = TimeSpecs::TimezoneWithName.new(name: "PST") + time = Time.now(in: zone) + + time.utc_offset.should == -9*60*60 + time.zone.should == zone + end + + it "raises ArgumentError if format is invalid" do + -> { Time.now(in: "+09:99") }.should raise_error(ArgumentError) + -> { Time.now(in: "ABC") }.should raise_error(ArgumentError) + end + + it "raises ArgumentError if String argument and hours greater than 23" do + -> { Time.now(in: "+24:00") }.should raise_error(ArgumentError, "utc_offset out of range") + -> { Time.now(in: "+2400") }.should raise_error(ArgumentError, "utc_offset out of range") + + -> { Time.now(in: "+99:00") }.should raise_error(ArgumentError, "utc_offset out of range") + -> { Time.now(in: "+9900") }.should raise_error(ArgumentError, "utc_offset out of range") + end + + it "raises ArgumentError if String argument and minutes greater than 59" do + -> { Time.now(in: "+00:60") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:60') + -> { Time.now(in: "+0060") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +0060') + + -> { Time.now(in: "+00:99") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:99') + -> { Time.now(in: "+0099") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +0099') + end + + ruby_bug '#20797', ''...'3.4' do + it "raises ArgumentError if String argument and seconds greater than 59" do + -> { Time.now(in: "+00:00:60") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:00:60') + -> { Time.now(in: "+000060") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +000060') + + -> { Time.now(in: "+00:00:99") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:00:99') + -> { Time.now(in: "+000099") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +000099') + end + end + end + + describe "Timezone object" do # https://bugs.ruby-lang.org/issues/17485 + it "raises TypeError if timezone does not implement #utc_to_local method" do + zone = Object.new + def zone.local_to_utc(time) + time + end + + -> { + Time.now(in: zone) + }.should raise_error(TypeError, /can't convert Object into an exact number/) + end + + it "does not raise exception if timezone does not implement #local_to_utc method" do + zone = Object.new + def zone.utc_to_local(time) + time + end + + Time.now(in: zone).should be_kind_of(Time) + end + + # The result also should be a Time or Time-like object (not necessary to be the same class) + # or Integer. The zone of the result is just ignored. + describe "returned value by #utc_to_local and #local_to_utc methods" do + it "could be Time instance" do + zone = Object.new + def zone.utc_to_local(t) + time = Time.new(t.year, t.mon, t.day, t.hour, t.min, t.sec, t.utc_offset) + time + 60 * 60 # + 1 hour + end + + Time.now(in: zone).should be_kind_of(Time) + Time.now(in: zone).utc_offset.should == 3600 + end + + it "could be Time subclass instance" do + zone = Object.new + def zone.utc_to_local(t) + time = Time.new(t.year, t.mon, t.day, t.hour, t.min, t.sec, t.utc_offset) + time += 60 * 60 # + 1 hour + + Class.new(Time).new(time.year, time.mon, time.day, time.hour, time.min, time.sec, time.utc_offset) + end + + Time.now(in: zone).should be_kind_of(Time) + Time.now(in: zone).utc_offset.should == 3600 + end + + it "could be Integer" do + zone = Object.new + def zone.utc_to_local(time) + time.to_i + 60*60 + end + + Time.now(in: zone).should be_kind_of(Time) + Time.now(in: zone).utc_offset.should == 60*60 + end + + it "could have any #zone and #utc_offset because they are ignored" do + zone = Object.new + def zone.utc_to_local(t) + Struct.new(:year, :mon, :mday, :hour, :min, :sec, :isdst, :to_i, :zone, :utc_offset) # rubocop:disable Lint/StructNewOverride + .new(t.year, t.mon, t.mday, t.hour, t.min, t.sec, t.isdst, t.to_i, 'America/New_York', -5*60*60) + end + Time.now(in: zone).utc_offset.should == 0 + + zone = Object.new + def zone.utc_to_local(t) + Struct.new(:year, :mon, :mday, :hour, :min, :sec, :isdst, :to_i, :zone, :utc_offset) # rubocop:disable Lint/StructNewOverride + .new(t.year, t.mon, t.mday, t.hour, t.min, t.sec, t.isdst, t.to_i, 'Asia/Tokyo', 9*60*60) + end + Time.now(in: zone).utc_offset.should == 0 + + zone = Object.new + def zone.utc_to_local(t) + Time.new(t.year, t.mon, t.mday, t.hour, t.min, t.sec, 9*60*60) + end + Time.now(in: zone).utc_offset.should == 0 + end + + it "raises ArgumentError if difference between argument and result is too large" do + zone = Object.new + def zone.utc_to_local(t) + time = Time.utc(t.year, t.mon, t.day, t.hour, t.min, t.sec, t.utc_offset) + time -= 24 * 60 * 60 # - 1 day + Time.utc(time.year, time.mon, time.day, time.hour, time.min, time.sec, time.utc_offset) + end + + -> { + Time.now(in: zone) + }.should raise_error(ArgumentError, "utc_offset out of range") + end + end + end +end diff --git a/spec/ruby/core/time/nsec_spec.rb b/spec/ruby/core/time/nsec_spec.rb new file mode 100644 index 0000000000..9338eb435a --- /dev/null +++ b/spec/ruby/core/time/nsec_spec.rb @@ -0,0 +1,31 @@ +require_relative '../../spec_helper' + +describe "Time#nsec" do + it "returns 0 for a Time constructed with a whole number of seconds" do + Time.at(100).nsec.should == 0 + end + + it "returns the nanoseconds part of a Time constructed with a Float number of seconds" do + Time.at(10.75).nsec.should == 750_000_000 + end + + it "returns the nanoseconds part of a Time constructed with an Integer number of microseconds" do + Time.at(0, 999_999).nsec.should == 999_999_000 + end + + it "returns the nanoseconds part of a Time constructed with an Float number of microseconds" do + Time.at(0, 3.75).nsec.should == 3750 + end + + it "returns the nanoseconds part of a Time constructed with a Rational number of seconds" do + Time.at(Rational(3, 2)).nsec.should == 500_000_000 + end + + it "returns the nanoseconds part of a Time constructed with an Rational number of microseconds" do + Time.at(0, Rational(99, 10)).nsec.should == 9900 + end + + it "returns a positive value for dates before the epoch" do + Time.utc(1969, 11, 12, 13, 18, 57, 404240).nsec.should == 404240000 + end +end diff --git a/spec/ruby/core/time/plus_spec.rb b/spec/ruby/core/time/plus_spec.rb new file mode 100644 index 0000000000..642393b615 --- /dev/null +++ b/spec/ruby/core/time/plus_spec.rb @@ -0,0 +1,118 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "Time#+" do + it "increments the time by the specified amount" do + (Time.at(0) + 100).should == Time.at(100) + end + + it "is a commutative operator" do + (Time.at(1.1) + 0.9).should == Time.at(0.9) + 1.1 + end + + it "adds a negative Float" do + t = Time.at(100) + -1.3 + t.usec.should == 699999 + t.to_i.should == 98 + end + + it "raises a TypeError if given argument is a coercible String" do + -> { Time.now + "1" }.should raise_error(TypeError) + -> { Time.now + "0.1" }.should raise_error(TypeError) + -> { Time.now + "1/3" }.should raise_error(TypeError) + end + + it "increments the time by the specified amount as rational numbers" do + (Time.at(Rational(11, 10)) + Rational(9, 10)).should == Time.at(2) + end + + it "accepts arguments that can be coerced into Rational" do + (obj = mock_numeric('10')).should_receive(:to_r).and_return(Rational(10)) + (Time.at(100) + obj).should == Time.at(110) + end + + it "raises TypeError on argument that can't be coerced into Rational" do + -> { Time.now + Object.new }.should raise_error(TypeError) + -> { Time.now + "stuff" }.should raise_error(TypeError) + end + + it "returns a UTC time if self is UTC" do + (Time.utc(2012) + 10).should.utc? + end + + it "returns a non-UTC time if self is non-UTC" do + (Time.local(2012) + 10).should_not.utc? + end + + it "returns a time with the same fixed offset as self" do + (Time.new(2012, 1, 1, 0, 0, 0, 3600) + 10).utc_offset.should == 3600 + end + + it "preserves time zone" do + time_with_zone = Time.now.utc + time_with_zone.zone.should == (time_with_zone + 1).zone + + time_with_zone = Time.now + time_with_zone.zone.should == (time_with_zone + 1).zone + end + + context "zone is a timezone object" do + it "preserves time zone" do + zone = TimeSpecs::Timezone.new(offset: (5*3600+30*60)) + time = Time.new(2012, 1, 1, 12, 0, 0, zone) + 1 + + time.zone.should == zone + end + end + + it "does not return a subclass instance" do + c = Class.new(Time) + x = c.now + 1 + x.should be_an_instance_of(Time) + end + + it "raises TypeError on Time argument" do + -> { Time.now + Time.now }.should raise_error(TypeError) + end + + it "raises TypeError on nil argument" do + -> { Time.now + nil }.should raise_error(TypeError) + end + + #see [ruby-dev:38446] + it "tracks microseconds" do + time = Time.at(0) + time += Rational(123_456, 1_000_000) + time.usec.should == 123_456 + time += Rational(654_321, 1_000_000) + time.usec.should == 777_777 + end + + it "tracks nanoseconds" do + time = Time.at(0) + time += Rational(123_456_789, 1_000_000_000) + time.nsec.should == 123_456_789 + time += Rational(876_543_210, 1_000_000_000) + time.nsec.should == 999_999_999 + end + + it "maintains precision" do + t = Time.at(0) + Rational(8_999_999_999_999_999, 1_000_000_000_000_000) + t.should_not == Time.at(9) + end + + it "maintains microseconds precision" do + time = Time.at(0) + Rational(8_999_999_999_999_999, 1_000_000_000_000_000) + time.usec.should == 999_999 + end + + it "maintains nanoseconds precision" do + time = Time.at(0) + Rational(8_999_999_999_999_999, 1_000_000_000_000_000) + time.nsec.should == 999_999_999 + end + + it "maintains subseconds precision" do + time = Time.at(0) + Rational(8_999_999_999_999_999, 1_000_000_000_000_000) + time.subsec.should == Rational(999_999_999_999_999, 1_000_000_000_000_000) + end +end diff --git a/spec/ruby/core/time/round_spec.rb b/spec/ruby/core/time/round_spec.rb new file mode 100644 index 0000000000..0cbed04ade --- /dev/null +++ b/spec/ruby/core/time/round_spec.rb @@ -0,0 +1,35 @@ +require_relative '../../spec_helper' + +describe "Time#round" do + before do + @time = Time.utc(2010, 3, 30, 5, 43, "25.123456789".to_r) + end + + it "defaults to rounding to 0 places" do + @time.round.should == Time.utc(2010, 3, 30, 5, 43, 25.to_r) + end + + it "rounds to 0 decimal places with an explicit argument" do + @time.round(0).should == Time.utc(2010, 3, 30, 5, 43, 25.to_r) + end + + it "rounds to 7 decimal places with an explicit argument" do + @time.round(7).should == Time.utc(2010, 3, 30, 5, 43, "25.1234568".to_r) + end + + it "returns an instance of Time, even if #round is called on a subclass" do + subclass = Class.new(Time) + instance = subclass.at(0) + instance.class.should equal subclass + instance.round.should be_an_instance_of(Time) + end + + it "copies own timezone to the returning value" do + @time.zone.should == @time.round.zone + + with_timezone "JST-9" do + time = Time.at 0, 1 + time.zone.should == time.round.zone + end + end +end diff --git a/spec/ruby/core/time/saturday_spec.rb b/spec/ruby/core/time/saturday_spec.rb new file mode 100644 index 0000000000..0e51407366 --- /dev/null +++ b/spec/ruby/core/time/saturday_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' + +describe "Time#saturday?" do + it "returns true if time represents Saturday" do + Time.local(2000, 1, 1).should.saturday? + end + + it "returns false if time doesn't represent Saturday" do + Time.local(2000, 1, 2).should_not.saturday? + end +end diff --git a/spec/ruby/core/time/sec_spec.rb b/spec/ruby/core/time/sec_spec.rb new file mode 100644 index 0000000000..73fc5ce1fc --- /dev/null +++ b/spec/ruby/core/time/sec_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' + +describe "Time#sec" do + it "returns the second of the minute(0..60) for time" do + Time.at(0).sec.should == 0 + end +end diff --git a/spec/ruby/core/time/shared/asctime.rb b/spec/ruby/core/time/shared/asctime.rb new file mode 100644 index 0000000000..d096666863 --- /dev/null +++ b/spec/ruby/core/time/shared/asctime.rb @@ -0,0 +1,6 @@ +describe :time_asctime, shared: true do + it "returns a canonical string representation of time" do + t = Time.now + t.send(@method).should == t.strftime("%a %b %e %H:%M:%S %Y") + end +end diff --git a/spec/ruby/core/time/shared/day.rb b/spec/ruby/core/time/shared/day.rb new file mode 100644 index 0000000000..472dc959c1 --- /dev/null +++ b/spec/ruby/core/time/shared/day.rb @@ -0,0 +1,15 @@ +describe :time_day, shared: true do + it "returns the day of the month (1..n) for a local Time" do + with_timezone("CET", 1) do + Time.local(1970, 1, 1).send(@method).should == 1 + end + end + + it "returns the day of the month for a UTC Time" do + Time.utc(1970, 1, 1).send(@method).should == 1 + end + + it "returns the day of the month for a Time with a fixed offset" do + Time.new(2012, 1, 1, 0, 0, 0, -3600).send(@method).should == 1 + end +end diff --git a/spec/ruby/core/time/shared/getgm.rb b/spec/ruby/core/time/shared/getgm.rb new file mode 100644 index 0000000000..3576365772 --- /dev/null +++ b/spec/ruby/core/time/shared/getgm.rb @@ -0,0 +1,9 @@ +describe :time_getgm, shared: true do + it "returns a new time which is the utc representation of time" do + # Testing with America/Regina here because it doesn't have DST. + with_timezone("CST", -6) do + t = Time.local(2007, 1, 9, 6, 0, 0) + t.send(@method).should == Time.gm(2007, 1, 9, 12, 0, 0) + end + end +end diff --git a/spec/ruby/core/time/shared/gm.rb b/spec/ruby/core/time/shared/gm.rb new file mode 100644 index 0000000000..0ee602c837 --- /dev/null +++ b/spec/ruby/core/time/shared/gm.rb @@ -0,0 +1,70 @@ +describe :time_gm, shared: true do + it "creates a time based on given values, interpreted as UTC (GMT)" do + Time.send(@method, 2000,"jan",1,20,15,1).inspect.should == "2000-01-01 20:15:01 UTC" + end + + it "creates a time based on given C-style gmtime arguments, interpreted as UTC (GMT)" do + time = Time.send(@method, 1, 15, 20, 1, 1, 2000, :ignored, :ignored, :ignored, :ignored) + time.inspect.should == "2000-01-01 20:15:01 UTC" + end + + it "interprets pre-Gregorian reform dates using Gregorian proleptic calendar" do + Time.send(@method, 1582, 10, 4, 12).to_i.should == -12220200000 # 2299150j + end + + it "interprets Julian-Gregorian gap dates using Gregorian proleptic calendar" do + Time.send(@method, 1582, 10, 14, 12).to_i.should == -12219336000 # 2299160j + end + + it "interprets post-Gregorian reform dates using Gregorian calendar" do + Time.send(@method, 1582, 10, 15, 12).to_i.should == -12219249600 # 2299161j + end + + it "handles fractional usec close to rounding limit" do + time = Time.send(@method, 2000, 1, 1, 12, 30, 0, 9999r/10000) + + time.usec.should == 0 + time.nsec.should == 999 + end + + guard -> { + with_timezone 'right/UTC' do + (Time.gm(1972, 6, 30, 23, 59, 59) + 1).sec == 60 + end + } do + it "handles real leap seconds in zone 'right/UTC'" do + with_timezone 'right/UTC' do + time = Time.send(@method, 1972, 6, 30, 23, 59, 60) + + time.sec.should == 60 + time.min.should == 59 + time.hour.should == 23 + time.day.should == 30 + time.month.should == 6 + end + end + end + + it "handles bad leap seconds by carrying values forward" do + with_timezone 'UTC' do + time = Time.send(@method, 2017, 7, 5, 23, 59, 60) + time.sec.should == 0 + time.min.should == 0 + time.hour.should == 0 + time.day.should == 6 + time.month.should == 7 + end + end + + it "handles a value of 60 for seconds by carrying values forward in zone 'UTC'" do + with_timezone 'UTC' do + time = Time.send(@method, 1972, 6, 30, 23, 59, 60) + + time.sec.should == 0 + time.min.should == 0 + time.hour.should == 0 + time.day.should == 1 + time.month.should == 7 + end + end +end diff --git a/spec/ruby/core/time/shared/gmt_offset.rb b/spec/ruby/core/time/shared/gmt_offset.rb new file mode 100644 index 0000000000..839566c249 --- /dev/null +++ b/spec/ruby/core/time/shared/gmt_offset.rb @@ -0,0 +1,59 @@ +describe :time_gmt_offset, shared: true do + it "returns the offset in seconds between the timezone of time and UTC" do + with_timezone("AST", 3) do + Time.new.send(@method).should == 10800 + end + end + + it "returns 0 when the date is UTC" do + with_timezone("AST", 3) do + Time.new.utc.send(@method).should == 0 + end + end + + platform_is_not :windows do + it "returns the correct offset for US Eastern time zone around daylight savings time change" do + # "2010-03-14 01:59:59 -0500" + 1 ==> "2010-03-14 03:00:00 -0400" + with_timezone("EST5EDT") do + t = Time.local(2010,3,14,1,59,59) + t.send(@method).should == -5*60*60 + (t + 1).send(@method).should == -4*60*60 + end + end + + it "returns the correct offset for Hawaii around daylight savings time change" do + # "2010-03-14 01:59:59 -1000" + 1 ==> "2010-03-14 02:00:00 -1000" + with_timezone("Pacific/Honolulu") do + t = Time.local(2010,3,14,1,59,59) + t.send(@method).should == -10*60*60 + (t + 1).send(@method).should == -10*60*60 + end + end + + it "returns the correct offset for New Zealand around daylight savings time change" do + # "2010-04-04 02:59:59 +1300" + 1 ==> "2010-04-04 02:00:00 +1200" + with_timezone("Pacific/Auckland") do + t = Time.local(2010,4,4,1,59,59) + (60 * 60) + t.send(@method).should == 13*60*60 + (t + 1).send(@method).should == 12*60*60 + end + end + end + + it "returns offset as Rational" do + Time.new(2010,4,4,1,59,59,7245).send(@method).should == 7245 + Time.new(2010,4,4,1,59,59,7245.5).send(@method).should == Rational(14491,2) + end + + context 'given positive offset' do + it 'returns a positive offset' do + Time.new(2013,3,17,nil,nil,nil,"+03:00").send(@method).should == 10800 + end + end + + context 'given negative offset' do + it 'returns a negative offset' do + Time.new(2013,3,17,nil,nil,nil,"-03:00").send(@method).should == -10800 + end + end +end diff --git a/spec/ruby/core/time/shared/gmtime.rb b/spec/ruby/core/time/shared/gmtime.rb new file mode 100644 index 0000000000..7b4f65f0b7 --- /dev/null +++ b/spec/ruby/core/time/shared/gmtime.rb @@ -0,0 +1,40 @@ +describe :time_gmtime, shared: true do + it "converts self to UTC, modifying the receiver" do + # Testing with America/Regina here because it doesn't have DST. + with_timezone("CST", -6) do + t = Time.local(2007, 1, 9, 6, 0, 0) + t.send(@method) + # Time#== compensates for time zones, so check all parts separately + t.year.should == 2007 + t.month.should == 1 + t.mday.should == 9 + t.hour.should == 12 + t.min.should == 0 + t.sec.should == 0 + t.zone.should == "UTC" + end + end + + it "returns self" do + with_timezone("CST", -6) do + t = Time.local(2007, 1, 9, 12, 0, 0) + t.send(@method).should equal(t) + end + end + + describe "on a frozen time" do + it "does not raise an error if already in UTC" do + time = Time.gm(2007, 1, 9, 12, 0, 0) + time.freeze + time.send(@method).should equal(time) + end + + it "raises a FrozenError if the time is not UTC" do + with_timezone("CST", -6) do + time = Time.now + time.freeze + -> { time.send(@method) }.should raise_error(FrozenError) + end + end + end +end diff --git a/spec/ruby/core/time/shared/inspect.rb b/spec/ruby/core/time/shared/inspect.rb new file mode 100644 index 0000000000..4133671924 --- /dev/null +++ b/spec/ruby/core/time/shared/inspect.rb @@ -0,0 +1,21 @@ +# -*- encoding: us-ascii -*- + +describe :inspect, shared: true do + it "formats the local time following the pattern 'yyyy-MM-dd HH:mm:ss Z'" do + with_timezone("PST", +1) do + Time.local(2000, 1, 1, 20, 15, 1).send(@method).should == "2000-01-01 20:15:01 +0100" + end + end + + it "formats the UTC time following the pattern 'yyyy-MM-dd HH:mm:ss UTC'" do + Time.utc(2000, 1, 1, 20, 15, 1).send(@method).should == "2000-01-01 20:15:01 UTC" + end + + it "formats the fixed offset time following the pattern 'yyyy-MM-dd HH:mm:ss +/-HHMM'" do + Time.new(2000, 1, 1, 20, 15, 01, 3600).send(@method).should == "2000-01-01 20:15:01 +0100" + end + + it "returns a US-ASCII encoded string" do + Time.now.send(@method).encoding.should equal(Encoding::US_ASCII) + end +end diff --git a/spec/ruby/core/time/shared/isdst.rb b/spec/ruby/core/time/shared/isdst.rb new file mode 100644 index 0000000000..bc6d139230 --- /dev/null +++ b/spec/ruby/core/time/shared/isdst.rb @@ -0,0 +1,8 @@ +describe :time_isdst, shared: true do + it "dst? returns whether time is during daylight saving time" do + with_timezone("America/Los_Angeles") do + Time.local(2007, 9, 9, 0, 0, 0).send(@method).should == true + Time.local(2007, 1, 9, 0, 0, 0).send(@method).should == false + end + end +end diff --git a/spec/ruby/core/time/shared/local.rb b/spec/ruby/core/time/shared/local.rb new file mode 100644 index 0000000000..068e314999 --- /dev/null +++ b/spec/ruby/core/time/shared/local.rb @@ -0,0 +1,42 @@ +describe :time_local, shared: true do + it "creates a time based on given values, interpreted in the local time zone" do + with_timezone("PST", -8) do + Time.send(@method, 2000, "jan", 1, 20, 15, 1).to_a.should == + [1, 15, 20, 1, 1, 2000, 6, 1, false, "PST"] + end + end + + platform_is_not :windows do + it "uses the 'CET' timezone with TZ=Europe/Amsterdam in 1970" do + with_timezone("Europe/Amsterdam") do + Time.send(@method, 1970, 5, 16).to_a.should == + [0, 0, 0, 16, 5, 1970, 6, 136, false, "CET"] + end + end + end +end + +describe :time_local_10_arg, shared: true do + it "creates a time based on given C-style gmtime arguments, interpreted in the local time zone" do + with_timezone("PST", -8) do + Time.send(@method, 1, 15, 20, 1, 1, 2000, :ignored, :ignored, :ignored, :ignored).to_a.should == + [1, 15, 20, 1, 1, 2000, 6, 1, false, "PST"] + end + end + + platform_is_not :windows do + it "creates the correct time just before dst change" do + with_timezone("America/New_York") do + time = Time.send(@method, 0, 30, 1, 30, 10, 2005, 0, 0, true, ENV['TZ']) + time.utc_offset.should == -4 * 3600 + end + end + + it "creates the correct time just after dst change" do + with_timezone("America/New_York") do + time = Time.send(@method, 0, 30, 1, 30, 10, 2005, 0, 0, false, ENV['TZ']) + time.utc_offset.should == -5 * 3600 + end + end + end +end diff --git a/spec/ruby/core/time/shared/month.rb b/spec/ruby/core/time/shared/month.rb new file mode 100644 index 0000000000..31ca679557 --- /dev/null +++ b/spec/ruby/core/time/shared/month.rb @@ -0,0 +1,15 @@ +describe :time_month, shared: true do + it "returns the month of the year for a local Time" do + with_timezone("CET", 1) do + Time.local(1970, 1).send(@method).should == 1 + end + end + + it "returns the month of the year for a UTC Time" do + Time.utc(1970, 1).send(@method).should == 1 + end + + it "returns the four digit year for a Time with a fixed offset" do + Time.new(2012, 1, 1, 0, 0, 0, -3600).send(@method).should == 1 + end +end diff --git a/spec/ruby/core/time/shared/now.rb b/spec/ruby/core/time/shared/now.rb new file mode 100644 index 0000000000..f4018d72f4 --- /dev/null +++ b/spec/ruby/core/time/shared/now.rb @@ -0,0 +1,33 @@ +require_relative '../fixtures/classes' + +describe :time_now, shared: true do + it "creates a subclass instance if called on a subclass" do + TimeSpecs::SubTime.send(@method).should be_an_instance_of(TimeSpecs::SubTime) + TimeSpecs::MethodHolder.send(@method).should be_an_instance_of(Time) + end + + it "sets the current time" do + now = TimeSpecs::MethodHolder.send(@method) + now.to_f.should be_close(Process.clock_gettime(Process::CLOCK_REALTIME), TIME_TOLERANCE) + end + + it "uses the local timezone" do + with_timezone("PDT", -8) do + now = TimeSpecs::MethodHolder.send(@method) + now.utc_offset.should == (-8 * 60 * 60) + end + end + + it "has at least microsecond precision" do + # The clock should not be less accurate than expected (times should + # not all be a multiple of the next precision up, assuming precisions + # are multiples of ten.) + expected = 1_000 + t = 0 + 10_000.times.find do + t = Time.now.nsec + t % (expected * 10) != 0 + end + (t % (expected * 10)).should != 0 + end +end diff --git a/spec/ruby/core/time/shared/time_params.rb b/spec/ruby/core/time/shared/time_params.rb new file mode 100644 index 0000000000..9832fd17fe --- /dev/null +++ b/spec/ruby/core/time/shared/time_params.rb @@ -0,0 +1,271 @@ +describe :time_params, shared: true do + it "accepts 1 argument (year)" do + Time.send(@method, 2000).should == + Time.send(@method, 2000, 1, 1, 0, 0, 0) + end + + it "accepts 2 arguments (year, month)" do + Time.send(@method, 2000, 2).should == + Time.send(@method, 2000, 2, 1, 0, 0, 0) + end + + it "accepts 3 arguments (year, month, day)" do + Time.send(@method, 2000, 2, 3).should == + Time.send(@method, 2000, 2, 3, 0, 0, 0) + end + + it "accepts 4 arguments (year, month, day, hour)" do + Time.send(@method, 2000, 2, 3, 4).should == + Time.send(@method, 2000, 2, 3, 4, 0, 0) + end + + it "accepts 5 arguments (year, month, day, hour, minute)" do + Time.send(@method, 2000, 2, 3, 4, 5).should == + Time.send(@method, 2000, 2, 3, 4, 5, 0) + end + + it "accepts a too big day of the month by going to the next month" do + Time.send(@method, 1999, 2, 31).should == + Time.send(@method, 1999, 3, 3) + end + + it "raises a TypeError if the year is nil" do + -> { Time.send(@method, nil) }.should raise_error(TypeError) + end + + it "accepts nil month, day, hour, minute, and second" do + Time.send(@method, 2000, nil, nil, nil, nil, nil).should == + Time.send(@method, 2000) + end + + it "handles a String year" do + Time.send(@method, "2000").should == + Time.send(@method, 2000) + end + + it "coerces the year with #to_int" do + m = mock(:int) + m.should_receive(:to_int).and_return(1) + Time.send(@method, m).should == Time.send(@method, 1) + end + + it "handles a String month given as a numeral" do + Time.send(@method, 2000, "12").should == + Time.send(@method, 2000, 12) + end + + it "handles a String month given as a short month name" do + Time.send(@method, 2000, "dec").should == + Time.send(@method, 2000, 12) + end + + it "coerces the month with #to_str" do + (obj = mock('12')).should_receive(:to_str).and_return("12") + Time.send(@method, 2008, obj).should == + Time.send(@method, 2008, 12) + end + + it "coerces the month with #to_int" do + m = mock(:int) + m.should_receive(:to_int).and_return(1) + Time.send(@method, 2008, m).should == Time.send(@method, 2008, 1) + end + + it "handles a String day" do + Time.send(@method, 2000, 12, "15").should == + Time.send(@method, 2000, 12, 15) + end + + it "coerces the day with #to_int" do + m = mock(:int) + m.should_receive(:to_int).and_return(1) + Time.send(@method, 2008, 1, m).should == Time.send(@method, 2008, 1, 1) + end + + it "handles a String hour" do + Time.send(@method, 2000, 12, 1, "5").should == + Time.send(@method, 2000, 12, 1, 5) + end + + it "coerces the hour with #to_int" do + m = mock(:int) + m.should_receive(:to_int).and_return(1) + Time.send(@method, 2008, 1, 1, m).should == Time.send(@method, 2008, 1, 1, 1) + end + + it "handles a String minute" do + Time.send(@method, 2000, 12, 1, 1, "8").should == + Time.send(@method, 2000, 12, 1, 1, 8) + end + + it "coerces the minute with #to_int" do + m = mock(:int) + m.should_receive(:to_int).and_return(1) + Time.send(@method, 2008, 1, 1, 0, m).should == Time.send(@method, 2008, 1, 1, 0, 1) + end + + it "handles a String second" do + Time.send(@method, 2000, 12, 1, 1, 1, "8").should == + Time.send(@method, 2000, 12, 1, 1, 1, 8) + end + + it "coerces the second with #to_int" do + m = mock(:int) + m.should_receive(:to_int).and_return(1) + Time.send(@method, 2008, 1, 1, 0, 0, m).should == Time.send(@method, 2008, 1, 1, 0, 0, 1) + end + + it "interprets all numerals as base 10" do + Time.send(@method, "2000", "08", "08", "08", "08", "08").should == Time.send(@method, 2000, 8, 8, 8, 8, 8) + Time.send(@method, "2000", "09", "09", "09", "09", "09").should == Time.send(@method, 2000, 9, 9, 9, 9, 9) + end + + it "handles fractional seconds as a Float" do + t = Time.send(@method, 2000, 1, 1, 20, 15, 1.75) + t.sec.should == 1 + t.usec.should == 750000 + end + + it "handles fractional seconds as a Rational" do + t = Time.send(@method, 2000, 1, 1, 20, 15, Rational(99, 10)) + t.sec.should == 9 + t.usec.should == 900000 + end + + it "handles years from 0 as such" do + 0.upto(2100) do |year| + t = Time.send(@method, year) + t.year.should == year + end + end + + it "accepts various year ranges" do + Time.send(@method, 1801, 12, 31, 23, 59, 59).wday.should == 4 + Time.send(@method, 3000, 12, 31, 23, 59, 59).wday.should == 3 + end + + it "raises an ArgumentError for out of range month" do + # For some reason MRI uses a different message for month in 13-15 and month>=16 + -> { + Time.send(@method, 2008, 16, 31, 23, 59, 59) + }.should raise_error(ArgumentError, /(mon|argument) out of range/) + end + + it "raises an ArgumentError for out of range day" do + -> { + Time.send(@method, 2008, 12, 32, 23, 59, 59) + }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError for out of range hour" do + -> { + Time.send(@method, 2008, 12, 31, 25, 59, 59) + }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError for out of range minute" do + -> { + Time.send(@method, 2008, 12, 31, 23, 61, 59) + }.should raise_error(ArgumentError) + end + + it "raises an ArgumentError for out of range second" do + # For some reason MRI uses different messages for seconds 61-63 and seconds >= 64 + -> { + Time.send(@method, 2008, 12, 31, 23, 59, 61) + }.should raise_error(ArgumentError, /(sec|argument) out of range/) + -> { + Time.send(@method, 2008, 12, 31, 23, 59, -1) + }.should raise_error(ArgumentError, "argument out of range") + end + + it "raises ArgumentError when given 8 arguments" do + -> { Time.send(@method, *[0]*8) }.should raise_error(ArgumentError) + end + + it "raises ArgumentError when given 9 arguments" do + -> { Time.send(@method, *[0]*9) }.should raise_error(ArgumentError) + end + + it "raises ArgumentError when given 11 arguments" do + -> { Time.send(@method, *[0]*11) }.should raise_error(ArgumentError) + end + + it "returns subclass instances" do + c = Class.new(Time) + c.send(@method, 2008, "12").should be_an_instance_of(c) + end +end + +describe :time_params_10_arg, shared: true do + it "handles string arguments" do + Time.send(@method, "1", "15", "20", "1", "1", "2000", :ignored, :ignored, + :ignored, :ignored).should == + Time.send(@method, 1, 15, 20, 1, 1, 2000, :ignored, :ignored, :ignored, :ignored) + end + + it "handles float arguments" do + Time.send(@method, 1.0, 15.0, 20.0, 1.0, 1.0, 2000.0, :ignored, :ignored, + :ignored, :ignored).should == + Time.send(@method, 1, 15, 20, 1, 1, 2000, :ignored, :ignored, :ignored, :ignored) + end + + it "raises an ArgumentError for out of range values" do + -> { + Time.send(@method, 61, 59, 23, 31, 12, 2008, :ignored, :ignored, :ignored, :ignored) + }.should raise_error(ArgumentError) # sec + + -> { + Time.send(@method, 59, 61, 23, 31, 12, 2008, :ignored, :ignored, :ignored, :ignored) + }.should raise_error(ArgumentError) # min + + -> { + Time.send(@method, 59, 59, 25, 31, 12, 2008, :ignored, :ignored, :ignored, :ignored) + }.should raise_error(ArgumentError) # hour + + -> { + Time.send(@method, 59, 59, 23, 32, 12, 2008, :ignored, :ignored, :ignored, :ignored) + }.should raise_error(ArgumentError) # day + + -> { + Time.send(@method, 59, 59, 23, 31, 13, 2008, :ignored, :ignored, :ignored, :ignored) + }.should raise_error(ArgumentError) # month + end +end + +describe :time_params_microseconds, shared: true do + it "handles microseconds" do + t = Time.send(@method, 2000, 1, 1, 20, 15, 1, 123) + t.usec.should == 123 + end + + it "raises an ArgumentError for out of range microsecond" do + -> { Time.send(@method, 2000, 1, 1, 20, 15, 1, 1000000) }.should raise_error(ArgumentError) + end + + it "handles fractional microseconds as a Float" do + t = Time.send(@method, 2000, 1, 1, 20, 15, 1, 1.75) + t.usec.should == 1 + t.nsec.should == 1750 + end + + it "handles fractional microseconds as a Rational" do + t = Time.send(@method, 2000, 1, 1, 20, 15, 1, Rational(99, 10)) + t.usec.should == 9 + t.nsec.should == 9900 + end + + it "ignores fractional seconds if a passed whole number of microseconds" do + t = Time.send(@method, 2000, 1, 1, 20, 15, 1.75, 2) + t.sec.should == 1 + t.usec.should == 2 + t.nsec.should == 2000 + end + + it "ignores fractional seconds if a passed fractional number of microseconds" do + t = Time.send(@method, 2000, 1, 1, 20, 15, 1.75, Rational(99, 10)) + t.sec.should == 1 + t.usec.should == 9 + t.nsec.should == 9900 + end +end diff --git a/spec/ruby/core/time/shared/to_i.rb b/spec/ruby/core/time/shared/to_i.rb new file mode 100644 index 0000000000..06c966b708 --- /dev/null +++ b/spec/ruby/core/time/shared/to_i.rb @@ -0,0 +1,16 @@ +describe :time_to_i, shared: true do + it "returns the value of time as an integer number of seconds since epoch" do + Time.at(0).send(@method).should == 0 + end + + it "doesn't return an actual number of seconds in time" do + Time.at(65.5).send(@method).should == 65 + end + + it "rounds fractional seconds toward zero" do + t = Time.utc(1960, 1, 1, 0, 0, 0, 999_999) + + t.to_f.to_i.should == -315619199 + t.to_i.should == -315619200 + end +end diff --git a/spec/ruby/core/time/shared/xmlschema.rb b/spec/ruby/core/time/shared/xmlschema.rb new file mode 100644 index 0000000000..d68c18df36 --- /dev/null +++ b/spec/ruby/core/time/shared/xmlschema.rb @@ -0,0 +1,31 @@ +describe :time_xmlschema, shared: true do + ruby_version_is "3.4" do + it "generates ISO-8601 strings in Z for UTC times" do + t = Time.utc(1985, 4, 12, 23, 20, 50, 521245) + t.send(@method).should == "1985-04-12T23:20:50Z" + t.send(@method, 2).should == "1985-04-12T23:20:50.52Z" + t.send(@method, 9).should == "1985-04-12T23:20:50.521245000Z" + end + + it "generates ISO-8601 string with timeone offset for non-UTC times" do + t = Time.new(1985, 4, 12, 23, 20, 50, "+02:00") + t.send(@method).should == "1985-04-12T23:20:50+02:00" + t.send(@method, 2).should == "1985-04-12T23:20:50.00+02:00" + end + + it "year is always at least 4 digits" do + t = Time.utc(12, 4, 12) + t.send(@method).should == "0012-04-12T00:00:00Z" + end + + it "year can be more than 4 digits" do + t = Time.utc(40_000, 4, 12) + t.send(@method).should == "40000-04-12T00:00:00Z" + end + + it "year can be negative" do + t = Time.utc(-2000, 4, 12) + t.send(@method).should == "-2000-04-12T00:00:00Z" + end + end +end diff --git a/spec/ruby/core/time/strftime_spec.rb b/spec/ruby/core/time/strftime_spec.rb new file mode 100644 index 0000000000..fd233f3577 --- /dev/null +++ b/spec/ruby/core/time/strftime_spec.rb @@ -0,0 +1,91 @@ +# encoding: utf-8 + +require_relative '../../spec_helper' +require_relative '../../shared/time/strftime_for_date' +require_relative '../../shared/time/strftime_for_time' + +describe "Time#strftime" do + before :all do + @new_date = -> y, m, d { Time.gm(y,m,d) } + @new_time = -> *args { Time.gm(*args) } + @new_time_in_zone = -> zone, offset, *args { + with_timezone(zone, offset) do + Time.new(*args) + end + } + @new_time_with_offset = -> y, m, d, h, min, s, offset { + Time.new(y,m,d,h,min,s,offset) + } + + @time = @new_time[2001, 2, 3, 4, 5, 6] + end + + it_behaves_like :strftime_date, :strftime + it_behaves_like :strftime_time, :strftime + + # Differences with date + it "requires an argument" do + -> { @time.strftime }.should raise_error(ArgumentError) + end + + # %Z is zone name or empty for Time + it "should be able to show the timezone if available" do + @time.strftime("%Z").should == @time.zone + with_timezone("UTC", 0) do + Time.gm(2000).strftime("%Z").should == "UTC" + end + + Time.new(2000, 1, 1, 0, 0, 0, 42).strftime("%Z").should == "" + end + + # %v is %e-%^b-%Y for Time + it "should be able to show the commercial week" do + @time.strftime("%v").should == " 3-FEB-2001" + @time.strftime("%v").should == @time.strftime('%e-%^b-%Y') + end + + # Date/DateTime round at creation time, but Time does it in strftime. + it "rounds an offset to the nearest second when formatting with %z" do + time = @new_time_with_offset[2012, 1, 1, 0, 0, 0, Rational(36645, 10)] + time.strftime("%::z").should == "+01:01:05" + end + + it "supports RFC 3339 UTC for unknown offset local time, -0000, as %-z" do + time = Time.gm(2022) + + time.strftime("%z").should == "+0000" + time.strftime("%-z").should == "-0000" + time.strftime("%-:z").should == "-00:00" + time.strftime("%-::z").should == "-00:00:00" + end + + it "applies '-' flag to UTC time" do + time = Time.utc(2022) + time.strftime("%-z").should == "-0000" + + time = Time.gm(2022) + time.strftime("%-z").should == "-0000" + + time = Time.new(2022, 1, 1, 0, 0, 0, "Z") + time.strftime("%-z").should == "-0000" + + time = Time.new(2022, 1, 1, 0, 0, 0, "-00:00") + time.strftime("%-z").should == "-0000" + + time = Time.new(2022, 1, 1, 0, 0, 0, "+03:00").utc + time.strftime("%-z").should == "-0000" + end + + it "ignores '-' flag for non-UTC time" do + time = Time.new(2022, 1, 1, 0, 0, 0, "+03:00") + time.strftime("%-z").should == "+0300" + end + + it "works correctly with width, _ and 0 flags, and :" do + Time.now.utc.strftime("%-_10z").should == " -000" + Time.now.utc.strftime("%-10z").should == "-000000000" + Time.now.utc.strftime("%-010:z").should == "-000000:00" + Time.now.utc.strftime("%-_10:z").should == " -0:00" + Time.now.utc.strftime("%-_10::z").should == " -0:00:00" + end +end diff --git a/spec/ruby/core/time/subsec_spec.rb b/spec/ruby/core/time/subsec_spec.rb new file mode 100644 index 0000000000..0f2c4eb856 --- /dev/null +++ b/spec/ruby/core/time/subsec_spec.rb @@ -0,0 +1,27 @@ +require_relative '../../spec_helper' + +describe "Time#subsec" do + it "returns 0 as an Integer for a Time with a whole number of seconds" do + Time.at(100).subsec.should eql(0) + end + + it "returns the fractional seconds as a Rational for a Time constructed with a Rational number of seconds" do + Time.at(Rational(3, 2)).subsec.should eql(Rational(1, 2)) + end + + it "returns the fractional seconds as a Rational for a Time constructed with a Float number of seconds" do + Time.at(10.75).subsec.should eql(Rational(3, 4)) + end + + it "returns the fractional seconds as a Rational for a Time constructed with an Integer number of microseconds" do + Time.at(0, 999999).subsec.should eql(Rational(999999, 1000000)) + end + + it "returns the fractional seconds as a Rational for a Time constructed with an Rational number of microseconds" do + Time.at(0, Rational(9, 10)).subsec.should eql(Rational(9, 10000000)) + end + + it "returns the fractional seconds as a Rational for a Time constructed with an Float number of microseconds" do + Time.at(0, 0.75).subsec.should eql(Rational(3, 4000000)) + end +end diff --git a/spec/ruby/core/time/sunday_spec.rb b/spec/ruby/core/time/sunday_spec.rb new file mode 100644 index 0000000000..0d46421132 --- /dev/null +++ b/spec/ruby/core/time/sunday_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' + +describe "Time#sunday?" do + it "returns true if time represents Sunday" do + Time.local(2000, 1, 2).should.sunday? + end + + it "returns false if time doesn't represent Sunday" do + Time.local(2000, 1, 1).should_not.sunday? + end +end diff --git a/spec/ruby/core/time/thursday_spec.rb b/spec/ruby/core/time/thursday_spec.rb new file mode 100644 index 0000000000..c11e79d2fa --- /dev/null +++ b/spec/ruby/core/time/thursday_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' + +describe "Time#thursday?" do + it "returns true if time represents Thursday" do + Time.local(2000, 1, 6).should.thursday? + end + + it "returns false if time doesn't represent Thursday" do + Time.local(2000, 1, 1).should_not.thursday? + end +end diff --git a/spec/ruby/core/time/time_spec.rb b/spec/ruby/core/time/time_spec.rb new file mode 100644 index 0000000000..b0803a7f21 --- /dev/null +++ b/spec/ruby/core/time/time_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' + +describe "Time" do + it "includes Comparable" do + Time.include?(Comparable).should == true + end +end diff --git a/spec/ruby/core/time/to_a_spec.rb b/spec/ruby/core/time/to_a_spec.rb new file mode 100644 index 0000000000..3728b8c526 --- /dev/null +++ b/spec/ruby/core/time/to_a_spec.rb @@ -0,0 +1,12 @@ +require_relative '../../spec_helper' + +describe "Time#to_a" do + platform_is_not :windows do + it "returns a 10 element array representing the deconstructed time" do + # Testing with America/Regina here because it doesn't have DST. + with_timezone("America/Regina") do + Time.at(0).to_a.should == [0, 0, 18, 31, 12, 1969, 3, 365, false, "CST"] + end + end + end +end diff --git a/spec/ruby/core/time/to_f_spec.rb b/spec/ruby/core/time/to_f_spec.rb new file mode 100644 index 0000000000..6101dcf871 --- /dev/null +++ b/spec/ruby/core/time/to_f_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' + +describe "Time#to_f" do + it "returns the float number of seconds + usecs since the epoch" do + Time.at(100, 100).to_f.should == 100.0001 + end +end diff --git a/spec/ruby/core/time/to_i_spec.rb b/spec/ruby/core/time/to_i_spec.rb new file mode 100644 index 0000000000..54929d1e18 --- /dev/null +++ b/spec/ruby/core/time/to_i_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/to_i' + +describe "Time#to_i" do + it_behaves_like :time_to_i, :to_i +end diff --git a/spec/ruby/core/time/to_r_spec.rb b/spec/ruby/core/time/to_r_spec.rb new file mode 100644 index 0000000000..6af2d9b7ea --- /dev/null +++ b/spec/ruby/core/time/to_r_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' + +describe "Time#to_r" do + it "returns the a Rational representing seconds and subseconds since the epoch" do + Time.at(Rational(11, 10)).to_r.should eql(Rational(11, 10)) + end + + it "returns a Rational even for a whole number of seconds" do + Time.at(2).to_r.should eql(Rational(2)) + end +end diff --git a/spec/ruby/core/time/to_s_spec.rb b/spec/ruby/core/time/to_s_spec.rb new file mode 100644 index 0000000000..ac6c0908ac --- /dev/null +++ b/spec/ruby/core/time/to_s_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/inspect' + +describe "Time#to_s" do + it_behaves_like :inspect, :to_s +end diff --git a/spec/ruby/core/time/tuesday_spec.rb b/spec/ruby/core/time/tuesday_spec.rb new file mode 100644 index 0000000000..0e7b9e7506 --- /dev/null +++ b/spec/ruby/core/time/tuesday_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' + +describe "Time#tuesday?" do + it "returns true if time represents Tuesday" do + Time.local(2000, 1, 4).should.tuesday? + end + + it "returns false if time doesn't represent Tuesday" do + Time.local(2000, 1, 1).should_not.tuesday? + end +end diff --git a/spec/ruby/core/time/tv_nsec_spec.rb b/spec/ruby/core/time/tv_nsec_spec.rb new file mode 100644 index 0000000000..feb7998b16 --- /dev/null +++ b/spec/ruby/core/time/tv_nsec_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../spec_helper' + +describe "Time#tv_nsec" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/time/tv_sec_spec.rb b/spec/ruby/core/time/tv_sec_spec.rb new file mode 100644 index 0000000000..f83e1fbfdd --- /dev/null +++ b/spec/ruby/core/time/tv_sec_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/to_i' + +describe "Time#tv_sec" do + it_behaves_like :time_to_i, :tv_sec +end diff --git a/spec/ruby/core/time/tv_usec_spec.rb b/spec/ruby/core/time/tv_usec_spec.rb new file mode 100644 index 0000000000..f0de4f4a9c --- /dev/null +++ b/spec/ruby/core/time/tv_usec_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../spec_helper' + +describe "Time#tv_usec" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/time/usec_spec.rb b/spec/ruby/core/time/usec_spec.rb new file mode 100644 index 0000000000..6ea52f5e79 --- /dev/null +++ b/spec/ruby/core/time/usec_spec.rb @@ -0,0 +1,43 @@ +require_relative '../../spec_helper' + +describe "Time#usec" do + it "returns 0 for a Time constructed with a whole number of seconds" do + Time.at(100).usec.should == 0 + end + + it "returns the microseconds part of a Time constructed with a Float number of seconds" do + Time.at(10.75).usec.should == 750_000 + end + + it "returns the microseconds part of a Time constructed with an Integer number of microseconds" do + Time.at(0, 999_999).usec.should == 999_999 + end + + it "returns the microseconds part of a Time constructed with an Float number of microseconds > 1" do + Time.at(0, 3.75).usec.should == 3 + end + + it "returns 0 for a Time constructed with an Float number of microseconds < 1" do + Time.at(0, 0.75).usec.should == 0 + end + + it "returns the microseconds part of a Time constructed with a Rational number of seconds" do + Time.at(Rational(3, 2)).usec.should == 500_000 + end + + it "returns the microseconds part of a Time constructed with an Rational number of microseconds > 1" do + Time.at(0, Rational(99, 10)).usec.should == 9 + end + + it "returns 0 for a Time constructed with an Rational number of microseconds < 1" do + Time.at(0, Rational(9, 10)).usec.should == 0 + end + + it "returns the microseconds for time created by Time#local" do + Time.local(1,2,3,4,5,Rational(6.78)).usec.should == 780000 + end + + it "returns a positive value for dates before the epoch" do + Time.utc(1969, 11, 12, 13, 18, 57, 404240).usec.should == 404240 + end +end diff --git a/spec/ruby/core/time/utc_offset_spec.rb b/spec/ruby/core/time/utc_offset_spec.rb new file mode 100644 index 0000000000..17c031b8c6 --- /dev/null +++ b/spec/ruby/core/time/utc_offset_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/gmt_offset' + +describe "Time#utc_offset" do + it_behaves_like :time_gmt_offset, :utc_offset +end diff --git a/spec/ruby/core/time/utc_spec.rb b/spec/ruby/core/time/utc_spec.rb new file mode 100644 index 0000000000..ab3c0df657 --- /dev/null +++ b/spec/ruby/core/time/utc_spec.rb @@ -0,0 +1,66 @@ +require_relative '../../spec_helper' +require_relative 'shared/gm' +require_relative 'shared/gmtime' +require_relative 'shared/time_params' + +describe "Time#utc?" do + it "returns true only if time represents a time in UTC (GMT)" do + Time.now.utc?.should == false + Time.now.utc.utc?.should == true + end + + it "treats time as UTC what was created in different ways" do + Time.now.utc.utc?.should == true + Time.now.gmtime.utc?.should == true + Time.now.getgm.utc?.should == true + Time.now.getutc.utc?.should == true + Time.utc(2022).utc?.should == true + end + + it "does treat time with 'UTC' offset as UTC" do + Time.new(2022, 1, 1, 0, 0, 0, "UTC").utc?.should == true + Time.now.localtime("UTC").utc?.should == true + Time.at(Time.now, in: 'UTC').utc?.should == true + + Time.new(2022, 1, 1, 0, 0, 0, in: "UTC").utc?.should == true + Time.now(in: "UTC").utc?.should == true + end + + it "does treat time with Z offset as UTC" do + Time.new(2022, 1, 1, 0, 0, 0, "Z").utc?.should == true + Time.now.localtime("Z").utc?.should == true + Time.at(Time.now, in: 'Z').utc?.should == true + + Time.new(2022, 1, 1, 0, 0, 0, in: "Z").utc?.should == true + Time.now(in: "Z").utc?.should == true + end + + it "does treat time with -00:00 offset as UTC" do + Time.new(2022, 1, 1, 0, 0, 0, "-00:00").utc?.should == true + Time.now.localtime("-00:00").utc?.should == true + Time.at(Time.now, in: '-00:00').utc?.should == true + end + + it "does not treat time with +00:00 offset as UTC" do + Time.new(2022, 1, 1, 0, 0, 0, "+00:00").utc?.should == false + Time.now.localtime("+00:00").utc?.should == false + Time.at(Time.now, in: "+00:00").utc?.should == false + end + + it "does not treat time with 0 offset as UTC" do + Time.new(2022, 1, 1, 0, 0, 0, 0).utc?.should == false + Time.now.localtime(0).utc?.should == false + Time.at(Time.now, in: 0).utc?.should == false + end +end + +describe "Time.utc" do + it_behaves_like :time_gm, :utc + it_behaves_like :time_params, :utc + it_behaves_like :time_params_10_arg, :utc + it_behaves_like :time_params_microseconds, :utc +end + +describe "Time#utc" do + it_behaves_like :time_gmtime, :utc +end diff --git a/spec/ruby/core/time/wday_spec.rb b/spec/ruby/core/time/wday_spec.rb new file mode 100644 index 0000000000..9f63f67de9 --- /dev/null +++ b/spec/ruby/core/time/wday_spec.rb @@ -0,0 +1,9 @@ +require_relative '../../spec_helper' + +describe "Time#wday" do + it "returns an integer representing the day of the week, 0..6, with Sunday being 0" do + with_timezone("GMT", 0) do + Time.at(0).wday.should == 4 + end + end +end diff --git a/spec/ruby/core/time/wednesday_spec.rb b/spec/ruby/core/time/wednesday_spec.rb new file mode 100644 index 0000000000..cc686681d7 --- /dev/null +++ b/spec/ruby/core/time/wednesday_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' + +describe "Time#wednesday?" do + it "returns true if time represents Wednesday" do + Time.local(2000, 1, 5).should.wednesday? + end + + it "returns false if time doesn't represent Wednesday" do + Time.local(2000, 1, 1).should_not.wednesday? + end +end diff --git a/spec/ruby/core/time/xmlschema_spec.rb b/spec/ruby/core/time/xmlschema_spec.rb new file mode 100644 index 0000000000..bdf1dc7923 --- /dev/null +++ b/spec/ruby/core/time/xmlschema_spec.rb @@ -0,0 +1,6 @@ +require_relative '../../spec_helper' +require_relative 'shared/xmlschema' + +describe "Time#xmlschema" do + it_behaves_like :time_xmlschema, :xmlschema +end diff --git a/spec/ruby/core/time/yday_spec.rb b/spec/ruby/core/time/yday_spec.rb new file mode 100644 index 0000000000..e920c2e28d --- /dev/null +++ b/spec/ruby/core/time/yday_spec.rb @@ -0,0 +1,12 @@ +require_relative '../../spec_helper' +require_relative '../../shared/time/yday' + +describe "Time#yday" do + it "returns an integer representing the day of the year, 1..366" do + with_timezone("UTC") do + Time.at(9999999).yday.should == 116 + end + end + + it_behaves_like :time_yday, -> year, month, day { Time.new(year, month, day).yday } +end diff --git a/spec/ruby/core/time/year_spec.rb b/spec/ruby/core/time/year_spec.rb new file mode 100644 index 0000000000..d2d50062c5 --- /dev/null +++ b/spec/ruby/core/time/year_spec.rb @@ -0,0 +1,17 @@ +require_relative '../../spec_helper' + +describe "Time#year" do + it "returns the four digit year for a local Time as an Integer" do + with_timezone("CET", 1) do + Time.local(1970).year.should == 1970 + end + end + + it "returns the four digit year for a UTC Time as an Integer" do + Time.utc(1970).year.should == 1970 + end + + it "returns the four digit year for a Time with a fixed offset" do + Time.new(2012, 1, 1, 0, 0, 0, -3600).year.should == 2012 + end +end diff --git a/spec/ruby/core/time/zone_spec.rb b/spec/ruby/core/time/zone_spec.rb new file mode 100644 index 0000000000..9a15bd569b --- /dev/null +++ b/spec/ruby/core/time/zone_spec.rb @@ -0,0 +1,111 @@ +require_relative '../../spec_helper' + +describe "Time#zone" do + platform_is_not :windows do + it "returns the time zone used for time" do + with_timezone("America/New_York") do + Time.new(2001, 1, 1, 0, 0, 0).zone.should == "EST" + Time.new(2001, 7, 1, 0, 0, 0).zone.should == "EDT" + %w[EST EDT].should include Time.now.zone + end + end + end + + it "returns nil for a Time with a fixed offset" do + Time.new(2001, 1, 1, 0, 0, 0, "+05:00").zone.should == nil + end + + platform_is_not :windows do + it "returns the correct timezone for a local time" do + t = Time.new(2005, 2, 27, 22, 50, 0, -3600) + + with_timezone("America/New_York") do + t.getlocal.zone.should == "EST" + end + end + end + + it "returns nil when getting the local time with a fixed offset" do + t = Time.new(2005, 2, 27, 22, 50, 0, -3600) + + with_timezone("America/New_York") do + t.getlocal("+05:00").zone.should be_nil + end + end + + describe "Encoding.default_internal is set" do + before :each do + @encoding = Encoding.default_internal + Encoding.default_internal = Encoding::UTF_8 + end + + after :each do + Encoding.default_internal = @encoding + end + + it "returns an ASCII string" do + t = Time.new(2005, 2, 27, 22, 50, 0, -3600) + + with_timezone("America/New_York") do + t.getlocal.zone.encoding.should == Encoding::US_ASCII + end + end + + it "doesn't raise errors for a Time with a fixed offset" do + Time.new(2001, 1, 1, 0, 0, 0, "+05:00").zone.should == nil + end + end + + it "returns UTC when called on a UTC time" do + Time.now.utc.zone.should == "UTC" + Time.now.gmtime.zone.should == "UTC" + Time.now.getgm.zone.should == "UTC" + Time.now.getutc.zone.should == "UTC" + Time.utc(2022).zone.should == "UTC" + Time.new(2022, 1, 1, 0, 0, 0, "UTC").zone.should == "UTC" + Time.new(2022, 1, 1, 0, 0, 0, "Z").zone.should == "UTC" + Time.now.localtime("UTC").zone.should == "UTC" + Time.now.localtime("Z").zone.should == "UTC" + Time.at(Time.now, in: 'UTC').zone.should == "UTC" + Time.at(Time.now, in: 'Z').zone.should == "UTC" + + Time.new(2022, 1, 1, 0, 0, 0, "-00:00").zone.should == "UTC" + Time.now.localtime("-00:00").zone.should == "UTC" + Time.at(Time.now, in: '-00:00').zone.should == "UTC" + + Time.new(2022, 1, 1, 0, 0, 0, in: "UTC").zone.should == "UTC" + Time.new(2022, 1, 1, 0, 0, 0, in: "Z").zone.should == "UTC" + + Time.now(in: 'UTC').zone.should == "UTC" + Time.now(in: 'Z').zone.should == "UTC" + + Time.at(Time.now, in: 'UTC').zone.should == "UTC" + Time.at(Time.now, in: 'Z').zone.should == "UTC" + end + + platform_is_not :aix, :windows do + it "defaults to UTC when bad zones given" do + with_timezone("hello-foo") do + Time.now.utc_offset.should == 0 + end + with_timezone("1,2") do + Time.now.utc_offset.should == 0 + end + with_timezone("Sun,Fri,2") do + Time.now.utc_offset.should == 0 + end + end + end + + platform_is :windows do + # See https://bugs.ruby-lang.org/issues/13591#note-11 + it "defaults to UTC when bad zones given" do + with_timezone("1,2") do + Time.now.utc_offset.should == 0 + end + with_timezone("12") do + Time.now.utc_offset.should == 0 + end + end + end +end diff --git a/spec/ruby/core/tracepoint/allow_reentry_spec.rb b/spec/ruby/core/tracepoint/allow_reentry_spec.rb new file mode 100644 index 0000000000..75e9e859a9 --- /dev/null +++ b/spec/ruby/core/tracepoint/allow_reentry_spec.rb @@ -0,0 +1,30 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe 'TracePoint.allow_reentry' do + it 'allows the reentrance in a given block' do + event_lines = [] + l1 = l2 = l3 = l4 = nil + TracePoint.new(:line) do |tp| + next unless TracePointSpec.target_thread? + + event_lines << tp.lineno + next if (__LINE__ + 2 .. __LINE__ + 4).cover?(tp.lineno) + TracePoint.allow_reentry do + a = 1; l3 = __LINE__ + b = 2; l4 = __LINE__ + end + end.enable do + c = 3; l1 = __LINE__ + d = 4; l2 = __LINE__ + end + + event_lines.should == [l1, l3, l4, l2, l3, l4] + end + + it 'raises RuntimeError when not called inside a TracePoint' do + -> { + TracePoint.allow_reentry{} + }.should raise_error(RuntimeError) + end +end diff --git a/spec/ruby/core/tracepoint/binding_spec.rb b/spec/ruby/core/tracepoint/binding_spec.rb new file mode 100644 index 0000000000..6a7ef5f85a --- /dev/null +++ b/spec/ruby/core/tracepoint/binding_spec.rb @@ -0,0 +1,21 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe 'TracePoint#binding' do + def test + secret = 42 + end + + it 'return the generated binding object from event' do + bindings = [] + TracePoint.new(:return) { |tp| + next unless TracePointSpec.target_thread? + bindings << tp.binding + }.enable { + test + } + bindings.size.should == 1 + bindings[0].should be_kind_of(Binding) + bindings[0].local_variables.should == [:secret] + end +end diff --git a/spec/ruby/core/tracepoint/callee_id_spec.rb b/spec/ruby/core/tracepoint/callee_id_spec.rb new file mode 100644 index 0000000000..cc08a45504 --- /dev/null +++ b/spec/ruby/core/tracepoint/callee_id_spec.rb @@ -0,0 +1,18 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "TracePoint#callee_id" do + it "returns the called name of the method being called" do + a = [] + obj = TracePointSpec::ClassWithMethodAlias.new + + TracePoint.new(:call) do |tp| + next unless TracePointSpec.target_thread? + a << tp.callee_id + end.enable do + obj.m_alias + end + + a.should == [:m_alias] + end +end diff --git a/spec/ruby/core/tracepoint/defined_class_spec.rb b/spec/ruby/core/tracepoint/defined_class_spec.rb new file mode 100644 index 0000000000..4593db6d1f --- /dev/null +++ b/spec/ruby/core/tracepoint/defined_class_spec.rb @@ -0,0 +1,27 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe 'TracePoint#defined_class' do + it 'returns class or module of the method being called' do + last_class_name = nil + TracePoint.new(:call) do |tp| + next unless TracePointSpec.target_thread? + last_class_name = tp.defined_class + end.enable do + TracePointSpec::B.new.foo + last_class_name.should equal(TracePointSpec::B) + + TracePointSpec::B.new.bar + last_class_name.should equal(TracePointSpec::A) + + c = TracePointSpec::C.new + last_class_name.should equal(TracePointSpec::C) + + c.foo + last_class_name.should equal(TracePointSpec::B) + + c.bar + last_class_name.should equal(TracePointSpec::A) + end + end +end diff --git a/spec/ruby/core/tracepoint/disable_spec.rb b/spec/ruby/core/tracepoint/disable_spec.rb new file mode 100644 index 0000000000..73a31b3b81 --- /dev/null +++ b/spec/ruby/core/tracepoint/disable_spec.rb @@ -0,0 +1,76 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe 'TracePoint#disable' do + it 'returns true if trace was enabled' do + called = false + trace = TracePoint.new(:line) do |tp| + next unless TracePointSpec.target_thread? + called = true + end + + trace.enable + begin + line_event = true + ensure + ret = trace.disable + ret.should == true + end + called.should == true + + # Check the TracePoint is disabled + called = false + line_event = true + called.should == false + end + + it 'returns false if trace was disabled' do + called = false + trace = TracePoint.new(:line) do |tp| + next unless TracePointSpec.target_thread? + called = true + end + + line_event = true + trace.disable.should == false + line_event = true + called.should == false + end + + it 'is disabled within a block & is enabled outside the block' do + enabled = nil + trace = TracePoint.new(:line) {} + trace.enable + begin + trace.disable { enabled = trace.enabled? } + enabled.should == false + trace.should.enabled? + ensure + trace.disable + end + end + + it 'returns the return value of the block' do + trace = TracePoint.new(:line) {} + trace.enable + begin + trace.disable { 42 }.should == 42 + trace.should.enabled? + ensure + trace.disable + end + end + + it 'can accept param within a block but it should not yield arguments' do + trace = TracePoint.new(:line) {} + trace.enable + begin + trace.disable do |*args| + args.should == [] + end + trace.should.enabled? + ensure + trace.disable + end + end +end diff --git a/spec/ruby/core/tracepoint/enable_spec.rb b/spec/ruby/core/tracepoint/enable_spec.rb new file mode 100644 index 0000000000..93a6b281e3 --- /dev/null +++ b/spec/ruby/core/tracepoint/enable_spec.rb @@ -0,0 +1,543 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe 'TracePoint#enable' do + describe 'without a block' do + it 'returns false if trace was disabled' do + called = false + trace = TracePoint.new(:line) do |tp| + next unless TracePointSpec.target_thread? + called = true + end + + line_event = true + called.should == false + + ret = trace.enable + begin + ret.should == false + line_event = true + called.should == true + ensure + trace.disable + end + end + + it 'returns true if trace was already enabled' do + called = false + trace = TracePoint.new(:line) do |tp| + next unless TracePointSpec.target_thread? + called = true + end + + line_event = true + called.should == false + + ret = trace.enable + begin + ret.should == false + + trace.enable.should == true + + line_event = true + called.should == true + ensure + trace.disable + trace.should_not.enabled? + end + end + end + + describe 'with a block' do + it 'enables the trace object within a block' do + event_name = nil + TracePoint.new(:line) do |tp| + next unless TracePointSpec.target_thread? + event_name = tp.event + end.enable { event_name.should equal(:line) } + end + + it 'enables the trace object only for the current thread' do + threads = [] + trace = TracePoint.new(:line) do |tp| + # Runs on purpose on any Thread + threads << Thread.current + end + + thread = nil + trace.enable do + line_event = true + thread = Thread.new do + event_in_other_thread = true + end + thread.join + end + + threads = threads.uniq + threads.should.include?(Thread.current) + threads.should_not.include?(thread) + end + + it 'can accept arguments within a block but it should not yield arguments' do + event_name = nil + trace = TracePoint.new(:line) do |tp| + next unless TracePointSpec.target_thread? + event_name = tp.event + end + trace.enable do |*args| + event_name.should equal(:line) + args.should == [] + end + trace.should_not.enabled? + end + + it 'enables trace object on calling with a block if it was already enabled' do + enabled = nil + trace = TracePoint.new(:line) {} + trace.enable + begin + trace.enable { enabled = trace.enabled? } + enabled.should == true + ensure + trace.disable + end + end + + it 'returns the return value of the block' do + trace = TracePoint.new(:line) {} + trace.enable { 42 }.should == 42 + end + + it 'disables the trace object outside the block' do + called = false + trace = TracePoint.new(:line) do + next unless TracePointSpec.target_thread? + called = true + end + trace.enable { + line_event = true + } + called.should == true + trace.should_not.enabled? + end + end + + describe "when nested" do + before do + @path_prefix = ' ' + end + + it "enables both TracePoints but only calls the respective callbacks" do + called = false + first = TracePoint.new(:line) do |tp| + next unless TracePointSpec.target_thread? + called = true + end + + all = [] + inspects = [] + second = TracePoint.new(:line) { |tp| + next unless TracePointSpec.target_thread? + all << tp + inspects << tp.inspect + } + + line = nil + first.enable do + second.enable do + line = __LINE__ + end + end + + all.uniq.should == [second] + inspects.uniq.should == ["#<TracePoint:line#{@path_prefix}#{__FILE__}:#{line}>"] + called.should == true + end + end + + describe 'target: option' do + before :each do + ScratchPad.record [] + end + + it 'enables trace point for specific location' do + trace = TracePoint.new(:call) do |tp| + next unless TracePointSpec.target_thread? + ScratchPad << tp.method_id + end + + obj = Object.new + def obj.foo; end + def obj.bar; end + + trace.enable(target: obj.method(:foo)) do + obj.foo + obj.bar + end + + ScratchPad.recorded.should == [:foo] + end + + it 'traces all the events triggered in specified location' do + trace = TracePoint.new(:line, :call, :return, :b_call, :b_return) do |tp| + next unless TracePointSpec.target_thread? + ScratchPad << tp.event + end + + obj = Object.new + def obj.foo + bar + -> {}.call + end + def obj.bar; end + + trace.enable(target: obj.method(:foo)) do + obj.foo + end + + ScratchPad.recorded.uniq.sort.should == [:call, :return, :b_call, :b_return, :line].sort + end + + it 'does not trace events in nested locations' do + trace = TracePoint.new(:call) do |tp| + next unless TracePointSpec.target_thread? + ScratchPad << tp.method_id + end + + obj = Object.new + def obj.foo + bar + end + def obj.bar + baz + end + def obj.baz + end + + trace.enable(target: obj.method(:foo)) do + obj.foo + end + + ScratchPad.recorded.should == [:foo] + end + + it "traces some events in nested blocks" do + klass = Class.new do + def foo + 1.times do + 1.times do + bar do + end + end + end + end + + def bar(&blk) + blk.call + end + end + + trace = TracePoint.new(:b_call) do |tp| + next unless TracePointSpec.target_thread? + ScratchPad << tp.lineno + end + + obj = klass.new + _, lineno = obj.method(:foo).source_location + + trace.enable(target: obj.method(:foo)) do + obj.foo + end + + ScratchPad.recorded.should == (lineno+1..lineno+3).to_a + end + + describe 'option value' do + it 'accepts Method' do + trace = TracePoint.new(:call) do |tp| + next unless TracePointSpec.target_thread? + ScratchPad << tp.method_id + end + + obj = Object.new + def obj.foo; end + + trace.enable(target: obj.method(:foo)) do + obj.foo + end + + ScratchPad.recorded.should == [:foo] + end + + it 'accepts UnboundMethod' do + trace = TracePoint.new(:call) do |tp| + next unless TracePointSpec.target_thread? + ScratchPad << tp.method_id + end + + klass = Class.new do + def foo; end + end + + unbound_method = klass.instance_method(:foo) + trace.enable(target: unbound_method) do + klass.new.foo + end + + ScratchPad.recorded.should == [:foo] + end + + it 'accepts Proc' do + trace = TracePoint.new(:b_call) do |tp| + next unless TracePointSpec.target_thread? + ScratchPad << tp.lineno + end + + block = proc {} + _, lineno = block.source_location + + trace.enable(target: block) do + block.call + end + + ScratchPad.recorded.should == [lineno] + lineno.should be_kind_of(Integer) + end + end + + it "raises ArgumentError if target object cannot trigger specified event" do + trace = TracePoint.new(:call) do |tp| + next unless TracePointSpec.target_thread? + ScratchPad << tp.method_id + end + + block = proc {} + + -> { + trace.enable(target: block) do + block.call # triggers :b_call and :b_return events + end + }.should raise_error(ArgumentError, /can not enable any hooks/) + end + + it "raises ArgumentError if passed not Method/UnboundMethod/Proc" do + trace = TracePoint.new(:call) {} + + -> { + trace.enable(target: Object.new) do + end + }.should raise_error(ArgumentError, /specified target is not supported/) + end + + context "nested enabling and disabling" do + it "raises ArgumentError if trace point already enabled with target is re-enabled with target" do + trace = TracePoint.new(:b_call) {} + + -> { + trace.enable(target: -> {}) do + trace.enable(target: -> {}) do + end + end + }.should raise_error(ArgumentError, /can't nest-enable a targett?ing TracePoint/) + end + + it "raises ArgumentError if trace point already enabled without target is re-enabled with target" do + trace = TracePoint.new(:b_call) {} + + -> { + trace.enable do + trace.enable(target: -> {}) do + end + end + }.should raise_error(ArgumentError, /can't nest-enable a targett?ing TracePoint/) + end + + it "raises ArgumentError if trace point already enabled with target is re-enabled without target" do + trace = TracePoint.new(:b_call) {} + + -> { + trace.enable(target: -> {}) do + trace.enable do + end + end + }.should raise_error(ArgumentError, /can't nest-enable a targett?ing TracePoint/) + end + + it "raises ArgumentError if trace point already enabled with target is disabled with block" do + trace = TracePoint.new(:b_call) {} + + -> { + trace.enable(target: -> {}) do + trace.disable do + end + end + }.should raise_error(ArgumentError, /can't disable a targett?ing TracePoint in a block/) + end + + it "traces events when trace point with target is enabled in another trace point enabled without target" do + trace_outer = TracePoint.new(:b_call) do |tp| + next unless TracePointSpec.target_thread? + ScratchPad << :outer + end + + trace_inner = TracePoint.new(:b_call) do |tp| + next unless TracePointSpec.target_thread? + ScratchPad << :inner + end + + target = -> {} + + trace_outer.enable do + trace_inner.enable(target: target) do + target.call + end + end + + ScratchPad.recorded.should == [:outer, :outer, :outer, :inner] + end + + it "traces events when trace point with target is enabled in another trace point enabled with target" do + trace_outer = TracePoint.new(:b_call) do |tp| + next unless TracePointSpec.target_thread? + ScratchPad << :outer + end + + trace_inner = TracePoint.new(:b_call) do |tp| + next unless TracePointSpec.target_thread? + ScratchPad << :inner + end + + target = -> {} + + trace_outer.enable(target: target) do + trace_inner.enable(target: target) do + target.call + end + end + + ScratchPad.recorded.should == [:inner, :outer] + end + + it "traces events when trace point without target is enabled in another trace point enabled with target" do + trace_outer = TracePoint.new(:b_call) do |tp| + next unless TracePointSpec.target_thread? + ScratchPad << :outer + end + + trace_inner = TracePoint.new(:b_call) do |tp| + next unless TracePointSpec.target_thread? + ScratchPad << :inner + end + + target = -> {} + + trace_outer.enable(target: target) do + trace_inner.enable do + target.call + end + end + + ScratchPad.recorded.should == [:inner, :inner, :outer] + end + end + end + + describe 'target_line: option' do + before :each do + ScratchPad.record [] + end + + it "traces :line events only on specified line of code" do + trace = TracePoint.new(:line) do |tp| + next unless TracePointSpec.target_thread? + ScratchPad << tp.lineno + end + + target = -> { + x = 1 + y = 2 # <= this line is target + z = x + y + } + _, lineno = target.source_location + target_line = lineno + 2 + + trace.enable(target_line: target_line, target: target) do + target.call + end + + ScratchPad.recorded.should == [target_line] + end + + it "raises ArgumentError if :target option isn't specified" do + trace = TracePoint.new(:line) {} + + -> { + trace.enable(target_line: 67) do + end + }.should raise_error(ArgumentError, /only target_line is specified/) + end + + it "raises ArgumentError if :line event isn't registered" do + trace = TracePoint.new(:call) {} + + target = -> { + x = 1 + y = 2 # <= this line is target + z = x + y + } + _, lineno = target.source_location + target_line = lineno + 2 + + -> { + trace.enable(target_line: target_line, target: target) do + end + }.should raise_error(ArgumentError, /target_line is specified, but line event is not specified/) + end + + it "raises ArgumentError if :target_line value is out of target code lines range" do + trace = TracePoint.new(:line) {} + + -> { + trace.enable(target_line: 1, target: -> { }) do + end + }.should raise_error(ArgumentError, /can not enable any hooks/) + end + + it "raises TypeError if :target_line value couldn't be coerced to Integer" do + trace = TracePoint.new(:line) {} + + -> { + trace.enable(target_line: Object.new, target: -> { }) do + end + }.should raise_error(TypeError, /no implicit conversion of \w+? into Integer/) + end + + it "raises ArgumentError if :target_line value is negative" do + trace = TracePoint.new(:line) {} + + -> { + trace.enable(target_line: -2, target: -> { }) do + end + }.should raise_error(ArgumentError, /can not enable any hooks/) + end + + it "accepts value that could be coerced to Integer" do + trace = TracePoint.new(:line) do |tp| + next unless TracePointSpec.target_thread? + ScratchPad << tp.lineno + end + + target = -> { + x = 1 # <= this line is target + } + _, lineno = target.source_location + target_line = lineno + 1 + + trace.enable(target_line: target_line.to_r, target: target) do + target.call + end + + ScratchPad.recorded.should == [target_line] + end + end +end diff --git a/spec/ruby/core/tracepoint/enabled_spec.rb b/spec/ruby/core/tracepoint/enabled_spec.rb new file mode 100644 index 0000000000..0e9566a02c --- /dev/null +++ b/spec/ruby/core/tracepoint/enabled_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe 'TracePoint#enabled?' do + it 'returns true when current status of the trace is enable' do + trace = TracePoint.new(:line) {} + trace.enable do + trace.should.enabled? + end + end + + it 'returns false when current status of the trace is disabled' do + TracePoint.new(:line) {}.should_not.enabled? + end +end diff --git a/spec/ruby/core/tracepoint/eval_script_spec.rb b/spec/ruby/core/tracepoint/eval_script_spec.rb new file mode 100644 index 0000000000..7ec53e7094 --- /dev/null +++ b/spec/ruby/core/tracepoint/eval_script_spec.rb @@ -0,0 +1,23 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "TracePoint#eval_script" do + it "is the evald source code" do + ScratchPad.record [] + + script = <<-CODE + def foo + p :hello + end + CODE + + TracePoint.new(:script_compiled) do |e| + next unless TracePointSpec.target_thread? + ScratchPad << e.eval_script + end.enable do + eval script + end + + ScratchPad.recorded.should == [script] + end +end diff --git a/spec/ruby/core/tracepoint/event_spec.rb b/spec/ruby/core/tracepoint/event_spec.rb new file mode 100644 index 0000000000..9dea24d125 --- /dev/null +++ b/spec/ruby/core/tracepoint/event_spec.rb @@ -0,0 +1,22 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe 'TracePoint#event' do + it 'returns the type of event' do + event_name = nil + TracePoint.new(:end, :call) do |tp| + next unless TracePointSpec.target_thread? + event_name = tp.event + end.enable do + TracePointSpec.test + event_name.should equal(:call) + + TracePointSpec::B.new.foo + event_name.should equal(:call) + + class TracePointSpec::B; end + event_name.should equal(:end) + end + + end +end diff --git a/spec/ruby/core/tracepoint/fixtures/classes.rb b/spec/ruby/core/tracepoint/fixtures/classes.rb new file mode 100644 index 0000000000..3ab1b00b16 --- /dev/null +++ b/spec/ruby/core/tracepoint/fixtures/classes.rb @@ -0,0 +1,40 @@ +module TracePointSpec + @thread = Thread.current + + def self.target_thread? + Thread.current == @thread + end + + class ClassWithMethodAlias + def m + end + alias_method :m_alias, :m + end + + module A + def bar; end + end + + class B + include A + + def foo; end; + end + + class C < B + def initialize + end + + def foo + super + end + + def bar + super + end + end + + def self.test + 'test' + end +end diff --git a/spec/ruby/core/tracepoint/inspect_spec.rb b/spec/ruby/core/tracepoint/inspect_spec.rb new file mode 100644 index 0000000000..6cc2ebe243 --- /dev/null +++ b/spec/ruby/core/tracepoint/inspect_spec.rb @@ -0,0 +1,141 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe 'TracePoint#inspect' do + before do + @path_prefix = ' ' + end + + it 'returns a string containing a human-readable TracePoint status' do + TracePoint.new(:line) {}.inspect.should == '#<TracePoint:disabled>' + end + + it "shows only whether it's enabled when outside the TracePoint handler" do + trace = TracePoint.new(:line) {} + trace.enable + + trace.inspect.should == '#<TracePoint:enabled>' + + trace.disable + end + + it 'returns a String showing the event, path and line' do + inspect = nil + line = nil + TracePoint.new(:line) { |tp| + next unless TracePointSpec.target_thread? + next unless tp.path == __FILE__ + + inspect ||= tp.inspect + }.enable do + line = __LINE__ + end + + inspect.should == "#<TracePoint:line#{@path_prefix}#{__FILE__}:#{line}>" + end + + it 'returns a String showing the event, method, path and line for a :call event' do + inspect = nil + line = nil + TracePoint.new(:call) { |tp| + next unless TracePointSpec.target_thread? + next unless tp.path == __FILE__ + + inspect ||= tp.inspect + }.enable do + line = __LINE__ + 1 + def trace_point_spec_test_call; end + trace_point_spec_test_call + end + + inspect.should =~ /\A#<TracePoint:call [`']trace_point_spec_test_call'#{@path_prefix}#{__FILE__}:#{line}>\z/ + end + + it 'returns a String showing the event, method, path and line for a :return event' do + inspect = nil + line = nil + TracePoint.new(:return) { |tp| + next unless TracePointSpec.target_thread? + next unless tp.path == __FILE__ + + inspect ||= tp.inspect + }.enable do + line = __LINE__ + 4 + def trace_point_spec_test_return + a = 1 + return a + end + trace_point_spec_test_return + end + ruby_version_is("3.4") { line -= 1 } + + inspect.should =~ /\A#<TracePoint:return [`']trace_point_spec_test_return'#{@path_prefix}#{__FILE__}:#{line}>\z/ + end + + it 'returns a String showing the event, method, path and line for a :c_call event' do + inspect = nil + tracepoint = TracePoint.new(:c_call) { |tp| + next unless TracePointSpec.target_thread? + next unless tp.path == __FILE__ + + inspect ||= tp.inspect + } + line = __LINE__ + 2 + tracepoint.enable do + [0, 1].max + end + + inspect.should =~ /\A#<TracePoint:c_call [`']max'#{@path_prefix}#{__FILE__}:#{line}>\z/ + end + + it 'returns a String showing the event, path and line for a :class event' do + inspect = nil + line = nil + TracePoint.new(:class) { |tp| + next unless TracePointSpec.target_thread? + next unless tp.path == __FILE__ + + inspect ||= tp.inspect + }.enable do + line = __LINE__ + 1 + class TracePointSpec::C + end + end + + inspect.should == "#<TracePoint:class#{@path_prefix}#{__FILE__}:#{line}>" + end + + it 'returns a String showing the event and thread for :thread_begin event' do + inspect = nil + thread = nil + thread_inspection = nil + TracePoint.new(:thread_begin) { |tp| + next unless Thread.current == thread + + inspect ||= tp.inspect + }.enable(target_thread: nil) do + thread = Thread.new {} + thread_inspection = thread.inspect + thread.join + end + + inspect.should == "#<TracePoint:thread_begin #{thread_inspection}>" + end + + it 'returns a String showing the event and thread for :thread_end event' do + inspect = nil + thread = nil + thread_inspection = nil + TracePoint.new(:thread_end) { |tp| + next unless Thread.current == thread + + inspect ||= tp.inspect + }.enable(target_thread: nil) do + thread = Thread.new {} + thread_inspection = thread.inspect + thread.join + end + + inspect.should == "#<TracePoint:thread_end #{thread_inspection}>" + end +end diff --git a/spec/ruby/core/tracepoint/lineno_spec.rb b/spec/ruby/core/tracepoint/lineno_spec.rb new file mode 100644 index 0000000000..77b3ac8b54 --- /dev/null +++ b/spec/ruby/core/tracepoint/lineno_spec.rb @@ -0,0 +1,20 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe 'TracePoint#lineno' do + it 'returns the line number of the event' do + lineno = nil + TracePoint.new(:line) { |tp| + next unless TracePointSpec.target_thread? + lineno = tp.lineno + }.enable do + line_event = true + end + lineno.should == __LINE__ - 2 + end + + it 'raises RuntimeError if accessed from outside' do + tp = TracePoint.new(:line) {} + -> { tp.lineno }.should raise_error(RuntimeError, 'access from outside') + end +end diff --git a/spec/ruby/core/tracepoint/method_id_spec.rb b/spec/ruby/core/tracepoint/method_id_spec.rb new file mode 100644 index 0000000000..43e23248b7 --- /dev/null +++ b/spec/ruby/core/tracepoint/method_id_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe 'TracePoint#method_id' do + it 'returns the name at the definition of the method being called' do + method_name = nil + TracePoint.new(:call) { |tp| + next unless TracePointSpec.target_thread? + method_name = tp.method_id + }.enable do + TracePointSpec.test + method_name.should equal(:test) + end + end +end diff --git a/spec/ruby/core/tracepoint/new_spec.rb b/spec/ruby/core/tracepoint/new_spec.rb new file mode 100644 index 0000000000..e53c2b04a2 --- /dev/null +++ b/spec/ruby/core/tracepoint/new_spec.rb @@ -0,0 +1,72 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe 'TracePoint.new' do + it 'returns a new TracePoint object, not enabled by default' do + TracePoint.new(:line) {}.enabled?.should be_false + end + + it 'includes :line event when event is not specified' do + event_name = nil + TracePoint.new { |tp| + next unless TracePointSpec.target_thread? + event_name = tp.event + }.enable do + event_name.should equal(:line) + + event_name = nil + TracePointSpec.test + event_name.should equal(:line) + + event_name = nil + TracePointSpec::B.new.foo + event_name.should equal(:line) + end + end + + it 'converts given event name as string into symbol using to_sym' do + event_name = nil + (o = mock('line')).should_receive(:to_sym).and_return(:line) + + TracePoint.new(o) { |tp| + next unless TracePointSpec.target_thread? + event_name = tp.event + }.enable do + line_event = true + event_name.should == :line + end + end + + it 'includes multiple events when multiple event names are passed as params' do + event_name = nil + TracePoint.new(:end, :call) do |tp| + next unless TracePointSpec.target_thread? + event_name = tp.event + end.enable do + TracePointSpec.test + event_name.should equal(:call) + + TracePointSpec::B.new.foo + event_name.should equal(:call) + + class TracePointSpec::B; end + event_name.should equal(:end) + end + end + + it 'raises a TypeError when the given object is not a string/symbol' do + o = mock('123') + -> { TracePoint.new(o) {} }.should raise_error(TypeError) + + o.should_receive(:to_sym).and_return(123) + -> { TracePoint.new(o) {} }.should raise_error(TypeError) + end + + it 'expects to be called with a block' do + -> { TracePoint.new(:line) }.should raise_error(ArgumentError, "must be called with a block") + end + + it "raises a Argument error when the given argument doesn't match an event name" do + -> { TracePoint.new(:test) }.should raise_error(ArgumentError, "unknown event: test") + end +end diff --git a/spec/ruby/core/tracepoint/parameters_spec.rb b/spec/ruby/core/tracepoint/parameters_spec.rb new file mode 100644 index 0000000000..82aee3caa4 --- /dev/null +++ b/spec/ruby/core/tracepoint/parameters_spec.rb @@ -0,0 +1,28 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe 'TracePoint#parameters' do + it 'returns the parameters of block' do + f = proc {|x, y, z| } + parameters = nil + TracePoint.new(:b_call) { |tp| + next unless TracePointSpec.target_thread? + parameters = tp.parameters + }.enable do + f.call + parameters.should == [[:opt, :x], [:opt, :y], [:opt, :z]] + end + end + + it 'returns the parameters of lambda block' do + f = -> x, y, z { } + parameters = nil + TracePoint.new(:b_call) { |tp| + next unless TracePointSpec.target_thread? + parameters = tp.parameters + }.enable do + f.call(1, 2, 3) + parameters.should == [[:req, :x], [:req, :y], [:req, :z]] + end + end +end diff --git a/spec/ruby/core/tracepoint/path_spec.rb b/spec/ruby/core/tracepoint/path_spec.rb new file mode 100644 index 0000000000..dc2ca840b8 --- /dev/null +++ b/spec/ruby/core/tracepoint/path_spec.rb @@ -0,0 +1,41 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe 'TracePoint#path' do + it 'returns the path of the file being run' do + path = nil + TracePoint.new(:line) { |tp| + next unless TracePointSpec.target_thread? + path = tp.path + }.enable do + line_event = true + end + path.should == "#{__FILE__}" + end + + ruby_version_is ""..."3.3" do + it 'equals (eval) inside an eval for :end event' do + path = nil + TracePoint.new(:end) { |tp| + next unless TracePointSpec.target_thread? + path = tp.path + }.enable do + eval("module TracePointSpec; end") + end + path.should == '(eval)' + end + end + + ruby_version_is "3.3" do + it 'equals "(eval at __FILE__:__LINE__)" inside an eval for :end event' do + path = nil + TracePoint.new(:end) { |tp| + next unless TracePointSpec.target_thread? + path = tp.path + }.enable do + eval("module TracePointSpec; end") + end + path.should == "(eval at #{__FILE__}:#{__LINE__ - 2})" + end + end +end diff --git a/spec/ruby/core/tracepoint/raised_exception_spec.rb b/spec/ruby/core/tracepoint/raised_exception_spec.rb new file mode 100644 index 0000000000..5ac8531840 --- /dev/null +++ b/spec/ruby/core/tracepoint/raised_exception_spec.rb @@ -0,0 +1,38 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe 'TracePoint#raised_exception' do + it 'returns value from exception raised on the :raise event' do + raised_exception, error_result = nil + trace = TracePoint.new(:raise) { |tp| + next unless TracePointSpec.target_thread? + raised_exception = tp.raised_exception + } + trace.enable do + begin + raise StandardError + rescue => e + error_result = e + end + raised_exception.should equal(error_result) + end + end + + ruby_version_is "3.3" do + it 'returns value from exception rescued on the :rescue event' do + raised_exception, error_result = nil + trace = TracePoint.new(:rescue) { |tp| + next unless TracePointSpec.target_thread? + raised_exception = tp.raised_exception + } + trace.enable do + begin + raise StandardError + rescue => e + error_result = e + end + raised_exception.should equal(error_result) + end + end + end +end diff --git a/spec/ruby/core/tracepoint/return_value_spec.rb b/spec/ruby/core/tracepoint/return_value_spec.rb new file mode 100644 index 0000000000..e84c7dd762 --- /dev/null +++ b/spec/ruby/core/tracepoint/return_value_spec.rb @@ -0,0 +1,17 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe 'TracePoint#return_value' do + def test; 'test' end + + it 'returns value from :return event' do + trace_value = nil + TracePoint.new(:return) { |tp| + next unless TracePointSpec.target_thread? + trace_value = tp.return_value + }.enable do + test + trace_value.should == 'test' + end + end +end diff --git a/spec/ruby/core/tracepoint/self_spec.rb b/spec/ruby/core/tracepoint/self_spec.rb new file mode 100644 index 0000000000..2098860e59 --- /dev/null +++ b/spec/ruby/core/tracepoint/self_spec.rb @@ -0,0 +1,26 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe 'TracePoint#self' do + it 'return the trace object from event' do + trace = nil + TracePoint.new(:line) { |tp| + next unless TracePointSpec.target_thread? + trace = tp.self + }.enable do + trace.equal?(self).should be_true + end + end + + it 'return the class object from a class event' do + trace = nil + TracePoint.new(:class) { |tp| + next unless TracePointSpec.target_thread? + trace = tp.self + }.enable do + class TracePointSpec::C + end + end + trace.should equal TracePointSpec::C + end +end diff --git a/spec/ruby/core/tracepoint/trace_spec.rb b/spec/ruby/core/tracepoint/trace_spec.rb new file mode 100644 index 0000000000..167f594bb9 --- /dev/null +++ b/spec/ruby/core/tracepoint/trace_spec.rb @@ -0,0 +1,10 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe 'TracePoint.trace' do + it 'activates the trace automatically' do + trace = TracePoint.trace(:line) {} + trace.should.enabled? + trace.disable + end +end diff --git a/spec/ruby/core/true/and_spec.rb b/spec/ruby/core/true/and_spec.rb new file mode 100644 index 0000000000..99e69d3ae0 --- /dev/null +++ b/spec/ruby/core/true/and_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' + +describe "TrueClass#&" do + it "returns false if other is nil or false, otherwise true" do + (true & true).should == true + (true & false).should == false + (true & nil).should == false + (true & "").should == true + (true & mock('x')).should == true + end +end diff --git a/spec/ruby/core/true/case_compare_spec.rb b/spec/ruby/core/true/case_compare_spec.rb new file mode 100644 index 0000000000..dee6dd0227 --- /dev/null +++ b/spec/ruby/core/true/case_compare_spec.rb @@ -0,0 +1,13 @@ +require_relative '../../spec_helper' + +describe "TrueClass#===" do + it "returns true for true" do + (true === true).should == true + end + + it "returns false for non-true object" do + (true === 1).should == false + (true === "").should == false + (true === Object).should == false + end +end diff --git a/spec/ruby/core/true/dup_spec.rb b/spec/ruby/core/true/dup_spec.rb new file mode 100644 index 0000000000..351457ed22 --- /dev/null +++ b/spec/ruby/core/true/dup_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' + +describe "TrueClass#dup" do + it "returns self" do + true.dup.should equal(true) + end +end diff --git a/spec/ruby/core/true/inspect_spec.rb b/spec/ruby/core/true/inspect_spec.rb new file mode 100644 index 0000000000..09d1914856 --- /dev/null +++ b/spec/ruby/core/true/inspect_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' + +describe "TrueClass#inspect" do + it "returns the string 'true'" do + true.inspect.should == "true" + end +end diff --git a/spec/ruby/core/true/or_spec.rb b/spec/ruby/core/true/or_spec.rb new file mode 100644 index 0000000000..9bf76a62b8 --- /dev/null +++ b/spec/ruby/core/true/or_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' + +describe "TrueClass#|" do + it "returns true" do + (true | true).should == true + (true | false).should == true + (true | nil).should == true + (true | "").should == true + (true | mock('x')).should == true + end +end diff --git a/spec/ruby/core/true/singleton_method_spec.rb b/spec/ruby/core/true/singleton_method_spec.rb new file mode 100644 index 0000000000..c06793850f --- /dev/null +++ b/spec/ruby/core/true/singleton_method_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../spec_helper' + +describe "TrueClass#singleton_method" do + ruby_version_is '3.3' do + it "raises regardless of whether TrueClass defines the method" do + -> { true.singleton_method(:foo) }.should raise_error(NameError) + begin + def (true).foo; end + -> { true.singleton_method(:foo) }.should raise_error(NameError) + ensure + TrueClass.send(:remove_method, :foo) + end + end + end +end diff --git a/spec/ruby/core/true/to_s_spec.rb b/spec/ruby/core/true/to_s_spec.rb new file mode 100644 index 0000000000..fa1b53a580 --- /dev/null +++ b/spec/ruby/core/true/to_s_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../spec_helper' + +describe "TrueClass#to_s" do + it "returns the string 'true'" do + true.to_s.should == "true" + end + + it "returns a frozen string" do + true.to_s.should.frozen? + end + + it "always returns the same string" do + true.to_s.should equal(true.to_s) + end +end diff --git a/spec/ruby/core/true/trueclass_spec.rb b/spec/ruby/core/true/trueclass_spec.rb new file mode 100644 index 0000000000..02af649d09 --- /dev/null +++ b/spec/ruby/core/true/trueclass_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../spec_helper' + +describe "TrueClass" do + it ".allocate raises a TypeError" do + -> do + TrueClass.allocate + end.should raise_error(TypeError) + end + + it ".new is undefined" do + -> do + TrueClass.new + end.should raise_error(NoMethodError) + end +end diff --git a/spec/ruby/core/true/xor_spec.rb b/spec/ruby/core/true/xor_spec.rb new file mode 100644 index 0000000000..8f5ecd5075 --- /dev/null +++ b/spec/ruby/core/true/xor_spec.rb @@ -0,0 +1,11 @@ +require_relative '../../spec_helper' + +describe "TrueClass#^" do + it "returns true if other is nil or false, otherwise false" do + (true ^ true).should == false + (true ^ false).should == true + (true ^ nil).should == true + (true ^ "").should == false + (true ^ mock('x')).should == false + end +end diff --git a/spec/ruby/core/unboundmethod/arity_spec.rb b/spec/ruby/core/unboundmethod/arity_spec.rb new file mode 100644 index 0000000000..cd700b9f9b --- /dev/null +++ b/spec/ruby/core/unboundmethod/arity_spec.rb @@ -0,0 +1,207 @@ +require_relative '../../spec_helper' + +describe "UnboundMethod#arity" do + SpecEvaluate.desc = "for method definition" + + context "returns zero" do + evaluate <<-ruby do + def m() end + ruby + + method(:m).unbind.arity.should == 0 + end + + evaluate <<-ruby do + def n(&b) end + ruby + + method(:n).unbind.arity.should == 0 + end + end + + context "returns positive values" do + evaluate <<-ruby do + def m(a) end + def n(a, b) end + def o(a, b, c) end + def p(a, b, c, d) end + ruby + + method(:m).unbind.arity.should == 1 + method(:n).unbind.arity.should == 2 + method(:o).unbind.arity.should == 3 + method(:p).unbind.arity.should == 4 + end + + evaluate <<-ruby do + def m(a:) end + def n(a:, b:) end + def o(a: 1, b:, c:, d: 2) end + ruby + + method(:m).unbind.arity.should == 1 + method(:n).unbind.arity.should == 1 + method(:o).unbind.arity.should == 1 + end + + evaluate <<-ruby do + def m(a, b:) end + def n(a, b:, &l) end + ruby + + method(:m).unbind.arity.should == 2 + method(:n).unbind.arity.should == 2 + end + + evaluate <<-ruby do + def m(a, b, c:, d: 1) end + def n(a, b, c:, d: 1, **k, &l) end + ruby + + method(:m).unbind.arity.should == 3 + method(:n).unbind.arity.should == 3 + end + end + + context "returns negative values" do + evaluate <<-ruby do + def m(a=1) end + def n(a=1, b=2) end + ruby + + method(:m).unbind.arity.should == -1 + method(:n).unbind.arity.should == -1 + end + + evaluate <<-ruby do + def m(a, b=1) end + def n(a, b, c=1, d=2) end + ruby + + method(:m).unbind.arity.should == -2 + method(:n).unbind.arity.should == -3 + end + + evaluate <<-ruby do + def m(a=1, *b) end + def n(a=1, b=2, *c) end + ruby + + method(:m).unbind.arity.should == -1 + method(:n).unbind.arity.should == -1 + end + + evaluate <<-ruby do + def m(*) end + def n(*a) end + ruby + + method(:m).unbind.arity.should == -1 + method(:n).unbind.arity.should == -1 + end + + evaluate <<-ruby do + def m(a, *) end + def n(a, *b) end + def o(a, b, *c) end + def p(a, b, c, *d) end + ruby + + method(:m).unbind.arity.should == -2 + method(:n).unbind.arity.should == -2 + method(:o).unbind.arity.should == -3 + method(:p).unbind.arity.should == -4 + end + + evaluate <<-ruby do + def m(*a, b) end + def n(*a, b, c) end + def o(*a, b, c, d) end + ruby + + method(:m).unbind.arity.should == -2 + method(:n).unbind.arity.should == -3 + method(:o).unbind.arity.should == -4 + end + + evaluate <<-ruby do + def m(a, *b, c) end + def n(a, b, *c, d, e) end + ruby + + method(:m).unbind.arity.should == -3 + method(:n).unbind.arity.should == -5 + end + + evaluate <<-ruby do + def m(a, b=1, c=2, *d, e, f) end + def n(a, b, c=1, *d, e, f, g) end + ruby + + method(:m).unbind.arity.should == -4 + method(:n).unbind.arity.should == -6 + end + + evaluate <<-ruby do + def m(a: 1) end + def n(a: 1, b: 2) end + ruby + + method(:m).unbind.arity.should == -1 + method(:n).unbind.arity.should == -1 + end + + evaluate <<-ruby do + def m(a=1, b: 2) end + def n(*a, b: 1) end + def o(a=1, b: 2) end + def p(a=1, *b, c: 2, &l) end + ruby + + method(:m).unbind.arity.should == -1 + method(:n).unbind.arity.should == -1 + method(:o).unbind.arity.should == -1 + method(:p).unbind.arity.should == -1 + end + + evaluate <<-ruby do + def m(**k, &l) end + def n(*a, **k) end + def o(a: 1, b: 2, **k) end + ruby + + method(:m).unbind.arity.should == -1 + method(:n).unbind.arity.should == -1 + method(:o).unbind.arity.should == -1 + end + + evaluate <<-ruby do + def m(a=1, *b, c:, d: 2, **k, &l) end + ruby + + method(:m).unbind.arity.should == -2 + end + + evaluate <<-ruby do + def m(a, b=1, *c, d, e:, f: 2, **k, &l) end + def n(a, b=1, *c, d:, e:, f: 2, **k, &l) end + def o(a=0, b=1, *c, d, e:, f: 2, **k, &l) end + def p(a=0, b=1, *c, d:, e:, f: 2, **k, &l) end + ruby + + method(:m).unbind.arity.should == -4 + method(:n).unbind.arity.should == -3 + method(:o).unbind.arity.should == -3 + method(:p).unbind.arity.should == -2 + end + end + + context "for a Method generated by respond_to_missing?" do + it "returns -1" do + obj = mock("method arity respond_to_missing") + obj.should_receive(:respond_to_missing?).and_return(true) + + obj.method(:m).unbind.arity.should == -1 + end + end +end diff --git a/spec/ruby/core/unboundmethod/bind_call_spec.rb b/spec/ruby/core/unboundmethod/bind_call_spec.rb new file mode 100644 index 0000000000..80b2095d86 --- /dev/null +++ b/spec/ruby/core/unboundmethod/bind_call_spec.rb @@ -0,0 +1,58 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "UnboundMethod#bind_call" do + before :each do + @normal_um = UnboundMethodSpecs::Methods.new.method(:foo).unbind + @parent_um = UnboundMethodSpecs::Parent.new.method(:foo).unbind + @child1_um = UnboundMethodSpecs::Child1.new.method(:foo).unbind + @child2_um = UnboundMethodSpecs::Child2.new.method(:foo).unbind + @normal_um_super = UnboundMethodSpecs::Mod.instance_method(:foo_super) + @parent_um_super = UnboundMethodSpecs::Parent.new.method(:foo_super).unbind + end + + it "raises TypeError if object is not kind_of? the Module the method defined in" do + -> { @normal_um.bind_call(UnboundMethodSpecs::B.new) }.should raise_error(TypeError) + end + + it "binds and calls the method if object is kind_of the Module the method defined in" do + @normal_um.bind_call(UnboundMethodSpecs::Methods.new).should == true + end + + it "binds and calls the method on any object when UnboundMethod is unbound from a module" do + UnboundMethodSpecs::Mod.instance_method(:from_mod).bind_call(Object.new).should == nil + end + + it "binds and calls the method for any object kind_of? the Module the method is defined in" do + @parent_um.bind_call(UnboundMethodSpecs::Child1.new).should == nil + @child1_um.bind_call(UnboundMethodSpecs::Parent.new).should == nil + @child2_um.bind_call(UnboundMethodSpecs::Child1.new).should == nil + end + + it "binds and calls a Kernel method retrieved from Object on BasicObject" do + Object.instance_method(:instance_of?).bind_call(BasicObject.new, BasicObject).should == true + end + + it "binds and calls a Parent's class method to any Child's class methods" do + um = UnboundMethodSpecs::Parent.method(:class_method).unbind + um.bind_call(UnboundMethodSpecs::Child1).should == "I am UnboundMethodSpecs::Child1" + end + + it "will raise when binding a an object singleton's method to another object" do + other = UnboundMethodSpecs::Parent.new + p = UnboundMethodSpecs::Parent.new + class << p + def singleton_method + :single + end + end + um = p.method(:singleton_method).unbind + ->{ um.bind_call(other) }.should raise_error(TypeError) + end + + it "allows calling super for module methods bound to hierarchies that do not already have that module" do + p = UnboundMethodSpecs::Parent.new + + @normal_um_super.bind_call(p).should == true + end +end diff --git a/spec/ruby/core/unboundmethod/bind_spec.rb b/spec/ruby/core/unboundmethod/bind_spec.rb new file mode 100644 index 0000000000..7658b664e5 --- /dev/null +++ b/spec/ruby/core/unboundmethod/bind_spec.rb @@ -0,0 +1,69 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "UnboundMethod#bind" do + before :each do + @normal_um = UnboundMethodSpecs::Methods.new.method(:foo).unbind + @parent_um = UnboundMethodSpecs::Parent.new.method(:foo).unbind + @child1_um = UnboundMethodSpecs::Child1.new.method(:foo).unbind + @child2_um = UnboundMethodSpecs::Child2.new.method(:foo).unbind + @normal_um_super = UnboundMethodSpecs::Mod.instance_method(:foo_super) + @parent_um_super = UnboundMethodSpecs::Parent.new.method(:foo_super).unbind + end + + it "raises TypeError if object is not kind_of? the Module the method defined in" do + -> { @normal_um.bind(UnboundMethodSpecs::B.new) }.should raise_error(TypeError) + end + + it "returns Method for any object that is kind_of? the Module method was extracted from" do + @normal_um.bind(UnboundMethodSpecs::Methods.new).should be_kind_of(Method) + end + + it "returns Method on any object when UnboundMethod is unbound from a module" do + UnboundMethodSpecs::Mod.instance_method(:from_mod).bind(Object.new).should be_kind_of(Method) + end + + it "the returned Method is equal to the one directly returned by obj.method" do + obj = UnboundMethodSpecs::Methods.new + @normal_um.bind(obj).should == obj.method(:foo) + end + + it "returns Method for any object kind_of? the Module the method is defined in" do + @parent_um.bind(UnboundMethodSpecs::Child1.new).should be_kind_of(Method) + @child1_um.bind(UnboundMethodSpecs::Parent.new).should be_kind_of(Method) + @child2_um.bind(UnboundMethodSpecs::Child1.new).should be_kind_of(Method) + end + + it "allows binding a Kernel method retrieved from Object on BasicObject" do + Object.instance_method(:instance_of?).bind(BasicObject.new).call(BasicObject).should == true + end + + it "returns a callable method" do + obj = UnboundMethodSpecs::Methods.new + @normal_um.bind(obj).call.should == obj.foo + end + + it "binds a Parent's class method to any Child's class methods" do + m = UnboundMethodSpecs::Parent.method(:class_method).unbind.bind(UnboundMethodSpecs::Child1) + m.should be_an_instance_of(Method) + m.call.should == "I am UnboundMethodSpecs::Child1" + end + + it "will raise when binding a an object singleton's method to another object" do + other = UnboundMethodSpecs::Parent.new + p = UnboundMethodSpecs::Parent.new + class << p + def singleton_method + :single + end + end + um = p.method(:singleton_method).unbind + ->{ um.bind(other) }.should raise_error(TypeError) + end + + it "allows calling super for module methods bound to hierarchies that do not already have that module" do + p = UnboundMethodSpecs::Parent.new + + @normal_um_super.bind(p).call.should == true + end +end diff --git a/spec/ruby/core/unboundmethod/clone_spec.rb b/spec/ruby/core/unboundmethod/clone_spec.rb new file mode 100644 index 0000000000..1e7fb18744 --- /dev/null +++ b/spec/ruby/core/unboundmethod/clone_spec.rb @@ -0,0 +1,13 @@ +require_relative '../../spec_helper' +require_relative 'shared/dup' + +describe "UnboundMethod#clone" do + it_behaves_like :unboundmethod_dup, :clone + + it "preserves frozen status" do + method = Class.instance_method(:instance_method) + method.freeze + method.frozen?.should == true + method.clone.frozen?.should == true + end +end diff --git a/spec/ruby/core/unboundmethod/dup_spec.rb b/spec/ruby/core/unboundmethod/dup_spec.rb new file mode 100644 index 0000000000..5a78dd8e36 --- /dev/null +++ b/spec/ruby/core/unboundmethod/dup_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../spec_helper' +require_relative 'shared/dup' + +describe "UnboundMethod#dup" do + ruby_version_is "3.4" do + it_behaves_like :unboundmethod_dup, :dup + + it "resets frozen status" do + method = Class.instance_method(:instance_method) + method.freeze + method.frozen?.should == true + method.dup.frozen?.should == false + end + end +end diff --git a/spec/ruby/core/unboundmethod/eql_spec.rb b/spec/ruby/core/unboundmethod/eql_spec.rb new file mode 100644 index 0000000000..cf2c6b5aae --- /dev/null +++ b/spec/ruby/core/unboundmethod/eql_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../spec_helper' + +describe "UnboundMethod#eql?" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/unboundmethod/equal_value_spec.rb b/spec/ruby/core/unboundmethod/equal_value_spec.rb new file mode 100644 index 0000000000..b2d78c50af --- /dev/null +++ b/spec/ruby/core/unboundmethod/equal_value_spec.rb @@ -0,0 +1,138 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +context "Creating UnboundMethods" do + specify "there is no difference between Method#unbind and Module#instance_method" do + UnboundMethodSpecs::Methods.instance_method(:foo).should be_kind_of(UnboundMethod) + UnboundMethodSpecs::Methods.new.method(:foo).unbind.should be_kind_of(UnboundMethod) + end +end + +describe "UnboundMethod#==" do + before :all do + @from_module = UnboundMethodSpecs::Methods.instance_method(:foo) + @from_unbind = UnboundMethodSpecs::Methods.new.method(:foo).unbind + + @with_block = UnboundMethodSpecs::Methods.instance_method(:with_block) + + @includee = UnboundMethodSpecs::Mod.instance_method(:from_mod) + @includer = UnboundMethodSpecs::Methods.instance_method(:from_mod) + + @alias_1 = UnboundMethodSpecs::Methods.instance_method(:alias_1) + @alias_2 = UnboundMethodSpecs::Methods.instance_method(:alias_2) + + @original_body = UnboundMethodSpecs::Methods.instance_method(:original_body) + @identical_body = UnboundMethodSpecs::Methods.instance_method(:identical_body) + + @parent = UnboundMethodSpecs::Parent.instance_method(:foo) + @child1 = UnboundMethodSpecs::Child1.instance_method(:foo) + @child2 = UnboundMethodSpecs::Child2.instance_method(:foo) + + @child1_alt = UnboundMethodSpecs::Child1.instance_method(:foo) + + @discard_1 = UnboundMethodSpecs::Methods.instance_method(:discard_1) + @discard_2 = UnboundMethodSpecs::Methods.instance_method(:discard_2) + + @method_one = UnboundMethodSpecs::Methods.instance_method(:one) + @method_two = UnboundMethodSpecs::Methods.instance_method(:two) + end + + it "returns true if objects refer to the same method" do + (@from_module == @from_module).should == true + (@from_unbind == @from_unbind).should == true + (@from_module == @from_unbind).should == true + (@from_unbind == @from_module).should == true + (@with_block == @with_block).should == true + end + + it "returns true if either is an alias for the other" do + (@from_module == @alias_1).should == true + (@alias_1 == @from_module).should == true + end + + it "returns true if both are aliases for a third method" do + (@from_module == @alias_1).should == true + (@alias_1 == @from_module).should == true + + (@from_module == @alias_2).should == true + (@alias_2 == @from_module).should == true + + (@alias_1 == @alias_2).should == true + (@alias_2 == @alias_1).should == true + end + + it "returns true if same method is extracted from the same subclass" do + (@child1 == @child1_alt).should == true + (@child1_alt == @child1).should == true + end + + it "returns false if UnboundMethods are different methods" do + (@method_one == @method_two).should == false + (@method_two == @method_one).should == false + end + + it "returns false if both have identical body but are not the same" do + (@original_body == @identical_body).should == false + (@identical_body == @original_body).should == false + end + + it "returns true if same method but one extracted from a subclass" do + (@parent == @child1).should == true + (@child1 == @parent).should == true + end + + it "returns true if same method but extracted from two different subclasses" do + (@child2 == @child1).should == true + (@child1 == @child2).should == true + end + + it "returns true if methods are the same but added from an included Module" do + (@includee == @includer).should == true + (@includer == @includee).should == true + end + + it "returns false if both have same Module, same name, identical body but not the same" do + class UnboundMethodSpecs::Methods + def discard_1; :discard; end + end + + (@discard_1 == UnboundMethodSpecs::Methods.instance_method(:discard_1)).should == false + end + + it "considers methods through aliasing equal" do + c = Class.new do + class << self + alias_method :n, :new + end + end + + c.method(:new).should == c.method(:n) + c.method(:n).should == Class.instance_method(:new).bind(c) + end + + # On CRuby < 3.2, the 2 specs below pass due to method/instance_method skipping zsuper methods. + # We are interested in the general pattern working, i.e. the combination of method/instance_method + # and #== exposes the wanted behavior. + it "considers methods through visibility change equal" do + c = Class.new do + class << self + private :new + end + end + + c.method(:new).should == Class.instance_method(:new).bind(c) + end + + it "considers methods through aliasing and visibility change equal" do + c = Class.new do + class << self + alias_method :n, :new + private :new + end + end + + c.method(:new).should == c.method(:n) + c.method(:n).should == Class.instance_method(:new).bind(c) + c.method(:new).should == Class.instance_method(:new).bind(c) + end +end diff --git a/spec/ruby/core/unboundmethod/fixtures/classes.rb b/spec/ruby/core/unboundmethod/fixtures/classes.rb new file mode 100644 index 0000000000..28d8e0a965 --- /dev/null +++ b/spec/ruby/core/unboundmethod/fixtures/classes.rb @@ -0,0 +1,107 @@ +module UnboundMethodSpecs + + + class SourceLocation + def self.location # This needs to be on this line + :location # for the spec to pass + end + + def self.redefined + :first + end + + def self.redefined + :last + end + + def original + end + + alias :aka :original + end + + module Mod + def from_mod; end + def foo_super; super; end + end + + class Methods + include Mod + + def foo + true + end + + def with_block(&block); end + + alias bar foo + alias baz bar + alias alias_1 foo + alias alias_2 foo + + def original_body(); :this; end + def identical_body(); :this; end + + def one; end + def two(a); end + def three(a, b); end + def four(a, b, &c); end + + def neg_one(*a); end + def neg_two(a, *b); end + def neg_three(a, b, *c); end + def neg_four(a, b, *c, &d); end + + def discard_1(); :discard; end + def discard_2(); :discard; end + + def my_public_method; end + def my_protected_method; end + def my_private_method; end + protected :my_protected_method + private :my_private_method + end + + class Parent + def foo; end + def foo_super + true + end + def self.class_method + "I am #{name}" + end + end + + class Child1 < Parent; end + class Child2 < Parent; end + class Child3 < Parent + class << self + alias_method :another_class_method, :class_method + end + end + + class A + def baz(a, b) + return [__FILE__, self.class] + end + def overridden; end + end + + class B < A + def overridden; end + end + + class C < B + def overridden; end + end + + module HashSpecs + class SuperClass + def foo + end + end + + class SubClass < SuperClass + end + end +end diff --git a/spec/ruby/core/unboundmethod/hash_spec.rb b/spec/ruby/core/unboundmethod/hash_spec.rb new file mode 100644 index 0000000000..6888675bc1 --- /dev/null +++ b/spec/ruby/core/unboundmethod/hash_spec.rb @@ -0,0 +1,22 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "UnboundMethod#hash" do + it "returns the same value for user methods that are eql?" do + foo, bar = UnboundMethodSpecs::Methods.instance_method(:foo), UnboundMethodSpecs::Methods.instance_method(:bar) + foo.hash.should == bar.hash + end + + # See also redmine #6048 + it "returns the same value for builtin methods that are eql?" do + to_s, inspect = Array.instance_method(:to_s), Array.instance_method(:inspect) + to_s.hash.should == inspect.hash + end + + it "equals a hash of the same method in the superclass" do + foo_in_superclass = UnboundMethodSpecs::HashSpecs::SuperClass.instance_method(:foo) + foo = UnboundMethodSpecs::HashSpecs::SubClass.instance_method(:foo) + + foo.hash.should == foo_in_superclass.hash + end +end diff --git a/spec/ruby/core/unboundmethod/inspect_spec.rb b/spec/ruby/core/unboundmethod/inspect_spec.rb new file mode 100644 index 0000000000..cecf542fcd --- /dev/null +++ b/spec/ruby/core/unboundmethod/inspect_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/to_s' + +describe "UnboundMethod#inspect" do + it_behaves_like :unboundmethod_to_s, :inspect +end diff --git a/spec/ruby/core/unboundmethod/name_spec.rb b/spec/ruby/core/unboundmethod/name_spec.rb new file mode 100644 index 0000000000..4d0fb34fc8 --- /dev/null +++ b/spec/ruby/core/unboundmethod/name_spec.rb @@ -0,0 +1,15 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "UnboundMethod#name" do + it "returns the name of the method" do + String.instance_method(:upcase).name.should == :upcase + end + + it "returns the name even when aliased" do + obj = UnboundMethodSpecs::Methods.new + obj.method(:foo).unbind.name.should == :foo + obj.method(:bar).unbind.name.should == :bar + UnboundMethodSpecs::Methods.instance_method(:bar).name.should == :bar + end +end diff --git a/spec/ruby/core/unboundmethod/original_name_spec.rb b/spec/ruby/core/unboundmethod/original_name_spec.rb new file mode 100644 index 0000000000..7280dcb2b4 --- /dev/null +++ b/spec/ruby/core/unboundmethod/original_name_spec.rb @@ -0,0 +1,22 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "UnboundMethod#original_name" do + it "returns the name of the method" do + String.instance_method(:upcase).original_name.should == :upcase + end + + it "returns the original name" do + obj = UnboundMethodSpecs::Methods.new + obj.method(:foo).unbind.original_name.should == :foo + obj.method(:bar).unbind.original_name.should == :foo + UnboundMethodSpecs::Methods.instance_method(:bar).original_name.should == :foo + end + + it "returns the original name even when aliased twice" do + obj = UnboundMethodSpecs::Methods.new + obj.method(:foo).unbind.original_name.should == :foo + obj.method(:baz).unbind.original_name.should == :foo + UnboundMethodSpecs::Methods.instance_method(:baz).original_name.should == :foo + end +end diff --git a/spec/ruby/core/unboundmethod/owner_spec.rb b/spec/ruby/core/unboundmethod/owner_spec.rb new file mode 100644 index 0000000000..b099c56de1 --- /dev/null +++ b/spec/ruby/core/unboundmethod/owner_spec.rb @@ -0,0 +1,31 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative '../method/fixtures/classes' + +describe "UnboundMethod#owner" do + it "returns the owner of the method" do + String.instance_method(:upcase).owner.should == String + end + + it "returns the same owner when aliased in the same classes" do + UnboundMethodSpecs::Methods.instance_method(:foo).owner.should == UnboundMethodSpecs::Methods + UnboundMethodSpecs::Methods.instance_method(:bar).owner.should == UnboundMethodSpecs::Methods + end + + it "returns the class/module it was defined in" do + UnboundMethodSpecs::C.instance_method(:baz).owner.should == UnboundMethodSpecs::A + UnboundMethodSpecs::Methods.instance_method(:from_mod).owner.should == UnboundMethodSpecs::Mod + end + + it "returns the new owner for aliased methods on singleton classes" do + parent_singleton_class = UnboundMethodSpecs::Parent.singleton_class + child_singleton_class = UnboundMethodSpecs::Child3.singleton_class + + child_singleton_class.instance_method(:class_method).owner.should == parent_singleton_class + child_singleton_class.instance_method(:another_class_method).owner.should == child_singleton_class + end + + it "returns the class on which public was called for a private method in ancestor" do + MethodSpecs::InheritedMethods::C.instance_method(:derp).owner.should == MethodSpecs::InheritedMethods::C + end +end diff --git a/spec/ruby/core/unboundmethod/parameters_spec.rb b/spec/ruby/core/unboundmethod/parameters_spec.rb new file mode 100644 index 0000000000..2a3cb18196 --- /dev/null +++ b/spec/ruby/core/unboundmethod/parameters_spec.rb @@ -0,0 +1,5 @@ +require_relative '../../spec_helper' + +describe "UnboundMethod#parameters" do + it "needs to be reviewed for spec completeness" +end diff --git a/spec/ruby/core/unboundmethod/private_spec.rb b/spec/ruby/core/unboundmethod/private_spec.rb new file mode 100644 index 0000000000..5a563939d1 --- /dev/null +++ b/spec/ruby/core/unboundmethod/private_spec.rb @@ -0,0 +1,9 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "UnboundMethod#private?" do + it "has been removed" do + obj = UnboundMethodSpecs::Methods.new + obj.method(:my_private_method).unbind.should_not.respond_to?(:private?) + end +end diff --git a/spec/ruby/core/unboundmethod/protected_spec.rb b/spec/ruby/core/unboundmethod/protected_spec.rb new file mode 100644 index 0000000000..70622d658d --- /dev/null +++ b/spec/ruby/core/unboundmethod/protected_spec.rb @@ -0,0 +1,9 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "UnboundMethod#protected?" do + it "has been removed" do + obj = UnboundMethodSpecs::Methods.new + obj.method(:my_protected_method).unbind.should_not.respond_to?(:protected?) + end +end diff --git a/spec/ruby/core/unboundmethod/public_spec.rb b/spec/ruby/core/unboundmethod/public_spec.rb new file mode 100644 index 0000000000..ae75e2601c --- /dev/null +++ b/spec/ruby/core/unboundmethod/public_spec.rb @@ -0,0 +1,9 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "UnboundMethod#public?" do + it "has been removed" do + obj = UnboundMethodSpecs::Methods.new + obj.method(:my_public_method).unbind.should_not.respond_to?(:public?) + end +end diff --git a/spec/ruby/core/unboundmethod/shared/dup.rb b/spec/ruby/core/unboundmethod/shared/dup.rb new file mode 100644 index 0000000000..194e2cc1a1 --- /dev/null +++ b/spec/ruby/core/unboundmethod/shared/dup.rb @@ -0,0 +1,32 @@ +describe :unboundmethod_dup, shared: true do + it "returns a copy of self" do + a = Class.instance_method(:instance_method) + b = a.send(@method) + + a.should == b + a.should_not equal(b) + end + + ruby_version_is "3.4" do + it "copies instance variables" do + method = Class.instance_method(:instance_method) + method.instance_variable_set(:@ivar, 1) + cl = method.send(@method) + cl.instance_variables.should == [:@ivar] + end + + it "copies the finalizer" do + code = <<-'RUBY' + obj = Class.instance_method(:instance_method) + + ObjectSpace.define_finalizer(obj, Proc.new { STDOUT.write "finalized\n" }) + + obj.clone + + exit 0 + RUBY + + ruby_exe(code).lines.sort.should == ["finalized\n", "finalized\n"] + end + end +end diff --git a/spec/ruby/core/unboundmethod/shared/to_s.rb b/spec/ruby/core/unboundmethod/shared/to_s.rb new file mode 100644 index 0000000000..6b2c9c3e79 --- /dev/null +++ b/spec/ruby/core/unboundmethod/shared/to_s.rb @@ -0,0 +1,33 @@ +require_relative '../../../spec_helper' +require_relative '../fixtures/classes' + +describe :unboundmethod_to_s, shared: true do + before :each do + @from_module = UnboundMethodSpecs::Methods.instance_method(:from_mod) + @from_method = UnboundMethodSpecs::Methods.new.method(:from_mod).unbind + end + + it "returns a String" do + @from_module.send(@method).should be_kind_of(String) + @from_method.send(@method).should be_kind_of(String) + end + + it "the String reflects that this is an UnboundMethod object" do + @from_module.send(@method).should =~ /\bUnboundMethod\b/ + @from_method.send(@method).should =~ /\bUnboundMethod\b/ + end + + it "the String shows the method name, Module defined in and Module extracted from" do + @from_module.send(@method).should =~ /\bfrom_mod\b/ + @from_module.send(@method).should =~ /\bUnboundMethodSpecs::Mod\b/ + end + + it "returns a String including all details" do + @from_module.send(@method).should.start_with? "#<UnboundMethod: UnboundMethodSpecs::Mod#from_mod" + @from_method.send(@method).should.start_with? "#<UnboundMethod: UnboundMethodSpecs::Mod#from_mod" + end + + it "does not show the defining module if it is the same as the origin" do + UnboundMethodSpecs::A.instance_method(:baz).send(@method).should.start_with? "#<UnboundMethod: UnboundMethodSpecs::A#baz" + end +end diff --git a/spec/ruby/core/unboundmethod/source_location_spec.rb b/spec/ruby/core/unboundmethod/source_location_spec.rb new file mode 100644 index 0000000000..9cc2198017 --- /dev/null +++ b/spec/ruby/core/unboundmethod/source_location_spec.rb @@ -0,0 +1,65 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' + +describe "UnboundMethod#source_location" do + before :each do + @method = UnboundMethodSpecs::SourceLocation.method(:location).unbind + end + + it "sets the first value to the path of the file in which the method was defined" do + file = @method.source_location[0] + file.should be_an_instance_of(String) + file.should == File.realpath('fixtures/classes.rb', __dir__) + end + + it "sets the second value to an Integer representing the line on which the method was defined" do + line = @method.source_location[1] + line.should be_an_instance_of(Integer) + line.should == 5 + end + + it "returns the last place the method was defined" do + UnboundMethodSpecs::SourceLocation.method(:redefined).unbind.source_location[1].should == 13 + end + + it "returns the location of the original method even if it was aliased" do + UnboundMethodSpecs::SourceLocation.instance_method(:aka).source_location[1].should == 17 + end + + it "works for define_method methods" do + line = nil + cls = Class.new do + line = __LINE__ + 1 + define_method(:foo) { } + end + + method = cls.instance_method(:foo) + method.source_location[0].should =~ /#{__FILE__}/ + method.source_location[1].should == line + end + + it "works for define_singleton_method methods" do + line = nil + cls = Class.new do + line = __LINE__ + 1 + define_singleton_method(:foo) { } + end + + method = cls.method(:foo) + method.source_location[0].should =~ /#{__FILE__}/ + method.source_location[1].should == line + end + + it "works for eval with a given line" do + c = Class.new do + eval('def m; end', nil, "foo", 100) + end + location = c.instance_method(:m).source_location + ruby_version_is(""..."4.1") do + location.should == ["foo", 100] + end + ruby_version_is("4.1") do + location.should == ["foo", 100, 0, 100, 10] + end + end +end diff --git a/spec/ruby/core/unboundmethod/super_method_spec.rb b/spec/ruby/core/unboundmethod/super_method_spec.rb new file mode 100644 index 0000000000..aa7c129377 --- /dev/null +++ b/spec/ruby/core/unboundmethod/super_method_spec.rb @@ -0,0 +1,49 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative '../method/fixtures/classes' + +describe "UnboundMethod#super_method" do + it "returns the method that would be called by super in the method" do + meth = UnboundMethodSpecs::C.instance_method(:overridden) + meth = meth.super_method + meth.should == UnboundMethodSpecs::B.instance_method(:overridden) + meth = meth.super_method + meth.should == UnboundMethodSpecs::A.instance_method(:overridden) + end + + it "returns nil when there's no super method in the parent" do + method = Kernel.instance_method(:method) + method.super_method.should == nil + end + + it "returns nil when the parent's method is removed" do + parent = Class.new { def foo; end } + child = Class.new(parent) { def foo; end } + + method = child.instance_method(:foo) + + parent.send(:undef_method, :foo) + + method.super_method.should == nil + end + + # https://github.com/jruby/jruby/issues/7240 + context "after changing an inherited methods visibility" do + it "calls the proper super method" do + method = MethodSpecs::InheritedMethods::C.instance_method(:derp) + method.bind(MethodSpecs::InheritedMethods::C.new).call.should == 'BA' + end + + it "returns the expected super_method" do + method = MethodSpecs::InheritedMethods::C.instance_method(:derp) + method.super_method.owner.should == MethodSpecs::InheritedMethods::A + end + end + + context "after aliasing an inherited method" do + it "returns the expected super_method" do + method = MethodSpecs::InheritedMethods::C.instance_method(:meow) + method.super_method.owner.should == MethodSpecs::InheritedMethods::A + end + end +end diff --git a/spec/ruby/core/unboundmethod/to_s_spec.rb b/spec/ruby/core/unboundmethod/to_s_spec.rb new file mode 100644 index 0000000000..a508229b49 --- /dev/null +++ b/spec/ruby/core/unboundmethod/to_s_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative 'shared/to_s' + +describe "UnboundMethod#to_s" do + it_behaves_like :unboundmethod_to_s, :to_s +end diff --git a/spec/ruby/core/warning/categories_spec.rb b/spec/ruby/core/warning/categories_spec.rb new file mode 100644 index 0000000000..1e310ef38b --- /dev/null +++ b/spec/ruby/core/warning/categories_spec.rb @@ -0,0 +1,12 @@ +require_relative '../../spec_helper' + +ruby_version_is "3.4" do + describe "Warning.categories" do + # There might be more, but these are standard across Ruby implementations + it "returns the list of possible warning categories" do + Warning.categories.should.include? :deprecated + Warning.categories.should.include? :experimental + Warning.categories.should.include? :performance + end + end +end diff --git a/spec/ruby/core/warning/element_reference_spec.rb b/spec/ruby/core/warning/element_reference_spec.rb new file mode 100644 index 0000000000..c0ed37ef13 --- /dev/null +++ b/spec/ruby/core/warning/element_reference_spec.rb @@ -0,0 +1,29 @@ +require_relative '../../spec_helper' + +describe "Warning.[]" do + it "returns default values for categories :deprecated and :experimental" do + # If any warning options were set on the Ruby that will be executed, then + # it's possible this test will fail. In this case we will skip this test. + skip if ruby_exe.any? { |opt| opt.start_with?("-W") } + + ruby_exe('p [Warning[:deprecated], Warning[:experimental]]').chomp.should == "[false, true]" + ruby_exe('p [Warning[:deprecated], Warning[:experimental]]', options: "-w").chomp.should == "[true, true]" + end + + ruby_version_is '3.3' do + it "returns default values for :performance category" do + ruby_exe('p Warning[:performance]').chomp.should == "false" + ruby_exe('p Warning[:performance]', options: "-w").chomp.should == "false" + end + end + + it "raises for unknown category" do + -> { Warning[:noop] }.should raise_error(ArgumentError, /unknown category: noop/) + end + + it "raises for non-Symbol category" do + -> { Warning[42] }.should raise_error(TypeError) + -> { Warning[false] }.should raise_error(TypeError) + -> { Warning["noop"] }.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/warning/element_set_spec.rb b/spec/ruby/core/warning/element_set_spec.rb new file mode 100644 index 0000000000..d59a7d4c9e --- /dev/null +++ b/spec/ruby/core/warning/element_set_spec.rb @@ -0,0 +1,41 @@ +require_relative '../../spec_helper' + +describe "Warning.[]=" do + it "emits and suppresses warnings for :deprecated" do + ruby_exe('Warning[:deprecated] = true; $; = ""', args: "2>&1").should =~ /is deprecated/ + ruby_exe('Warning[:deprecated] = false; $; = ""', args: "2>&1").should == "" + end + + describe ":experimental" do + before do + @src = 'warn "This is experimental warning.", category: :experimental' + end + + it "emits and suppresses warnings for :experimental" do + ruby_exe("Warning[:experimental] = true; eval('#{@src}')", args: "2>&1").should =~ /is experimental/ + ruby_exe("Warning[:experimental] = false; eval('#{@src}')", args: "2>&1").should == "" + end + end + + ruby_version_is '3.3' do + it "enables or disables performance warnings" do + original = Warning[:performance] + begin + Warning[:performance] = !original + Warning[:performance].should == !original + ensure + Warning[:performance] = original + end + end + end + + it "raises for unknown category" do + -> { Warning[:noop] = false }.should raise_error(ArgumentError, /unknown category: noop/) + end + + it "raises for non-Symbol category" do + -> { Warning[42] = false }.should raise_error(TypeError) + -> { Warning[false] = false }.should raise_error(TypeError) + -> { Warning["noop"] = false }.should raise_error(TypeError) + end +end diff --git a/spec/ruby/core/warning/performance_warning_spec.rb b/spec/ruby/core/warning/performance_warning_spec.rb new file mode 100644 index 0000000000..ab0badcd3d --- /dev/null +++ b/spec/ruby/core/warning/performance_warning_spec.rb @@ -0,0 +1,28 @@ +require_relative '../../spec_helper' + + +describe "Performance warnings" do + guard -> { ruby_version_is("3.4") || RUBY_ENGINE == "truffleruby" } do + # Optimising Integer, Float or Symbol methods is kind of implementation detail + # but multiple implementations do so. So it seems reasonable to have a test case + # for at least one such common method. + # See https://bugs.ruby-lang.org/issues/20429 + context "when redefined optimised methods" do + it "emits performance warning for redefining Integer#+" do + code = <<~CODE + Warning[:performance] = true + + class Integer + ORIG_METHOD = instance_method(:+) + + def +(...) + ORIG_METHOD.bind(self).call(...) + end + end + CODE + + ruby_exe(code, args: "2>&1").should.include?("warning: Redefining 'Integer#+' disables interpreter and JIT optimizations") + end + end + end +end diff --git a/spec/ruby/core/warning/warn_spec.rb b/spec/ruby/core/warning/warn_spec.rb new file mode 100644 index 0000000000..2e4a822e02 --- /dev/null +++ b/spec/ruby/core/warning/warn_spec.rb @@ -0,0 +1,187 @@ +require_relative '../../spec_helper' + +describe "Warning.warn" do + it "complains" do + -> { + Warning.warn("Chunky bacon!") + }.should complain("Chunky bacon!") + end + + it "does not add a newline" do + ruby_exe("Warning.warn('test')", args: "2>&1").should == "test" + end + + it "returns nil" do + ruby_exe("p Warning.warn('test')", args: "2>&1").should == "testnil\n" + end + + it "extends itself" do + Warning.singleton_class.ancestors.should include(Warning) + end + + it "has Warning as the method owner" do + ruby_exe("p Warning.method(:warn).owner").should == "Warning\n" + end + + it "can be overridden" do + code = <<-RUBY + $stdout.sync = true + $stderr.sync = true + def Warning.warn(msg) + if msg.start_with?("A") + puts msg.upcase + else + super + end + end + Warning.warn("A warning!") + Warning.warn("warning from stderr\n") + RUBY + ruby_exe(code, args: "2>&1").should == %Q[A WARNING!\nwarning from stderr\n] + end + + it "is called by parser warnings" do + Warning.should_receive(:warn) + verbose = $VERBOSE + $VERBOSE = false + begin + eval "{ key: :value, key: :value2 }" + ensure + $VERBOSE = verbose + end + end + + it "is called by Kernel.warn with nil category keyword" do + Warning.should_receive(:warn).with("Chunky bacon!\n", category: nil) + verbose = $VERBOSE + $VERBOSE = false + begin + Kernel.warn("Chunky bacon!") + ensure + $VERBOSE = verbose + end + end + + it "is called by Kernel.warn with given category keyword converted to a symbol" do + Warning.should_receive(:warn).with("Chunky bacon!\n", category: :deprecated) + verbose = $VERBOSE + $VERBOSE = false + begin + Kernel.warn("Chunky bacon!", category: "deprecated") + ensure + $VERBOSE = verbose + end + end + + it "warns when category is :deprecated and Warning[:deprecated] is true" do + warn_deprecated = Warning[:deprecated] + Warning[:deprecated] = true + begin + -> { + Warning.warn("foo", category: :deprecated) + }.should complain("foo") + ensure + Warning[:deprecated] = warn_deprecated + end + end + + it "warns when category is :experimental and Warning[:experimental] is true" do + warn_experimental = Warning[:experimental] + Warning[:experimental] = true + begin + -> { + Warning.warn("foo", category: :experimental) + }.should complain("foo") + ensure + Warning[:experimental] = warn_experimental + end + end + + ruby_version_is "3.4" do + it "warns when category is :strict_unused_block but Warning[:strict_unused_block] is false" do + warn_strict_unused_block = Warning[:strict_unused_block] + Warning[:strict_unused_block] = true + begin + -> { + Warning.warn("foo", category: :strict_unused_block) + }.should complain("foo") + ensure + Warning[:strict_unused_block] = warn_strict_unused_block + end + end + end + + it "doesn't print message when category is :deprecated but Warning[:deprecated] is false" do + warn_deprecated = Warning[:deprecated] + Warning[:deprecated] = false + begin + -> { + Warning.warn("foo", category: :deprecated) + }.should_not complain + ensure + Warning[:deprecated] = warn_deprecated + end + end + + it "doesn't print message when category is :experimental but Warning[:experimental] is false" do + warn_experimental = Warning[:experimental] + Warning[:experimental] = false + begin + -> { + Warning.warn("foo", category: :experimental) + }.should_not complain + ensure + Warning[:experimental] = warn_experimental + end + end + + ruby_version_is "3.4" do + it "doesn't print message when category is :strict_unused_block but Warning[:strict_unused_block] is false" do + warn_strict_unused_block = Warning[:strict_unused_block] + Warning[:strict_unused_block] = false + begin + -> { + Warning.warn("foo", category: :strict_unused_block) + }.should_not complain + ensure + Warning[:strict_unused_block] = warn_strict_unused_block + end + end + end + + ruby_bug '#20573', ''...'3.4' do + it "isn't called by Kernel.warn when category is :deprecated but Warning[:deprecated] is false" do + warn_deprecated = Warning[:deprecated] + begin + Warning[:deprecated] = false + Warning.should_not_receive(:warn) + Kernel.warn("foo", category: :deprecated) + ensure + Warning[:deprecated] = warn_deprecated + end + end + + it "isn't called by Kernel.warn when category is :experimental but Warning[:experimental] is false" do + warn_experimental = Warning[:experimental] + begin + Warning[:experimental] = false + Warning.should_not_receive(:warn) + Kernel.warn("foo", category: :experimental) + ensure + Warning[:experimental] = warn_experimental + end + end + end + + it "prints the message when VERBOSE is false" do + -> { Warning.warn("foo") }.should complain("foo") + end + + it "prints the message when VERBOSE is nil" do + -> { Warning.warn("foo") }.should complain("foo", verbose: nil) + end + + it "prints the message when VERBOSE is true" do + -> { Warning.warn("foo") }.should complain("foo", verbose: true) + end +end |
