rubocop-rspec-guide 0.2.1 → 0.4.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 +4 -4
- data/.rubocop.yml +6 -6
- data/.yardopts +9 -0
- data/CHANGELOG.md +92 -0
- data/CONTRIBUTING.md +358 -0
- data/INTEGRATION_TESTING.md +324 -0
- data/README.md +443 -16
- data/Rakefile +49 -0
- data/benchmark/README.md +349 -0
- data/benchmark/baseline_v0.3.1.txt +67 -0
- data/benchmark/baseline_v0.4.0.txt +167 -0
- data/benchmark/benchmark_helper.rb +92 -0
- data/benchmark/compare_versions.rb +136 -0
- data/benchmark/cops_benchmark.rb +428 -0
- data/benchmark/cops_performance.rb +109 -0
- data/benchmark/quick_comparison.rb +58 -0
- data/benchmark/quick_invariant_bench.rb +52 -0
- data/benchmark/rspec_base_integration.rb +86 -0
- data/benchmark/save_baseline.rb +18 -0
- data/benchmark/scalability_benchmark.rb +181 -0
- data/config/default.yml +43 -2
- data/config/obsoletion.yml +6 -0
- data/lib/rubocop/cop/factory_bot_guide/dynamic_attribute_evaluation.rb +193 -0
- data/lib/rubocop/cop/factory_bot_guide/dynamic_attributes_for_time_and_random.rb +10 -106
- data/lib/rubocop/cop/rspec_guide/characteristics_and_contexts.rb +13 -78
- data/lib/rubocop/cop/rspec_guide/context_setup.rb +81 -30
- data/lib/rubocop/cop/rspec_guide/duplicate_before_hooks.rb +89 -22
- data/lib/rubocop/cop/rspec_guide/duplicate_let_values.rb +91 -22
- data/lib/rubocop/cop/rspec_guide/happy_path_first.rb +52 -21
- data/lib/rubocop/cop/rspec_guide/invariant_examples.rb +60 -19
- data/lib/rubocop/cop/rspec_guide/minimum_behavioral_coverage.rb +165 -0
- data/lib/rubocop/rspec/guide/inject.rb +26 -0
- data/lib/rubocop/rspec/guide/plugin.rb +45 -0
- data/lib/rubocop/rspec/guide/version.rb +1 -1
- data/lib/rubocop-rspec-guide.rb +4 -0
- metadata +49 -1
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
require "benchmark/ips"
|
|
2
|
+
require_relative "../lib/rubocop-rspec-guide"
|
|
3
|
+
|
|
4
|
+
SAMPLE = <<~RUBY
|
|
5
|
+
describe 'Example' do
|
|
6
|
+
context 'case A' do
|
|
7
|
+
let(:value) { 42 }
|
|
8
|
+
before { setup }
|
|
9
|
+
it 'works' do
|
|
10
|
+
expect(true).to be true
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
context 'case B' do
|
|
14
|
+
let(:value) { 42 }
|
|
15
|
+
before { setup }
|
|
16
|
+
it 'works' do
|
|
17
|
+
expect(true).to be true
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
RUBY
|
|
22
|
+
|
|
23
|
+
source = RuboCop::ProcessedSource.new(SAMPLE, RUBY_VERSION.to_f)
|
|
24
|
+
config = RuboCop::Config.new({
|
|
25
|
+
"RSpec" => {
|
|
26
|
+
"Language" => {
|
|
27
|
+
"ExampleGroups" => {"Regular" => %w[describe context], "Skipped" => [], "Focused" => []},
|
|
28
|
+
"Examples" => {"Regular" => %w[it], "Focused" => [], "Skipped" => [], "Pending" => []},
|
|
29
|
+
"Helpers" => %w[let let!],
|
|
30
|
+
"Hooks" => %w[before after],
|
|
31
|
+
"Subjects" => %w[subject]
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
RuboCop::RSpec::Language.config = config['RSpec']['Language']
|
|
37
|
+
|
|
38
|
+
puts "Quick Performance Check (Optimized Version)"
|
|
39
|
+
puts "=" * 60
|
|
40
|
+
|
|
41
|
+
Benchmark.ips do |x|
|
|
42
|
+
x.config(time: 2, warmup: 1)
|
|
43
|
+
|
|
44
|
+
[
|
|
45
|
+
RuboCop::Cop::RSpecGuide::HappyPathFirst,
|
|
46
|
+
RuboCop::Cop::RSpecGuide::DuplicateLetValues,
|
|
47
|
+
RuboCop::Cop::RSpecGuide::DuplicateBeforeHooks,
|
|
48
|
+
RuboCop::Cop::RSpecGuide::InvariantExamples
|
|
49
|
+
].each do |cop_class|
|
|
50
|
+
x.report(cop_class.badge.to_s) do
|
|
51
|
+
cop = cop_class.new(config)
|
|
52
|
+
commissioner = RuboCop::Cop::Commissioner.new([cop], [], raise_error: false)
|
|
53
|
+
commissioner.investigate(source)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
x.compare!
|
|
58
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
require "benchmark/ips"
|
|
2
|
+
require_relative "../lib/rubocop-rspec-guide"
|
|
3
|
+
|
|
4
|
+
SAMPLE = <<~RUBY
|
|
5
|
+
describe 'Validator' do
|
|
6
|
+
context 'with valid data' do
|
|
7
|
+
it 'responds to valid?' do
|
|
8
|
+
expect(subject).to respond_to(:valid?)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
context 'with invalid data' do
|
|
12
|
+
it 'responds to valid?' do
|
|
13
|
+
expect(subject).to respond_to(:valid?)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
context 'with edge case' do
|
|
17
|
+
it 'responds to valid?' do
|
|
18
|
+
expect(subject).to respond_to(:valid?)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
RUBY
|
|
23
|
+
|
|
24
|
+
source = RuboCop::ProcessedSource.new(SAMPLE, RUBY_VERSION.to_f)
|
|
25
|
+
config = RuboCop::Config.new({
|
|
26
|
+
"RSpec" => {
|
|
27
|
+
"Language" => {
|
|
28
|
+
"ExampleGroups" => {"Regular" => %w[describe context], "Skipped" => [], "Focused" => []},
|
|
29
|
+
"Examples" => {"Regular" => %w[it], "Focused" => [], "Skipped" => [], "Pending" => []},
|
|
30
|
+
"Helpers" => %w[let],
|
|
31
|
+
"Hooks" => %w[before],
|
|
32
|
+
"Subjects" => %w[subject]
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"RSpecGuide/InvariantExamples" => {"MinLeafContexts" => 3}
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
RuboCop::RSpec::Language.config = config['RSpec']['Language']
|
|
39
|
+
|
|
40
|
+
puts "Benchmarking InvariantExamples (optimized with local matcher)"
|
|
41
|
+
puts "Expected baseline: ~1504 i/s, Previous (slow): ~854 i/s"
|
|
42
|
+
puts ""
|
|
43
|
+
|
|
44
|
+
Benchmark.ips do |x|
|
|
45
|
+
x.config(time: 2, warmup: 1)
|
|
46
|
+
|
|
47
|
+
x.report("InvariantExamples") do
|
|
48
|
+
cop = RuboCop::Cop::RSpecGuide::InvariantExamples.new(config)
|
|
49
|
+
commissioner = RuboCop::Cop::Commissioner.new([cop], [], raise_error: false)
|
|
50
|
+
commissioner.investigate(source)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "benchmark/ips"
|
|
5
|
+
require "rubocop"
|
|
6
|
+
require_relative "../lib/rubocop-rspec-guide"
|
|
7
|
+
|
|
8
|
+
# Create a sample RSpec file to analyze
|
|
9
|
+
SAMPLE_CODE = <<~RUBY
|
|
10
|
+
describe 'UserService' do
|
|
11
|
+
context 'when user is admin' do
|
|
12
|
+
let(:user) { create(:user, :admin) }
|
|
13
|
+
let(:service) { UserService.new(user) }
|
|
14
|
+
|
|
15
|
+
before { setup_admin_permissions }
|
|
16
|
+
|
|
17
|
+
it 'has admin access' do
|
|
18
|
+
expect(service.admin?).to be true
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'can modify settings' do
|
|
22
|
+
expect(service.can_modify_settings?).to be true
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
context 'when user is regular' do
|
|
27
|
+
let(:user) { create(:user) }
|
|
28
|
+
let(:service) { UserService.new(user) }
|
|
29
|
+
|
|
30
|
+
before { setup_admin_permissions }
|
|
31
|
+
|
|
32
|
+
it 'does not have admin access' do
|
|
33
|
+
expect(service.admin?).to be false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'cannot modify settings' do
|
|
37
|
+
expect(service.can_modify_settings?).to be false
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
RUBY
|
|
42
|
+
|
|
43
|
+
# Configure RuboCop
|
|
44
|
+
config = RuboCop::ConfigStore.new
|
|
45
|
+
config_hash = {
|
|
46
|
+
"AllCops" => { "TargetRubyVersion" => 3.0 },
|
|
47
|
+
"RSpecGuide/MinimumBehavioralCoverage" => { "Enabled" => true },
|
|
48
|
+
"RSpecGuide/HappyPathFirst" => { "Enabled" => true },
|
|
49
|
+
"RSpecGuide/ContextSetup" => { "Enabled" => true },
|
|
50
|
+
"RSpecGuide/DuplicateLetValues" => { "Enabled" => true },
|
|
51
|
+
"RSpecGuide/DuplicateBeforeHooks" => { "Enabled" => true },
|
|
52
|
+
"RSpecGuide/InvariantExamples" => { "Enabled" => true }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
puts "=" * 80
|
|
56
|
+
puts "RuboCop::Cop::RSpec::Base Integration Benchmark"
|
|
57
|
+
puts "=" * 80
|
|
58
|
+
puts "\nSample code size: #{SAMPLE_CODE.lines.count} lines"
|
|
59
|
+
puts "Cops enabled: 6 RSpec cops"
|
|
60
|
+
puts "\n"
|
|
61
|
+
|
|
62
|
+
# Run benchmark
|
|
63
|
+
Benchmark.ips do |x|
|
|
64
|
+
x.config(time: 10, warmup: 3)
|
|
65
|
+
|
|
66
|
+
x.report("Process sample RSpec file") do
|
|
67
|
+
runner = RuboCop::Runner.new(
|
|
68
|
+
{ format: "quiet" },
|
|
69
|
+
RuboCop::ConfigStore.new
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Create a temporary file
|
|
73
|
+
require "tempfile"
|
|
74
|
+
file = Tempfile.new(["benchmark_spec", ".rb"])
|
|
75
|
+
begin
|
|
76
|
+
file.write(SAMPLE_CODE)
|
|
77
|
+
file.flush
|
|
78
|
+
runner.run([file.path])
|
|
79
|
+
ensure
|
|
80
|
+
file.close
|
|
81
|
+
file.unlink
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
x.compare!
|
|
86
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# This script runs benchmarks and saves them as a baseline for future comparison
|
|
5
|
+
require_relative "benchmark_helper"
|
|
6
|
+
|
|
7
|
+
version = ENV["VERSION"] || "unknown"
|
|
8
|
+
output_file = "benchmark/baseline_v#{version}.txt"
|
|
9
|
+
|
|
10
|
+
puts "Saving baseline benchmark for version #{version}"
|
|
11
|
+
puts "Output file: #{output_file}"
|
|
12
|
+
puts ""
|
|
13
|
+
|
|
14
|
+
# Run cops_benchmark and save output
|
|
15
|
+
system("ruby benchmark/cops_benchmark.rb > #{output_file} 2>&1")
|
|
16
|
+
|
|
17
|
+
puts ""
|
|
18
|
+
puts "Baseline saved to #{output_file}"
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "benchmark_helper"
|
|
5
|
+
|
|
6
|
+
# Benchmark cops with different file sizes
|
|
7
|
+
puts "=" * 80
|
|
8
|
+
puts "RuboCop RSpec Guide - Scalability Benchmark"
|
|
9
|
+
puts "Testing how cops perform with different file sizes"
|
|
10
|
+
puts "=" * 80
|
|
11
|
+
puts ""
|
|
12
|
+
|
|
13
|
+
# Configure benchmark timing based on environment
|
|
14
|
+
if ENV["FULL_BENCHMARK"]
|
|
15
|
+
WARMUP_TIME = 2
|
|
16
|
+
MEASUREMENT_TIME = 5
|
|
17
|
+
puts "Mode: FULL (accurate measurements)"
|
|
18
|
+
else
|
|
19
|
+
WARMUP_TIME = 1
|
|
20
|
+
MEASUREMENT_TIME = 2
|
|
21
|
+
puts "Mode: QUICK (fast feedback)"
|
|
22
|
+
end
|
|
23
|
+
puts ""
|
|
24
|
+
|
|
25
|
+
# Test with increasing number of contexts
|
|
26
|
+
Benchmark.ips do |x|
|
|
27
|
+
x.config(time: MEASUREMENT_TIME, warmup: WARMUP_TIME)
|
|
28
|
+
|
|
29
|
+
[5, 10, 20, 50].each do |contexts_count|
|
|
30
|
+
source = BenchmarkHelper.generate_context_code(
|
|
31
|
+
contexts_count: contexts_count,
|
|
32
|
+
examples_per_context: 3
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
x.report("MinimumBehavioralCoverage - #{contexts_count} contexts") do
|
|
36
|
+
BenchmarkHelper.run_cop(
|
|
37
|
+
RuboCop::Cop::RSpecGuide::MinimumBehavioralCoverage,
|
|
38
|
+
source
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
x.compare!
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
puts "\n"
|
|
47
|
+
|
|
48
|
+
# Test with increasing number of examples
|
|
49
|
+
Benchmark.ips do |x|
|
|
50
|
+
x.config(time: MEASUREMENT_TIME, warmup: WARMUP_TIME)
|
|
51
|
+
|
|
52
|
+
[10, 25, 50, 100].each do |examples_count|
|
|
53
|
+
source = BenchmarkHelper.generate_rspec_code(examples_count: examples_count)
|
|
54
|
+
|
|
55
|
+
x.report("ContextSetup - #{examples_count} examples") do
|
|
56
|
+
BenchmarkHelper.run_cop(
|
|
57
|
+
RuboCop::Cop::RSpecGuide::ContextSetup,
|
|
58
|
+
source
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
x.compare!
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
puts "\n"
|
|
67
|
+
|
|
68
|
+
# Test DuplicateLetValues with nested contexts
|
|
69
|
+
Benchmark.ips do |x|
|
|
70
|
+
x.config(time: MEASUREMENT_TIME, warmup: WARMUP_TIME)
|
|
71
|
+
|
|
72
|
+
[3, 5, 10, 15].each do |nesting_level|
|
|
73
|
+
contexts = (1..nesting_level).map do |i|
|
|
74
|
+
indent = " " * i
|
|
75
|
+
<<~RUBY
|
|
76
|
+
#{indent}context "level #{i}" do
|
|
77
|
+
#{indent} let(:value) { #{i} }
|
|
78
|
+
#{indent}
|
|
79
|
+
#{indent} it "works" do
|
|
80
|
+
#{indent} expect(value).to eq(#{i})
|
|
81
|
+
#{indent} end
|
|
82
|
+
RUBY
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
source = <<~RUBY
|
|
86
|
+
RSpec.describe MyClass do
|
|
87
|
+
#{contexts.join("\n")}
|
|
88
|
+
#{" end\n" * nesting_level}
|
|
89
|
+
end
|
|
90
|
+
RUBY
|
|
91
|
+
|
|
92
|
+
x.report("DuplicateLetValues - #{nesting_level} nesting levels") do
|
|
93
|
+
BenchmarkHelper.run_cop(
|
|
94
|
+
RuboCop::Cop::RSpecGuide::DuplicateLetValues,
|
|
95
|
+
source
|
|
96
|
+
)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
x.compare!
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
puts "\n"
|
|
104
|
+
|
|
105
|
+
# Test InvariantExamples with many duplicates
|
|
106
|
+
Benchmark.ips do |x|
|
|
107
|
+
x.config(time: MEASUREMENT_TIME, warmup: WARMUP_TIME)
|
|
108
|
+
|
|
109
|
+
[2, 5, 10, 15].each do |duplicates_count|
|
|
110
|
+
contexts = (1..duplicates_count).map do |i|
|
|
111
|
+
<<~RUBY
|
|
112
|
+
context "scenario #{i}" do
|
|
113
|
+
it "validates input" do
|
|
114
|
+
expect(subject).to respond_to(:valid?)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it "has specific behavior" do
|
|
118
|
+
expect(subject.call(#{i})).to eq(#{i})
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
RUBY
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
source = <<~RUBY
|
|
125
|
+
RSpec.describe MyClass do
|
|
126
|
+
#{contexts.join("\n")}
|
|
127
|
+
end
|
|
128
|
+
RUBY
|
|
129
|
+
|
|
130
|
+
x.report("InvariantExamples - #{duplicates_count} duplicates") do
|
|
131
|
+
BenchmarkHelper.run_cop(
|
|
132
|
+
RuboCop::Cop::RSpecGuide::InvariantExamples,
|
|
133
|
+
source
|
|
134
|
+
)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
x.compare!
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
puts "\n"
|
|
142
|
+
|
|
143
|
+
# Memory usage test
|
|
144
|
+
require "objspace"
|
|
145
|
+
|
|
146
|
+
puts "=" * 80
|
|
147
|
+
puts "Memory Usage Analysis"
|
|
148
|
+
puts "=" * 80
|
|
149
|
+
puts ""
|
|
150
|
+
|
|
151
|
+
large_source = BenchmarkHelper.generate_context_code(
|
|
152
|
+
contexts_count: 50,
|
|
153
|
+
examples_per_context: 10
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
cops = [
|
|
157
|
+
RuboCop::Cop::RSpecGuide::MinimumBehavioralCoverage,
|
|
158
|
+
RuboCop::Cop::RSpecGuide::HappyPathFirst,
|
|
159
|
+
RuboCop::Cop::RSpecGuide::ContextSetup,
|
|
160
|
+
RuboCop::Cop::RSpecGuide::DuplicateLetValues,
|
|
161
|
+
RuboCop::Cop::RSpecGuide::DuplicateBeforeHooks,
|
|
162
|
+
RuboCop::Cop::RSpecGuide::InvariantExamples,
|
|
163
|
+
RuboCop::Cop::FactoryBotGuide::DynamicAttributeEvaluation
|
|
164
|
+
]
|
|
165
|
+
|
|
166
|
+
cops.each do |cop_class|
|
|
167
|
+
GC.start
|
|
168
|
+
before = ObjectSpace.memsize_of_all
|
|
169
|
+
|
|
170
|
+
BenchmarkHelper.run_cop(cop_class, large_source)
|
|
171
|
+
|
|
172
|
+
after = ObjectSpace.memsize_of_all
|
|
173
|
+
memory_used = (after - before) / 1024.0 / 1024.0
|
|
174
|
+
|
|
175
|
+
puts "#{cop_class.cop_name}: #{"%.2f" % memory_used} MB"
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
puts "\n"
|
|
179
|
+
puts "=" * 80
|
|
180
|
+
puts "Scalability benchmark completed!"
|
|
181
|
+
puts "=" * 80
|
data/config/default.yml
CHANGED
|
@@ -1,38 +1,79 @@
|
|
|
1
1
|
---
|
|
2
|
+
# RSpec Language configuration for rubocop-rspec integration
|
|
3
|
+
# This enables RuboCop::Cop::RSpec::Base matchers to work properly
|
|
4
|
+
RSpec:
|
|
5
|
+
Enabled: true
|
|
6
|
+
Language:
|
|
7
|
+
inherit_mode:
|
|
8
|
+
merge:
|
|
9
|
+
- Expectations
|
|
10
|
+
- Helpers
|
|
11
|
+
- Hooks
|
|
12
|
+
- Subjects
|
|
13
|
+
Helpers:
|
|
14
|
+
- let
|
|
15
|
+
- let!
|
|
16
|
+
- let_it_be
|
|
17
|
+
- let_it_be!
|
|
18
|
+
|
|
19
|
+
RSpecGuide/MinimumBehavioralCoverage:
|
|
20
|
+
Description: "Require at least 2 behavioral variations in describe block (2+ contexts OR it-blocks + contexts)"
|
|
21
|
+
Enabled: true
|
|
22
|
+
VersionAdded: "0.3.0"
|
|
23
|
+
StyleGuideUrl: "https://github.com/AlexeyMatskevich/rspec-guide"
|
|
24
|
+
|
|
2
25
|
RSpecGuide/CharacteristicsAndContexts:
|
|
3
|
-
Description: "
|
|
26
|
+
Description: "[DEPRECATED] Use RSpecGuide/MinimumBehavioralCoverage instead"
|
|
4
27
|
Enabled: true
|
|
28
|
+
VersionAdded: "0.1.0"
|
|
29
|
+
VersionChanged: "0.3.0"
|
|
5
30
|
StyleGuideUrl: "https://github.com/AlexeyMatskevich/rspec-guide"
|
|
6
31
|
|
|
7
32
|
RSpecGuide/DuplicateLetValues:
|
|
8
33
|
Description: "Detect duplicate let values in sibling contexts (ERROR if in all, WARNING if partial)"
|
|
9
34
|
Enabled: true
|
|
10
35
|
WarnOnPartialDuplicates: true
|
|
36
|
+
VersionAdded: "0.1.0"
|
|
37
|
+
VersionChanged: "0.2.2"
|
|
11
38
|
StyleGuideUrl: "https://github.com/AlexeyMatskevich/rspec-guide"
|
|
12
39
|
|
|
13
40
|
RSpecGuide/DuplicateBeforeHooks:
|
|
14
41
|
Description: "Detect duplicate before hooks in sibling contexts (ERROR if in all, WARNING if partial)"
|
|
15
42
|
Enabled: true
|
|
16
43
|
WarnOnPartialDuplicates: true
|
|
44
|
+
VersionAdded: "0.1.0"
|
|
17
45
|
StyleGuideUrl: "https://github.com/AlexeyMatskevich/rspec-guide"
|
|
18
46
|
|
|
19
47
|
RSpecGuide/InvariantExamples:
|
|
20
48
|
Description: "Invariant examples should be in leaf contexts or extracted to shared_examples"
|
|
21
49
|
Enabled: true
|
|
22
50
|
MinLeafContexts: 3
|
|
51
|
+
VersionAdded: "0.1.0"
|
|
23
52
|
StyleGuideUrl: "https://github.com/AlexeyMatskevich/rspec-guide"
|
|
24
53
|
|
|
25
54
|
RSpecGuide/HappyPathFirst:
|
|
26
55
|
Description: "Corner cases should not be first context (happy path first)"
|
|
27
56
|
Enabled: true
|
|
57
|
+
VersionAdded: "0.1.0"
|
|
58
|
+
VersionChanged: "0.2.1"
|
|
28
59
|
StyleGuideUrl: "https://github.com/AlexeyMatskevich/rspec-guide"
|
|
29
60
|
|
|
30
61
|
RSpecGuide/ContextSetup:
|
|
31
62
|
Description: "Context must have setup (let/before) to distinguish it from parent. Subject should be at describe level."
|
|
32
63
|
Enabled: true
|
|
64
|
+
VersionAdded: "0.1.0"
|
|
65
|
+
VersionChanged: "0.2.0"
|
|
66
|
+
StyleGuideUrl: "https://github.com/AlexeyMatskevich/rspec-guide"
|
|
67
|
+
|
|
68
|
+
FactoryBotGuide/DynamicAttributeEvaluation:
|
|
69
|
+
Description: "Wrap method calls in blocks for dynamic evaluation (Time.now, SecureRandom, etc.)"
|
|
70
|
+
Enabled: true
|
|
71
|
+
VersionAdded: "0.3.0"
|
|
33
72
|
StyleGuideUrl: "https://github.com/AlexeyMatskevich/rspec-guide"
|
|
34
73
|
|
|
35
74
|
FactoryBotGuide/DynamicAttributesForTimeAndRandom:
|
|
36
|
-
Description: "
|
|
75
|
+
Description: "[DEPRECATED] Use FactoryBotGuide/DynamicAttributeEvaluation instead"
|
|
37
76
|
Enabled: true
|
|
77
|
+
VersionAdded: "0.1.0"
|
|
78
|
+
VersionChanged: "0.3.0"
|
|
38
79
|
StyleGuideUrl: "https://github.com/AlexeyMatskevich/rspec-guide"
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Cop obsoletion configuration for rubocop-rspec-guide
|
|
2
|
+
# This file defines renamed, removed, and deprecated cops
|
|
3
|
+
|
|
4
|
+
renamed:
|
|
5
|
+
RSpecGuide/CharacteristicsAndContexts: RSpecGuide/MinimumBehavioralCoverage
|
|
6
|
+
FactoryBotGuide/DynamicAttributesForTimeAndRandom: FactoryBotGuide/DynamicAttributeEvaluation
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module FactoryBotGuide
|
|
6
|
+
# Checks that method calls in FactoryBot attribute definitions
|
|
7
|
+
# are wrapped in blocks for dynamic evaluation.
|
|
8
|
+
#
|
|
9
|
+
# Without blocks, method calls are evaluated once at factory definition time,
|
|
10
|
+
# not at factory instantiation time. This causes all factory instances to share
|
|
11
|
+
# the same value, leading to subtle bugs and test pollution.
|
|
12
|
+
#
|
|
13
|
+
# This is particularly problematic for:
|
|
14
|
+
# - Time-related methods (Time.now, Date.today, 1.day.from_now, etc.)
|
|
15
|
+
# - Random values (SecureRandom.hex, SecureRandom.uuid, etc.)
|
|
16
|
+
# - Object constructors (Array.new, Hash.new, etc.)
|
|
17
|
+
# - Any method calls that should return different values per instance
|
|
18
|
+
#
|
|
19
|
+
# @safety
|
|
20
|
+
# This cop is safe to run automatically. It detects all method calls
|
|
21
|
+
# in factory attributes, not just Time/Random methods.
|
|
22
|
+
#
|
|
23
|
+
# @example Time-related methods
|
|
24
|
+
# # bad - all users get the same timestamp
|
|
25
|
+
# factory :user do
|
|
26
|
+
# created_at Time.now
|
|
27
|
+
# updated_at DateTime.now
|
|
28
|
+
# birth_date Date.today
|
|
29
|
+
# expires_at 1.day.from_now
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
# # good - each user gets a fresh timestamp
|
|
33
|
+
# factory :user do
|
|
34
|
+
# created_at { Time.now }
|
|
35
|
+
# updated_at { DateTime.now }
|
|
36
|
+
# birth_date { Date.today }
|
|
37
|
+
# expires_at { 1.day.from_now }
|
|
38
|
+
# end
|
|
39
|
+
#
|
|
40
|
+
# @example Random values
|
|
41
|
+
# # bad - all users share the same token
|
|
42
|
+
# factory :user do
|
|
43
|
+
# token SecureRandom.hex
|
|
44
|
+
# uuid SecureRandom.uuid
|
|
45
|
+
# end
|
|
46
|
+
#
|
|
47
|
+
# # good - each user gets a unique token
|
|
48
|
+
# factory :user do
|
|
49
|
+
# token { SecureRandom.hex }
|
|
50
|
+
# uuid { SecureRandom.uuid }
|
|
51
|
+
# end
|
|
52
|
+
#
|
|
53
|
+
# @example Object constructors
|
|
54
|
+
# # bad - all users share the same array/hash instance!
|
|
55
|
+
# factory :user do
|
|
56
|
+
# tags Array.new
|
|
57
|
+
# metadata Hash.new
|
|
58
|
+
# end
|
|
59
|
+
#
|
|
60
|
+
# # This causes test pollution:
|
|
61
|
+
# user1 = create(:user)
|
|
62
|
+
# user1.tags << 'admin'
|
|
63
|
+
# user2 = create(:user)
|
|
64
|
+
# user2.tags # => ['admin'] - unexpected!
|
|
65
|
+
#
|
|
66
|
+
# # good - each user gets a new array/hash
|
|
67
|
+
# factory :user do
|
|
68
|
+
# tags { Array.new }
|
|
69
|
+
# metadata { Hash.new }
|
|
70
|
+
# end
|
|
71
|
+
#
|
|
72
|
+
# @example Static values (no block needed)
|
|
73
|
+
# # good - static values don't need blocks
|
|
74
|
+
# factory :user do
|
|
75
|
+
# name "John Doe"
|
|
76
|
+
# age 30
|
|
77
|
+
# active true
|
|
78
|
+
# end
|
|
79
|
+
#
|
|
80
|
+
# @example Complex expressions
|
|
81
|
+
# # bad - method chains evaluated once
|
|
82
|
+
# factory :user do
|
|
83
|
+
# full_name current_user.profile.display_name
|
|
84
|
+
# end
|
|
85
|
+
#
|
|
86
|
+
# # good - method chains evaluated per instance
|
|
87
|
+
# factory :user do
|
|
88
|
+
# full_name { current_user.profile.display_name }
|
|
89
|
+
# end
|
|
90
|
+
#
|
|
91
|
+
class DynamicAttributeEvaluation < Base
|
|
92
|
+
extend AutoCorrector
|
|
93
|
+
|
|
94
|
+
MSG = "Use block syntax for attribute `%<attribute>s` because `%<method>s` " \
|
|
95
|
+
"is evaluated once at factory definition time. " \
|
|
96
|
+
"Wrap in block: `%<attribute>s { %<value>s }`"
|
|
97
|
+
|
|
98
|
+
TIME_CLASSES = %w[Time Date DateTime].freeze
|
|
99
|
+
RANDOM_CLASSES = %w[SecureRandom].freeze
|
|
100
|
+
|
|
101
|
+
# @!method factory_block?(node)
|
|
102
|
+
def_node_matcher :factory_block?, <<~PATTERN
|
|
103
|
+
(block
|
|
104
|
+
(send {nil? (const {nil? cbase} :FactoryBot)} :factory ...)
|
|
105
|
+
...)
|
|
106
|
+
PATTERN
|
|
107
|
+
|
|
108
|
+
# @!method attribute_assignment?(node)
|
|
109
|
+
def_node_matcher :attribute_assignment?, <<~PATTERN
|
|
110
|
+
(send nil? $_ $_value)
|
|
111
|
+
PATTERN
|
|
112
|
+
|
|
113
|
+
def on_block(node)
|
|
114
|
+
return unless factory_block?(node)
|
|
115
|
+
|
|
116
|
+
# Check all attribute assignments within the factory
|
|
117
|
+
node.each_descendant(:send) do |send_node|
|
|
118
|
+
check_attribute(send_node)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
private
|
|
123
|
+
|
|
124
|
+
def check_attribute(node)
|
|
125
|
+
attribute_assignment?(node) do |attribute_name, value|
|
|
126
|
+
# Skip if value is already a block
|
|
127
|
+
next if value.block_type?
|
|
128
|
+
|
|
129
|
+
# Check if the value is a method call that needs dynamic evaluation
|
|
130
|
+
next unless requires_dynamic_evaluation?(value)
|
|
131
|
+
|
|
132
|
+
add_offense(
|
|
133
|
+
node,
|
|
134
|
+
message: format(
|
|
135
|
+
MSG,
|
|
136
|
+
attribute: attribute_name,
|
|
137
|
+
method: method_description(value),
|
|
138
|
+
value: value.source
|
|
139
|
+
)
|
|
140
|
+
) do |corrector|
|
|
141
|
+
# Wrap the value in a block: value -> { value }
|
|
142
|
+
corrector.replace(value, "{ #{value.source} }")
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def requires_dynamic_evaluation?(node)
|
|
148
|
+
# Only method calls need dynamic evaluation
|
|
149
|
+
# Static values (strings, symbols, numbers, etc.) are fine as-is
|
|
150
|
+
return false unless node.send_type?
|
|
151
|
+
|
|
152
|
+
# Time.now, Date.today, DateTime.now, etc.
|
|
153
|
+
return true if time_method?(node)
|
|
154
|
+
|
|
155
|
+
# SecureRandom.hex, SecureRandom.uuid, etc.
|
|
156
|
+
return true if random_method?(node)
|
|
157
|
+
|
|
158
|
+
# Any other method calls (e.g., 1.day.ago, Array.new, etc.)
|
|
159
|
+
# should be wrapped in blocks for dynamic evaluation
|
|
160
|
+
true
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def time_method?(node)
|
|
164
|
+
return false unless node.receiver
|
|
165
|
+
|
|
166
|
+
receiver_name = if node.receiver.const_type?
|
|
167
|
+
node.receiver.const_name
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
TIME_CLASSES.include?(receiver_name)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def random_method?(node)
|
|
174
|
+
return false unless node.receiver
|
|
175
|
+
|
|
176
|
+
receiver_name = if node.receiver.const_type?
|
|
177
|
+
node.receiver.const_name
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
RANDOM_CLASSES.include?(receiver_name)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def method_description(node)
|
|
184
|
+
if node.receiver
|
|
185
|
+
"#{node.receiver.source}.#{node.method_name}"
|
|
186
|
+
else
|
|
187
|
+
node.method_name.to_s
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|