crspec 0.1.1 → 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.
Files changed (192) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +86 -0
  3. data/README.md +1 -1
  4. data/lib/crspec/cli.rb +86 -5
  5. data/lib/crspec/configuration.rb +62 -9
  6. data/lib/crspec/dsl.rb +66 -2
  7. data/lib/crspec/example.rb +60 -6
  8. data/lib/crspec/example_group.rb +363 -63
  9. data/lib/crspec/execution_context.rb +10 -6
  10. data/lib/crspec/expectations.rb +62 -2
  11. data/lib/crspec/file_fixtures.rb +18 -0
  12. data/lib/crspec/formatters/progress_formatter.rb +37 -14
  13. data/lib/crspec/matchers.rb +642 -0
  14. data/lib/crspec/mock/argument_matchers.rb +157 -0
  15. data/lib/crspec/mock/double.rb +246 -13
  16. data/lib/crspec/mock/interceptor.rb +32 -9
  17. data/lib/crspec/mock/space.rb +14 -0
  18. data/lib/crspec/process_runner.rb +332 -0
  19. data/lib/crspec/rails/assets_shim.rb +33 -0
  20. data/lib/crspec/rails/database_isolation.rb +179 -8
  21. data/lib/crspec/rails/parallel.rb +29 -29
  22. data/lib/crspec/rails/request_helpers.rb +70 -14
  23. data/lib/crspec/rails/system_server.rb +7 -0
  24. data/lib/crspec/rails/warden_shim.rb +18 -0
  25. data/lib/crspec/runner.rb +143 -29
  26. data/lib/crspec/shared_examples.rb +70 -0
  27. data/lib/crspec/status_persistence.rb +46 -0
  28. data/lib/crspec/transpiler/cli.rb +106 -15
  29. data/lib/crspec/transpiler/rewriter.rb +179 -25
  30. data/lib/crspec/version.rb +1 -1
  31. data/lib/crspec.rb +6 -0
  32. data/lib/rspec/core.rb +2 -0
  33. data/lib/rspec/expectations.rb +2 -0
  34. data/lib/rspec/mocks.rb +2 -0
  35. data/lib/rspec/rails.rb +2 -0
  36. data/lib/rspec.rb +2 -0
  37. metadata +27 -156
  38. data/benchmark/README.md +0 -56
  39. data/benchmark/run.rb +0 -72
  40. data/benchmark/spec/user_service_crspec_spec.rb +0 -15
  41. data/benchmark/spec/user_service_rspec_spec.rb +0 -16
  42. data/benchmark/target_service.rb +0 -12
  43. data/benchmark/test/user_service_test.rb +0 -18
  44. data/demo.rb +0 -129
  45. data/samples/book_store/.dockerignore +0 -51
  46. data/samples/book_store/.gitattributes +0 -9
  47. data/samples/book_store/.github/dependabot.yml +0 -12
  48. data/samples/book_store/.github/workflows/ci.yml +0 -130
  49. data/samples/book_store/.gitignore +0 -35
  50. data/samples/book_store/.kamal/hooks/docker-setup.sample +0 -3
  51. data/samples/book_store/.kamal/hooks/post-app-boot.sample +0 -3
  52. data/samples/book_store/.kamal/hooks/post-deploy.sample +0 -14
  53. data/samples/book_store/.kamal/hooks/post-proxy-reboot.sample +0 -3
  54. data/samples/book_store/.kamal/hooks/pre-app-boot.sample +0 -3
  55. data/samples/book_store/.kamal/hooks/pre-build.sample +0 -51
  56. data/samples/book_store/.kamal/hooks/pre-connect.sample +0 -47
  57. data/samples/book_store/.kamal/hooks/pre-deploy.sample +0 -122
  58. data/samples/book_store/.kamal/hooks/pre-proxy-reboot.sample +0 -3
  59. data/samples/book_store/.kamal/secrets +0 -20
  60. data/samples/book_store/.rubocop.yml +0 -8
  61. data/samples/book_store/.ruby-version +0 -1
  62. data/samples/book_store/Dockerfile +0 -77
  63. data/samples/book_store/Gemfile +0 -67
  64. data/samples/book_store/Gemfile.lock +0 -572
  65. data/samples/book_store/README.md +0 -24
  66. data/samples/book_store/Rakefile +0 -6
  67. data/samples/book_store/app/assets/images/.keep +0 -0
  68. data/samples/book_store/app/assets/stylesheets/application.css +0 -10
  69. data/samples/book_store/app/controllers/application_controller.rb +0 -2
  70. data/samples/book_store/app/controllers/books_controller.rb +0 -30
  71. data/samples/book_store/app/controllers/comments_controller.rb +0 -21
  72. data/samples/book_store/app/controllers/concerns/.keep +0 -0
  73. data/samples/book_store/app/controllers/users_controller.rb +0 -21
  74. data/samples/book_store/app/helpers/application_helper.rb +0 -2
  75. data/samples/book_store/app/helpers/books_helper.rb +0 -2
  76. data/samples/book_store/app/helpers/comments_helper.rb +0 -2
  77. data/samples/book_store/app/helpers/users_helper.rb +0 -2
  78. data/samples/book_store/app/javascript/application.js +0 -3
  79. data/samples/book_store/app/javascript/controllers/application.js +0 -9
  80. data/samples/book_store/app/javascript/controllers/hello_controller.js +0 -7
  81. data/samples/book_store/app/javascript/controllers/index.js +0 -4
  82. data/samples/book_store/app/jobs/application_job.rb +0 -7
  83. data/samples/book_store/app/mailers/application_mailer.rb +0 -4
  84. data/samples/book_store/app/models/application_record.rb +0 -3
  85. data/samples/book_store/app/models/book.rb +0 -10
  86. data/samples/book_store/app/models/comment.rb +0 -6
  87. data/samples/book_store/app/models/concerns/.keep +0 -0
  88. data/samples/book_store/app/models/user.rb +0 -4
  89. data/samples/book_store/app/views/layouts/application.html.erb +0 -29
  90. data/samples/book_store/app/views/layouts/mailer.html.erb +0 -13
  91. data/samples/book_store/app/views/layouts/mailer.text.erb +0 -1
  92. data/samples/book_store/app/views/pwa/manifest.json.erb +0 -22
  93. data/samples/book_store/app/views/pwa/service-worker.js +0 -26
  94. data/samples/book_store/bin/brakeman +0 -7
  95. data/samples/book_store/bin/bundler-audit +0 -6
  96. data/samples/book_store/bin/ci +0 -6
  97. data/samples/book_store/bin/dev +0 -2
  98. data/samples/book_store/bin/docker-entrypoint +0 -8
  99. data/samples/book_store/bin/importmap +0 -4
  100. data/samples/book_store/bin/jobs +0 -6
  101. data/samples/book_store/bin/kamal +0 -16
  102. data/samples/book_store/bin/rails +0 -4
  103. data/samples/book_store/bin/rake +0 -4
  104. data/samples/book_store/bin/rubocop +0 -8
  105. data/samples/book_store/bin/setup +0 -35
  106. data/samples/book_store/bin/thrust +0 -5
  107. data/samples/book_store/config/application.rb +0 -27
  108. data/samples/book_store/config/boot.rb +0 -4
  109. data/samples/book_store/config/bundler-audit.yml +0 -5
  110. data/samples/book_store/config/cable.yml +0 -17
  111. data/samples/book_store/config/cache.yml +0 -16
  112. data/samples/book_store/config/ci.rb +0 -24
  113. data/samples/book_store/config/credentials.yml.enc +0 -1
  114. data/samples/book_store/config/database.yml +0 -45
  115. data/samples/book_store/config/deploy.yml +0 -119
  116. data/samples/book_store/config/environment.rb +0 -5
  117. data/samples/book_store/config/environments/development.rb +0 -78
  118. data/samples/book_store/config/environments/production.rb +0 -90
  119. data/samples/book_store/config/environments/test.rb +0 -53
  120. data/samples/book_store/config/importmap.rb +0 -7
  121. data/samples/book_store/config/initializers/assets.rb +0 -7
  122. data/samples/book_store/config/initializers/content_security_policy.rb +0 -29
  123. data/samples/book_store/config/initializers/filter_parameter_logging.rb +0 -8
  124. data/samples/book_store/config/initializers/inflections.rb +0 -16
  125. data/samples/book_store/config/locales/en.yml +0 -31
  126. data/samples/book_store/config/puma.rb +0 -42
  127. data/samples/book_store/config/queue.yml +0 -18
  128. data/samples/book_store/config/recurring.yml +0 -15
  129. data/samples/book_store/config/routes.rb +0 -17
  130. data/samples/book_store/config/storage.yml +0 -27
  131. data/samples/book_store/config.ru +0 -6
  132. data/samples/book_store/db/cable_schema.rb +0 -11
  133. data/samples/book_store/db/cache_schema.rb +0 -12
  134. data/samples/book_store/db/migrate/20260731035441_create_users.rb +0 -10
  135. data/samples/book_store/db/migrate/20260731035506_create_books.rb +0 -11
  136. data/samples/book_store/db/migrate/20260731035528_create_comments.rb +0 -11
  137. data/samples/book_store/db/queue_schema.rb +0 -129
  138. data/samples/book_store/db/schema.rb +0 -39
  139. data/samples/book_store/db/seeds.rb +0 -9
  140. data/samples/book_store/lib/tasks/.keep +0 -0
  141. data/samples/book_store/log/.keep +0 -0
  142. data/samples/book_store/public/400.html +0 -135
  143. data/samples/book_store/public/404.html +0 -135
  144. data/samples/book_store/public/406-unsupported-browser.html +0 -135
  145. data/samples/book_store/public/422.html +0 -135
  146. data/samples/book_store/public/500.html +0 -135
  147. data/samples/book_store/public/icon.png +0 -0
  148. data/samples/book_store/public/icon.svg +0 -3
  149. data/samples/book_store/public/robots.txt +0 -1
  150. data/samples/book_store/script/.keep +0 -0
  151. data/samples/book_store/spec/models/book_spec.rb +0 -42
  152. data/samples/book_store/spec/models/comment_spec.rb +0 -22
  153. data/samples/book_store/spec/models/user_advanced_spec.rb +0 -27
  154. data/samples/book_store/spec/models/user_spec.rb +0 -30
  155. data/samples/book_store/spec/rails_helper.rb +0 -36
  156. data/samples/book_store/spec/rails_parallel_spec.rb +0 -23
  157. data/samples/book_store/spec/requests/books_spec.rb +0 -21
  158. data/samples/book_store/spec/requests/users_spec.rb +0 -14
  159. data/samples/book_store/spec/services/inventory_service_spec.rb +0 -28
  160. data/samples/book_store/spec/services/time_dependent_service_spec.rb +0 -26
  161. data/samples/book_store/spec/spec_helper.rb +0 -16
  162. data/samples/book_store/spec/system/book_store_system_spec.rb +0 -18
  163. data/samples/book_store/storage/.keep +0 -0
  164. data/samples/book_store/test/controllers/.keep +0 -0
  165. data/samples/book_store/test/controllers/books_controller_test.rb +0 -7
  166. data/samples/book_store/test/controllers/comments_controller_test.rb +0 -7
  167. data/samples/book_store/test/controllers/users_controller_test.rb +0 -7
  168. data/samples/book_store/test/fixtures/books.yml +0 -11
  169. data/samples/book_store/test/fixtures/comments.yml +0 -11
  170. data/samples/book_store/test/fixtures/files/.keep +0 -0
  171. data/samples/book_store/test/fixtures/users.yml +0 -9
  172. data/samples/book_store/test/helpers/.keep +0 -0
  173. data/samples/book_store/test/integration/.keep +0 -0
  174. data/samples/book_store/test/mailers/.keep +0 -0
  175. data/samples/book_store/test/models/.keep +0 -0
  176. data/samples/book_store/test/models/book_test.rb +0 -7
  177. data/samples/book_store/test/models/comment_test.rb +0 -7
  178. data/samples/book_store/test/models/user_test.rb +0 -7
  179. data/samples/book_store/test/test_helper.rb +0 -15
  180. data/samples/book_store/tmp/.keep +0 -0
  181. data/samples/book_store/tmp/pids/.keep +0 -0
  182. data/samples/book_store/tmp/storage/.keep +0 -0
  183. data/samples/book_store/vendor/.keep +0 -0
  184. data/samples/book_store/vendor/javascript/.keep +0 -0
  185. data/samples/user_spec.rb +0 -31
  186. data/test/crspec/configuration_test.rb +0 -76
  187. data/test/crspec/execution_context_test.rb +0 -61
  188. data/test/crspec/mock_test.rb +0 -55
  189. data/test/crspec/rails_test.rb +0 -62
  190. data/test/crspec/runner_test.rb +0 -68
  191. data/test/crspec/transpiler_test.rb +0 -41
  192. data/test/test_helper.rb +0 -6
