crspec 0.1.2 → 0.1.3
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/CHANGELOG.md +86 -0
- data/README.md +1 -1
- data/lib/crspec/cli.rb +86 -5
- data/lib/crspec/configuration.rb +62 -9
- data/lib/crspec/dsl.rb +66 -2
- data/lib/crspec/example.rb +60 -6
- data/lib/crspec/example_group.rb +363 -63
- data/lib/crspec/execution_context.rb +10 -6
- data/lib/crspec/expectations.rb +62 -2
- data/lib/crspec/file_fixtures.rb +18 -0
- data/lib/crspec/formatters/progress_formatter.rb +37 -14
- data/lib/crspec/matchers.rb +642 -0
- data/lib/crspec/mock/argument_matchers.rb +157 -0
- data/lib/crspec/mock/double.rb +246 -13
- data/lib/crspec/mock/interceptor.rb +32 -9
- data/lib/crspec/mock/space.rb +14 -0
- data/lib/crspec/process_runner.rb +332 -0
- data/lib/crspec/rails/assets_shim.rb +33 -0
- data/lib/crspec/rails/database_isolation.rb +179 -8
- data/lib/crspec/rails/parallel.rb +29 -29
- data/lib/crspec/rails/request_helpers.rb +70 -14
- data/lib/crspec/rails/system_server.rb +7 -0
- data/lib/crspec/rails/warden_shim.rb +18 -0
- data/lib/crspec/runner.rb +143 -29
- data/lib/crspec/shared_examples.rb +70 -0
- data/lib/crspec/status_persistence.rb +46 -0
- data/lib/crspec/transpiler/cli.rb +106 -15
- data/lib/crspec/transpiler/rewriter.rb +179 -25
- data/lib/crspec/version.rb +1 -1
- data/lib/crspec.rb +6 -0
- data/lib/rspec/core.rb +2 -0
- data/lib/rspec/expectations.rb +2 -0
- data/lib/rspec/mocks.rb +2 -0
- data/lib/rspec/rails.rb +2 -0
- data/lib/rspec.rb +2 -0
- metadata +27 -1
|
@@ -4,14 +4,26 @@ require "prism"
|
|
|
4
4
|
|
|
5
5
|
module Crspec
|
|
6
6
|
module Transpiler
|
|
7
|
+
# Lint severities:
|
|
8
|
+
# :error - construct is rejected by Crspec at load/run time
|
|
9
|
+
# :warning - construct is concurrency-unsafe and needs manual review
|
|
10
|
+
# :info - construct was auto-rewritten or is worth double-checking
|
|
11
|
+
Lint = Struct.new(:line, :severity, :code, :message)
|
|
12
|
+
|
|
7
13
|
class Rewriter < Prism::Visitor
|
|
8
|
-
attr_reader :transformed_code, :warnings
|
|
14
|
+
attr_reader :transformed_code, :warnings, :lints
|
|
9
15
|
|
|
10
16
|
def initialize(source_code)
|
|
11
17
|
super()
|
|
12
18
|
@source_code = source_code
|
|
13
19
|
@replacements = []
|
|
14
|
-
@
|
|
20
|
+
@lints = []
|
|
21
|
+
@inside_example_depth = 0
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Backwards-compatible string warnings.
|
|
25
|
+
def warnings
|
|
26
|
+
@lints.map { |l| "Line #{l.line}: [#{l.severity}] #{l.message}" }
|
|
15
27
|
end
|
|
16
28
|
|
|
17
29
|
def transpile
|
|
@@ -20,46 +32,188 @@ module Crspec
|
|
|
20
32
|
apply_replacements
|
|
21
33
|
end
|
|
22
34
|
|
|
23
|
-
def
|
|
24
|
-
|
|
25
|
-
if node.receiver.is_a?(Prism::ConstantReadNode) &&
|
|
26
|
-
node.receiver.name == :RSpec &&
|
|
27
|
-
node.name == :describe
|
|
28
|
-
@replacements << {
|
|
29
|
-
start_offset: node.receiver.location.start_offset,
|
|
30
|
-
end_offset: node.receiver.location.end_offset,
|
|
31
|
-
text: "Crspec"
|
|
32
|
-
}
|
|
33
|
-
end
|
|
35
|
+
def safety_score
|
|
36
|
+
return 100 if @lints.empty?
|
|
34
37
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
deductions = @lints.sum do |l|
|
|
39
|
+
case l.severity
|
|
40
|
+
when :error then 25
|
|
41
|
+
when :warning then 10
|
|
42
|
+
else 2
|
|
40
43
|
end
|
|
41
44
|
end
|
|
45
|
+
[100 - deductions, 0].max
|
|
46
|
+
end
|
|
42
47
|
|
|
43
|
-
|
|
48
|
+
def visit_call_node(node)
|
|
49
|
+
rewrite_rspec_receiver(node)
|
|
50
|
+
rewrite_matcher_define(node)
|
|
51
|
+
rewrite_before_all(node)
|
|
52
|
+
rewrite_focus_aliases(node)
|
|
53
|
+
lint_any_instance(node)
|
|
54
|
+
lint_env_mutation(node)
|
|
55
|
+
lint_time_mutation(node)
|
|
56
|
+
|
|
57
|
+
example_call = %i[it specify example].include?(node.name)
|
|
58
|
+
@inside_example_depth += 1 if example_call
|
|
59
|
+
begin
|
|
60
|
+
super
|
|
61
|
+
ensure
|
|
62
|
+
@inside_example_depth -= 1 if example_call
|
|
63
|
+
end
|
|
44
64
|
end
|
|
45
65
|
|
|
46
66
|
def visit_constant_read_node(node)
|
|
47
67
|
if node.name == :RSpec
|
|
48
|
-
|
|
49
|
-
start_offset: node.location.start_offset,
|
|
50
|
-
end_offset: node.location.end_offset,
|
|
51
|
-
text: "Crspec"
|
|
52
|
-
}
|
|
68
|
+
replace(node.location, "Crspec")
|
|
53
69
|
end
|
|
54
70
|
super
|
|
55
71
|
end
|
|
56
72
|
|
|
73
|
+
def visit_class_variable_write_node(node)
|
|
74
|
+
add_lint(node, :warning, :class_variable,
|
|
75
|
+
"class variable mutation (#{node.name}) is shared across all threads/fibers")
|
|
76
|
+
super
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def visit_class_variable_read_node(node)
|
|
80
|
+
add_lint(node, :warning, :class_variable,
|
|
81
|
+
"class variable read (#{node.name}) implies shared mutable state")
|
|
82
|
+
super
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def visit_constant_write_node(node)
|
|
86
|
+
if @inside_example_depth.positive?
|
|
87
|
+
add_lint(node, :warning, :constant_mutation,
|
|
88
|
+
"constant assignment (#{node.name}) inside an example mutates global state")
|
|
89
|
+
end
|
|
90
|
+
super
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def visit_index_operator_write_node(node)
|
|
94
|
+
lint_env_index(node)
|
|
95
|
+
super
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def visit_index_and_write_node(node)
|
|
99
|
+
lint_env_index(node)
|
|
100
|
+
super
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def visit_index_or_write_node(node)
|
|
104
|
+
lint_env_index(node)
|
|
105
|
+
super
|
|
106
|
+
end
|
|
107
|
+
|
|
57
108
|
private
|
|
58
109
|
|
|
110
|
+
def rewrite_rspec_receiver(node)
|
|
111
|
+
return unless node.receiver.is_a?(Prism::ConstantReadNode) && node.receiver.name == :RSpec
|
|
112
|
+
|
|
113
|
+
replace(node.receiver.location, "Crspec")
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# RSpec::Matchers.define -> Crspec::Matchers.define
|
|
117
|
+
def rewrite_matcher_define(node)
|
|
118
|
+
return unless node.name == :define
|
|
119
|
+
return unless node.receiver.is_a?(Prism::ConstantPathNode)
|
|
120
|
+
|
|
121
|
+
path = node.receiver
|
|
122
|
+
parent = path.parent
|
|
123
|
+
return unless parent.is_a?(Prism::ConstantReadNode) && parent.name == :RSpec && path.name == :Matchers
|
|
124
|
+
|
|
125
|
+
replace(parent.location, "Crspec")
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# before(:all)/before(:context) -> before(:each) with annotation.
|
|
129
|
+
def rewrite_before_all(node)
|
|
130
|
+
return unless %i[before after].include?(node.name) && node.arguments
|
|
131
|
+
|
|
132
|
+
first_arg = node.arguments.arguments.first
|
|
133
|
+
return unless first_arg.is_a?(Prism::SymbolNode) && %w[all context suite].include?(first_arg.value)
|
|
134
|
+
|
|
135
|
+
add_lint(node, :info, :before_all_rewritten,
|
|
136
|
+
"#{node.name}(:#{first_arg.value}) auto-rewritten to #{node.name}(:each); " \
|
|
137
|
+
"review for per-example cost (consider let/let!)")
|
|
138
|
+
replace(first_arg.location, ":each")
|
|
139
|
+
annotate_line(node, "# CRSPEC-MIGRATION: was #{node.name}(:#{first_arg.value}) — now runs per example; consider let/let! for expensive setup")
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# fit/fdescribe/fcontext are focus aliases; xit etc are already
|
|
143
|
+
# supported natively. Normalize focus to plain forms with a note.
|
|
144
|
+
def rewrite_focus_aliases(node)
|
|
145
|
+
mapping = { fit: "it", fdescribe: "describe", fcontext: "context" }
|
|
146
|
+
replacement = mapping[node.name]
|
|
147
|
+
return unless replacement
|
|
148
|
+
return if node.receiver
|
|
149
|
+
|
|
150
|
+
add_lint(node, :info, :focus_removed,
|
|
151
|
+
"#{node.name} (focus) normalized to #{replacement}")
|
|
152
|
+
replace(node.message_loc, replacement)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def lint_any_instance(node)
|
|
156
|
+
if %i[any_instance allow_any_instance_of expect_any_instance_of].include?(node.name)
|
|
157
|
+
add_lint(node, :error, :any_instance,
|
|
158
|
+
"#{node.name} mutates shared class hierarchies at runtime and is rejected by Crspec; " \
|
|
159
|
+
"inject a double or stub the specific instance instead")
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def lint_env_mutation(node)
|
|
164
|
+
return unless node.name == :[]= && node.receiver.is_a?(Prism::ConstantReadNode) && node.receiver.name == :ENV
|
|
165
|
+
|
|
166
|
+
add_lint(node, :warning, :env_mutation,
|
|
167
|
+
"ENV mutation inside specs is process-global and races across threads")
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def lint_env_index(node)
|
|
171
|
+
return unless node.receiver.is_a?(Prism::ConstantReadNode) && node.receiver.name == :ENV
|
|
172
|
+
|
|
173
|
+
add_lint(node, :warning, :env_mutation,
|
|
174
|
+
"ENV mutation inside specs is process-global and races across threads")
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
TIME_MUTATORS = %i[travel travel_to travel_back freeze return].freeze
|
|
178
|
+
|
|
179
|
+
def lint_time_mutation(node)
|
|
180
|
+
receiver = node.receiver
|
|
181
|
+
if receiver.is_a?(Prism::ConstantReadNode) && receiver.name == :Timecop
|
|
182
|
+
add_lint(node, :warning, :time_mutation,
|
|
183
|
+
"Timecop.#{node.name} mutates global time state; use ActiveSupport::Testing::TimeHelpers " \
|
|
184
|
+
"scoped per example, or inject a clock")
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def add_lint(node, severity, code, message)
|
|
189
|
+
@lints << Lint.new(node.location.start_line, severity, code, message)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def replace(location, text)
|
|
193
|
+
@replacements << {
|
|
194
|
+
start_offset: location.start_offset,
|
|
195
|
+
end_offset: location.end_offset,
|
|
196
|
+
text: text
|
|
197
|
+
}
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Insert a comment line above the node's line.
|
|
201
|
+
def annotate_line(node, comment)
|
|
202
|
+
line_start = @source_code.rindex("\n", node.location.start_offset)
|
|
203
|
+
line_start = line_start.nil? ? 0 : line_start + 1
|
|
204
|
+
indent = @source_code[line_start..node.location.start_offset][/\A[ \t]*/]
|
|
205
|
+
@replacements << {
|
|
206
|
+
start_offset: line_start,
|
|
207
|
+
end_offset: line_start,
|
|
208
|
+
text: "#{indent}#{comment}\n"
|
|
209
|
+
}
|
|
210
|
+
end
|
|
211
|
+
|
|
59
212
|
def apply_replacements
|
|
60
213
|
buffer = @source_code.dup
|
|
61
214
|
# Sort replacements backwards so offsets remain valid during splicing
|
|
62
|
-
sorted = @replacements.uniq { |r| [r[:start_offset], r[:end_offset]
|
|
215
|
+
sorted = @replacements.uniq { |r| [r[:start_offset], r[:end_offset], r[:text]] }
|
|
216
|
+
.sort_by { |r| [-r[:start_offset], -r[:end_offset]] }
|
|
63
217
|
sorted.each do |r|
|
|
64
218
|
buffer[r[:start_offset]...r[:end_offset]] = r[:text]
|
|
65
219
|
end
|
data/lib/crspec/version.rb
CHANGED
data/lib/crspec.rb
CHANGED
|
@@ -5,13 +5,17 @@ require_relative "crspec/configuration"
|
|
|
5
5
|
require_relative "crspec/execution_context"
|
|
6
6
|
require_relative "crspec/matchers"
|
|
7
7
|
require_relative "crspec/expectations"
|
|
8
|
+
require_relative "crspec/file_fixtures"
|
|
8
9
|
require_relative "crspec/example"
|
|
10
|
+
require_relative "crspec/shared_examples"
|
|
9
11
|
require_relative "crspec/example_group"
|
|
10
12
|
require_relative "crspec/mock/interceptor"
|
|
11
13
|
require_relative "crspec/mock/space"
|
|
12
14
|
require_relative "crspec/mock/double"
|
|
13
15
|
require_relative "crspec/formatters/progress_formatter"
|
|
16
|
+
require_relative "crspec/status_persistence"
|
|
14
17
|
require_relative "crspec/runner"
|
|
18
|
+
require_relative "crspec/process_runner"
|
|
15
19
|
require_relative "crspec/dsl"
|
|
16
20
|
require_relative "crspec/rails/database_isolation"
|
|
17
21
|
require_relative "crspec/rails/system_server"
|
|
@@ -24,3 +28,5 @@ require_relative "crspec/generators/init"
|
|
|
24
28
|
module Crspec
|
|
25
29
|
class Error < StandardError; end
|
|
26
30
|
end
|
|
31
|
+
|
|
32
|
+
RSpec = Crspec unless defined?(RSpec)
|
data/lib/rspec/core.rb
ADDED
data/lib/rspec/mocks.rb
ADDED
data/lib/rspec/rails.rb
ADDED
data/lib/rspec.rb
ADDED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: crspec
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Aboobacker MK
|
|
@@ -10,6 +10,20 @@ bindir: exe
|
|
|
10
10
|
cert_chain: []
|
|
11
11
|
date: 2026-07-31 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: async
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '2.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '2.0'
|
|
13
27
|
- !ruby/object:Gem::Dependency
|
|
14
28
|
name: prism
|
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -48,20 +62,32 @@ files:
|
|
|
48
62
|
- lib/crspec/example_group.rb
|
|
49
63
|
- lib/crspec/execution_context.rb
|
|
50
64
|
- lib/crspec/expectations.rb
|
|
65
|
+
- lib/crspec/file_fixtures.rb
|
|
51
66
|
- lib/crspec/formatters/progress_formatter.rb
|
|
52
67
|
- lib/crspec/generators/init.rb
|
|
53
68
|
- lib/crspec/matchers.rb
|
|
69
|
+
- lib/crspec/mock/argument_matchers.rb
|
|
54
70
|
- lib/crspec/mock/double.rb
|
|
55
71
|
- lib/crspec/mock/interceptor.rb
|
|
56
72
|
- lib/crspec/mock/space.rb
|
|
73
|
+
- lib/crspec/process_runner.rb
|
|
74
|
+
- lib/crspec/rails/assets_shim.rb
|
|
57
75
|
- lib/crspec/rails/database_isolation.rb
|
|
58
76
|
- lib/crspec/rails/parallel.rb
|
|
59
77
|
- lib/crspec/rails/request_helpers.rb
|
|
60
78
|
- lib/crspec/rails/system_server.rb
|
|
79
|
+
- lib/crspec/rails/warden_shim.rb
|
|
61
80
|
- lib/crspec/runner.rb
|
|
81
|
+
- lib/crspec/shared_examples.rb
|
|
82
|
+
- lib/crspec/status_persistence.rb
|
|
62
83
|
- lib/crspec/transpiler/cli.rb
|
|
63
84
|
- lib/crspec/transpiler/rewriter.rb
|
|
64
85
|
- lib/crspec/version.rb
|
|
86
|
+
- lib/rspec.rb
|
|
87
|
+
- lib/rspec/core.rb
|
|
88
|
+
- lib/rspec/expectations.rb
|
|
89
|
+
- lib/rspec/mocks.rb
|
|
90
|
+
- lib/rspec/rails.rb
|
|
65
91
|
- sig/crspec.rbs
|
|
66
92
|
homepage: https://github.com/crspec/crspec
|
|
67
93
|
licenses:
|