rubocop-minitest 0.9.0 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -3
  3. data/CHANGELOG.md +22 -0
  4. data/Gemfile +1 -1
  5. data/README.md +2 -2
  6. data/Rakefile +29 -0
  7. data/config/default.yml +95 -16
  8. data/docs/antora.yml +7 -0
  9. data/docs/modules/ROOT/nav.adoc +6 -0
  10. data/docs/modules/ROOT/pages/cops.adoc +37 -0
  11. data/docs/modules/ROOT/pages/cops_minitest.adoc +1014 -0
  12. data/docs/modules/ROOT/pages/index.adoc +5 -0
  13. data/docs/modules/ROOT/pages/installation.adoc +15 -0
  14. data/docs/modules/ROOT/pages/usage.adoc +32 -0
  15. data/{manual → legacy-docs}/cops.md +0 -0
  16. data/{manual → legacy-docs}/cops_minitest.md +0 -0
  17. data/{manual → legacy-docs}/index.md +0 -0
  18. data/{manual → legacy-docs}/installation.md +0 -0
  19. data/{manual → legacy-docs}/usage.md +0 -0
  20. data/lib/rubocop/cop/generator.rb +56 -0
  21. data/lib/rubocop/cop/minitest/assert_empty_literal.rb +15 -0
  22. data/lib/rubocop/cop/minitest/assert_in_delta.rb +27 -0
  23. data/lib/rubocop/cop/minitest/assert_kind_of.rb +25 -0
  24. data/lib/rubocop/cop/minitest/assert_output.rb +49 -0
  25. data/lib/rubocop/cop/minitest/assert_path_exists.rb +58 -0
  26. data/lib/rubocop/cop/minitest/assert_silent.rb +45 -0
  27. data/lib/rubocop/cop/minitest/assertion_in_lifecycle_hook.rb +43 -0
  28. data/lib/rubocop/cop/minitest/global_expectations.rb +2 -4
  29. data/lib/rubocop/cop/minitest/literal_as_actual_argument.rb +52 -0
  30. data/lib/rubocop/cop/minitest/multiple_assertions.rb +63 -0
  31. data/lib/rubocop/cop/minitest/refute_in_delta.rb +27 -0
  32. data/lib/rubocop/cop/minitest/refute_kind_of.rb +25 -0
  33. data/lib/rubocop/cop/minitest/refute_path_exists.rb +58 -0
  34. data/lib/rubocop/cop/minitest/test_method_name.rb +70 -0
  35. data/lib/rubocop/cop/minitest/unspecified_exception.rb +36 -0
  36. data/lib/rubocop/cop/minitest_cops.rb +15 -0
  37. data/lib/rubocop/cop/mixin/argument_range_helper.rb +10 -0
  38. data/lib/rubocop/cop/mixin/in_delta_mixin.rb +50 -0
  39. data/lib/rubocop/cop/mixin/minitest_exploration_helpers.rb +84 -0
  40. data/lib/rubocop/minitest/version.rb +1 -1
  41. data/mkdocs.yml +2 -2
  42. data/relnotes/v0.10.0.md +21 -0
  43. data/rubocop-minitest.gemspec +3 -3
  44. data/tasks/cops_documentation.rake +83 -52
  45. data/tasks/cut_release.rake +16 -0
  46. metadata +35 -11
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Minitest
6
+ # This cop checks if test cases contain too many assertion calls.
7
+ # The maximum allowed assertion calls is configurable.
8
+ #
9
+ # @example Max: 1
10
+ # # bad
11
+ # class FooTest < Minitest::Test
12
+ # def test_asserts_twice
13
+ # assert_equal(42, do_something)
14
+ # assert_empty(array)
15
+ # end
16
+ # end
17
+ #
18
+ # # good
19
+ # class FooTest < Minitest::Test
20
+ # def test_asserts_once
21
+ # assert_equal(42, do_something)
22
+ # end
23
+ #
24
+ # def test_another_asserts_once
25
+ # assert_empty(array)
26
+ # end
27
+ # end
28
+ #
29
+ class MultipleAssertions < Cop
30
+ include ConfigurableMax
31
+ include MinitestExplorationHelpers
32
+
33
+ MSG = 'Test case has too many assertions [%<total>d/%<max>d].'
34
+
35
+ def on_class(class_node)
36
+ return unless test_class?(class_node)
37
+
38
+ test_cases(class_node).each do |node|
39
+ assertions_count = assertions_count(node)
40
+
41
+ next unless assertions_count > max_assertions
42
+
43
+ self.max = assertions_count
44
+
45
+ message = format(MSG, total: assertions_count, max: max_assertions)
46
+ add_offense(node, location: :name, message: message)
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def assertions_count(node)
53
+ base = assertion?(node) ? 1 : 0
54
+ base + node.each_child_node.sum { |c| assertions_count(c) }
55
+ end
56
+
57
+ def max_assertions
58
+ Integer(cop_config.fetch('Max', 3))
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Minitest
6
+ # This cop enforces the test to use `refute_in_delta`
7
+ # instead of using `refute_equal` to compare floats.
8
+ #
9
+ # @example
10
+ # # bad
11
+ # refute_equal(0.2, actual)
12
+ # refute_equal(0.2, actual, 'message')
13
+ #
14
+ # # good
15
+ # refute_in_delta(0.2, actual)
16
+ # refute_in_delta(0.2, actual, 0.001, 'message')
17
+ #
18
+ class RefuteInDelta < Cop
19
+ include InDeltaMixin
20
+
21
+ def_node_matcher :equal_floats_call, <<~PATTERN
22
+ (send nil? :refute_equal $_ $_ $...)
23
+ PATTERN
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Minitest
6
+ # This cop enforces the use of `refute_kind_of(Class, object)`
7
+ # over `refute(object.kind_of?(Class))`.
8
+ #
9
+ # @example
10
+ # # bad
11
+ # refute(object.kind_of?(Class))
12
+ # refute(object.kind_of?(Class), 'message')
13
+ #
14
+ # # good
15
+ # refute_kind_of(Class, object)
16
+ # refute_kind_of(Class, object, 'message')
17
+ #
18
+ class RefuteKindOf < Cop
19
+ extend MinitestCopRule
20
+
21
+ define_rule :refute, target_method: :kind_of?, inverse: true
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Minitest
6
+ # This cop enforces the test to use `refute_path_exists`
7
+ # instead of using `refute(File.exist?(path))`.
8
+ #
9
+ # @example
10
+ # # bad
11
+ # refute(File.exist?(path))
12
+ # refute(File.exist?(path), 'message')
13
+ #
14
+ # # good
15
+ # refute_path_exists(path)
16
+ # refute_path_exists(path, 'message')
17
+ #
18
+ class RefutePathExists < Cop
19
+ MSG = 'Prefer using `%<good_method>s` over `%<bad_method>s`.'
20
+
21
+ def_node_matcher :refute_file_exists, <<~PATTERN
22
+ (send nil? :refute
23
+ (send
24
+ (const _ :File) {:exist? :exists?} $_)
25
+ $...)
26
+ PATTERN
27
+
28
+ def on_send(node)
29
+ refute_file_exists(node) do |path, failure_message|
30
+ failure_message = failure_message.first
31
+ good_method = build_good_method(path, failure_message)
32
+ message = format(MSG, good_method: good_method, bad_method: node.source)
33
+
34
+ add_offense(node, message: message)
35
+ end
36
+ end
37
+
38
+ def autocorrect(node)
39
+ refute_file_exists(node) do |path, failure_message|
40
+ failure_message = failure_message.first
41
+
42
+ lambda do |corrector|
43
+ replacement = build_good_method(path, failure_message)
44
+ corrector.replace(node, replacement)
45
+ end
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def build_good_method(path, message)
52
+ args = [path.source, message&.source].compact.join(', ')
53
+ "refute_path_exists(#{args})"
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Minitest
6
+ # This cop enforces that test method names start with `test_` prefix.
7
+ #
8
+ # @example
9
+ # # bad
10
+ # class FooTest < Minitest::Test
11
+ # def does_something
12
+ # assert_equal 42, do_something
13
+ # end
14
+ # end
15
+ #
16
+ # # good
17
+ # class FooTest < Minitest::Test
18
+ # def test_does_something
19
+ # assert_equal 42, do_something
20
+ # end
21
+ # end
22
+ #
23
+ class TestMethodName < Cop
24
+ include MinitestExplorationHelpers
25
+ include DefNode
26
+
27
+ MSG = 'Test method name should start with `test_` prefix.'
28
+
29
+ def on_class(class_node)
30
+ return unless test_class?(class_node)
31
+
32
+ class_elements(class_node).each do |node|
33
+ add_offense(node, location: :name) if offense?(node)
34
+ end
35
+ end
36
+
37
+ def autocorrect(node)
38
+ lambda do |corrector|
39
+ corrector.replace(node.loc.name, "test_#{node.method_name}")
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def class_elements(class_node)
46
+ class_def = class_node.body
47
+ return [] unless class_def
48
+
49
+ if class_def.def_type?
50
+ [class_def]
51
+ else
52
+ class_def.each_child_node(:def).to_a
53
+ end
54
+ end
55
+
56
+ def offense?(node)
57
+ public?(node) && !test_method_name?(node) && !lifecycle_hook_method?(node)
58
+ end
59
+
60
+ def public?(node)
61
+ !non_public?(node)
62
+ end
63
+
64
+ def test_method_name?(node)
65
+ node.method_name.to_s.start_with?('test_')
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Minitest
6
+ # This cop checks for a specified error in `assert_raises`.
7
+ #
8
+ # @example
9
+ # # bad
10
+ # assert_raises { raise FooException }
11
+ # assert_raises('This should have raised') { raise FooException }
12
+ #
13
+ # # good
14
+ # assert_raises(FooException) { raise FooException }
15
+ # assert_raises(FooException, 'This should have raised') { raise FooException }
16
+ #
17
+ class UnspecifiedException < Cop
18
+ MSG = 'Specify the exception being captured.'
19
+
20
+ def on_block(block_node)
21
+ node = block_node.send_node
22
+ return unless node.method?(:assert_raises)
23
+
24
+ add_offense(node) if unspecified_exception?(node)
25
+ end
26
+
27
+ private
28
+
29
+ def unspecified_exception?(node)
30
+ args = node.arguments
31
+ args.empty? || (args.size == 1 && args[0].str_type?)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -1,22 +1,37 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'mixin/argument_range_helper'
4
+ require_relative 'mixin/in_delta_mixin'
4
5
  require_relative 'mixin/minitest_cop_rule'