@@ -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
- @warnings = []
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 visit_call_node(node)
24
- # Convert RSpec.describe -> Crspec.describe
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
- # Flag thread-unsafe before(:all) blocks
36
- if node.name == :before && node.arguments
37
- first_arg = node.arguments.arguments.first
38
- if first_arg.is_a?(Prism::SymbolNode) && first_arg.value == "all"
39
- @warnings << "Line #{node.location.start_line}: [Thread Safety Warning] before(:all) mutates global state across examples."
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
- super
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
- @replacements << {
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]] }.sort_by { |r| -r[:start_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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Crspec
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.3"
5
5
  end
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
@@ -0,0 +1,2 @@
1
+ # frozen_string_literal: true
2
+ require_relative "../crspec"
@@ -0,0 +1,2 @@
1
+ # frozen_string_literal: true
2
+ require_relative "../crspec"
@@ -0,0 +1,2 @@
1
+ # frozen_string_literal: true
2
+ require_relative "../crspec"
@@ -0,0 +1,2 @@
1
+ # frozen_string_literal: true
2
+ require_relative "../crspec"
data/lib/rspec.rb ADDED
@@ -0,0 +1,2 @@
1
+ # frozen_string_literal: true
2
+ require_relative "crspec"
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.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
@@ -38,13 +52,6 @@ files:
38
52
  - LICENSE.txt
39
53
  - README.md
40
54
  - Rakefile
41
- - benchmark/README.md
42
- - benchmark/run.rb
43
- - benchmark/spec/user_service_crspec_spec.rb
44
- - benchmark/spec/user_service_rspec_spec.rb
45
- - benchmark/target_service.rb
46
- - benchmark/test/user_service_test.rb
47
- - demo.rb
48
55
  - exe/crspec
49
56
  - exe/crspec-transpile
50
57
  - lib/crspec.rb
@@ -55,169 +62,33 @@ files:
55
62
  - lib/crspec/example_group.rb
56
63
  - lib/crspec/execution_context.rb
57
64
  - lib/crspec/expectations.rb
65
+ - lib/crspec/file_fixtures.rb
58
66
  - lib/crspec/formatters/progress_formatter.rb
59
67
  - lib/crspec/generators/init.rb
60
68
  - lib/crspec/matchers.rb
69
+ - lib/crspec/mock/argument_matchers.rb
61
70
  - lib/crspec/mock/double.rb
62
71
  - lib/crspec/mock/interceptor.rb
63
72
  - lib/crspec/mock/space.rb
73
+ - lib/crspec/process_runner.rb
74
+ - lib/crspec/rails/assets_shim.rb
64
75
  - lib/crspec/rails/database_isolation.rb
65
76
  - lib/crspec/rails/parallel.rb
66
77
  - lib/crspec/rails/request_helpers.rb
67
78
  - lib/crspec/rails/system_server.rb
79
+ - lib/crspec/rails/warden_shim.rb
68
80
  - lib/crspec/runner.rb
81
+ - lib/crspec/shared_examples.rb
82
+ - lib/crspec/status_persistence.rb
69
83
  - lib/crspec/transpiler/cli.rb
70
84
  - lib/crspec/transpiler/rewriter.rb
71
85
  - lib/crspec/version.rb
72
- - samples/book_store/.dockerignore
73
- - samples/book_store/.gitattributes
74
- - samples/book_store/.github/dependabot.yml
75
- - samples/book_store/.github/workflows/ci.yml
76
- - samples/book_store/.gitignore
77
- - samples/book_store/.kamal/hooks/docker-setup.sample
78
- - samples/book_store/.kamal/hooks/post-app-boot.sample
79
- - samples/book_store/.kamal/hooks/post-deploy.sample
80
- - samples/book_store/.kamal/hooks/post-proxy-reboot.sample
81
- - samples/book_store/.kamal/hooks/pre-app-boot.sample
82
- - samples/book_store/.kamal/hooks/pre-build.sample
83
- - samples/book_store/.kamal/hooks/pre-connect.sample
84
- - samples/book_store/.kamal/hooks/pre-deploy.sample
85
- - samples/book_store/.kamal/hooks/pre-proxy-reboot.sample
86
- - samples/book_store/.kamal/secrets
87
- - samples/book_store/.rubocop.yml
88
- - samples/book_store/.ruby-version
89
- - samples/book_store/Dockerfile
90
- - samples/book_store/Gemfile
91
- - samples/book_store/Gemfile.lock
92
- - samples/book_store/README.md
93
- - samples/book_store/Rakefile
94
- - samples/book_store/app/assets/images/.keep
95
- - samples/book_store/app/assets/stylesheets/application.css
96
- - samples/book_store/app/controllers/application_controller.rb
97
- - samples/book_store/app/controllers/books_controller.rb
98
- - samples/book_store/app/controllers/comments_controller.rb
99
- - samples/book_store/app/controllers/concerns/.keep
100
- - samples/book_store/app/controllers/users_controller.rb
101
- - samples/book_store/app/helpers/application_helper.rb
102
- - samples/book_store/app/helpers/books_helper.rb
103
- - samples/book_store/app/helpers/comments_helper.rb
104
- - samples/book_store/app/helpers/users_helper.rb
105
- - samples/book_store/app/javascript/application.js
106
- - samples/book_store/app/javascript/controllers/application.js
107
- - samples/book_store/app/javascript/controllers/hello_controller.js
108
- - samples/book_store/app/javascript/controllers/index.js
109
- - samples/book_store/app/jobs/application_job.rb
110
- - samples/book_store/app/mailers/application_mailer.rb
111
- - samples/book_store/app/models/application_record.rb
112
- - samples/book_store/app/models/book.rb
113
- - samples/book_store/app/models/comment.rb
114
- - samples/book_store/app/models/concerns/.keep
115
- - samples/book_store/app/models/user.rb
116
- - samples/book_store/app/views/layouts/application.html.erb
117
- - samples/book_store/app/views/layouts/mailer.html.erb
118
- - samples/book_store/app/views/layouts/mailer.text.erb
119
- - samples/book_store/app/views/pwa/manifest.json.erb
120
- - samples/book_store/app/views/pwa/service-worker.js
121
- - samples/book_store/bin/brakeman
122
- - samples/book_store/bin/bundler-audit
123
- - samples/book_store/bin/ci
124
- - samples/book_store/bin/dev
125
- - samples/book_store/bin/docker-entrypoint
126
- - samples/book_store/bin/importmap
127
- - samples/book_store/bin/jobs
128
- - samples/book_store/bin/kamal
129
- - samples/book_store/bin/rails
130
- - samples/book_store/bin/rake
131
- - samples/book_store/bin/rubocop
132
- - samples/book_store/bin/setup
133
- - samples/book_store/bin/thrust
134
- - samples/book_store/config.ru
135
- - samples/book_store/config/application.rb
136
- - samples/book_store/config/boot.rb
137
- - samples/book_store/config/bundler-audit.yml
138
- - samples/book_store/config/cable.yml
139
- - samples/book_store/config/cache.yml
140
- - samples/book_store/config/ci.rb
141
- - samples/book_store/config/credentials.yml.enc
142
- - samples/book_store/config/database.yml
143
- - samples/book_store/config/deploy.yml
144
- - samples/book_store/config/environment.rb
145
- - samples/book_store/config/environments/development.rb
146
- - samples/book_store/config/environments/production.rb
147
- - samples/book_store/config/environments/test.rb
148
- - samples/book_store/config/importmap.rb
149
- - samples/book_store/config/initializers/assets.rb
150
- - samples/book_store/config/initializers/content_security_policy.rb
151
- - samples/book_store/config/initializers/filter_parameter_logging.rb
152
- - samples/book_store/config/initializers/inflections.rb
153
- - samples/book_store/config/locales/en.yml
154
- - samples/book_store/config/puma.rb
155
- - samples/book_store/config/queue.yml
156
- - samples/book_store/config/recurring.yml
157
- - samples/book_store/config/routes.rb
158
- - samples/book_store/config/storage.yml
159
- - samples/book_store/db/cable_schema.rb
160
- - samples/book_store/db/cache_schema.rb
161
- - samples/book_store/db/migrate/20260731035441_create_users.rb
162
- - samples/book_store/db/migrate/20260731035506_create_books.rb
163
- - samples/book_store/db/migrate/20260731035528_create_comments.rb
164
- - samples/book_store/db/queue_schema.rb
165
- - samples/book_store/db/schema.rb
166
- - samples/book_store/db/seeds.rb
167
- - samples/book_store/lib/tasks/.keep
168
- - samples/book_store/log/.keep
169
- - samples/book_store/public/400.html
170
- - samples/book_store/public/404.html
171
- - samples/book_store/public/406-unsupported-browser.html
172
- - samples/book_store/public/422.html
173
- - samples/book_store/public/500.html
174
- - samples/book_store/public/icon.png
175
- - samples/book_store/public/icon.svg
176
- - samples/book_store/public/robots.txt
177
- - samples/book_store/script/.keep
178
- - samples/book_store/spec/models/book_spec.rb
179
- - samples/book_store/spec/models/comment_spec.rb
180
- - samples/book_store/spec/models/user_advanced_spec.rb
181
- - samples/book_store/spec/models/user_spec.rb
182
- - samples/book_store/spec/rails_helper.rb
183
- - samples/book_store/spec/rails_parallel_spec.rb
184
- - samples/book_store/spec/requests/books_spec.rb
185
- - samples/book_store/spec/requests/users_spec.rb
186
- - samples/book_store/spec/services/inventory_service_spec.rb
187
- - samples/book_store/spec/services/time_dependent_service_spec.rb
188
- - samples/book_store/spec/spec_helper.rb
189
- - samples/book_store/spec/system/book_store_system_spec.rb
190
- - samples/book_store/storage/.keep
191
- - samples/book_store/test/controllers/.keep
192
- - samples/book_store/test/controllers/books_controller_test.rb
193
- - samples/book_store/test/controllers/comments_controller_test.rb
194
- - samples/book_store/test/controllers/users_controller_test.rb
195
- - samples/book_store/test/fixtures/books.yml
196
- - samples/book_store/test/fixtures/comments.yml
197
- - samples/book_store/test/fixtures/files/.keep
198
- - samples/book_store/test/fixtures/users.yml
199
- - samples/book_store/test/helpers/.keep
200
- - samples/book_store/test/integration/.keep
201
- - samples/book_store/test/mailers/.keep
202
- - samples/book_store/test/models/.keep
203
- - samples/book_store/test/models/book_test.rb
204
- - samples/book_store/test/models/comment_test.rb
205
- - samples/book_store/test/models/user_test.rb
206
- - samples/book_store/test/test_helper.rb
207
- - samples/book_store/tmp/.keep
208
- - samples/book_store/tmp/pids/.keep
209
- - samples/book_store/tmp/storage/.keep
210
- - samples/book_store/vendor/.keep
211
- - samples/book_store/vendor/javascript/.keep
212
- - samples/user_spec.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
213
91
  - sig/crspec.rbs
214
- - test/crspec/configuration_test.rb
215
- - test/crspec/execution_context_test.rb
216
- - test/crspec/mock_test.rb
217
- - test/crspec/rails_test.rb
218
- - test/crspec/runner_test.rb
219
- - test/crspec/transpiler_test.rb
220
- - test/test_helper.rb
221
92
  homepage: https://github.com/crspec/crspec
222
93
  licenses:
223
94
  - MIT
data/benchmark/README.md DELETED
@@ -1,56 +0,0 @@
1
- # Crspec Native Benchmark Suite
2
-
3
- This directory contains benchmarking utilities comparing default native framework runner performance across **crspec**, **RSpec**, and **Minitest** without any manual thread-slicing or concurrency controls in the benchmark script.
4
-
5
- ---
6
-
7
- ## Directory Structure
8
-
9
- ```text
10
- benchmark/
11
- ├── target_service.rb # Domain target class under test (UserService)
12
- ├── spec/
13
- │ ├── user_service_crspec_spec.rb # Idiomatic Crspec spec (Crspec.describe)
14
- │ └── user_service_rspec_spec.rb # Idiomatic RSpec spec (RSpec.describe)
15
- ├── test/
16
- │ └── user_service_test.rb # Idiomatic Minitest test (class UserServiceTest < Minitest::Test)
17
- ├── run.rb # Clean benchmark runner script
18
- └── README.md
19
- ```
20
-
21
- ---
22
-
23
- ## Evaluated Configurations
24
-
25
- Each framework is executed using its native default runner invocation:
26
- - **Crspec (Native Default Runner)**: Uses `Crspec::Runner.new` default multi-threaded / fiber execution kernel.
27
- - **RSpec (Native Default Runner)**: Native standard RSpec runner.
28
- - **Minitest (Native Default Runner)**: Native Minitest runner.
29
-
30
- ---
31
-
32
- ## How to Run
33
-
34
- ```bash
35
- mise exec -- ruby benchmark/run.rb
36
- ```
37
-
38
- ---
39
-
40
- ## Benchmark Results (macOS arm64 / Ruby 4.0.2)
41
-
42
- ```text
43
- ==========================================================================
44
- Crspec vs RSpec vs Minitest Benchmark Suite
45
- Test Workload: 100 examples per framework
46
- Evaluating Default Native Framework Runners (No Manual Concurrency Controls)
47
- ==========================================================================
48
-
49
- Results Summary:
50
- Framework Engine | Duration (s) | Throughput (ops/s)
51
- ------------------------------------------------------------------------------
52
- Crspec (Native Default Runner) | 0.0211 | 4749.47
53
- RSpec (Native Default Runner) | 0.1375 | 727.15
54
- Minitest (Native Default Runner) | 0.1259 | 794.45
55
- ==============================================================================
56
- ```
data/benchmark/run.rb DELETED
@@ -1,72 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
4
- $LOAD_PATH.unshift File.expand_path(__dir__)
5
-
6
- require "crspec"
7
- require "rspec/core"
8
- require "minitest"
9
- require "stringio"
10
-
11
- NUMBER_OF_TESTS = 100
12
-
13
- Rails.logger.debug "=========================================================================="
14
- Rails.logger.debug " Crspec vs RSpec vs Minitest Benchmark Suite"
15
- Rails.logger.debug { " Test Workload: #{NUMBER_OF_TESTS} examples per framework" }
16
- Rails.logger.debug " Evaluating Default Native Framework Runners (No Manual Concurrency Controls)"
17
- Rails.logger.debug "=========================================================================="
18
-
19
- results = {}
20
- null_formatter = Crspec::Formatters::NullFormatter.new
21
-
22
- # -----------------------------------------------------------------------------
23
- # 1. Crspec Native Execution (Default Concurrency Kernel)
24
- # -----------------------------------------------------------------------------
25
- Crspec.reset!
26
- load File.expand_path("spec/user_service_crspec_spec.rb", __dir__)
27
-
28
- t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
29
- Crspec::Runner.new(formatter: null_formatter).run(Crspec.world.example_groups)
30
- t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
31
- results["Crspec (Native Default Runner)"] = t1 - t0
32
-
33
- # -----------------------------------------------------------------------------
34
- # 2. RSpec Native Execution
35
- # -----------------------------------------------------------------------------
36
- config = RSpec::Core::Configuration.new
37
- config.output_stream = StringIO.new
38
- world = RSpec::Core::World.new(config)
39
- RSpec.instance_variable_set(:@world, world)
40
- load File.expand_path("spec/user_service_rspec_spec.rb", __dir__)
41
-
42
- t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
43
- world.example_groups.first.run(config.reporter)
44
- t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
45
- results["RSpec (Native Default Runner)"] = t1 - t0
46
-
47
- # -----------------------------------------------------------------------------
48
- # 3. Minitest Native Execution
49
- # -----------------------------------------------------------------------------
50
- load File.expand_path("test/user_service_test.rb", __dir__)
51
-
52
- t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
53
- Minitest::CompositeReporter.new
54
- UserServiceTest.runnable_methods.each do |method_name|
55
- UserServiceTest.new(method_name).run
56
- end
57
- t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
58
- results["Minitest (Native Default Runner)"] = t1 - t0
59
-
60
- # -----------------------------------------------------------------------------
61
- # Summary Output
62
- # -----------------------------------------------------------------------------
63
- Rails.logger.debug "\nResults Summary:"
64
- Rails.logger.debug format("%-45s | %-12s | %-15s", "Framework Engine", "Duration (s)", "Throughput (ops/s)")
65
- Rails.logger.debug "-" * 78
66
-
67
- results.each do |engine, duration|
68
- throughput = (NUMBER_OF_TESTS / duration).round(2)
69
- Rails.logger.debug format("%-45s | %-12.4f | %-15.2f", engine, duration, throughput)
70
- end
71
-
72
- Rails.logger.debug "=" * 78
@@ -1,15 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../target_service"
4
-
5
- Crspec.describe UserService do
6
- 100.times do |i|
7
- let(:"user_#{i}") { UserService.process_user(i, "User #{i}", "user#{i}@example.com") }
8
-
9
- it "processes user #{i}" do
10
- u = send(:"user_#{i}")
11
- expect(u[:valid]).to be(true)
12
- expect(u[:token]).not_to be_nil
13
- end
14
- end
15
- end
@@ -1,16 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "rspec/core"
4
- require_relative "../target_service"
5
-
6
- RSpec.describe UserService do
7
- 100.times do |i|
8
- let(:"user_#{i}") { UserService.process_user(i, "User #{i}", "user#{i}@example.com") }
9
-
10
- it "processes user #{i}" do
11
- u = send(:"user_#{i}")
12
- expect(u[:valid]).to be(true)
13
- expect(u[:token]).not_to be_nil
14
- end
15
- end
16
- end