Skip to content

Commit 350ef7e

Browse files
authored
Merge pull request rails#46644 from brasic/another-try-at-test-parsing
Replace `method_source` gem with Ripper (again)
2 parents 7069d15 + effe47c commit 350ef7e

File tree

6 files changed

+226
-13
lines changed

6 files changed

+226
-13
lines changed

Gemfile.lock

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,6 @@ PATH
9494
railties (7.1.0.alpha)
9595
actionpack (= 7.1.0.alpha)
9696
activesupport (= 7.1.0.alpha)
97-
method_source
9897
rake (>= 12.2)
9998
thor (~> 1.0)
10099
zeitwerk (~> 2.6)
@@ -320,7 +319,6 @@ GEM
320319
marcel (1.0.2)
321320
matrix (0.4.2)
322321
memoist (0.16.2)
323-
method_source (1.0.0)
324322
mini_magick (4.11.0)
325323
mini_mime (1.1.2)
326324
mini_portile2 (2.8.0)

railties/lib/rails/test_unit/runner.rb

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# frozen_string_literal: true
22

33
require "shellwords"
4-
require "method_source"
54
require "rake/file_list"
65
require "active_support"
76
require "active_support/core_ext/module/attribute_accessors"
7+
require "rails/test_unit/test_parser"
88

99
module Rails
1010
module TestUnit
@@ -168,10 +168,7 @@ def ===(method)
168168

169169
private
170170
def definition_for(method)
171-
file, start_line = method.source_location
172-
end_line = method.source.count("\n") + start_line - 1
173-
174-
return file, start_line..end_line
171+
TestParser.definition_for(method)
175172
end
176173
end
177174
end
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# frozen_string_literal: true
2+
3+
require "ripper"
4+
5+
module Rails
6+
module TestUnit
7+
# Parse a test file to extract the line ranges of all tests in both
8+
# method-style (def test_foo) and declarative-style (test "foo" do)
9+
class TestParser < Ripper # :nodoc:
10+
# Helper to translate a method object into the path and line range where
11+
# the method was defined.
12+
def self.definition_for(method_obj)
13+
path, begin_line = method_obj.source_location
14+
begins_to_ends = new(File.read(path), path).parse
15+
return unless end_line = begins_to_ends[begin_line]
16+
[path, (begin_line..end_line)]
17+
end
18+
19+
def initialize(*)
20+
# A hash mapping the 1-indexed line numbers that tests start on to where they end.
21+
@begins_to_ends = {}
22+
super
23+
end
24+
25+
def parse
26+
super
27+
@begins_to_ends
28+
end
29+
30+
# method test e.g. `def test_some_description`
31+
# This event's first argument gets the `ident` node containing the method
32+
# name, which we have overridden to return the line number of the ident
33+
# instead.
34+
def on_def(begin_line, *)
35+
@begins_to_ends[begin_line] = lineno
36+
end
37+
38+
# Everything past this point is to support declarative tests, which
39+
# require more work to get right because of the many different ways
40+
# methods can be invoked in ruby, all of which are parsed differently.
41+
#
42+
# The approach is just to store the current line number when the
43+
# "test" method is called and pass it up the tree so it's available at
44+
# the point when we also know the line where the associated block ends.
45+
46+
def on_method_add_block(begin_line, end_line)
47+
if begin_line && end_line
48+
@begins_to_ends[begin_line] = end_line
49+
end
50+
end
51+
52+
def on_command_call(*, begin_lineno, _args)
53+
begin_lineno
54+
end
55+
56+
def first_arg(arg, *)
57+
arg
58+
end
59+
60+
def just_lineno(*)
61+
lineno
62+
end
63+
64+
alias on_method_add_arg first_arg
65+
alias on_command first_arg
66+
alias on_stmts_add first_arg
67+
alias on_arg_paren first_arg
68+
alias on_bodystmt first_arg
69+
70+
alias on_ident just_lineno
71+
alias on_do_block just_lineno
72+
alias on_stmts_new just_lineno
73+
alias on_brace_block just_lineno
74+
75+
def on_args_new
76+
[]
77+
end
78+
79+
def on_args_add(parts, part)
80+
parts << part
81+
end
82+
83+
def on_args_add_block(args, *rest)
84+
args.first
85+
end
86+
end
87+
end
88+
end

railties/railties.gemspec

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ Gem::Specification.new do |s|
4242

4343
s.add_dependency "rake", ">= 12.2"
4444
s.add_dependency "thor", "~> 1.0"
45-
s.add_dependency "method_source"
4645
s.add_dependency "zeitwerk", "~> 2.6"
4746

4847
s.add_development_dependency "actionview", version

railties/test/application/test_runner_test.rb

Lines changed: 78 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,7 @@ def test_line_filter_does_not_run_this
376376
end
377377
end
378378

