diff options
Diffstat (limited to 'spec/ruby/core/kernel/shared')
| -rw-r--r-- | spec/ruby/core/kernel/shared/dup_clone.rb | 91 | ||||
| -rw-r--r-- | spec/ruby/core/kernel/shared/kind_of.rb | 55 | ||||
| -rw-r--r-- | spec/ruby/core/kernel/shared/lambda.rb | 11 | ||||
| -rw-r--r-- | spec/ruby/core/kernel/shared/load.rb | 215 | ||||
| -rw-r--r-- | spec/ruby/core/kernel/shared/method.rb | 56 | ||||
| -rw-r--r-- | spec/ruby/core/kernel/shared/require.rb | 848 | ||||
| -rw-r--r-- | spec/ruby/core/kernel/shared/sprintf.rb | 987 | ||||
| -rw-r--r-- | spec/ruby/core/kernel/shared/sprintf_encoding.rb | 67 | ||||
| -rw-r--r-- | spec/ruby/core/kernel/shared/then.rb | 20 |
9 files changed, 2350 insertions, 0 deletions
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 |