6
+ require_relative 'mixin/minitest_exploration_helpers'
5
7
  require_relative 'minitest/assert_empty'
6
8
  require_relative 'minitest/assert_empty_literal'
7
9
  require_relative 'minitest/assert_equal'
10
+ require_relative 'minitest/assert_in_delta'
11
+ require_relative 'minitest/assertion_in_lifecycle_hook'
12
+ require_relative 'minitest/assert_kind_of'
8
13
  require_relative 'minitest/assert_nil'
9
14
  require_relative 'minitest/assert_includes'
10
15
  require_relative 'minitest/assert_instance_of'
11
16
  require_relative 'minitest/assert_match'
17
+ require_relative 'minitest/assert_output'
18
+ require_relative 'minitest/assert_path_exists'
12
19
  require_relative 'minitest/assert_respond_to'
20
+ require_relative 'minitest/assert_silent'
13
21
  require_relative 'minitest/assert_truthy'
14
22
  require_relative 'minitest/global_expectations'
23
+ require_relative 'minitest/literal_as_actual_argument'
24
+ require_relative 'minitest/multiple_assertions'
15
25
  require_relative 'minitest/refute_empty'
16
26
  require_relative 'minitest/refute_false'
17
27
  require_relative 'minitest/refute_equal'
28
+ require_relative 'minitest/refute_in_delta'
29
+ require_relative 'minitest/refute_kind_of'
18
30
  require_relative 'minitest/refute_nil'
