test-prof 0.10.2 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
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