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 +4 -4
- data/CHANGELOG.md +7 -0
- data/lib/test_prof/any_fixture.rb +5 -0
- data/lib/test_prof/cops/inject.rb +23 -0
- data/lib/test_prof/cops/rspec/aggregate_examples.rb +199 -0
- data/lib/test_prof/cops/rspec/aggregate_examples/its.rb +98 -0
- data/lib/test_prof/cops/rspec/aggregate_examples/line_range_helpers.rb +31 -0
- data/lib/test_prof/cops/rspec/aggregate_examples/matchers_with_side_effects.rb +83 -0
- data/lib/test_prof/cops/rspec/aggregate_examples/metadata_helpers.rb +67 -0
- data/lib/test_prof/cops/rspec/aggregate_examples/node_matchers.rb +79 -0
- data/lib/test_prof/cops/rspec/aggregate_failures.rb +13 -155
- data/lib/test_prof/cops/rspec/language.rb +74 -0
- data/lib/test_prof/recipes/rspec/let_it_be.rb +21 -19
- data/lib/test_prof/rubocop.rb +11 -0
- data/lib/test_prof/version.rb +1 -1
- metadata +10 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e2060dd26a4bec62bd9a3b699e1165c60b8914a74c9fc25b3a0edf84afbd7d2b
|
4
|
+
data.tar.gz: 9b8e3eb78764c130ae78a575a5cdfe5d9e9a440d7b748c108b5088d99dc7261d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
@@ -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
|
-
|
10
|
-
|
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
|
-
|
52
|
-
|
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
|
-
|
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
|
165
|
-
|
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)
|
56
|
-
|
57
|
-
|
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
|
138
|
-
|
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
|
144
|
-
|
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
|
data/lib/test_prof/rubocop.rb
CHANGED
@@ -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"
|
data/lib/test_prof/version.rb
CHANGED
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.
|
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-
|
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
|