Ruby 3.2, You’ll be able to measure coverages of ERB template!! (1) eval, ERB part

igsr5 (Ichigo)
6 min readOct 2, 2022

--

Photo by Akhila Katuri on Unsplash

2022/09/22, Ruby had got an interesting change for code coverage. Specifically, it seems that code coverage in eval can now be taken, which was not possible before.

You may not sound familiar with “in eval”, but ERB and HAML templates can benefit from these changes because they use “eval”.

Overview

Last week, 2022/09/22, an interesting change for code coverage had been merged into ruby’s master branch.

With these changes, we may be able to measure coverages in the “eval” program!!

  • What we can do now
    - measuring coverage in “eval” programs.
  • Its use cases
    - measuring coverage in “View” of Rails.

So, in this post, I will survey the code coverage of “eval” and ERB, and finally, as a practical use case, confirm that the coverage of “View Template” in Rails Application can be measured coverages.

Prerequisite Knowledge

Code Coverage

Code Coverage is the records of which codes were executed. A Common use case includes using it to measure coverages of test code. (“test coverage”)
In addition, “oneshot coverage”, which is implemented in some languages, is useful for detecting dead code.

Ruby offers this feature by Coverageclass as an official library.

Code Coverage in eval

The aforementioned Coverage class hasn’t supported measuring coverage in eval until today.

For example, executing coverage of eval program.

1  def foo(n)
2 if n <= 10
3   p "n < 10"
4  else
5 p "n >= 10"
6 end
7 end
8
9 eval <<~RUBY, nil, __FILE__, __LINE__ + 1
10 def bar(n)
11 if n <= 10
12 p "n < 10"
13 else
14 p "n >= 10"
15 end
16 end
17 RUBY
18
19 foo(1)
20 foo(2)
21 bar(1)
22 bar(2)

This program measured code coverage.

require "coverage"Coverage.start(lines: true, eval: true)
load "lib/foo.rb"
p Coverage.result

This result.

