mirror of
https://github.com/ruby/ruby.git
synced 2025-08-15 13:39:04 +02:00

This commit inlines instructions for Class#new. To make this work, we added a new YARV instructions, `opt_new`. `opt_new` checks whether or not the `new` method is the default allocator method. If it is, it allocates the object, and pushes the instance on the stack. If not, the instruction jumps to the "slow path" method call instructions. Old instructions: ``` > ruby --dump=insns -e'Object.new' == disasm: #<ISeq:<main>@-e:1 (1,0)-(1,10)> 0000 opt_getconstant_path <ic:0 Object> ( 1)[Li] 0002 opt_send_without_block <calldata!mid:new, argc:0, ARGS_SIMPLE> 0004 leave ``` New instructions: ``` > ./miniruby --dump=insns -e'Object.new' == disasm: #<ISeq:<main>@-e:1 (1,0)-(1,10)> 0000 opt_getconstant_path <ic:0 Object> ( 1)[Li] 0002 putnil 0003 swap 0004 opt_new <calldata!mid:new, argc:0, ARGS_SIMPLE>, 11 0007 opt_send_without_block <calldata!mid:initialize, argc:0, FCALL|ARGS_SIMPLE> 0009 jump 14 0011 opt_send_without_block <calldata!mid:new, argc:0, ARGS_SIMPLE> 0013 swap 0014 pop 0015 leave ``` This commit speeds up basic object allocation (`Foo.new`) by 60%, but classes that take keyword parameters see an even bigger benefit because no hash is allocated when instantiating the object (3x to 6x faster). Here is an example that uses `Hash.new(capacity: 0)`: ``` > hyperfine "ruby --disable-gems -e'i = 0; while i < 10_000_000; Hash.new(capacity: 0); i += 1; end'" "./ruby --disable-gems -e'i = 0; while i < 10_000_000; Hash.new(capacity: 0); i += 1; end'" Benchmark 1: ruby --disable-gems -e'i = 0; while i < 10_000_000; Hash.new(capacity: 0); i += 1; end' Time (mean ± σ): 1.082 s ± 0.004 s [User: 1.074 s, System: 0.008 s] Range (min … max): 1.076 s … 1.088 s 10 runs Benchmark 2: ./ruby --disable-gems -e'i = 0; while i < 10_000_000; Hash.new(capacity: 0); i += 1; end' Time (mean ± σ): 627.9 ms ± 3.5 ms [User: 622.7 ms, System: 4.8 ms] Range (min … max): 622.7 ms … 633.2 ms 10 runs Summary ./ruby --disable-gems -e'i = 0; while i < 10_000_000; Hash.new(capacity: 0); i += 1; end' ran 1.72 ± 0.01 times faster than ruby --disable-gems -e'i = 0; while i < 10_000_000; Hash.new(capacity: 0); i += 1; end' ``` This commit changes the backtrace for `initialize`: ``` aaron@tc ~/g/ruby (inline-new)> cat test.rb class Foo def initialize puts caller end end def hello Foo.new end hello aaron@tc ~/g/ruby (inline-new)> ruby -v test.rb ruby 3.4.2 (2025-02-15 revisiond2930f8e7a
) +PRISM [arm64-darwin24] test.rb:8:in 'Class#new' test.rb:8:in 'Object#hello' test.rb:11:in '<main>' aaron@tc ~/g/ruby (inline-new)> ./miniruby -v test.rb ruby 3.5.0dev (2025-03-28T23:59:40Z inline-new c4157884e4) +PRISM [arm64-darwin24] test.rb:8:in 'Object#hello' test.rb:11:in '<main>' ``` It also increases memory usage for calls to `new` by 122 bytes: ``` aaron@tc ~/g/ruby (inline-new)> cat test.rb require "objspace" class Foo def initialize puts caller end end def hello Foo.new end puts ObjectSpace.memsize_of(RubyVM::InstructionSequence.of(method(:hello))) aaron@tc ~/g/ruby (inline-new)> make runruby RUBY_ON_BUG='gdb -x ./.gdbinit -p' ./miniruby -I./lib -I. -I.ext/common ./tool/runruby.rb --extout=.ext -- --disable-gems ./test.rb 656 aaron@tc ~/g/ruby (inline-new)> ruby -v test.rb ruby 3.4.2 (2025-02-15 revisiond2930f8e7a
) +PRISM [arm64-darwin24] 544 ``` Thanks to @ko1 for coming up with this idea! Co-Authored-By: John Hawthorn <john@hawthorn.email>
747 lines
17 KiB
Ruby
747 lines
17 KiB
Ruby
# -*- coding: us-ascii -*-
|
|
# frozen_string_literal: false
|
|
require 'test/unit'
|
|
require 'erb'
|
|
require 'stringio'
|
|
|
|
class TestERB < Test::Unit::TestCase
|
|
class MyError < RuntimeError ; end
|
|
|
|
def test_without_filename
|
|
erb = ERB.new("<% raise ::TestERB::MyError %>")
|
|
e = assert_raise(MyError) {
|
|
erb.result
|
|
}
|
|
assert_match(/\A\(erb\):1\b/, e.backtrace[0])
|
|
end
|
|
|
|
def test_with_filename
|
|
erb = ERB.new("<% raise ::TestERB::MyError %>")
|
|
erb.filename = "test filename"
|
|
e = assert_raise(MyError) {
|
|
erb.result
|
|
}
|
|
assert_match(/\Atest filename:1\b/, e.backtrace[0])
|
|
end
|
|
|
|
# [deprecated] This will be removed later
|
|
def test_without_filename_with_safe_level
|
|
erb = EnvUtil.suppress_warning do
|
|
ERB.new("<% raise ::TestERB::MyError %>", 1)
|
|
end
|
|
e = assert_raise(MyError) {
|
|
erb.result
|
|
}
|
|
assert_match(/\A\(erb\):1\b/, e.backtrace[0])
|
|
end
|
|
|
|
# [deprecated] This will be removed later
|
|
def test_with_filename_and_safe_level
|
|
erb = EnvUtil.suppress_warning do
|
|
ERB.new("<% raise ::TestERB::MyError %>", 1)
|
|
end
|
|
erb.filename = "test filename"
|
|
e = assert_raise(MyError) {
|
|
erb.result
|
|
}
|
|
assert_match(/\Atest filename:1\b/, e.backtrace[0])
|
|
end
|
|
|
|
def test_with_filename_lineno
|
|
erb = ERB.new("<% raise ::TestERB::MyError %>")
|
|
erb.filename = "test filename"
|
|
erb.lineno = 100
|
|
e = assert_raise(MyError) {
|
|
erb.result
|
|
}
|
|
assert_match(/\Atest filename:101\b/, e.backtrace[0])
|
|
end
|
|
|
|
def test_with_location
|
|
erb = ERB.new("<% raise ::TestERB::MyError %>")
|
|
erb.location = ["test filename", 200]
|
|
e = assert_raise(MyError) {
|
|
erb.result
|
|
}
|
|
assert_match(/\Atest filename:201\b/, e.backtrace[0])
|
|
end
|
|
|
|
def test_html_escape
|
|
assert_equal(" !"\#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~",
|
|
ERB::Util.html_escape(" !\"\#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"))
|
|
|
|
assert_equal("", ERB::Util.html_escape(""))
|
|
assert_equal("abc", ERB::Util.html_escape("abc"))
|
|
assert_equal("<<", ERB::Util.html_escape("<\<"))
|
|
assert_equal("'&"><", ERB::Util.html_escape("'&\"><"))
|
|
|
|
assert_equal("", ERB::Util.html_escape(nil))
|
|
assert_equal("123", ERB::Util.html_escape(123))
|
|
end
|
|
|
|
def test_html_escape_to_s
|
|
object = Object.new
|
|
def object.to_s
|
|
"object"
|
|
end
|
|
assert_equal("object", ERB::Util.html_escape(object))
|
|
end
|
|
|
|
def test_html_escape_extension
|
|
assert_nil(ERB::Util.method(:html_escape).source_location)
|
|
end if RUBY_ENGINE == 'ruby'
|
|
|
|
def test_concurrent_default_binding
|
|
# This test randomly fails with JRuby -- NameError: undefined local variable or method `template2'
|
|
pend if RUBY_ENGINE == 'jruby'
|
|
|
|
template1 = 'one <%= ERB.new(template2).result %>'
|
|
|
|
eval 'template2 = "two"', TOPLEVEL_BINDING
|
|
|
|
bug7046 = '[ruby-core:47638]'
|
|
assert_equal("one two", ERB.new(template1).result, bug7046)
|
|
end
|
|
end
|
|
|
|
class TestERBCore < Test::Unit::TestCase
|
|
def setup
|
|
@erb = ERB
|
|
end
|
|
|
|
def test_version
|
|
assert_equal(String, @erb.version.class)
|
|
end
|
|
|
|
def test_core
|
|
# [deprecated] Fix initializer later
|
|
EnvUtil.suppress_warning do
|
|
_test_core(nil)
|
|
_test_core(0)
|
|
_test_core(1)
|
|
end
|
|
end
|
|
|
|
def _test_core(safe)
|
|
erb = @erb.new("hello")
|
|
assert_equal("hello", erb.result)
|
|
|
|
erb = @erb.new("hello", safe, 0)
|
|
assert_equal("hello", erb.result)
|
|
|
|
erb = @erb.new("hello", safe, 1)
|
|
assert_equal("hello", erb.result)
|
|
|
|
erb = @erb.new("hello", safe, 2)
|
|
assert_equal("hello", erb.result)
|
|
|
|
src = <<EOS
|
|
%% hi
|
|
= hello
|
|
<% 3.times do |n| %>
|
|
% n=0
|
|
* <%= n %>
|
|
<% end %>
|
|
EOS
|
|
|
|
ans = <<EOS
|
|
%% hi
|
|
= hello
|
|
|
|
% n=0
|
|
* 0
|
|
|
|
% n=0
|
|
* 1
|
|
|
|
% n=0
|
|
* 2
|
|
|
|
EOS
|
|
erb = @erb.new(src)
|
|
assert_equal(ans, erb.result)
|
|
erb = @erb.new(src, safe, 0)
|
|
assert_equal(ans, erb.result)
|
|
erb = @erb.new(src, safe, '')
|
|
assert_equal(ans, erb.result)
|
|
|
|
ans = <<EOS
|
|
%% hi
|
|
= hello
|
|
% n=0
|
|
* 0% n=0
|
|
* 1% n=0
|
|
* 2
|
|
EOS
|
|
erb = @erb.new(src, safe, 1)
|
|
assert_equal(ans.chomp, erb.result)
|
|
erb = @erb.new(src, safe, '>')
|
|
assert_equal(ans.chomp, erb.result)
|
|
|
|
ans = <<EOS
|
|
%% hi
|
|
= hello
|
|
% n=0
|
|
* 0
|
|
% n=0
|
|
* 1
|
|
% n=0
|
|
* 2
|
|
EOS
|
|
|
|
erb = @erb.new(src, safe, 2)
|
|
assert_equal(ans, erb.result)
|
|
erb = @erb.new(src, safe, '<>')
|
|
assert_equal(ans, erb.result)
|
|
|
|
ans = <<EOS
|
|
% hi
|
|
= hello
|
|
|
|
* 0
|
|
|
|
* 0
|
|
|
|
* 0
|
|
|
|
EOS
|
|
erb = @erb.new(src, safe, '%')
|
|
assert_equal(ans, erb.result)
|
|
|
|
ans = <<EOS
|
|
% hi
|
|
= hello
|
|
* 0* 0* 0
|
|
EOS
|
|
erb = @erb.new(src, safe, '%>')
|
|
assert_equal(ans.chomp, erb.result)
|
|
|
|
ans = <<EOS
|
|
% hi
|
|
= hello
|
|
* 0
|
|
* 0
|
|
* 0
|
|
EOS
|
|
erb = @erb.new(src, safe, '%<>')
|
|
assert_equal(ans, erb.result)
|
|
end
|
|
|
|
def test_trim_line1_with_carriage_return
|
|
erb = @erb.new("<% 3.times do %>\r\nline\r\n<% end %>\r\n", trim_mode: '>')
|
|
assert_equal("line\r\n" * 3, erb.result)
|
|
|
|
erb = @erb.new("<% 3.times do %>\r\nline\r\n<% end %>\r\n", trim_mode: '%>')
|
|
assert_equal("line\r\n" * 3, erb.result)
|
|
end
|
|
|
|
def test_trim_line2_with_carriage_return
|
|
erb = @erb.new("<% 3.times do %>\r\nline\r\n<% end %>\r\n", trim_mode: '<>')
|
|
assert_equal("line\r\n" * 3, erb.result)
|
|
|
|
erb = @erb.new("<% 3.times do %>\r\nline\r\n<% end %>\r\n", trim_mode: '%<>')
|
|
assert_equal("line\r\n" * 3, erb.result)
|
|
end
|
|
|
|
def test_explicit_trim_line_with_carriage_return
|
|
erb = @erb.new("<%- 3.times do -%>\r\nline\r\n<%- end -%>\r\n", trim_mode: '-')
|
|
assert_equal("line\r\n" * 3, erb.result)
|
|
|
|
erb = @erb.new("<%- 3.times do -%>\r\nline\r\n<%- end -%>\r\n", trim_mode: '%-')
|
|
assert_equal("line\r\n" * 3, erb.result)
|
|
end
|
|
|
|
def test_safe_level_warning
|
|
assert_warning(/#{__FILE__}:#{__LINE__ + 1}/) do
|
|
@erb.new("", 1)
|
|
end
|
|
end
|
|
|
|
def test_invalid_trim_mode
|
|
pend if RUBY_ENGINE == 'truffleruby'
|
|
|
|
assert_warning(/#{__FILE__}:#{__LINE__ + 1}/) do
|
|
@erb.new("", trim_mode: 'abc-def')
|
|
end
|
|
|
|
assert_warning(/Invalid ERB trim mode/) do
|
|
@erb.new("", trim_mode: 'abc-def')
|
|
end
|
|
|
|
assert_warning(/Invalid ERB trim mode/) do
|
|
@erb.new("", trim_mode: '%<')
|
|
end
|
|
|
|
assert_warning(/Invalid ERB trim mode/) do
|
|
@erb.new("", trim_mode: '%<>-')
|
|
end
|
|
|
|
assert_warning(/Invalid ERB trim mode/) do
|
|
@erb.new("", trim_mode: 3)
|
|
end
|
|
end
|
|
|
|
def test_run
|
|
out = StringIO.new
|
|
orig, $stdout = $stdout, out
|
|
|
|
num = 3
|
|
@erb.new('<%= num * 3 %>').run(binding)
|
|
|
|
$stdout = orig
|
|
out.rewind
|
|
assert_equal('9', out.read)
|
|
return unless num # to remove warning
|
|
end
|
|
|
|
class Foo; end
|
|
|
|
def test_def_class
|
|
erb = @erb.new('hello')
|
|
cls = erb.def_class
|
|
assert_equal(Object, cls.superclass)
|
|
assert_respond_to(cls.new, 'result')
|
|
cls = erb.def_class(Foo)
|
|
assert_equal(Foo, cls.superclass)
|
|
assert_respond_to(cls.new, 'result')
|
|
cls = erb.def_class(Object, 'erb')
|
|
assert_equal(Object, cls.superclass)
|
|
assert_respond_to(cls.new, 'erb')
|
|
end
|
|
|
|
def test_percent
|
|
src = <<EOS
|
|
%n = 1
|
|
<%= n%>
|
|
EOS
|
|
assert_equal("1\n", ERB.new(src, trim_mode: '%').result(binding))
|
|
|
|
src = <<EOS
|
|
<%
|
|
%>
|
|
EOS
|
|
ans = "\n"
|
|
assert_equal(ans, ERB.new(src, trim_mode: '%').result(binding))
|
|
|
|
src = "<%\n%>"
|
|
# ans = "\n"
|
|
ans = ""
|
|
assert_equal(ans, ERB.new(src, trim_mode: '%').result(binding))
|
|
|
|
src = <<EOS
|
|
<%
|
|
n = 1
|
|
%><%= n%>
|
|
EOS
|
|
assert_equal("1\n", ERB.new(src, trim_mode: '%').result(binding))
|
|
|
|
src = <<EOS
|
|
%n = 1
|
|
%% <% n = 2
|
|
n.times do |i|%>
|
|
%% %%><%%<%= i%><%
|
|
end%>
|
|
%%%
|
|
EOS
|
|
ans = <<EOS
|
|
%\s
|
|
% %%><%0
|
|
% %%><%1
|
|
%%
|
|
EOS
|
|
assert_equal(ans, ERB.new(src, trim_mode: '%').result(binding))
|
|
end
|
|
|
|
def test_def_erb_method
|
|
klass = Class.new
|
|
klass.module_eval do
|
|
extend ERB::DefMethod
|
|
fname = File.join(File.dirname(File.expand_path(__FILE__)), 'hello.erb')
|
|
def_erb_method('hello', fname)
|
|
end
|
|
assert_respond_to(klass.new, 'hello')
|
|
|
|
assert_not_respond_to(klass.new, 'hello_world')
|
|
erb = @erb.new('hello, world')
|
|
klass.module_eval do
|
|
def_erb_method('hello_world', erb)
|
|
end
|
|
assert_respond_to(klass.new, 'hello_world')
|
|
end
|
|
|
|
def test_def_method_without_filename
|
|
klass = Class.new
|
|
erb = ERB.new("<% raise ::TestERB::MyError %>")
|
|
erb.filename = "test filename"
|
|
assert_not_respond_to(klass.new, 'my_error')
|
|
erb.def_method(klass, 'my_error')
|
|
e = assert_raise(::TestERB::MyError) {
|
|
klass.new.my_error
|
|
}
|
|
assert_match(/\A\(ERB\):1\b/, e.backtrace[0])
|
|
end
|
|
|
|
def test_def_method_with_fname
|
|
klass = Class.new
|
|
erb = ERB.new("<% raise ::TestERB::MyError %>")
|
|
erb.filename = "test filename"
|
|
assert_not_respond_to(klass.new, 'my_error')
|
|
erb.def_method(klass, 'my_error', 'test fname')
|
|
e = assert_raise(::TestERB::MyError) {
|
|
klass.new.my_error
|
|
}
|
|
assert_match(/\Atest fname:1\b/, e.backtrace[0])
|
|
end
|
|
|
|
def test_def_module
|
|
klass = Class.new
|
|
klass.include ERB.new('<%= val %>').def_module('render(val)')
|
|
assert_equal('1', klass.new.render(1))
|
|
end
|
|
|
|
def test_escape
|
|
src = <<EOS
|
|
1.<%% : <%="<%%"%>
|
|
2.%%> : <%="%%>"%>
|
|
3.
|
|
% x = "foo"
|
|
<%=x%>
|
|
4.
|
|
%% print "foo"
|
|
5.
|
|
%% <%="foo"%>
|
|
6.<%="
|
|
% print 'foo'
|
|
"%>
|
|
7.<%="
|
|
%% print 'foo'
|
|
"%>
|
|
EOS
|
|
ans = <<EOS
|
|
1.<% : <%%
|
|
2.%%> : %>
|
|
3.
|
|
foo
|
|
4.
|
|
% print "foo"
|
|
5.
|
|
% foo
|
|
6.
|
|
% print 'foo'
|
|
|
|
7.
|
|
%% print 'foo'
|
|
|
|
EOS
|
|
assert_equal(ans, ERB.new(src, trim_mode: '%').result)
|
|
end
|
|
|
|
def test_keep_lineno
|
|
src = <<EOS
|
|
Hello,\s
|
|
% x = "World"
|
|
<%= x%>
|
|
% raise("lineno")
|
|
EOS
|
|
|
|
erb = ERB.new(src, trim_mode: '%')
|
|
e = assert_raise(RuntimeError) {
|
|
erb.result
|
|
}
|
|
assert_match(/\A\(erb\):4\b/, e.backtrace[0].to_s)
|
|
|
|
src = <<EOS
|
|
%>
|
|
Hello,\s
|
|
<% x = "World%%>
|
|
"%>
|
|
<%= x%>
|
|
EOS
|
|
|
|
ans = <<EOS
|
|
%>Hello,\s
|
|
World%>
|
|
EOS
|
|
assert_equal(ans, ERB.new(src, trim_mode: '>').result)
|
|
|
|
ans = <<EOS
|
|
%>
|
|
Hello,\s
|
|
|
|
World%>
|
|
EOS
|
|
assert_equal(ans, ERB.new(src, trim_mode: '<>').result)
|
|
|
|
ans = <<EOS
|
|
%>
|
|
Hello,\s
|
|
|
|
World%>
|
|
|
|
EOS
|
|
assert_equal(ans, ERB.new(src).result)
|
|
|
|
src = <<EOS
|
|
Hello,\s
|
|
<% x = "World%%>
|
|
"%>
|
|
<%= x%>
|
|
<% raise("lineno") %>
|
|
EOS
|
|
|
|
erb = ERB.new(src)
|
|
e = assert_raise(RuntimeError) {
|
|
erb.result
|
|
}
|
|
assert_match(/\A\(erb\):5\b/, e.backtrace[0].to_s)
|
|
|
|
erb = ERB.new(src, trim_mode: '>')
|
|
e = assert_raise(RuntimeError) {
|
|
erb.result
|
|
}
|
|
assert_match(/\A\(erb\):5\b/, e.backtrace[0].to_s)
|
|
|
|
erb = ERB.new(src, trim_mode: '<>')
|
|
e = assert_raise(RuntimeError) {
|
|
erb.result
|
|
}
|
|
assert_match(/\A\(erb\):5\b/, e.backtrace[0].to_s)
|
|
|
|
src = <<EOS
|
|
% y = 'Hello'
|
|
<%- x = "World%%>
|
|
"-%>
|
|
<%= x %><%- x = nil -%>\s
|
|
<% raise("lineno") %>
|
|
EOS
|
|
|
|
erb = ERB.new(src, trim_mode: '-')
|
|
e = assert_raise(RuntimeError) {
|
|
erb.result
|
|
}
|
|
assert_match(/\A\(erb\):5\b/, e.backtrace[0].to_s)
|
|
|
|
erb = ERB.new(src, trim_mode: '%-')
|
|
e = assert_raise(RuntimeError) {
|
|
erb.result
|
|
}
|
|
assert_match(/\A\(erb\):5\b/, e.backtrace[0].to_s)
|
|
end
|
|
|
|
def test_explicit
|
|
src = <<EOS
|
|
<% x = %w(hello world) -%>
|
|
NotSkip <%- y = x -%> NotSkip
|
|
<% x.each do |w| -%>
|
|
<%- up = w.upcase -%>
|
|
* <%= up %>
|
|
<% end -%>
|
|
<%- z = nil -%> NotSkip <%- z = x %>
|
|
<%- z.each do |w| -%>
|
|
<%- down = w.downcase -%>
|
|
* <%= down %>
|
|
<%- up = w.upcase -%>
|
|
* <%= up %>
|
|
<%- end -%>
|
|
KeepNewLine <%- z = nil -%>\s
|
|
EOS
|
|
|
|
ans = <<EOS
|
|
NotSkip NotSkip
|
|
* HELLO
|
|
* WORLD
|
|
NotSkip\s
|
|
* hello
|
|
* HELLO
|
|
* world
|
|
* WORLD
|
|
KeepNewLine \s
|
|
EOS
|
|
assert_equal(ans, ERB.new(src, trim_mode: '-').result)
|
|
assert_equal(ans, ERB.new(src, trim_mode: '-%').result)
|
|
end
|
|
|
|
def test_url_encode
|
|
assert_equal("Programming%20Ruby%3A%20%20The%20Pragmatic%20Programmer%27s%20Guide",
|
|
ERB::Util.url_encode("Programming Ruby: The Pragmatic Programmer's Guide"))
|
|
|
|
assert_equal("%A5%B5%A5%F3%A5%D7%A5%EB",
|
|
ERB::Util.url_encode("\xA5\xB5\xA5\xF3\xA5\xD7\xA5\xEB".force_encoding("EUC-JP")))
|
|
|
|
assert_equal("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~",
|
|
ERB::Util.url_encode("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"),
|
|
"should not escape any unreserved characters, as per RFC3986 Section 2.3")
|
|
end
|
|
|
|
def test_percent_after_etag
|
|
assert_equal("1%", @erb.new("<%= 1 %>%", trim_mode: "%").result)
|
|
end
|
|
|
|
def test_token_extension
|
|
extended_erb = Class.new(ERB)
|
|
extended_erb.module_eval do
|
|
def make_compiler(trim_mode)
|
|
compiler = Class.new(ERB::Compiler)
|
|
compiler.module_eval do
|
|
def compile_stag(stag, out, scanner)
|
|
case stag
|
|
when '<%=='
|
|
scanner.stag = stag
|
|
add_put_cmd(out, content) if content.size > 0
|
|
self.content = ''
|
|
else
|
|
super
|
|
end
|
|
end
|
|
|
|
def compile_content(stag, out)
|
|
case stag
|
|
when '<%=='
|
|
out.push("#{@insert_cmd}(::ERB::Util.html_escape(#{content}))")
|
|
else
|
|
super
|
|
end
|
|
end
|
|
|
|
def make_scanner(src)
|
|
scanner = Class.new(ERB::Compiler::SimpleScanner)
|
|
scanner.module_eval do
|
|
def stags
|
|
['<%=='] + super
|
|
end
|
|
end
|
|
scanner.new(src, @trim_mode, @percent)
|
|
end
|
|
end
|
|
compiler.new(trim_mode)
|
|
end
|
|
end
|
|
|
|
src = <<~EOS
|
|
<% tag = '<>' \%>
|
|
<\%= tag \%>
|
|
<\%== tag \%>
|
|
EOS
|
|
ans = <<~EOS
|
|
|
|
<>
|
|
<>
|
|
EOS
|
|
assert_equal(ans, extended_erb.new(src).result)
|
|
end
|
|
|
|
def test_frozen_string_literal
|
|
bug12031 = '[ruby-core:73561] [Bug #12031]'
|
|
e = @erb.new("<%#encoding: us-ascii%>a")
|
|
e.src.sub!(/\A#(?:-\*-)?(.*)(?:-\*-)?/) {
|
|
'# -*- \1; frozen-string-literal: true -*-'
|
|
}
|
|
assert_equal("a", e.result, bug12031)
|
|
|
|
%w(false true).each do |flag|
|
|
erb = @erb.new("<%#frozen-string-literal: #{flag}%><%=''.frozen?%>")
|
|
assert_equal(flag, erb.result)
|
|
end
|
|
end
|
|
|
|
def test_result_with_hash
|
|
erb = @erb.new("<%= foo %>")
|
|
assert_equal("1", erb.result_with_hash(foo: "1"))
|
|
end
|
|
|
|
def test_result_with_hash_does_not_use_caller_local_variables
|
|
erb = @erb.new("<%= foo %>")
|
|
foo = 1
|
|
assert_raise(NameError) { erb.result_with_hash({}) }
|
|
assert_equal("1", erb.result_with_hash(foo: foo))
|
|
end
|
|
|
|
def test_result_with_hash_does_not_modify_caller_binding
|
|
erb = @erb.new("<%= foo %>")
|
|
erb.result_with_hash(foo: "1")
|
|
assert_equal(false, binding.local_variable_defined?(:foo))
|
|
end
|
|
|
|
def test_result_with_hash_does_not_modify_toplevel_binding
|
|
erb = @erb.new("<%= foo %>")
|
|
erb.result_with_hash(foo: "1")
|
|
assert_equal(false, TOPLEVEL_BINDING.local_variable_defined?(:foo))
|
|
TOPLEVEL_BINDING.eval 'template2 = "two"'
|
|
erb = @erb.new("<%= template2 %>")
|
|
erb.result_with_hash(template2: "TWO")
|
|
assert_equal "two", TOPLEVEL_BINDING.local_variable_get("template2")
|
|
end
|
|
|
|
# This depends on the behavior that #local_variable_set raises TypeError by invalid key.
|
|
def test_result_with_hash_with_invalid_keys_raises_type_error
|
|
erb = @erb.new("<%= 1 %>")
|
|
assert_raise(TypeError) { erb.result_with_hash({ 1 => "1" }) }
|
|
end
|
|
|
|
# Bug#14243
|
|
def test_half_working_comment_backward_compatibility
|
|
assert_nothing_raised do
|
|
@erb.new("<% # comment %>\n").result
|
|
end
|
|
end
|
|
|
|
# [deprecated] These interfaces will be removed later
|
|
def test_deprecated_interface_warnings
|
|
[nil, 0, 1, 2].each do |safe|
|
|
assert_warn(/2nd argument of ERB.new is deprecated/) do
|
|
ERB.new('', safe)
|
|
end
|
|
end
|
|
|
|
[nil, '', '%', '%<>'].each do |trim|
|
|
assert_warn(/3rd argument of ERB.new is deprecated/) do
|
|
ERB.new('', nil, trim)
|
|
end
|
|
end
|
|
|
|
[nil, '_erbout', '_hamlout'].each do |eoutvar|
|
|
assert_warn(/4th argument of ERB.new is deprecated/) do
|
|
ERB.new('', nil, nil, eoutvar)
|
|
end
|
|
end
|
|
end
|
|
|
|
def test_prohibited_marshal_dump
|
|
erb = ERB.new("")
|
|
assert_raise(TypeError) {Marshal.dump(erb)}
|
|
end
|
|
|
|
def test_prohibited_marshal_load
|
|
erb = ERB.allocate
|
|
erb.instance_variable_set(:@src, "")
|
|
erb.instance_variable_set(:@lineno, 1)
|
|
erb.instance_variable_set(:@_init, true)
|
|
erb = Marshal.load(Marshal.dump(erb))
|
|
assert_raise(ArgumentError) {erb.result}
|
|
end
|
|
|
|
def test_multi_line_comment_lineno
|
|
erb = ERB.new(<<~EOS)
|
|
<%= __LINE__ %>
|
|
<%#
|
|
%><%= __LINE__ %>
|
|
EOS
|
|
assert_equal <<~EOS, erb.result
|
|
1
|
|
3
|
|
EOS
|
|
end
|
|
end
|
|
|
|
class TestERBCoreWOStrScan < TestERBCore
|
|
def setup
|
|
@save_map = ERB::Compiler::Scanner.instance_variable_get('@scanner_map')
|
|
map = {[nil, false]=>ERB::Compiler::SimpleScanner}
|
|
ERB::Compiler::Scanner.instance_variable_set('@scanner_map', map)
|
|
super
|
|
end
|
|
|
|
def teardown
|
|
ERB::Compiler::Scanner.instance_variable_set('@scanner_map', @save_map)
|
|
end
|
|
end
|