379-
def test_more_than_one_line_filter
379+
def test_more_than_one_line_filter_macro_syntax
380380
app_file "test/models/post_test.rb", <<-RUBY
381381
require "test_helper"
382382
@@ -397,10 +397,83 @@ class PostTest < ActiveSupport::TestCase
397397
end
398398
RUBY
399399

400-
run_test_command("test/models/post_test.rb:4:13").tap do |output|
401-
assert_match "PostTest:FirstFilter", output
402-
assert_match "PostTest:SecondFilter", output
403-
assert_match "2 runs, 2 assertions", output
400+
pos_cases = {
401+
"first line of each test" => "test/models/post_test.rb:4:13",
402+
"interior of tests" => "test/models/post_test.rb:5:14",
403+
"last line of each test" => "test/models/post_test.rb:7:16"
404+
}
405+
406+
pos_cases.each do |name, cmd|
407+
output = run_test_command(cmd)
408+
assert_match "PostTest:FirstFilter", output, "for #{cmd} (#{name})"
409+
assert_match "PostTest:SecondFilter", output, "for #{cmd} (#{name})"
410+
assert_match "2 runs, 2 assertions", output, "for #{cmd} (#{name})"
411+
end
412+
413+
# one past the end of each test matches nothing
414+
run_test_command("test/models/post_test.rb:8:17").tap do |output|
415+
assert_match "0 runs, 0 assertions", output
416+
end
417+
end
418+
419+
def test_more_than_one_line_filter_test_method_syntax
420+
app_file "test/models/post_test.rb", <<-RUBY
421+
require "test_helper"
422+
423+
class PostTest < ActiveSupport::TestCase
424+
def test_first_filter
425+
puts 'PostTest:FirstFilter'
426+
assert true
427+
end
428+
429+
def test_second_filter
430+
puts 'PostTest:SecondFilter'
431+
assert true
432+
end
433+
434+
def test_line_filter_does_not_run_this
435+
assert true
436+
end
437+
end
438+
RUBY
439+
440+
pos_cases = {
441+
"first line of each test" => "test/models/post_test.rb:4:9",
442+
"interior of tests" => "test/models/post_test.rb:5:10",
443+
"last line of each test" => "test/models/post_test.rb:7:12"
444+
}
445+
446+
pos_cases.each do |name, cmd|
447+
output = run_test_command(cmd)
448+
assert_match "PostTest:FirstFilter", output, "for #{cmd} (#{name})"
449+
assert_match "PostTest:SecondFilter", output, "for #{cmd} (#{name})"
450+
assert_match "2 runs, 2 assertions", output, "for #{cmd} (#{name})"
451+
end
452+
453+
# one past the end of each test matches nothing
454+
run_test_command("test/models/post_test.rb:8:13").tap do |output|
455+
assert_match "0 runs, 0 assertions", output
456+
end
457+
end
458+
459+
def test_multiple_tests_on_same_line
460+
app_file "test/models/account_test.rb", <<-RUBY
461+
require "test_helper"
462+
463+
class AccountTest < ActiveSupport::TestCase
464+
test "first" do puts :first; end; def test_second; puts :second; end
465+
test "third" do
466+
puts :third
467+
assert false
468+
end
469+
end
470+
RUBY
471+
472+
run_test_command("test/models/account_test.rb:4").tap do |output|
473+
assert_match "first", output
474+
assert_match "second", output
475+
assert_no_match "third", output
476+
assert_match "2 runs, 0 assertions, 0 failures", output
404477
end
405478
end
406479

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# frozen_string_literal: true
2+
3+
require "active_support/test_case"
4+
require "active_support/testing/autorun"
5+
require "rails/test_unit/test_parser"
6+
7+
class TestParserTest < ActiveSupport::TestCase
8+
def test_parser
9+
example_test = <<~RUBY
10+
require "test_helper"
11+
12+
class ExampleTest < ActiveSupport::TestCase
13+
def test_method
14+
assert true
15+
16+
17+
end
18+
19+
def test_oneline; assert true; end
20+
21+
test "declarative" do
22+
assert true
23+
end
24+
25+
test("declarative w/parens") do
26+
assert true
27+
28+
end
29+
30+
self.test "declarative explicit receiver" do
31+
assert true
32+
end
33+
34+
test("declarative oneline") { assert true }
35+
36+
test("declarative oneline do") do assert true end
37+
38+
test("declarative multiline w/ braces") {
39+
assert true
40+
refute false
41+
}
42+
end
43+
RUBY
44+
45+
parser = Rails::TestUnit::TestParser.new(example_test, "example_test.rb")
46+
expected_map = {
47+
4 => 8, # test_method
48+
10 => 10, # test_oneline
49+
12 => 14, # declarative
50+
16 => 19, # declarative w/parens
51+
21 => 23, # declarative explicit receiver
52+
25 => 25, # declarative oneline
53+
27 => 27, # declarative oneilne do
54+
29 => 32 # declarative multiline w/braces
55+
}
56+
assert_equal expected_map, parser.parse
57+
end
58+
end

0 commit comments

Comments
 (0)