"n < 10"
"n < 10"
"n < 10"
"n < 10"
{"lib/boo.rb"=>{:lines=>[1, 2, 2, nil, 0, nil, nil, nil, 1, nil, nil, nil, nil, nil, nil, nil, nil, nil, 1, 1, 1, 1]}

Lines between 10 and 17, coverage cannot be measured coverage.

About “View” of Rails

View Template(ERB, HAML, …) of Rails use “eval” internally.
Also, there is a problem in that the coverage in the “View” cannot be measured due to the coverage nature in the eval as described in the previous section.

Verification

In this verification, we will check the following four points on the latest master branch of ruby. (2022/09/28)

  1. can measure coverage in “eval”. (this post)
  2. can measure coverage in “ERB” template. (this post)
  3. can measure coverage in the “View” of the Rails application. (next post)
    - ERB
    - HAML

In addition, this post will introduce 1 and 2, and 3 will be explained in the next post.

There are codes used for verification.
https://github.com/igsr5/inspection-of-latest-ruby-for-eval-coverage

Verification environment

In this verification, we will use two versions of ruby, 3.1.0 and 2022-09-28 master to compare results.

1. can measure coverage in “eval”

This is the simple eval program we mentioned earlier.
we expect this verification to measure coverage in “eval” when only 2022–09–28 master.

1  def foo(n)
2 if n <= 10
3   p "n < 10"
4  else
5 p "n >= 10"
6 end
7 end
8
9 eval <<~RUBY, nil, __FILE__, __LINE__ + 1
10 def bar(n)
11 if n <= 10
12 p "n < 10"
13 else
14 p "n >= 10"
15 end
16 end
17 RUBY
18
19 foo(1)
20 foo(2)
21 bar(1)
22 bar(2)

This program measured code coverage. Note that eval mode has been added.

require "coverage"Coverage.start(lines: true, eval: true)
load "lib/foo.rb"
p Coverage.result

This result.

Case 3.1.0`:

"n < 10"
"n < 10"
"n < 10"
"n < 10"
{"lib/boo.rb"=>{:lines=>[1, 2, 2, nil, 0, nil, nil, nil, 1, nil, nil, nil, nil, nil, nil, nil, nil, nil, 1, 1, 1, 1]}}

Case 2022-09-28 master :

"n < 10"
"n < 10"
"n < 10"
"n < 10"
{"lib/boo.rb"=>{:lines=>[1, 2, 2, nil, 0, nil, nil, nil, 1, 1, 2, 2, nil, 0, nil, nil, nil, nil, 1, 1, 1, 1]}}

In 2022-09-28 master compared to 3.1.0, we can see the coverage for lines between 10 and 17 has been able to measure.

2. can measure coverage in “ERB” template

Next, the code coverage of using the “ERB” template is verified.

“ERB” class is using “eval” when rendering the template, so it is expected that code coverage in the “ERB” template can be taken as in Verification 1.

Using program.

require "erb"# Build template data class.
class Product
def initialize( code, name, desc, cost )
@code = code
@name = name
@desc = desc
@cost = cost
@features = [ ]
end
def add_feature( feature )
@features << feature
end
# Support templating of member data.
def get_binding
binding
end
# ...
end
# Create template.
template = %{
<html>
<head><title>Ruby Toys -- <%= @name %></title></head>
<body>
<h1><%= @name %> (<%= @code %>)</h1>
<p><%= @desc %></p>
<ul>
<% @features.each do |f| %>
<li><b><%= f %></b></li>
<% end %>
</ul>
<p>
<% if @cost < 10 %>
<b>Only <%= @cost %>!!!</b>
<% else %>
Call for a price, today!
<% end %>
</p>
</body>
</html>
}.gsub(/^ /, '')
rhtml = ERB.new(template)# Set up template data.
toy = Product.new( "TZ-1002",
"Rubysapien",
"Geek's Best Friend! Responds to Ruby commands...",
999.95 )
toy.add_feature("Listens for verbal commands in the Ruby language!")
toy.add_feature("Ignores Perl, Java, and all C variants.")
toy.add_feature("Karate-Chop Action!!!")
toy.add_feature("Matz signature on left leg.")
toy.add_feature("Gem studded eyes... Rubies, of course!")
# Produce result.
rhtml.run(toy.get_binding)

Coverage.

require "coverage"Coverage.start(lines: true, eval: true)
load "lib/erb_sample.rb"
p Coverage.result

This Result.

Case 3.1.0:

{"lib/erb.rb"=>{:oneshot_lines=>[3, 6, 7, 16, 21, 29, 55, 58, 8, 9, 10, 11, 13, 62, 17, 63, 64, 65, 66, 69, 22]}, "/usr/lib/ruby/2.7.0/erb.rb"=>{:oneshot_lines=>[15, 258, 259, 262, 269, 340, 341, 342, 345, 346, 349, 350, 351, 352, 355, 358, 362, 367, 368, 369, 375, 376, 378, 381, 382, 401, 413, 426, 435, 449, 471, 489, 490, 495, 359, 498, 501, 502, 513, 353, 515, 516, 536, 539, 540, 550, 552, 556, 562, 572, 576,  582, 605, 629, 642, 659, 688, 694, 701, 704, 707, 710, 713, 715, 718, 720, 736, 744, 809, 830, 831, 832,  833, 838, 843, 846, 850, 854, 871, 881, 889, 901, 910, 922, 932, 941, 958, 977, 986, 988, 989, 1002,  1005,1006, 1007, 1021, 1026, 1027, 1028, 1034, 1063, 1064, 1067, 1077, 811, 814, 818, 823, 839, 695, 660,  666, 696, 697, 698, 699, 824, 882, 883, 884, 885, 825, 583, 584, 585, 586, 721, 722, 723, 733, 587, 541,  542, 543, 544, 545, 546, 547, 553, 589, 590, 689, 363, 364, 370, 371, 372, 373, 591, 503, 504, 505, 506,  507, 508, 592, 593, 594, 595, 606, 625, 509, 615, 616, 573, 617, 597, 630, 638, 632, 643, 653, 577, 633,  634, 645, 650, 600, 601, 563, 564, 565, 567, 568, 602, 826, 827, 828, 890, 902, 905]},  "/usr/lib/ruby/2.7.0/cgi/util.rb"=>{:oneshot_lines=>[2, 3, 4, 5, 7, 8, 12, 22, 30, 41, 58, 65, 122, 125,  140, 160, 172, 175, 178, 181, 187, 211, 222]}}

Case 2022-09-28 master:

{"lib/erb.rb"=>{:oneshot_lines=>[3, 6, 7, 16, 21, 29, 55, 58, 8, 9, 10, 11, 13, 62, 17, 63, 64, 65, 66, 69,  22]}, "/root/.rubies/ruby-master/lib/ruby/3.2.0+2/erb.rb"=>{:oneshot_lines=>[15, 16, 259, 260, 261, 264,  271, 342, 343, 344, 347, 348, 351, 352, 353, 354, 357, 360, 364, 369, 370, 371, 377, 378, 380, 383, 384,  403, 415, 428, 437, 451, 473, 491, 492, 497, 361, 500, 503, 504, 515, 355, 517, 518, 538, 541, 542, 552,  554, 558, 564, 574, 578, 584, 607, 631, 644, 661, 690, 696, 703, 706, 709, 712, 715, 717, 720, 722, 738,  746, 811, 832, 833, 838, 843, 846, 850, 854, 871, 881, 889, 901, 910, 922, 932, 941, 958, 977, 986, 988,  989, 1002, 1005, 1006, 1007, 1021, 1026, 1027, 1028, 1034, 1063, 1064, 1067, 1077, 813, 816, 820, 825, 839,  697, 662, 668, 698, 699, 700, 701, 826, 882, 883, 884, 885, 827, 585, 586, 587, 588, 723, 724, 725, 735,  589, 543, 544, 545, 546, 547, 548, 549, 555, 591, 592, 691, 365, 366, 372, 373, 374, 375, 593, 505, 506,  507, 508, 509, 510, 594, 595, 596, 597, 608, 627, 511, 617, 618, 575, 619, 599, 632, 640, 634, 645, 655,  579, 635, 636, 647, 652, 602, 603, 565, 566, 567, 569, 570, 604, 828, 829, 830, 890, 902, 905]},  "/root/.rubies/ruby-master/lib/ruby/3.2.0+2/cgi/util.rb"=>{:oneshot_lines=>[2, 3, 4, 5, 7, 8, 14, 27, 41,  53, 63, 74, 94, 101, 160, 163, 178, 198, 210, 213, 219, 240, 251]}, "/root/.rubies/ruby- master/lib/ruby/3.2.0+2/erb/version.rb"=>{:oneshot_lines=>[2, 3, 4]},  "(erb)"=>{:oneshot_lines=>[1, 3, 6, 7, 10, 10, 11, 12, 16, 18, 20, 25]

It’s a little confusing, in 2022-09-28 master compared to 3.1.0, we can see the coverage for “ERB” template.

Next verification…

In this post, we have verified to be able to measure code coverage for “eval” and “ERB”. Next post, as a more practical use case, we will verify to be able to measure code coverage for “View” template of Rails. In addition, we will use simplecov for the verification.

--

--

igsr5 (Ichigo)

Frontend and Backend, Infrastructure Engineer. I love to solve complex problems in combination with some technology fields. more info, https://igsr5.dev