Permalink
Cannot retrieve contributors at this time
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
rinruby/spec/rinruby_spec.rb
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
441 lines (423 sloc)
14.5 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require File.expand_path(File.dirname(__FILE__) + '/spec_helper') | |
require 'rinruby' | |
puts "RinRuby #{RinRuby::VERSION} specification" | |
shared_examples 'RinRubyCore' do | |
let(:params){ | |
{ | |
:echo_enabled => false, | |
:interactive => false, | |
:executable => nil, | |
:port_number => 38500, | |
:port_width => 1000, | |
} | |
} | |
describe "on init" do | |
after{(r.quit rescue nil) if defined?(r)} | |
it "should accept parameters as specified on Dahl & Crawford(2009)" do | |
expect(r.echo_enabled).to be_falsy | |
expect(r.interactive).to be_falsy | |
case r.instance_variable_get(:@platform) | |
when /^windows-cygwin/ then | |
expect(r.executable).to match(/(^R|Rterm\.exe["']?)$/) | |
when /^windows/ then | |
expect(r.executable).to match(/Rterm\.exe["']?$/) | |
else | |
expect(r.executable).to eq("R") | |
end | |
end | |
it "should accept :echo and :interactive parameters" do | |
params.merge!(:echo_enabled => true, :interactive => true) | |
expect(r.echo_enabled).to be_truthy | |
expect(r.interactive).to be_truthy | |
end | |
it "should accept custom :port_number" do | |
params.merge!(:port_number => 38442+rand(3), :port_width => 1) | |
expect(r.port_number).to eq(params[:port_number]) | |
end | |
it "should accept custom :port_width" do | |
params.merge!(:port_number => 38442, :port_width => rand(10)+1) | |
expect(r.port_width).to eq(params[:port_width]) | |
expect(r.port_number).to satisfy {|v| | |
((params[:port_number])...(params[:port_number] + params[:port_width])).include?(v) | |
} | |
end | |
end | |
describe "R interface" do | |
# In before(:each) or let(including subject) blocks, Assignment to instance variable | |
# having a same name defined in before(:all) will not work intentionally, | |
# because a new instance variable will be created for the following examples. | |
# For workaround, two-step indirect assignment to a hash created in before(:all) is applied. | |
before(:all){@cached_env = {:r => nil}} # make placeholder | |
subject{@cached_env[:r] ||= r} | |
after(:all){@cached_env[:r].quit rescue nil} | |
describe "basic methods" do | |
it {is_expected.to respond_to(:eval)} | |
it {is_expected.to respond_to(:assign)} | |
it {is_expected.to respond_to(:pull)} | |
it {is_expected.to respond_to(:quit)} | |
it {is_expected.to respond_to(:echo)} | |
it {is_expected.to respond_to(:prompt)} | |
it "return true for complete? for correct expressions" do | |
["", "x<-1", "x<-\n1", "'123\n456'", "1+\n2+\n3"].each{|str| | |
expect(subject.complete?(str)).to be true | |
} | |
end | |
it "return false for complete? for incorrect expressions" do | |
["x<-", "'123\n", "1+\n2+\n"].each{|str| | |
expect(subject.complete?(str)).to be false | |
} | |
end | |
it "raise error for complete? for unrecoverable expression" do | |
[";", "x<-;"].each{|str| | |
expect{subject.complete?(str)}.to raise_error(RinRuby::ParseError) | |
} | |
end | |
it "correct eval should return true" do | |
["", "x<-1", "x<-\n1", "'123\n456'"].each{|str| | |
expect(subject.eval(str)).to be_truthy | |
} | |
end | |
it "incorrect eval should raise an ParseError" do | |
[ | |
"x<-", "'123\n", # incomplete | |
";", "x<-;", # unrecoverable | |
].each{|str| | |
expect{subject.eval(str)}.to raise_error(RinRuby::ParseError) | |
} | |
end | |
end | |
def gen_matrix_cmp_per_elm_proc(&cmp_proc) | |
proc{|a, b| | |
expect(a.row_size).to eql(b.row_size) | |
expect(a.column_size).to eql(b.column_size) | |
a.row_size.times{|i| | |
a.column_size.times{|j| | |
cmp_proc.call(a[i,j], b[i,j]) | |
} | |
} | |
} | |
end | |
context "on pull" do | |
it "should pull a Character" do | |
['Value', ''].each{|v| # normal string and zero-length string | |
subject.eval("x<-'#{v}'") | |
expect(subject.pull('x')).to eql(v) | |
} | |
subject.eval("x<-as.character(NA)") | |
expect(subject.pull('x')).to eql(nil) | |
end | |
it "should pull an Integer" do | |
[0x12345678, -0x12345678].each{|v| # for check endian, and range | |
subject.eval("x<-#{v}L") | |
expect(subject.pull('x')).to eql(v) | |
} | |
end | |
it "should pull a Double" do | |
[1.5, 1.0].each{|v| | |
subject.eval("x<-#{v}e0") | |
expect(subject.pull('x')).to eql(v) | |
} | |
[1 << 32, -(1 << 32)].each{|v| # big integer will be treated as double | |
subject.eval("x<-#{v}") | |
expect(subject.pull('x')).to eql(v.to_f) | |
} | |
subject.eval("x<-NaN") | |
expect(subject.pull('x').nan?).to be_truthy | |
subject.eval("x<-as.numeric(NA)") | |
expect(subject.pull('x')).to eql(nil) | |
end | |
it "should pull a Logical" do | |
{:T => true, :F => false, :NA => nil}.each{|k, v| | |
subject.eval("x<-#{k}") | |
expect(subject.pull('x')).to eql(v) | |
} | |
end | |
it "should pull an Array of Character" do | |
{ | |
"c('a','b','',NA)" => ['a','b','',nil], | |
"as.character(NULL)" => [], | |
}.each{|k, v| | |
subject.eval("x<-#{k}") | |
expect(subject.pull('x')).to eql(v) | |
} | |
end | |
it "should pull an Array of Integer" do | |
{ | |
"c(1L,2L,-5L,-3L,NA)" => [1,2,-5,-3,nil], | |
"as.integer(NULL)" => [], | |
}.each{|k, v| | |
subject.eval("x<-#{k}") | |
expect(subject.pull('x')).to eql(v) | |
} | |
end | |
it "should pull an Array of Double" do | |
subject.eval("x<-c(1.1,2.2,5,3,NA,NaN)") # auto-conversion to numeric vector | |
expect(subject.pull('x')[0..-2]).to eql([1.1,2.2,5.0,3.0,nil]) | |
expect(subject.pull('x')[-1].nan?).to be_truthy | |
subject.eval("x<-c(1L,2L,5L,3.0,NA,NaN)") # auto-conversion to numeric vector | |
expect(subject.pull('x')[0..-2]).to eql([1.0,2.0,5.0,3.0,nil]) | |
expect(subject.pull('x')[-1].nan?).to be_truthy | |
subject.eval("x<-as.numeric(NULL)") | |
expect(subject.pull('x')).to eql([]) | |
end | |
it "should pull an Array of Logical" do | |
{ | |
"c(T, F, NA)" => [true, false, nil], | |
"as.logical(NULL)" => [], | |
}.each{|k, v| | |
subject.eval("x<-#{k}") | |
expect(subject.pull('x')).to eql(v) | |
} | |
end | |
it "should pull a Matrix" do | |
threshold = 1e-8 | |
[ | |
proc{ # integer matrix | |
v = rand(100000000) # get 8 digits | |
[v, "#{v}L"] | |
}, | |
[ # double matrix | |
proc{ | |
v = rand(100000000) # get 8 digits | |
[Float("0.#{v}"), "0.#{v}"] | |
}, | |
gen_matrix_cmp_per_elm_proc{|a, b| | |
expect(a).to be_within(threshold).of(b) | |
} | |
], | |
].each{|gen_proc, cmp_proc| | |
nrow, ncol = [10, 10] # 10 x 10 small matrix | |
subject.eval("x<-matrix(nrow=#{nrow}, ncol=#{ncol})") | |
rx = Matrix[*((1..nrow).collect{|i| | |
(1..ncol).collect{|j| | |
v_rb, v_R = gen_proc.call | |
subject.eval("x[#{i},#{j}]<-#{v_R}") | |
v_rb | |
} | |
})] | |
(cmp_proc || proc{|a, b| expect(a).to eql(b)}).call(subject.pull('x'), rx) | |
} | |
end | |
it "should pull partially" do | |
subject.eval("x<-c(1L,2L,-5L,-3L,NA)") | |
[1,2,-5,-3,nil].each.with_index{|v, i| | |
expect(subject.pull("x[[#{i + 1}]]")).to eql(v) | |
} | |
end | |
it "should be the same using pull than R# methods" do | |
subject.eval("x <- #{rand(100000000)}") | |
expect(subject.pull("x")).to eql(subject.x) | |
end | |
it "should raise an NoMethod error on getter with 1 or more parameters" do | |
expect{subject.unknown_method(1)}.to raise_error(NoMethodError) | |
end | |
end | |
context "on assign (PREREQUISITE: all pull tests are passed)" do | |
it "should assign a Character" do | |
x = 'Value' | |
subject.assign("x", x) | |
expect(subject.pull('x')).to eql(x) | |
end | |
it "should assign an Integer" do | |
[0x12345678, -0x12345678].each{|x| | |
subject.assign("x", x) | |
expect(subject.pull('x')).to eql(x) | |
} | |
end | |
it "should assign a Double" do | |
[rand, 1 << 32, -(1 << 32)].each{|x| | |
subject.assign("x", x) | |
expect(subject.pull('x')).to eql(x.to_f) | |
} | |
subject.assign("x", Float::NAN) | |
expect(subject.pull('x').nan?).to be_truthy | |
end | |
it "should assign a Logical" do | |
[true, false, nil].each{|x| | |
subject.assign("x", x) | |
expect(subject.pull('x')).to eql(x) | |
} | |
end | |
it "should assign an Array of Character" do | |
x = ['a', 'b', nil] | |
subject.assign("x", x) | |
expect(subject.pull('x')).to eql(x) | |
end | |
it "should assign an Array of Integer" do | |
x = [1, 2, -5, -3, nil] | |
subject.assign("x", x) | |
expect(subject.pull('x')).to eql(x) | |
end | |
it "should assign an Array of Double" do | |
x = [rand(100000000), rand(0x1000) << 32, # Integer | |
rand, Rational(rand(1000), rand(1000) + 1), # Numeric except for Complex with available .to_f | |
nil, Float::NAN] | |
subject.assign("x", x) | |
expect(subject.pull('x')[0..-2]).to eql(x[0..-3].collect{|v| v.to_f} + [nil]) | |
expect(subject.pull('x')[-1].nan?).to be_truthy | |
end | |
it "should assign an Array of Logical" do | |
x = [true, false, nil] | |
subject.assign("x", x) | |
expect(subject.pull('x')).to eql(x) | |
end | |
it "should assign a Matrix" do | |
threshold = Float::EPSILON * 100 | |
[ | |
proc{rand(100000000)}, # integer matrix | |
proc{v = rand(100000000); v > 50000000 ? nil : v}, # integer matrix with NA | |
[ # double matrix | |
proc{rand}, | |
gen_matrix_cmp_per_elm_proc{|a, b| | |
expect(a).to be_within(threshold).of(b) | |
}, | |
], | |
[ # double matrix with NA | |
proc{v = rand; v > 0.5 ? nil : v}, | |
gen_matrix_cmp_per_elm_proc{|a, b| | |
if b.kind_of?(Numeric) then | |
expect(a).to be_within(threshold).of(b) | |
else | |
expect(a).to eql(nil) | |
end | |
}, | |
], | |
].each{|gen_proc, cmp_proc| | |
x = Matrix::build(100, 200){|i, j| gen_proc.call} # 100 x 200 matrix | |
subject.assign("x", x) | |
(cmp_proc || proc{|a, b| expect(a).to eql(b)}).call(subject.pull('x'), x) | |
} | |
end | |
it "should assign partially" do | |
x = [1, 2, -5, -3, nil] | |
subject.assign("x", x) | |
expect(subject.pull('x')).to eql(x) | |
subject.assign("x[[3]]", x[2] *= 10) | |
expect(subject.pull('x')).to eql(x) | |
end | |
it "should be the same using assign than R#= methods" do | |
x = rand(100000000) | |
subject.assign("x1", x) | |
subject.x2 = x | |
expect(subject.pull("x1")).to eql(subject.pull("x2")) | |
end | |
it "should raise an ArgumentError error on setter with 0 parameters" do | |
expect{subject.unknown_method=() }.to raise_error(ArgumentError) | |
end | |
end | |
end | |
describe "echo changes eval output" do | |
def check_output(echo_args, stdout, stderr) | |
r.echo(*echo_args) | |
expect{r.eval("write('out', stdout())")}.to output(stdout ? /^out/ : "").to_stdout | |
expect{r.eval("write('err', stderr())")}.to output(stderr ? /^err/ : "").to_stdout | |
end | |
it "should output both stdout and stderr when echo(true, true)" do | |
check_output([true, true], true, true) | |
end | |
it "should output stdout only when echo(true, false)" do | |
check_output([true, false], true, false) | |
end | |
it "should output nothing when echo(false)" do | |
check_output(false, false, false) | |
end | |
end | |
context "on eval in interactive mode" do | |
let(:params){ | |
super().merge({:interactive => true}) | |
} | |
it "should be interrupted by SIGINT" do | |
if r.instance_variable_get(:@platform) =~ /java$/ then | |
pending("JRuby does not give fully support for signal handling") | |
fail | |
end | |
int_invoked = false | |
int_handler = Signal::trap(:INT){int_invoked = true} | |
printed = [] | |
eval_res = r.eval(<<-__TEXT__){|line| | |
for(i in 1:10){ | |
print(i) | |
Sys.sleep(1) | |
} | |
__TEXT__ | |
line =~ /^\[1\] *(\S+)/ | |
printed << Integer($1) | |
Process::kill(:INT, $$) if (printed[-1] > 2) | |
} | |
Signal::trap(:INT, int_handler) | |
expect(int_invoked).to be_truthy | |
expect(eval_res).to be_falsy | |
expect(printed).not_to include(10) | |
end | |
end | |
context "on prompt" do | |
let(:params){ | |
super().merge({:interactive => true}) | |
} | |
let(:input){@input ||= []} | |
before(:all){ | |
begin | |
require 'readline' | |
rescue LoadError | |
end | |
} | |
before(:each){ | |
allow(Readline).to receive(:readline){|prompt, add_hist| | |
print(prompt) | |
input.shift | |
} if defined?(Readline) | |
allow(r).to receive(:gets){input.shift} | |
r.echo(true, true) | |
} | |
it "should exit with exit() input" do | |
['exit()', ' exit ( ) '].each{|str| | |
input.replace([str]) | |
expect{r.prompt}.to output(/^> /).to_stdout | |
} | |
end | |
it "should respond normally with correct inputs" do | |
[ | |
[['1'], "> [1] 1"], | |
[['1 +', '2'], "> + [1] 3"], | |
[['1 +', '2 +', '3'], "> + + [1] 6"], | |
[['a <- 1'], "> "], | |
[['a <-', '1'], "> + "], | |
].each{|src, dst| | |
input.replace(src + ['exit()']) | |
expect{r.prompt}.to output(/^#{Regexp::escape(dst)}/).to_stdout | |
} | |
end | |
it "should print error gently with incorrect inputs" do | |
[ | |
['1 +;'], | |
['a <-;'], | |
].each{|src| | |
input.replace(src + ['exit()']) | |
expect{r.prompt}.to output(/Unrecoverable parse error/).to_stdout | |
} | |
end | |
it "should print R error gently" do | |
[ | |
['stop("something wrong!"); print("skip")', 'print("do other")'], | |
].each{|src| | |
input.replace(src + ['exit()']) | |
expect{r.prompt}.to output(/something wrong\!.*(?!skip).*do other/m).to_stdout | |
} | |
end | |
end | |
context "on quit" do | |
it "return true" do | |
expect(r.quit).to be_truthy | |
end | |
it "returns an error if used again" do | |
r.quit | |
expect{r.eval("x=1")}.to raise_error(RinRuby::EngineClosed) | |
end | |
end | |
end | |
describe RinRuby do | |
let(:r){ | |
RinRuby.new(*([:echo_enabled, :interactive, :executable, :port_number, :port_width].collect{|k| params[k]})) | |
} | |
include_examples 'RinRubyCore' | |
end |