Skip to content

Commit 0bbd01b

Browse files
authored
Engine: Add helpers to compare compiled/rendered outputs of the Engine (#774)
This commit adds comprehensive tooling to compare `Herb::Engine`'s output against `Erubi::Engine` for both the compiled templates and the rendered templates. It introduces three CLI tools: `bin/compare` for full comparisons, `bin/compare-compile` for compiled source comparisons, and `bin/compare-render` for rendered output comparisons. All tools use `difftastic` to provide rich diffs showing both string-level and semantic differences (Ruby AST for compiled code, HTML structure for rendered output). <img width="2115" height="1411" alt="CleanShot 2025-11-03 at 21 17 47@2x" src="https://github.com/user-attachments/assets/f5017f6e-d514-4c00-9ca9-9e1680795693" /> <img width="1246" height="934" alt="CleanShot 2025-11-03 at 21 17 40@2x" src="https://github.com/user-attachments/assets/35ff4f15-04d1-4e37-99f7-74211f6543e4" /> --- It also adds these helpers in `snapshot_utils.rb` so we can also run the comparison for all `assert_evaluated_snapshot` and `assert_compiled_snapshot` in the test suite using `COMPARE_WITH_ERUBI=true` ``` COMPARE_WITH_ERUBI=true mtest COMPARE_WITH_ERUBI=true mtest test/engine/evaluation_test.rb ``` Erubi compatibility is critical for Herb's adoption. Erubi is the de facto standard ERB implementation used across the Ruby ecosystem, particularly in Rails. These comparison helpers serve as both documentation and verification of our compatibility commitment. They help catch regressions during development and make it trivial to investigate any differences between implementations.
1 parent a50c5dc commit 0bbd01b

File tree

8 files changed

+549
-6
lines changed

8 files changed

+549
-6
lines changed

.rubocop.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ Style/StringLiterals:
1212
Enabled: true
1313
EnforcedStyle: double_quotes
1414

15+
Style/MixinUsage:
16+
Exclude:
17+
- bin/compare
18+
- bin/compare-render
19+
- bin/compare-compile
20+
1521
Style/ClassAndModuleChildren:
1622
Enabled: false
1723

@@ -94,6 +100,7 @@ Metrics/ModuleLength:
94100
Exclude:
95101
- test/**/*.rb
96102
- templates/**/*.rb
103+
- bin/lib/compare_helpers.rb
97104

98105
Metrics/BlockLength:
99106
Max: 30
@@ -160,6 +167,8 @@ Security/Eval:
160167
- lib/herb/cli.rb
161168
- test/**/*.rb
162169
- bin/erubi-render
170+
- bin/compare-render
171+
- bin/compare-render
163172

164173
Security/MarshalLoad:
165174
Exclude:

Gemfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ gemspec
77
gem "prism", github: "ruby/prism", tag: "v1.6.0"
88

99
gem "actionview", "~> 8.0"
10+
gem "difftastic", "~> 0.7"
11+
gem "erubi"
1012
gem "lz_string"
1113
gem "maxitest"
1214
gem "minitest-difftastic", "~> 0.2"

Gemfile.lock

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,8 @@ PLATFORMS
183183

184184
DEPENDENCIES
185185
actionview (~> 8.0)
186+
difftastic (~> 0.7)
187+
erubi
186188
herb!
187189
lz_string
188190
maxitest

bin/compare

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
require "pathname"
5+
require "optparse"
6+
7+
require_relative "lib/compare_helpers"
8+
9+
include CompareHelpers
10+
11+
def usage
12+
puts "Usage: #{$PROGRAM_NAME} [options] <file>"
13+
puts ""
14+
puts "Compares an ERB template with both Herb and Erubi (compile and render)."
15+
puts ""
16+
puts "Options:"
17+
puts " --no-escape Disable HTML escaping"
18+
puts " --escape Enable HTML escaping (default: false)"
19+
puts " -h, --help Show this help message"
20+
exit(1)
21+
end
22+
23+
options = {}
24+
options[:escape] = false
25+
26+
OptionParser.new do |opts|
27+
opts.banner = "Usage: #{$PROGRAM_NAME} [options] <file>"
28+
29+
opts.on("--no-escape", "Disable HTML escaping") do
30+
options[:escape] = false
31+
end
32+
33+
opts.on("--escape", "Enable HTML escaping") do
34+
options[:escape] = true
35+
end
36+
37+
opts.on("-h", "--help", "Show this help message") do
38+
usage
39+
end
40+
end.parse!
41+
42+
file_path = ARGV[0]
43+
44+
unless file_path
45+
puts "Error: No file specified"
46+
usage
47+
end
48+
49+
unless File.exist?(file_path)
50+
puts "Error: File '#{file_path}' not found"
51+
exit(1)
52+
end
53+
54+
bin_dir = File.expand_path(__dir__)
55+
56+
template = File.read(file_path)
57+
show_template(file_path, template)
58+
59+
args = []
60+
args << "--escape" if options[:escape]
61+
args << "--no-escape" unless options[:escape]
62+
args << "--no-template"
63+
args << file_path
64+
65+
box_header("Compiled HTML+ERB Template Output")
66+
puts ""
67+
68+
compile_result = system(File.join(bin_dir, "compare-compile"), *args)
69+
70+
puts ""
71+
box_header("Rendered HTML+ERB Template Output")
72+
puts ""
73+
74+
render_result = system(File.join(bin_dir, "compare-render"), *args)
75+
76+
puts ""
77+
box_header("Summary")
78+
79+
if render_result && compile_result
80+
puts "✓ All comparisons passed!"
81+
puts ""
82+
puts " • Rendered outputs match"
83+
puts " • Compiled sources match"
84+
exit(0)
85+
elsif render_result
86+
puts "⚠ Rendered outputs match, but compiled sources differ"
87+
puts ""
88+
puts " • ✓ Rendered outputs match (what matters!)"
89+
puts " • ✗ Compiled sources differ (different formatting is OK)"
90+
puts ""
91+
puts "This is usually fine. Herb and Erubi format generated code differently,"
92+
puts "but produce the same final output."
93+
exit(0)
94+
else
95+
puts "✗ Comparisons failed"
96+
puts ""
97+
puts " • Rendered output: #{render_result ? "✓ Match" : "✗ Differ"}"
98+
puts " • Compiled source: #{compile_result ? "✓ Match" : "✗ Differ"}"
99+
exit(1)
100+
end

bin/compare-compile

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
require "pathname"
5+
require "optparse"
6+
require_relative "lib/compare_helpers"
7+
8+
include CompareHelpers
9+
10+
def usage
11+
puts "Usage: #{$PROGRAM_NAME} [options] <file>"
12+
puts ""
13+
puts "Compiles an ERB template with both Erubi::Engine and Herb::Engine and shows the diff."
14+
puts ""
15+
puts "Options:"
16+
puts " --no-escape Disable HTML escaping"
17+
puts " --escape Enable HTML escaping (default: false)"
18+
puts " -h, --help Show this help message"
19+
exit(1)
20+
end
21+
22+
options = {}
23+
options[:escape] = false
24+
25+
OptionParser.new do |opts|
26+
opts.banner = "Usage: #{$PROGRAM_NAME} [options] <file>"
27+
parse_common_options(opts, options) { usage }
28+
end.parse!
29+
30+
options[:show_template] = true unless options.key?(:show_template)
31+
32+
file_path = ARGV[0]
33+
34+
unless file_path
35+
puts "Error: No file specified"
36+
usage
37+
end
38+
39+
unless File.exist?(file_path)
40+
puts "Error: File '#{file_path}' not found"
41+
exit(1)
42+
end
43+
44+
load_dependencies
45+
46+
template = File.read(file_path)
47+
show_template(file_path, template) if options[:show_template]
48+
49+
begin
50+
herb_engine = Herb::Engine.new(template, options)
51+
herb_src = herb_engine.src
52+
rescue StandardError => e
53+
puts "Herb compile error: #{e.message}"
54+
exit(1)
55+
end
56+
57+
begin
58+
erubi_engine = Erubi::Engine.new(template, options)
59+
erubi_src = erubi_engine.src
60+
rescue StandardError => e
61+
puts "Erubi compile error: #{e.message}"
62+
exit(1)
63+
end
64+
65+
herb_normalized = herb_src.gsub("__herb", "__engine")
66+
erubi_normalized = erubi_src.gsub("__erubi", "__engine")
67+
68+
if herb_normalized == erubi_normalized
69+
puts "✓ Compiled sources match (after normalization)!"
70+
puts ""
71+
puts "Herb output:"
72+
puts herb_src
73+
exit(0)
74+
elsif herb_src == erubi_src
75+
puts "✓ Compiled sources match exactly!"
76+
puts ""
77+
puts "Output:"
78+
puts herb_src
79+
exit(0)
80+
else
81+
puts "✗ Compiled sources differ!"
82+
puts ""
83+
puts ""
84+
85+
diff_output = diff_compiled_sources(erubi_src, herb_src)
86+
puts diff_output if diff_output
87+
88+
exit(1)
89+
end

bin/compare-render

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
require "pathname"
5+
require "optparse"
6+
require_relative "lib/compare_helpers"
7+
8+
include CompareHelpers
9+
10+
def usage
11+
puts "Usage: #{$PROGRAM_NAME} [options] <file>"
12+
puts ""
13+
puts "Renders an ERB template with both Erubi::Engine and Herb::Engine and shows the diff."
14+
puts ""
15+
puts "Options:"
16+
puts " --no-escape Disable HTML escaping"
17+
puts " --escape Enable HTML escaping (default: false)"
18+
puts " -h, --help Show this help message"
19+
exit(1)
20+
end
21+
22+
options = {}
23+
options[:escape] = false
24+
25+
OptionParser.new do |opts|
26+
opts.banner = "Usage: #{$PROGRAM_NAME} [options] <file>"
27+
parse_common_options(opts, options) { usage }
28+
end.parse!
29+
30+
options[:show_template] = true unless options.key?(:show_template)
31+
32+
file_path = ARGV[0]
33+
34+
unless file_path
35+
puts "Error: No file specified"
36+
usage
37+
end
38+
39+
unless File.exist?(file_path)
40+
puts "Error: File '#{file_path}' not found"
41+
exit(1)
42+
end
43+
44+
load_dependencies
45+
46+
template = File.read(file_path)
47+
show_template(file_path, template) if options[:show_template]
48+
49+
begin
50+
herb_engine = Herb::Engine.new(template, options)
51+
herb_output = eval(herb_engine.src)
52+
rescue StandardError => e
53+
puts "Herb render error: #{e.message}"
54+
exit(1)
55+
end
56+
57+
begin
58+
erubi_engine = Erubi::Engine.new(template, options)
59+
erubi_output = eval(erubi_engine.src)
60+
rescue StandardError => e
61+
puts "Erubi render error: #{e.message}"
62+
exit(1)
63+
end
64+
65+
if herb_output == erubi_output
66+
puts "✓ Outputs match!"
67+
puts ""
68+
puts "Output:"
69+
puts herb_output
70+
exit(0)
71+
else
72+
puts "✗ Outputs differ!"
73+
puts ""
74+
puts ""
75+
76+
diff_output = diff_rendered_outputs(erubi_output, herb_output)
77+
puts diff_output if diff_output
78+
79+
exit(1)
80+
end

0 commit comments

Comments
 (0)