19
31
  require_relative 'minitest/refute_includes'
20
32
  require_relative 'minitest/refute_match'
21
33
  require_relative 'minitest/refute_instance_of'
34
+ require_relative 'minitest/refute_path_exists'
22
35
  require_relative 'minitest/refute_respond_to'
36
+ require_relative 'minitest/test_method_name'
37
+ require_relative 'minitest/unspecified_exception'
@@ -26,6 +26,16 @@ module RuboCop
26
26
  second_argument.source_range.end_pos
27
27
  )
28
28
  end
29
+
30
+ def all_arguments_range(node)
31
+ first_argument = node.first_argument
32
+ last_argument = node.arguments.last
33
+
34
+ range_between(
35
+ first_argument.source_range.begin_pos,
36
+ last_argument.source_range.end_pos
37
+ )
38
+ end
29
39
  end
30
40
  end
31
41
  end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ # Common functionality for `AssertInDelta` and `RefuteInDelta` cops.
6
+ module InDeltaMixin
7
+ MSG = 'Prefer using `%<good_method>s` over `%<bad_method>s`.'
8
+
9
+ def on_send(node)
10
+ equal_floats_call(node) do |expected, actual, message|
11
+ message = message.first
12
+
13
+ if expected.float_type? || actual.float_type?
14
+ message = format(MSG,
15
+ good_method: build_good_method(expected, actual, message),
16
+ bad_method: node.source)
17
+
18
+ add_offense(node, message: message)
19
+ end
20
+ end
21
+ end
22
+
23
+ def autocorrect(node)
24
+ equal_floats_call(node) do |expected, actual, message|
25
+ message = message.first
26
+ replacement = build_good_method(expected, actual, message)
27
+
28
+ lambda do |corrector|
29
+ corrector.replace(node, replacement)
30
+ end
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def build_good_method(expected, actual, message)
37
+ if message
38
+ "#{assertion_method}_in_delta(#{expected.source}, #{actual.source}, 0.001, #{message.source})"
39
+ else
40
+ "#{assertion_method}_in_delta(#{expected.source}, #{actual.source})"
41
+ end
42
+ end
43
+
44
+ def assertion_method
45
+ class_name = self.class.name.split('::').last
46
+ class_name[/\A[[:upper:]][[:lower:]]+/].downcase
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ # Helper methods for different explorations against test files and test cases.
8
+ module MinitestExplorationHelpers
9
+ extend NodePattern::Macros
10
+
11
+ ASSERTION_PREFIXES = %w[assert refute].freeze
12
+
13
+ LIFECYCLE_HOOK_METHODS = %i[
14
+ before_setup
15
+ setup
16
+ after_setup
17
+ before_teardown
18
+ teardown
19
+ after_teardown
20
+ ].to_set.freeze
21
+
22
+ private
23
+
24
+ def test_class?(class_node)
25
+ class_node.parent_class && class_node.identifier.source.end_with?('Test')
26
+ end
27
+
28
+ def test_case?(node)
29
+ return false unless node.def_type? && test_case_name?(node.method_name)
30
+
31
+ class_ancestor = node.each_ancestor(:class).first
32
+ test_class?(class_ancestor)
33
+ end
34
+
35
+ def test_cases(class_node)
36
+ class_def_nodes(class_node)
37
+ .select { |def_node| test_case_name?(def_node.method_name) }
38
+ end
39
+
40
+ def lifecycle_hooks(class_node)
41
+ class_def_nodes(class_node)
42
+ .select { |def_node| lifecycle_hook_method?(def_node) }
43
+ end
44
+
45
+ def class_def_nodes(class_node)
46
+ class_def = class_node.body
47
+ return [] unless class_def
48
+
49
+ if class_def.def_type?
50
+ [class_def]
51
+ else
52
+ class_def.each_child_node(:def).to_a
53
+ end
54
+ end
55
+
56
+ def test_case_name?(name)
57
+ name.to_s.start_with?('test_')
58
+ end
59
+
60
+ def assertions(def_node)
61
+ method_def = def_node.body
62
+ return [] unless method_def
63
+
64
+ send_nodes =
65
+ if method_def.send_type?
66
+ [method_def]
67
+ else
68
+ method_def.each_child_node(:send)
69
+ end
70
+
71
+ send_nodes.select { |send_node| assertion?(send_node) }
72
+ end
73
+
74
+ def assertion?(node)
75
+ node.send_type? &&
76
+ ASSERTION_PREFIXES.any? { |prefix| node.method_name.to_s.start_with?(prefix) }
77
+ end
78
+
79
+ def lifecycle_hook_method?(node)
80
+ node.def_type? && LIFECYCLE_HOOK_METHODS.include?(node.method_name)
81
+ end
82
+ end
83
+ end
84
+ end