test-prof 0.10.2 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bfc20add611581574506a80294e893a77997e5905ba0fe521838fbdd190b2360
4
- data.tar.gz: 6f0505b7f2b96c65624b81ee9fd4c086b387d7c8efdc7150e0409062efd712a7
3
+ metadata.gz: e2060dd26a4bec62bd9a3b699e1165c60b8914a74c9fc25b3a0edf84afbd7d2b
4
+ data.tar.gz: 9b8e3eb78764c130ae78a575a5cdfe5d9e9a440d7b748c108b5088d99dc7261d
5
5
  SHA512:
6
- metadata.gz: ca925fb486b3b3e1aff9508b670c19028bfe6b0a5b1c3dfe8d3f69b1de74b8cf19187497eb9c899dfae5cacab8390966a58e08ee2f80705cb62b61b2f002686a
7
- data.tar.gz: 65a5ab2c334fdb2625ce450a7184316946cdabe684d052b260e6911648d5f94200e888a083b0b31cb078e4391f106875e0cad145599b7a1a71bfc4cbaf77c10a
6
+ metadata.gz: 6998b3cb30fa82bf6b2311dfa733f0c32694964741aa923b1300115d670c0cfc7e834ad55a9a9b4295e5b02e85d18e529d75d775b16d00ecc44956bee30231aa
7
+ data.tar.gz: b42a91195b9e6a49522bd33ede09ae9ee769b7ee1678de97d2a0bcf593ab076811f20938f2f2deabe864e22e6c9cab5b0e1b7431da91492b9d7959e7b6b52769
data/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## master (unreleased)
4
4
 
5
+ ## 0.11.0 (2020-02-09)
6
+
7
+ - Fix `let_it_be` issue when initialized with an array/enumerable or an AR relation. ([@pirj][])
8
+
9
+ - Improve `RSpec/AggregateExamples` (formerly `RSpec/AggregateFailures`) cop. ([@pirj][])
10
+
5
11
  ## 0.10.2 (2020-01-07) 🎄
6
12
 
7
13
  - Fix Ruby 2.7 deprecations. ([@lostie][])
