Parse tests with prism

This changes TestParser to parse with prism instead of ripper if it
is available for the current version of Ruby. It's within the margin
for the speed, and its significantly less code that is easier to
read and should be easier to maintain.
This commit is contained in:
Kevin Newton 2024-02-07 23:45:12 -05:00
parent 11a97e6bca
commit 105e05e891
No known key found for this signature in database
GPG Key ID: 0EAD74C79EC73F26
2 changed files with 67 additions and 70 deletions

@ -12,38 +12,29 @@ module Rails
module TestUnit
# Parse a test file to extract the line ranges of all tests in both
# method-style (def test_foo) and declarative-style (test "foo" do)
class TestParser < Prism::Visitor
module TestParser
# Helper to translate a method object into the path and line range where
# the method was defined.
def self.definition_for(method)
filepath, start_line = method.source_location
Prism.parse_file(filepath).value.accept(new(ranges = {}))
(end_line = ranges[start_line]) && [filepath, (start_line..end_line)]
end
queue = [Prism.parse_file(filepath).value]
def self.definitions_for(source, filepath)
Prism.parse(source, filepath: filepath).value.accept(new(ranges = {}))
ranges
end
while (node = queue.shift)
case node.type
when :def_node
if node.name.start_with?("test") && node.location.start_line == start_line
return [filepath, start_line..node.location.end_line]
end
when :call_node
if node.name == :test && node.location.start_line == start_line
return [filepath, start_line..node.location.end_line]
end
end
attr_reader :ranges
def initialize(ranges)
@ranges = ranges
end
def visit_def_node(node)
if node.name.start_with?("test")
ranges[node.location.start_line] = node.location.end_line
queue.concat(node.compact_child_nodes)
end
super
end
def visit_call_node(node)
if node.name == :test
ranges[node.location.start_line] = node.location.end_line
end
super
nil
end
end
end

@ -5,55 +5,61 @@
require "active_support/testing/autorun"
require "rails/test_unit/test_parser"
class TestParserTestFixture < ActiveSupport::TestCase
def test_method
assert true
assert true
end
def test_oneline; assert true; end
test "declarative" do
assert true
assert true
end
test("declarative w/parens") do
assert true
end
self.test "declarative explicit receiver" do
assert true
assert true
end
test("declarative oneline") { assert true }
test("declarative oneline do") do assert true end
test("declarative multiline w/ braces") {
assert true
assert_not false
}
end
class TestParserTest < ActiveSupport::TestCase
def test_parser
example_test = <<~RUBY
require "test_helper"
actual =
TestParserTestFixture
.instance_methods(false)
.map { |method| TestParserTestFixture.instance_method(method) }
.sort_by { |method| method.source_location[1] }
.map { |method| [method.name, *Rails::TestUnit::TestParser.definition_for(method)] }
class ExampleTest < ActiveSupport::TestCase
def test_method
assert true
expected = [
[:test_method, __FILE__, 9..13],
[:test_oneline, __FILE__, 15..15],
[:test_declarative, __FILE__, 17..21],
[:"test_declarative_w/parens", __FILE__, 23..25],
[:test_declarative_explicit_receiver, __FILE__, 27..31],
[:test_declarative_oneline, __FILE__, 33..33],
[:test_declarative_oneline_do, __FILE__, 35..35],
[:"test_declarative_multiline_w/_braces", __FILE__, 37..40]
]
end
def test_oneline; assert true; end
test "declarative" do
assert true
end
test("declarative w/parens") do
assert true
end
self.test "declarative explicit receiver" do
assert true
end
test("declarative oneline") { assert true }
test("declarative oneline do") do assert true end
test("declarative multiline w/ braces") {
assert true
refute false
}
end
RUBY
actual_map = Rails::TestUnit::TestParser.definitions_for(example_test, "example_test.rb")
expected_map = {
4 => 8, # test_method
10 => 10, # test_oneline
12 => 14, # declarative
16 => 19, # declarative w/parens
21 => 23, # declarative explicit receiver
25 => 25, # declarative oneline
27 => 27, # declarative oneilne do
29 => 32 # declarative multiline w/braces
}
assert_equal expected_map, actual_map
assert_equal expected, actual
end
end