Skip to content

Commit d9c2633

Browse files
committed
Add bin/compare
Allows to compare the output of two different prism versions. It compiles prism twice and forks to allow for different versions in the same script. It then passes over minimal data to see if anything has changed. Most bugfixes should impact little to no real code and the test suite is already very extensive. Running this can give you even more confidence by comparing against real-world-rails or similar. There are some performance gains to be had here. Basically it is already parallelized because of `fork` but it can likely be even better. For simplicity (and because I don't usually write such code) I leave that as an exercise for the future. Just check it manually via the already existing tools.
1 parent 1da0733 commit d9c2633

File tree

1 file changed

+140
-0
lines changed

1 file changed

+140
-0
lines changed

bin/compare

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
# Usage: bin/compare main feature-branch .
5+
6+
$:.unshift(File.expand_path("../lib", __dir__))
7+
8+
require "erb"
9+
require "json"
10+
require "socket"
11+
12+
# InspectVisitor produces good-looking output but is not very efficient.
13+
VISITOR_TEMPLATE = <<~RUBY
14+
<%- R = Prism::Reflection %>
15+
class CompareVisitor < Prism::Visitor
16+
attr_reader :result
17+
18+
def initialize
19+
@result = +""
20+
end
21+
22+
<%- Prism::Node.subclasses.each_with_index do |clazz, i| -%>
23+
def visit_<%= clazz.type %>(node)
24+
@result << "<%= i %>"
25+
@result << node.start_offset.to_s
26+
@result << node.end_offset.to_s
27+
<%- clazz.fields.each do |field| -%>
28+
<%- case field -%>
29+
<%- when R::FlagsField -%>
30+
@result << node.instance_variable_get(:@<%= field.name %>).to_s
31+
<%- when R::LocationField, R::OptionalLocationField -%>
32+
if (field = node.<%= field.name %>)
33+
@result << field.start_offset.to_s
34+
@result << field.length.to_s
35+
end
36+
<%- when R::StringField, R::IntegerField, R::FloatField -%>
37+
@result << node.<%= field.name %>.to_s
38+
<%- when R::ConstantField, R::OptionalConstantField -%>
39+
if (field = node.<%= field.name %>)
40+
@result << field.name
41+
end
42+
<%- when R::ConstantListField -%>
43+
node.<%= field.name %>.each { |c| @result << c.name }
44+
<%- when R::NodeField, R::NodeListField, R::OptionalNodeField %>
45+
<%- # Will get visited later %>
46+
<%- else -%>
47+
raise "Unhandled field type <%= field.class.name %>"
48+
<%- end %>
49+
<%- end %>
50+
super
51+
end
52+
<%- end -%>
53+
end
54+
RUBY
55+
56+
def create_prism(ref)
57+
parent_socket, child_socket = UNIXSocket.pair
58+
59+
system("git checkout #{ref}", exception: true)
60+
system("bundle exec rake compile", exception: true)
61+
62+
pid = fork do
63+
parent_socket.close
64+
require "prism"
65+
66+
eval(ERB.new(VISITOR_TEMPLATE, trim_mode: "-").result)
67+
68+
child_socket.puts("Compiling done for #{ref}")
69+
70+
while(path = child_socket.gets(chomp: true))
71+
begin
72+
result = Prism.parse(File.read(path), version: "latest")
73+
child_socket.puts(serialize_parse_result(result).to_json)
74+
rescue Errno::EISDIR
75+
# Folder might end with `.rb` and get caught by the glob
76+
child_socket.puts("{}")
77+
end
78+
end
79+
80+
exit!(0)
81+
end
82+
83+
child_socket.close
84+
parent_socket.gets
85+
[pid, parent_socket]
86+
end
87+
88+
def serialize_parse_result(parse_result)
89+
visitor = CompareVisitor.new
90+
parse_result.value.accept(visitor)
91+
{
92+
valid: parse_result.success?,
93+
errors: parse_result.errors_format.hash,
94+
ast: visitor.result.hash,
95+
}
96+
end
97+
98+
base_ref = ARGV.shift
99+
compare_ref = ARGV.shift
100+
path = ARGV.shift
101+
102+
pid_baseline, socket_baseline = create_prism(base_ref)
103+
pid_compare, socket_compare = create_prism(compare_ref)
104+
105+
result = +""
106+
files = Dir.glob(File.join(path, "**/*.rb"))
107+
108+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
109+
110+
files.each_with_index do |source_path, i|
111+
puts "#{i}/#{files.size}" if i % 1000 == 0
112+
113+
socket_baseline.puts(source_path)
114+
socket_compare.puts(source_path)
115+
116+
baseline = JSON.parse(socket_baseline.gets(chomp: true), symbolize_names: true)
117+
compare = JSON.parse(socket_compare.gets(chomp: true), symbolize_names: true)
118+
119+
if compare[:valid] != baseline[:valid]
120+
result << "#{source_path} changed from valid(#{baseline[:valid]}) to valid(#{compare[:valid]})\n"
121+
elsif compare[:valid] && baseline[:valid] && compare[:ast] != baseline[:ast]
122+
result << "#{source_path} is syntax valid with changed ast}\n"
123+
elsif !compare[:valid] && !baseline[:valid] && compare[:errors] != baseline[:errors]
124+
result << "#{source_path} is syntax invalid with changed errors\n"
125+
end
126+
end
127+
128+
if result.empty?
129+
puts "All good!"
130+
else
131+
puts "Oops:"
132+
puts result
133+
end
134+
135+
puts "Took #{Process.clock_gettime(Process::CLOCK_MONOTONIC) - start} seconds"
136+
137+
socket_baseline.close
138+
socket_compare.close
139+
Process.wait(pid_baseline)
140+
Process.wait(pid_compare)

0 commit comments

Comments
 (0)