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.location.start_line == start_line return [filepath, start_line..node.location.end_line] end when :call_node if 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