@@ -515,3 +521,4 @@ Fixes [#10](https://github.com/palkan/test-prof/issues/10).
515
521
  [@Envek]: https://github.com/Envek
516
522
  [@tyleriguchi]: https://github.com/tyleriguchi
517
523
  [@lostie]: https://github.com/lostie
524
+ [@pirj]: https://github.com/pirj
@@ -80,6 +80,11 @@ module TestProf
80
80
  end
81
81
 
82
82
  def report_stats
83
+ if cache.stats.empty?
84
+ log :info, "AnyFixture has not been used"
85
+ return
86
+ end
87
+
83
88
  msgs = []
84
89
 
85
90
  msgs <<
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This is shamelessly borrowed from RuboCop RSpec
4
+ # https://github.com/rubocop-hq/rubocop-rspec/blob/master/lib/rubocop/rspec/inject.rb
5
+ module RuboCop
6
+ # Because RuboCop doesn't yet support plugins, we have to monkey patch in a
7
+ # bit of our configuration.
8
+ module Inject
9
+ PROJECT_ROOT = Pathname.new(__dir__).parent.parent.parent.expand_path.freeze
10
+ CONFIG_DEFAULT = PROJECT_ROOT.join("config", "default.yml").freeze
11
+
12
+ def self.defaults!
13
+ path = CONFIG_DEFAULT.to_s
14
+ hash = ConfigLoader.send(:load_yaml_configuration, path)
15
+ config = Config.new(hash, path)
16
+ puts "configuration from #{path}" if ConfigLoader.debug?
17
+ config = ConfigLoader.merge_with_default(config, path)
18
+ ConfigLoader.instance_variable_set(:@default_configuration, config)
19
+ end
20
+ end
21
+ end
22
+
23
+ RuboCop::Inject.defaults!
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "aggregate_examples/line_range_helpers"
4
+ require_relative "aggregate_examples/metadata_helpers"
5
+ require_relative "aggregate_examples/node_matchers"
6
+
7
+ require_relative "aggregate_examples/its"
8
+ require_relative "aggregate_examples/matchers_with_side_effects"
9
+
10
+ module RuboCop
11
+ module Cop
12
+ module RSpec
13
+ # Checks if example groups contain two or more aggregatable examples.
14
+ #
15
+ # @see https://github.com/rubocop-hq/rspec-style-guide#expectations-per-example
16
+ #
17
+ # This cop is primarily for reducing the cost of repeated expensive
18
+ # context initialization.
19
+ #
20
+ # @example
21
+ #
22
+ # # bad
23
+ # describe do
24
+ # specify do
25
+ # expect(number).to be_positive
26
+ # expect(number).to be_odd
27
+ # end
28
+ #
29
+ # it { is_expected.to be_prime }
30
+ # end
31
+ #
32
+ # # good
33
+ # describe do
34
+ # specify do
35
+ # expect(number).to be_positive
36
+ # expect(number).to be_odd
37
+ # is_expected.to be_prime
38
+ # end
39
+ # end
40
+ #
41
+ # # fair - subject has side effects
42
+ # describe do
43
+ # specify do
44
+ # expect(multiply_by(2)).to be_multiple_of(2)
45
+ # end
46
+ #
47
+ # specify do
48
+ # expect(multiply_by(3)).to be_multiple_of(3)
49
+ # end
50
+ # end
51
+ #
52
+ # Block expectation syntax is deliberately not supported due to:
53
+ #
54
+ # 1. `subject { -> { ... } }` syntax being hard to detect, e.g. the
55
+ # following looks like an example with non-block syntax, but it might
56
+ # be, depending on how the subject is defined:
57
+ #
58
+ # it { is_expected.to do_something }
59
+ #
60
+ # If the subject is defined in a `shared_context`, it's impossible to
61
+ # detect that at all.
62
+ #
63
+ # 2. Aggregation should use composition with an `.and`. Also, aggregation
64
+ # of the `not_to` expectations is barely possible when a matcher
65
+ # doesn't provide a negated variant.
66
+ #
67
+ # 3. Aggregation of block syntax with non-block syntax should be in a
68
+ # specific order.
69
+ #
70
+ # RSpec [comes with an `aggregate_failures` helper](https://relishapp.com/rspec/rspec-expectations/docs/aggregating-failures)
71
+ # not to fail the example on first unmet expectation that might come
72
+ # handy with aggregated examples.
73
+ # It can be [used in metadata form](https://relishapp.com/rspec/rspec-core/docs/expectation-framework-integration/aggregating-failures#use-%60:aggregate-failures%60-metadata),
74
+ # or [enabled globally](https://relishapp.com/rspec/rspec-core/docs/expectation-framework-integration/aggregating-failures#enable-failure-aggregation-globally-using-%60define-derived-metadata%60).
75
+ #
76
+ # @example Globally enable `aggregate_failures`
77
+ #
78
+ # # spec/spec_helper.rb
79
+ # config.define_derived_metadata do |metadata|
80
+ # unless metadata.key?(:aggregate_failures)
81
+ # metadata[:aggregate_failures] = true
82
+ # end
83
+ # end
84
+ #
85
+ # To match the style being used in the spec suite, AggregateExamples
86
+ # can be configured to add `:aggregate_failures` metadata to the
87
+ # example or not. The option not to add metadata can be also used
88
+ # when it's not desired to make expectations after previously failed
89
+ # ones, commonly known as fail-fast.
90
+ #
91
+ # The terms "aggregate examples" and "aggregate failures" not to be
92
+ # confused. The former stands for putting several expectations to
93
+ # a single example. The latter means to run all the expectations in
94
+ # the example instead of aborting on the first one.
95
+ #
96
+ # @example AddAggregateFailuresMetadata: true (default)
97
+ #
98
+ # # Metadata set using a symbol
99
+ # specify(:aggregate_failures) do
100
+ # expect(number).to be_positive
101
+ # expect(number).to be_odd
102
+ # end
103
+ #
104
+ # @example AddAggregateFailuresMetadata: false
105
+ #
106
+ # specify do
107
+ # expect(number).to be_positive
108
+ # expect(number).to be_odd
109
+ # end
110
+ #
111
+ class AggregateExamples < Cop
112
+ include LineRangeHelpers
113
+ include MetadataHelpers
114
+ include NodeMatchers
115
+
116
+ # Methods from the following modules override and extend methods of this
117
+ # class, extracting specific behavior.
118
+ prepend Its
119
+ prepend MatchersWithSideEffects
120
+
121
+ MSG = "Aggregate with the example at line %d."
122
+
123
+ def on_block(node)
124
+ example_group_with_several_examples(node) do |all_examples|
125
+ example_clusters(all_examples).each do |_, examples|
126
+ examples[1..-1].each do |example|
127
+ add_offense(example,
128
+ location: :expression,
129
+ message: message_for(example, examples[0]))
130
+ end
131
+ end
132
+ end
133
+ end
134
+
135
+ def autocorrect(example_node)
136
+ clusters = example_clusters_for_autocorrect(example_node)
137
+ return if clusters.empty?
138
+
139
+ lambda do |corrector|
140
+ clusters.each do |metadata, examples|
141
+ range = range_for_replace(examples)
142
+ replacement = aggregated_example(examples, metadata)
143
+ corrector.replace(range, replacement)
144
+ examples[1..-1].map { |example| drop_example(corrector, example) }
145
+ end
146
+ end
147
+ end
148
+
149
+ private
150
+
151
+ # Clusters of examples in the same example group, on the same nesting
152
+ # level that can be aggregated.
153
+ def example_clusters(all_examples)
154
+ all_examples
155
+ .select { |example| example_with_expectations_only?(example) }
156
+ .group_by { |example| metadata_without_aggregate_failures(example) }
157
+ .select { |_, examples| examples.count > 1 }
158
+ end
159
+
160
+ # Clusters of examples that can be aggregated without losing any
161
+ # information (e.g. metadata or docstrings)
162
+ def example_clusters_for_autocorrect(example_node)
163
+ examples_in_group = example_node.parent.each_child_node(:block)
164
+ .select { |example| example_for_autocorrect?(example) }
165
+ example_clusters(examples_in_group)
166
+ end
167
+
168
+ def message_for(_example, first_example)
169
+ format(MSG, first_example.loc.line)
170
+ end
171
+
172
+ def drop_example(corrector, example)
173
+ aggregated_range = range_by_whole_lines(example.source_range,
174
+ include_final_newline: true)
175
+ corrector.remove(aggregated_range)
176
+ end
177
+
178
+ def aggregated_example(examples, metadata)
179
+ base_indent = " " * examples.first.source_range.column
180
+ metadata = metadata_for_aggregated_example(metadata)
181
+ [
182
+ "#{base_indent}specify#{metadata} do",
183
+ *examples.map { |example| transform_body(example, base_indent) },
184
+ "#{base_indent}end\n"
185
+ ].join("\n")
186
+ end
187
+
188
+ # Extracts and transforms the body, keeping proper indentation.
189
+ def transform_body(node, base_indent)
190
+ "#{base_indent} #{new_body(node)}"
191
+ end
192
+
193
+ def new_body(node)
194
+ node.body.source
195
+ end
196
+ end
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpec
6
+ class AggregateExamples < Cop
7
+ # @example `its`
8
+ #
9
+ # # Supports regular `its` call with an attribute/method name,
10
+ # # or a chain of methods expressed as a string with dots.
11
+ #
12
+ # its(:one) { is_expected.to be(true) }
13
+ # its('two') { is_expected.to be(false) }
14
+ # its('phone_numbers.size') { is_expected.to be 2 }
15
+ #
16
+ # @example `its` with single-element array argument
17
+ #
18
+ # # Also supports single-element array argument.
19
+ #
20
+ # its(['headers']) { is_expected.to include(encoding: 'text') }
21
+ #
22
+ # @example `its` with multi-element array argument is ambiguous
23
+ #
24
+ # # Does not support `its` with multi-element array argument due to
25
+ # # an ambiguity. Transformation depends on the type of the subject:
26
+ # # - a Hash: `hash[element1][element2]...`
27
+ # # - and arbitrary type: `hash[element1, element2, ...]`
28
+ # # It is impossible to infer the type to propose a proper correction.
29
+ #
30
+ # its(['ambiguous', 'elements']) { ... }
31
+ #
32
+ # @example `its` with metadata
33
+ #
34
+ # its('header', html: true) { is_expected.to include(text: 'hello') }
35
+ #
36
+ module Its
37
+ extend RuboCop::NodePattern::Macros
38
+
39
+ private
40
+
41
+ # It's impossible to aggregate `its` body as is, it needs to be
42
+ # converted to `expect(subject.something).to ...`
43
+ def new_body(node)
44
+ return super unless its?(node)
45
+
46
+ transform_its(node.body, node.send_node.arguments)
47
+ end
48
+
49
+ def transform_its(body, arguments)
50
+ argument = arguments.first
51
+ replacement = case argument.type
52
+ when :array
53
+ key = argument.values.first
54
+ "expect(subject[#{key.source}])"
55
+ else
56
+ property = argument.value
57
+ "expect(subject.#{property})"
58
+ end
59
+ body.source.gsub(/is_expected|are_expected/, replacement)
60
+ end
61
+
62
+ def example_metadata(example)
63
+ return super unless its?(example.send_node)
64
+
65
+ # First parameter to `its` is not metadata.
66
+ example.send_node.arguments[1..-1]
67
+ end
68
+
69
+ def its?(node)
70
+ node.method_name == :its
71
+ end
72
+
73
+ # In addition to base definition, matches examples with:
74
+ # - no `its` with an multiple-element array argument due to
75
+ # an ambiguity, when SUT can be a hash, and result will be defined
76
+ # by calling `[]` on SUT subsequently, e.g. `subject[one][two]`,
77
+ # or any other type of object implementing `[]`, and then all the
78
+ # array arguments are passed to `[]`, e.g. `subject[one, two]`.
79
+ def_node_matcher :example_for_autocorrect?, <<-PATTERN
80
+ [
81
+ #super
82
+ !#its_with_multi_element_array_argument?
83
+ !#its_with_send_or_var_argument?
84
+ ]
85
+ PATTERN
86
+
87
+ def_node_matcher :its_with_multi_element_array_argument?, <<-PATTERN
88
+ (block (send nil? :its (array _ _ ...)) ...)
89
+ PATTERN
90
+
91
+ def_node_matcher :its_with_send_or_var_argument?, <<-PATTERN
92
+ (block (send nil? :its { send lvar ivar cvar gvar const }) ...)
93
+ PATTERN
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpec
6
+ class AggregateExamples < Cop
7
+ # @internal Support methods for keeping newlines around examples.
8
+ module LineRangeHelpers
9
+ include RangeHelp
10
+
11
+ private
12
+
13
+ def range_for_replace(examples)
14
+ range = range_by_whole_lines(examples.first.source_range,
15
+ include_final_newline: true)
16
+ next_range = range_by_whole_lines(examples[1].source_range)
17
+ if adjacent?(range, next_range)
18
+ range.resize(range.length + 1)
19
+ else
20
+ range
21
+ end
22
+ end
23
+
24
+ def adjacent?(range, another_range)
25
+ range.end_pos + 1 == another_range.begin_pos
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../language"
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module RSpec
8
+ class AggregateExamples < Cop
9
+ # When aggregated, the expectations will fail when not supposed to or
10
+ # have a risk of not failing when expected to. One example is
11
+ # `validate_presence_of :comment` as it leaves an empty comment after
12
+ # itself on the subject making it invalid and the subsequent expectation
13
+ # to fail.
14
+ # Examples with those matchers are not supposed to be aggregated.
15
+ #
16
+ # @example MatchersWithSideEffects
17
+ #
18
+ # # .rubocop.yml
19
+ # # RSpec/AggregateExamples:
20
+ # # MatchersWithSideEffects:
21
+ # # - allow_value
22
+ # # - allow_values
23
+ # # - validate_presence_of
24
+ #
25
+ # # bad, but isn't automatically correctable
26
+ # describe do
27
+ # it { is_expected.to validate_presence_of(:comment) }
28
+ # it { is_expected.to be_valid }
29
+ # end
30
+ #
31
+ # @internal
32
+ # Support for taking special care of the matchers that have side
33
+ # effects, i.e. leave the subject in a modified state.
34
+ module MatchersWithSideEffects
35
+ extend RuboCop::NodePattern::Macros
36
+ include RuboCop::Cop::RSpec::Language
37
+
38
+ MSG_FOR_EXPECTATIONS_WITH_SIDE_EFFECTS =
39
+ "Aggregate with the example at line %d. IMPORTANT! Pay attention " \
40
+ "to the expectation order, some of the matchers have side effects."
41
+
42
+ private
43
+
44
+ def message_for(example, first_example)
45
+ return super unless example_with_side_effects?(example)
46
+
47
+ format(MSG_FOR_EXPECTATIONS_WITH_SIDE_EFFECTS, first_example.loc.line)
48
+ end
49
+
50
+ def matcher_with_side_effects_names
51
+ cop_config.fetch("MatchersWithSideEffects", [])
52
+ .map(&:to_sym)
53
+ end
54
+
55
+ def matcher_with_side_effects_name?(matcher_name)
56
+ matcher_with_side_effects_names.include?(matcher_name)
57
+ end
58
+
59
+ # In addition to base definition, matches examples with:
60
+ # - no matchers known to have side-effects
61
+ def_node_matcher :example_for_autocorrect?, <<-PATTERN
62
+ [ #super !#example_with_side_effects? ]
63
+ PATTERN
64
+
65
+ # Matches the example with matcher with side effects
66
+ def_node_matcher :example_with_side_effects?, <<-PATTERN
67
+ (block #{Examples::EXAMPLES.send_pattern} _ #expectation_with_side_effects?)
68
+ PATTERN
69
+
70
+ # Matches the expectation with matcher with side effects
71
+ def_node_matcher :expectation_with_side_effects?, <<-PATTERN
72
+ (send #expectation? #{Runners::ALL.node_pattern_union} #matcher_with_side_effects?)
73
+ PATTERN
74
+
75
+ # Matches the matcher with side effects
76
+ def_node_search :matcher_with_side_effects?, <<-PATTERN
77
+ (send nil? #matcher_with_side_effects_name? ...)
78
+ PATTERN
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpec
6
+ class AggregateExamples < Cop
7
+ # @internal
8
+ # Support methods for example metadata.
9
+ # Examples with similar metadata are grouped.
10
+ #
11
+ # Depending on the configuration, `aggregate_failures` metadata
12
+ # is added to aggregated examples.
13
+ module MetadataHelpers
14
+ private
15
+
16
+ def metadata_for_aggregated_example(metadata)
17
+ metadata_to_add = metadata.compact.map(&:source)
18
+ if add_aggregate_failures_metadata?
19
+ metadata_to_add.unshift(":aggregate_failures")
20
+ end
21
+ if metadata_to_add.any?
22
+ "(#{metadata_to_add.join(", ")})"
23
+ else
24
+ ""
25
+ end
26
+ end
27
+
28
+ # Used to group examples for aggregation. `aggregate_failures`
29
+ # and `aggregate_failures: true` metadata are not taken in
30
+ # consideration, as it is dynamically set basing on cofiguration.
31
+ # If `aggregate_failures: false` is set on the example, it's
32
+ # preserved and is treated as regular metadata.
33
+ def metadata_without_aggregate_failures(example)
34
+ metadata = example_metadata(example) || []
35
+
36
+ symbols = metadata_symbols_without_aggregate_failures(metadata)
37
+ pairs = metadata_pairs_without_aggegate_failures(metadata)
38
+
39
+ [*symbols, pairs].flatten.compact
40
+ end
41
+
42
+ def example_metadata(example)
43
+ example.send_node.arguments
44
+ end
45
+
46
+ def metadata_symbols_without_aggregate_failures(metadata)
47
+ metadata
48
+ .select(&:sym_type?)
49
+ .reject { |item| item.value == :aggregate_failures }
50
+ end
51
+
52
+ def metadata_pairs_without_aggegate_failures(metadata)
53
+ map = metadata.find(&:hash_type?)
54
+ pairs = map&.pairs || []
55
+ pairs.reject do |pair|
56
+ pair.key.value == :aggregate_failures && pair.value.true_type?
57
+ end
58
+ end
59
+
60
+ def add_aggregate_failures_metadata?
61
+ cop_config.fetch("AddAggregateFailuresMetadata", false)
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../language"
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module RSpec
8
+ class AggregateExamples < Cop
9
+ # @internal
10
+ # Node matchers and searchers.
11
+ module NodeMatchers
12
+ extend RuboCop::NodePattern::Macros
13
+ include RuboCop::Cop::RSpec::Language
14
+
15
+ private
16
+
17
+ def_node_matcher :example_group_with_several_examples, <<-PATTERN
18
+ (block
19
+ #{ExampleGroups::ALL.send_pattern}
20
+ _
21
+ (begin $...)
22
+ )
23
+ PATTERN
24
+
25
+ def example_method?(method_name)
26
+ %i[it specify example scenario].include?(method_name)
27
+ end
28
+
29
+ # Matches examples with:
30
+ # - expectation statements exclusively
31
+ # - no title (e.g. `it('jumps over the lazy dog')`)
32
+ # - no HEREDOC
33
+ def_node_matcher :example_for_autocorrect?, <<-PATTERN
34
+ [
35
+ #example_with_expectations_only?
36
+ !#example_has_title?
37
+ !#contains_heredoc?
38
+ ]
39
+ PATTERN
40
+
41
+ def_node_matcher :example_with_expectations_only?, <<-PATTERN
42
+ (block #{Examples::EXAMPLES.send_pattern} _
43
+ { #single_expectation? (begin #single_expectation?+) }
44
+ )
45
+ PATTERN
46
+
47
+ # Matches the example with a title (e.g. `it('is valid')`)
48
+ def_node_matcher :example_has_title?, <<-PATTERN
49
+ (block
50
+ (send nil? #example_method? str ...)
51
+ ...
52
+ )
53
+ PATTERN
54
+
55
+ # Searches for HEREDOC in examples. It can be tricky to aggregate,
56
+ # especially when interleaved with parenthesis or curly braces.
57
+ def contains_heredoc?(node)
58
+ node.each_descendant(:str, :xstr, :dstr).any?(&:heredoc?)
59
+ end
60
+
61
+ def_node_matcher :subject_with_no_args?, <<-PATTERN
62
+ (send _ _)
63
+ PATTERN
64
+
65
+ def_node_matcher :expectation?, <<-PATTERN
66
+ {
67
+ (send nil? {:is_expected :are_expected})
68
+ (send nil? :expect #subject_with_no_args?)
69
+ }
70
+ PATTERN
71
+
72
+ def_node_matcher :single_expectation?, <<-PATTERN
73
+ (send #expectation? #{Runners::ALL.node_pattern_union} _)
74
+ PATTERN
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -1,168 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rubocop"
4
- require "test_prof/utils"
5
-
6
3
  module RuboCop
7
4
  module Cop
8
5
  module RSpec
9
- # Rejects and auto-corrects the usage of one-liners examples in favour of
10
- # :aggregate_failures feature.
11
- #
12
- # Example:
13
- #
14
- # # bad
15
- # it { is_expected.to be_success }
16
- # it { is_expected.to have_header('X-TOTAL-PAGES', 10) }
17
- # it { is_expected.to have_header('X-NEXT-PAGE', 2) }
18
- # its(:status) { is_expected.to eq(200) }
19
- #
20
- # # good
21
- # it "returns the second page", :aggregate_failures do
22
- # is_expected.to be_success
23
- # is_expected.to have_header('X-TOTAL-PAGES', 10)
24
- # is_expected.to have_header('X-NEXT-PAGE', 2)
25
- # expect(subject.status).to eq(200)
26
- # end
27
- #
28
- class AggregateFailures < RuboCop::Cop::Cop
29
- # From https://github.com/backus/rubocop-rspec/blob/master/lib/rubocop/rspec/language.rb
30
- GROUP_BLOCKS = %i[
31
- describe context feature example_group
32
- ].freeze
33
-
34
- EXAMPLE_BLOCKS = %i[
35
- it its specify example scenario
36
- ].freeze
37
-
38
- class << self
39
- def supported?
40
- return @supported if instance_variable_defined?(:@supported)
41
- @supported = TestProf::Utils.verify_gem_version("rubocop", at_least: "0.51.0")
42
-
43
- unless @supported
44
- warn "RSpec/AggregateFailures cop requires RuboCop >= 0.51.0. Skipping"
45
- end
46
-
47
- @supported
48
- end
6
+ class AggregateExamples
7
+ def self.inherited(subclass)
8
+ superclass.registry.enlist(subclass)
49
9
  end
10
+ end
50
11
 
51
- def on_block(node)
52
- return unless self.class.supported?
53
-
54
- method, _args, body = *node
55
- return unless body&.begin_type?
56
-
57
- _receiver, method_name, _object = *method
58
- return unless GROUP_BLOCKS.include?(method_name)
59
-
60
- return if check_node(body)
61
-
62
- add_offense(
63
- node,
64
- location: :expression,
65
- message: "Use :aggregate_failures instead of several one-liners."
66
- )
67
- end
68
-
69
- def autocorrect(node)
70
- _method, _args, body = *node
71
- iter = body.children.each
72
-
73
- first_example = loop do
74
- child = iter.next
75
- break child if oneliner?(child)
76
- end
77
-
78
- base_indent = " " * first_example.source_range.column
79
-
80
- replacements = [
81
- header_from(first_example),
82
- body_from(first_example, base_indent)
83
- ]
84
-
85
- last_example = nil
86
-
87
- loop do
88
- child = iter.next
89
- break unless oneliner?(child)
90
- last_example = child
91
- replacements << body_from(child, base_indent)
92
- end
93
-
94
- replacements << "#{base_indent}end"
95
-
96
- range = first_example.source_range.begin.join(
97
- last_example.source_range.end
98
- )
99
-
100
- replacement = replacements.join("\n")
101
-
102
- lambda do |corrector|
103
- corrector.replace(range, replacement)
104
- end
105
- end
106
-
107
- private
108
-
109
- def check_node(node)
110
- offenders = 0
111
-
112
- node.children.each do |child|
113
- if oneliner?(child)
114
- offenders += 1
115
- elsif example_node?(child)
116
- break if offenders > 1
117
- offenders = 0
118
- end
119
- end
120
-
121
- offenders < 2
122
- end
123
-
124
- def oneliner?(node)
125
- node&.block_type? &&
126
- (node.source.lines.size == 1) &&
127
- example_node?(node)
128
- end
129
-
130
- def example_node?(node)
131
- method, _args, _body = *node
132
- _receiver, method_name, _object = *method
133
- EXAMPLE_BLOCKS.include?(method_name)
134
- end
135
-
136
- def header_from(node)
137
- method, _args, _body = *node
138
- _receiver, method_name, _object = *method
139
- method_name = :it if method_name == :its
140
- %(#{method_name} "works", :aggregate_failures do)
141
- end
142
-
143
- def body_from(node, base_indent = "")
144
- method, _args, body = *node
145
- body_source = method.method_name == :its ? body_from_its(method, body) : body.source
146
- "#{base_indent}#{indent}#{body_source}"
147
- end
148
-
149
- def body_from_its(method, body)
150
- subject_attribute = method.arguments.first
151
- expectation = body.method_name
152
- match = body.arguments.first.source
153
-
154
- if subject_attribute.array_type?
155
- hash_keys = subject_attribute.values.map(&:value).join(", ")
156
- attribute = "dig(#{hash_keys})"
157
- else
158
- attribute = subject_attribute.value
159
- end
12
+ class AggregateFailures < AggregateExamples
13
+ raise "Remove me" if TestProf::VERSION >= "1.0"
160
14
 
161
- "expect(subject.#{attribute}).#{expectation} #{match}"
15
+ def initialize(*)
16
+ super
17
+ self.class.just_once { warn "`AggregateFailures` cop has been renamed to `AggregateExamples`." }
162
18
  end
163
19
 
164
- def indent
165
- @indent ||= " " * (config.for_cop("IndentationWidth")["Width"] || 2)
20
+ def self.just_once
21
+ return if @already_done
22
+ yield
23
+ @already_done = true
166
24
  end
167
25
  end
168
26
  end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This is shamelessly borrowed from RuboCop RSpec
4
+ # https://github.com/rubocop-hq/rubocop-rspec/blob/master/lib/rubocop/rspec/language.rb
5
+ module RuboCop
6
+ module Cop
7
+ module RSpec
8
+ # RSpec public API methods that are commonly used in cops
9
+ module Language
10
+ RSPEC = "{(const {nil? cbase} :RSpec) nil?}"
11
+
12
+ # Set of method selectors
13
+ class SelectorSet
14
+ def initialize(selectors)
15
+ @selectors = selectors
16
+ end
17
+
18
+ def ==(other)
19
+ selectors.eql?(other.selectors)
20
+ end
21
+
22
+ def +(other)
23
+ self.class.new(selectors + other.selectors)
24
+ end
25
+
26
+ def include?(selector)
27
+ selectors.include?(selector)
28
+ end
29
+
30
+ def block_pattern
31
+ "(block #{send_pattern} ...)"
32
+ end
33
+
34
+ def send_pattern
35
+ "(send #{RSPEC} #{node_pattern_union} ...)"
36
+ end
37
+
38
+ def node_pattern_union
39
+ "{#{node_pattern}}"
40
+ end
41
+
42
+ def node_pattern
43
+ selectors.map(&:inspect).join(" ")
44
+ end
45
+
46
+ protected
47
+
48
+ attr_reader :selectors
49
+ end
50
+
51
+ module ExampleGroups
52
+ GROUPS = SelectorSet.new(%i[describe context feature example_group])
53
+ SKIPPED = SelectorSet.new(%i[xdescribe xcontext xfeature])
54
+ FOCUSED = SelectorSet.new(%i[fdescribe fcontext ffeature])
55
+
56
+ ALL = GROUPS + SKIPPED + FOCUSED
57
+ end
58
+
59
+ module Examples
60
+ EXAMPLES = SelectorSet.new(%i[it specify example scenario its])
61
+ FOCUSED = SelectorSet.new(%i[fit fspecify fexample fscenario focus])
62
+ SKIPPED = SelectorSet.new(%i[xit xspecify xexample xscenario skip])
63
+ PENDING = SelectorSet.new(%i[pending])
64
+
65
+ ALL = EXAMPLES + FOCUSED + SKIPPED + PENDING
66
+ end
67
+
68
+ module Runners
69
+ ALL = SelectorSet.new(%i[to to_not not_to])
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -4,15 +4,6 @@ require "test_prof"
4
4
  require_relative "./before_all"
5
5
 
6
6
  module TestProf
7
- # Add `#map` to Object as an alias for `then` to use in modifiers
8
- using(Module.new do
9
- refine Object do
10
- def map
11
- yield self
12
- end
13
- end
14
- end)
15
-
16
7
  # Just like `let`, but persist the result for the whole group.
17
8
  # NOTE: Experimental and magical, for more control use `before_all`.
18
9
  module LetItBe
@@ -47,15 +38,14 @@ module TestProf
47
38
  end
48
39
 
49
40
  def wrap_with_modifiers(mods, &block)
50
- validate_modifiers! mods
51
-
52
41
  return block if mods.empty?
53
42
 
43
+ validate_modifiers! mods
44
+
54
45
  -> {
55
- instance_eval(&block).map do |record|
56
- mods.inject(record) do |rec, (k, v)|
57
- LetItBe.modifiers.fetch(k).call(rec, v)
58
- end
46
+ record = instance_eval(&block)
47
+ mods.inject(record) do |rec, (k, v)|
48
+ LetItBe.modifiers.fetch(k).call(rec, v)
59
49
  end
60
50
  }
61
51
  end
@@ -134,14 +124,26 @@ if defined?(::ActiveRecord)
134
124
  TestProf::LetItBe.configure do |config|
135
125
  config.register_modifier :reload do |record, val|
136
126
  next record unless val
137
- next record unless record.is_a?(::ActiveRecord::Base)
138
- record.reload
127
+ next record.reload if record.is_a?(::ActiveRecord::Base)
128
+
129
+ if record.respond_to?(:map)
130
+ next record.map do |rec|
131
+ rec.is_a?(::ActiveRecord::Base) ? rec.reload : rec
132
+ end
133
+ end
134
+ record
139
135
  end
140
136
 
141
137
  config.register_modifier :refind do |record, val|
142
138
  next record unless val
143
- next record unless record.is_a?(::ActiveRecord::Base)
144
- record.refind
139
+ next record.refind if record.is_a?(::ActiveRecord::Base)
140
+
141
+ if record.respond_to?(:map)
142
+ next record.map do |rec|
143
+ rec.is_a?(::ActiveRecord::Base) ? rec.refind : rec
144
+ end
145
+ end
146
+ record
145
147
  end
146
148
  end
147
149
  end
@@ -1,3 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "test_prof/utils"
4
+ supported = TestProf::Utils.verify_gem_version("rubocop", at_least: "0.51.0")
5
+ unless supported
6
+ warn "TestProf cops require RuboCop >= 0.51.0 to run."
7
+ return
8
+ end
9
+
10
+ require "rubocop"
11
+
12
+ require_relative "cops/inject"
13
+ require "test_prof/cops/rspec/aggregate_examples"
3
14
  require "test_prof/cops/rspec/aggregate_failures"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TestProf
4
- VERSION = "0.10.2"
4
+ VERSION = "0.11.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: test-prof
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.2
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vladimir Dementyev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-01-07 00:00:00.000000000 Z
11
+ date: 2020-02-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -128,7 +128,15 @@ files:
128
128
  - lib/test_prof/before_all.rb
129
129
  - lib/test_prof/before_all/adapters/active_record.rb
130
130
  - lib/test_prof/before_all/isolator.rb
131
+ - lib/test_prof/cops/inject.rb
132
+ - lib/test_prof/cops/rspec/aggregate_examples.rb
133
+ - lib/test_prof/cops/rspec/aggregate_examples/its.rb
134
+ - lib/test_prof/cops/rspec/aggregate_examples/line_range_helpers.rb
135
+ - lib/test_prof/cops/rspec/aggregate_examples/matchers_with_side_effects.rb
136
+ - lib/test_prof/cops/rspec/aggregate_examples/metadata_helpers.rb
137
+ - lib/test_prof/cops/rspec/aggregate_examples/node_matchers.rb
131
138
  - lib/test_prof/cops/rspec/aggregate_failures.rb
139
+ - lib/test_prof/cops/rspec/language.rb
132
140
  - lib/test_prof/event_prof.rb
133
141
  - lib/test_prof/event_prof/custom_events.rb
134
142
  - lib/test_prof/event_prof/custom_events/factory_create.rb