railties/lib/rails/test_unit/test_parser.rb
# frozen_string_literal: true
begin
require "prism"
rescue LoadError
# If Prism isn't available (because of using an older Ruby version) then we'll
# define a fallback parser using ripper.
end
if defined?(Prism)
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)
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
queue = [Prism.parse_file(filepath).value]
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
queue.concat(node.compact_child_nodes)
end
nil
end
end
end
end
# If we have Prism, then we don't need to define the fallback parser using
# ripper.
return
end
require "ripper"
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 < Ripper # :nodoc:
# Helper to translate a method object into the path and line range where
# the method was defined.
def self.definition_for(method_obj)
path, begin_line = method_obj.source_location
begins_to_ends = new(File.read(path), path).parse
return unless end_line = begins_to_ends[begin_line]
[path, (begin_line..end_line)]
end
def initialize(*)
# A hash mapping the 1-indexed line numbers that tests start on to where they end.
@begins_to_ends = {}
super
end
def parse
super
@begins_to_ends
end
# method test e.g. `def test_some_description`
# This event's first argument gets the `ident` node containing the method
# name, which we have overridden to return the line number of the ident
# instead.
def on_def(begin_line, *)
@begins_to_ends[begin_line] = lineno
end
# Everything past this point is to support declarative tests, which
# require more work to get right because of the many different ways
# methods can be invoked in ruby, all of which are parsed differently.
#
# The approach is just to store the current line number when the
# "test" method is called and pass it up the tree so it's available at
# the point when we also know the line where the associated block ends.
def on_method_add_block(begin_line, end_line)
if begin_line && end_line
@begins_to_ends[begin_line] = end_line
end
end
def on_command_call(*, begin_lineno, _args)
begin_lineno
end
def first_arg(arg, *)
arg
end
def just_lineno(*)
lineno
end
alias on_method_add_arg first_arg
alias on_command first_arg
alias on_stmts_add first_arg
alias on_arg_paren first_arg
alias on_bodystmt first_arg
alias on_ident just_lineno
alias on_do_block just_lineno
alias on_stmts_new just_lineno
alias on_brace_block just_lineno
def on_args_new
[]
end
def on_args_add(parts, part)
parts << part
end
def on_args_add_block(args, *rest)
args.first
end
end
end
end