diff --git a/core/src/main/java/org/jruby/RubyArgsFile.java b/core/src/main/java/org/jruby/RubyArgsFile.java index a8e7c6999ed..1528c2d9774 100644 --- a/core/src/main/java/org/jruby/RubyArgsFile.java +++ b/core/src/main/java/org/jruby/RubyArgsFile.java @@ -395,12 +395,18 @@ private static IRubyObject argf_getline(ThreadContext context, final int callInf while (true) { if (!data.next_argv(context)) return context.nil; - RubyIO currentFile = (RubyIO) data.currentFile; - if (isGenericInput(context, data)) { context.callInfo = callInfo; // restore callInfo for kwargs - line = data.currentFile.callMethod(context, "gets", args); + line = sites(context).gets.call(context, argsFile(context), data.currentFile, args); + + // An IO updates the line number itself, but other objects do not + if (!line.isNil() && !(data.currentFile instanceof RubyIO)) { + context.runtime.setCurrentLine(context.runtime.getCurrentLine() + 1); + } } else { + // $stdin may be any object with gets (e.g. StringIO), so only cast once we know it is not generic input + RubyIO currentFile = (RubyIO) data.currentFile; + if (args.length == 0 && context.runtime.getRecordSeparatorVar().get() == globalVariables(context).getDefaultSeparator()) { line = (currentFile).gets(context); } else { @@ -443,6 +449,16 @@ public static IRubyObject gets(ThreadContext context, IRubyObject recv, IRubyObj */ @JRubyMethod(name = "readline", optional = 2, keywords = true, checkArity = false, writes = LASTLINE) public static IRubyObject readline(ThreadContext context, IRubyObject recv, IRubyObject[] args) { + final int callInfo = ThreadContext.resetCallInfo(context); + ArgsFileData data = ArgsFileData.getArgsFileData(context.runtime); + + // MRI: argf_readline forwards to $stdin.readline when $stdin is not an IO + if (data.next_argv(context) && !(data.currentFile instanceof RubyIO)) { + context.callInfo = callInfo; // restore callInfo for kwargs + return sites(context).readline.call(context, recv, data.currentFile, args); + } + + context.callInfo = callInfo; // restore callInfo for kwargs IRubyObject line = gets(context, recv, args); if (line.isNil()) throw context.runtime.newEOFError(); @@ -461,7 +477,7 @@ public static IRubyObject readlines(ThreadContext context, IRubyObject recv, IRu if (!(data.currentFile instanceof RubyIO)) { context.callInfo = callInfo; // restore callInfo for kwargs - return data.currentFile.callMethod(context, "readlines", args); + return sites(context).readlines.call(context, recv, data.currentFile, args); } var ary = newArray(context); diff --git a/core/src/main/java/org/jruby/runtime/JavaSites.java b/core/src/main/java/org/jruby/runtime/JavaSites.java index 6320b48046e..22c3565174e 100644 --- a/core/src/main/java/org/jruby/runtime/JavaSites.java +++ b/core/src/main/java/org/jruby/runtime/JavaSites.java @@ -7,6 +7,7 @@ import org.jruby.runtime.callsite.DivCallSite; import org.jruby.runtime.callsite.FunctionalCachingCallSite; import org.jruby.runtime.callsite.MulCallSite; +import org.jruby.runtime.callsite.NormalCachingCallSite; import org.jruby.runtime.callsite.PlusCallSite; import org.jruby.runtime.callsite.RespondToCallSite; import org.jruby.runtime.callsite.SuperCallSite; @@ -484,6 +485,11 @@ public static class ZlibSites { public static class ArgfSites { public final CallSite each_codepoint = new FunctionalCachingCallSite("each_codepoint"); + // MRI: forward_current calls only public methods on $stdin (rb_funcallv_public_kw). A functional call + // would reach the private Kernel#gets etc. on a plain object, which read from ARGF again and recurse. + public final CallSite gets = new NormalCachingCallSite("gets"); + public final CallSite readline = new NormalCachingCallSite("readline"); + public final CallSite readlines = new NormalCachingCallSite("readlines"); } public static class TracePointSites { diff --git a/spec/ruby/core/argf/gets_spec.rb b/spec/ruby/core/argf/gets_spec.rb index cc7673b1909..e8994299a3f 100644 --- a/spec/ruby/core/argf/gets_spec.rb +++ b/spec/ruby/core/argf/gets_spec.rb @@ -47,3 +47,26 @@ end end + +describe "ARGF.gets when reading $stdin and $stdin is not an IO" do + before :each do + @stdin = $stdin + $stdin = StringIO.new("a\nb\n") + @argf_stdin = ARGF.class.new + end + + after :each do + $stdin = @stdin + end + + it "increments lineno for each line read" do + @argf_stdin.gets + @argf_stdin.gets + @argf_stdin.lineno.should == 2 + end + + it "raises NoMethodError when $stdin has no public gets" do + $stdin = Object.new + -> { ARGF.class.new.gets }.should raise_error(NoMethodError) + end +end diff --git a/spec/ruby/core/argf/readline_spec.rb b/spec/ruby/core/argf/readline_spec.rb index db53c499e95..04fcc12aaf1 100644 --- a/spec/ruby/core/argf/readline_spec.rb +++ b/spec/ruby/core/argf/readline_spec.rb @@ -21,3 +21,33 @@ end end end + +describe "ARGF.readline when reading $stdin and $stdin is not an IO" do + before :each do + @stdin = $stdin + end + + after :each do + $stdin = @stdin + end + + it "calls readline on $stdin" do + $stdin = mock("stdin") + $stdin.should_receive(:readline).with("\n", 1, chomp: true).and_return("line") + ARGF.class.new.readline("\n", 1, chomp: true).should == "line" + end + + it "raises an EOFError when reaching end of $stdin" do + $stdin = StringIO.new("a\n") + argf = ARGF.class.new + argf.readline.should == "a\n" + -> { argf.readline }.should raise_error(EOFError) + end + + it "raises NoMethodError when $stdin has no public readline" do + stdin = Object.new + def stdin.gets(*) = "line\n" + $stdin = stdin + -> { ARGF.class.new.readline }.should raise_error(NoMethodError) + end +end diff --git a/spec/ruby/core/argf/readlines_spec.rb b/spec/ruby/core/argf/readlines_spec.rb index 30be936dab0..50a7f9b965f 100644 --- a/spec/ruby/core/argf/readlines_spec.rb +++ b/spec/ruby/core/argf/readlines_spec.rb @@ -4,3 +4,18 @@ describe "ARGF.readlines" do it_behaves_like :argf_readlines, :readlines end + +describe "ARGF.readlines when reading $stdin and $stdin is not an IO" do + before :each do + @stdin = $stdin + end + + after :each do + $stdin = @stdin + end + + it "raises NoMethodError when $stdin has no public readlines" do + $stdin = Object.new + -> { ARGF.class.new.readlines }.should raise_error(NoMethodError) + end +end diff --git a/spec/ruby/core/argf/shared/gets.rb b/spec/ruby/core/argf/shared/gets.rb index 94c2cdf0d53..9e7c1bf79b6 100644 --- a/spec/ruby/core/argf/shared/gets.rb +++ b/spec/ruby/core/argf/shared/gets.rb @@ -1,3 +1,5 @@ +require 'stringio' + describe :argf_gets, shared: true do before :each do @file1_name = fixture __FILE__, "file1.txt" @@ -33,6 +35,27 @@ end end + describe "when reading $stdin and $stdin is not an IO" do + before :each do + @stdin = $stdin + $stdin = StringIO.new("a\nb\n") + @stdin_argf = ARGF.class.new + end + + after :each do + $stdin = @stdin + end + + it "reads each line of $stdin" do + @stdin_argf.send(@method).should == "a\n" + @stdin_argf.send(@method).should == "b\n" + end + + it "passes keywords on to $stdin" do + @stdin_argf.send(@method, chomp: true).should == "a" + end + end + it "reads all lines of a file" do argf [@file1_name] do lines = []