crspec 0.1.0 → 0.1.1

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 (164) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +67 -17
  3. data/benchmark/README.md +56 -0
  4. data/benchmark/run.rb +72 -0
  5. data/benchmark/spec/user_service_crspec_spec.rb +15 -0
  6. data/benchmark/spec/user_service_rspec_spec.rb +16 -0
  7. data/benchmark/target_service.rb +12 -0
  8. data/benchmark/test/user_service_test.rb +18 -0
  9. data/demo.rb +6 -3
  10. data/lib/crspec/cli.rb +34 -3
  11. data/lib/crspec/configuration.rb +59 -0
  12. data/lib/crspec/example_group.rb +31 -0
  13. data/lib/crspec/execution_context.rb +1 -1
  14. data/lib/crspec/generators/init.rb +94 -0
  15. data/lib/crspec/matchers.rb +55 -0
  16. data/lib/crspec/mock/double.rb +8 -1
  17. data/lib/crspec/rails/database_isolation.rb +6 -15
  18. data/lib/crspec/rails/parallel.rb +1 -1
  19. data/lib/crspec/rails/request_helpers.rb +96 -0
  20. data/lib/crspec/transpiler/cli.rb +6 -6
  21. data/lib/crspec/version.rb +1 -1
  22. data/lib/crspec.rb +3 -0
  23. data/samples/book_store/.dockerignore +51 -0
  24. data/samples/book_store/.gitattributes +9 -0
  25. data/samples/book_store/.github/dependabot.yml +12 -0
  26. data/samples/book_store/.github/workflows/ci.yml +130 -0
  27. data/samples/book_store/.gitignore +35 -0
  28. data/samples/book_store/.kamal/hooks/docker-setup.sample +3 -0
  29. data/samples/book_store/.kamal/hooks/post-app-boot.sample +3 -0
  30. data/samples/book_store/.kamal/hooks/post-deploy.sample +14 -0
  31. data/samples/book_store/.kamal/hooks/post-proxy-reboot.sample +3 -0
  32. data/samples/book_store/.kamal/hooks/pre-app-boot.sample +3 -0
  33. data/samples/book_store/.kamal/hooks/pre-build.sample +51 -0
  34. data/samples/book_store/.kamal/hooks/pre-connect.sample +47 -0
  35. data/samples/book_store/.kamal/hooks/pre-deploy.sample +122 -0
  36. data/samples/book_store/.kamal/hooks/pre-proxy-reboot.sample +3 -0
  37. data/samples/book_store/.kamal/secrets +20 -0
  38. data/samples/book_store/.rubocop.yml +8 -0
  39. data/samples/book_store/.ruby-version +1 -0
  40. data/samples/book_store/Dockerfile +77 -0
  41. data/samples/book_store/Gemfile +67 -0
  42. data/samples/book_store/Gemfile.lock +572 -0
  43. data/samples/book_store/README.md +24 -0
  44. data/samples/book_store/Rakefile +6 -0
  45. data/samples/book_store/app/assets/images/.keep +0 -0
  46. data/samples/book_store/app/assets/stylesheets/application.css +10 -0
  47. data/samples/book_store/app/controllers/application_controller.rb +2 -0
  48. data/samples/book_store/app/controllers/books_controller.rb +30 -0
  49. data/samples/book_store/app/controllers/comments_controller.rb +21 -0
  50. data/samples/book_store/app/controllers/concerns/.keep +0 -0
  51. data/samples/book_store/app/controllers/users_controller.rb +21 -0
  52. data/samples/book_store/app/helpers/application_helper.rb +2 -0
  53. data/samples/book_store/app/helpers/books_helper.rb +2 -0
  54. data/samples/book_store/app/helpers/comments_helper.rb +2 -0
  55. data/samples/book_store/app/helpers/users_helper.rb +2 -0
  56. data/samples/book_store/app/javascript/application.js +3 -0
  57. data/samples/book_store/app/javascript/controllers/application.js +9 -0
  58. data/samples/book_store/app/javascript/controllers/hello_controller.js +7 -0
  59. data/samples/book_store/app/javascript/controllers/index.js +4 -0
  60. data/samples/book_store/app/jobs/application_job.rb +7 -0
  61. data/samples/book_store/app/mailers/application_mailer.rb +4 -0
  62. data/samples/book_store/app/models/application_record.rb +3 -0
  63. data/samples/book_store/app/models/book.rb +10 -0
  64. data/samples/book_store/app/models/comment.rb +6 -0
  65. data/samples/book_store/app/models/concerns/.keep +0 -0
  66. data/samples/book_store/app/models/user.rb +4 -0
  67. data/samples/book_store/app/views/layouts/application.html.erb +29 -0
  68. data/samples/book_store/app/views/layouts/mailer.html.erb +13 -0
  69. data/samples/book_store/app/views/layouts/mailer.text.erb +1 -0
  70. data/samples/book_store/app/views/pwa/manifest.json.erb +22 -0
  71. data/samples/book_store/app/views/pwa/service-worker.js +26 -0
  72. data/samples/book_store/bin/brakeman +7 -0
  73. data/samples/book_store/bin/bundler-audit +6 -0
  74. data/samples/book_store/bin/ci +6 -0
  75. data/samples/book_store/bin/dev +2 -0
  76. data/samples/book_store/bin/docker-entrypoint +8 -0
  77. data/samples/book_store/bin/importmap +4 -0
  78. data/samples/book_store/bin/jobs +6 -0
  79. data/samples/book_store/bin/kamal +16 -0
  80. data/samples/book_store/bin/rails +4 -0
  81. data/samples/book_store/bin/rake +4 -0
  82. data/samples/book_store/bin/rubocop +8 -0
  83. data/samples/book_store/bin/setup +35 -0
  84. data/samples/book_store/bin/thrust +5 -0
  85. data/samples/book_store/config/application.rb +27 -0
  86. data/samples/book_store/config/boot.rb +4 -0
  87. data/samples/book_store/config/bundler-audit.yml +5 -0
  88. data/samples/book_store/config/cable.yml +17 -0
  89. data/samples/book_store/config/cache.yml +16 -0
  90. data/samples/book_store/config/ci.rb +24 -0
  91. data/samples/book_store/config/credentials.yml.enc +1 -0
  92. data/samples/book_store/config/database.yml +45 -0
  93. data/samples/book_store/config/deploy.yml +119 -0
  94. data/samples/book_store/config/environment.rb +5 -0
  95. data/samples/book_store/config/environments/development.rb +78 -0
  96. data/samples/book_store/config/environments/production.rb +90 -0
  97. data/samples/book_store/config/environments/test.rb +53 -0
  98. data/samples/book_store/config/importmap.rb +7 -0
  99. data/samples/book_store/config/initializers/assets.rb +7 -0
  100. data/samples/book_store/config/initializers/content_security_policy.rb +29 -0
  101. data/samples/book_store/config/initializers/filter_parameter_logging.rb +8 -0
  102. data/samples/book_store/config/initializers/inflections.rb +16 -0
  103. data/samples/book_store/config/locales/en.yml +31 -0
  104. data/samples/book_store/config/puma.rb +42 -0
  105. data/samples/book_store/config/queue.yml +18 -0
  106. data/samples/book_store/config/recurring.yml +15 -0
  107. data/samples/book_store/config/routes.rb +17 -0
  108. data/samples/book_store/config/storage.yml +27 -0
  109. data/samples/book_store/config.ru +6 -0
  110. data/samples/book_store/db/cable_schema.rb +11 -0
  111. data/samples/book_store/db/cache_schema.rb +12 -0
  112. data/samples/book_store/db/migrate/20260731035441_create_users.rb +10 -0
  113. data/samples/book_store/db/migrate/20260731035506_create_books.rb +11 -0
  114. data/samples/book_store/db/migrate/20260731035528_create_comments.rb +11 -0
  115. data/samples/book_store/db/queue_schema.rb +129 -0
  116. data/samples/book_store/db/schema.rb +39 -0
  117. data/samples/book_store/db/seeds.rb +9 -0
  118. data/samples/book_store/lib/tasks/.keep +0 -0
  119. data/samples/book_store/log/.keep +0 -0
  120. data/samples/book_store/public/400.html +135 -0
  121. data/samples/book_store/public/404.html +135 -0
  122. data/samples/book_store/public/406-unsupported-browser.html +135 -0
  123. data/samples/book_store/public/422.html +135 -0
  124. data/samples/book_store/public/500.html +135 -0
  125. data/samples/book_store/public/icon.png +0 -0
  126. data/samples/book_store/public/icon.svg +3 -0
  127. data/samples/book_store/public/robots.txt +1 -0
  128. data/samples/book_store/script/.keep +0 -0
  129. data/samples/book_store/spec/models/book_spec.rb +42 -0
  130. data/samples/book_store/spec/models/comment_spec.rb +22 -0
  131. data/samples/book_store/spec/models/user_advanced_spec.rb +27 -0
  132. data/samples/book_store/spec/models/user_spec.rb +30 -0
  133. data/samples/book_store/spec/rails_helper.rb +36 -0
  134. data/samples/book_store/spec/rails_parallel_spec.rb +23 -0
  135. data/samples/book_store/spec/requests/books_spec.rb +21 -0
  136. data/samples/book_store/spec/requests/users_spec.rb +14 -0
  137. data/samples/book_store/spec/services/inventory_service_spec.rb +28 -0
  138. data/samples/book_store/spec/services/time_dependent_service_spec.rb +26 -0
  139. data/samples/book_store/spec/spec_helper.rb +16 -0
  140. data/samples/book_store/spec/system/book_store_system_spec.rb +18 -0
  141. data/samples/book_store/storage/.keep +0 -0
  142. data/samples/book_store/test/controllers/.keep +0 -0
  143. data/samples/book_store/test/controllers/books_controller_test.rb +7 -0
  144. data/samples/book_store/test/controllers/comments_controller_test.rb +7 -0
  145. data/samples/book_store/test/controllers/users_controller_test.rb +7 -0
  146. data/samples/book_store/test/fixtures/books.yml +11 -0
  147. data/samples/book_store/test/fixtures/comments.yml +11 -0
  148. data/samples/book_store/test/fixtures/files/.keep +0 -0
  149. data/samples/book_store/test/fixtures/users.yml +9 -0
  150. data/samples/book_store/test/helpers/.keep +0 -0
  151. data/samples/book_store/test/integration/.keep +0 -0
  152. data/samples/book_store/test/mailers/.keep +0 -0
  153. data/samples/book_store/test/models/.keep +0 -0
  154. data/samples/book_store/test/models/book_test.rb +7 -0
  155. data/samples/book_store/test/models/comment_test.rb +7 -0
  156. data/samples/book_store/test/models/user_test.rb +7 -0
  157. data/samples/book_store/test/test_helper.rb +15 -0
  158. data/samples/book_store/tmp/.keep +0 -0
  159. data/samples/book_store/tmp/pids/.keep +0 -0
  160. data/samples/book_store/tmp/storage/.keep +0 -0
  161. data/samples/book_store/vendor/.keep +0 -0
  162. data/samples/book_store/vendor/javascript/.keep +0 -0
  163. data/test/crspec/configuration_test.rb +76 -0
  164. metadata +152 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 649f2c29f7f22f52d1291691f9c96c2c9718ab907c38e8ea27604fab5ec9ef8f
4
- data.tar.gz: cfaac3973542770dc81f08c921bb29b054ed64f3dae0913ca75b175d42438242
3
+ metadata.gz: 7abc092d66324287e2b4c429b9961025c131dfb3f02dfd40e2d61d8342a9db45
4
+ data.tar.gz: c5a3573fb6acddcea79ee61b34f861b0b6f781ef3d810d7e384faedb4f0bc650
5
5
  SHA512:
6
- metadata.gz: 85dc17da65b7d4bb59766cb7a00220f9009fec3b4e869969d3dde17fbafe7c8f63b190b083d2fbd243f3e6b3e02470f0e3076a9142b38b5d5e65ddb7ad595894
7
- data.tar.gz: '0257282967b6ed5ca718bd01cf7329a920f8fca18266d44a59e196453bbd8db2d0bdaf52e646b9553cf86eed375c2f00c7e33139435d36876ae43bb4ddbd689a'
6
+ metadata.gz: c57dca0e4f4c8108e81d66ae6e36c12a313d1a89c8b0f19e0a67486d9d27c7db8de96abab20d177b2f9c5ae74d854da6e4b638dc918d4e113618bdecc7a6e0a0
7
+ data.tar.gz: 92aa3ae4a9b7310059ba7fce8a6e17fe1807bf6ca6c21e0a348cf9744074d8ca755a3bffafa37a1ac7003b2d6491d91bbc94da5cb15a8493fc81e4dded0652cb
data/README.md CHANGED
@@ -8,9 +8,10 @@
8
8
 
9
9
  - **Fiber Storage Context Isolation**: Solves legacy thread-local (`Thread.current`) state leakage by routing all execution metadata and `let` memoization through inherited Fiber storage (`Fiber[:crspec_execution_context]`).
10
10
  - **Fiber-Aware Mocking (`crspec-mock`)**: Prevents mock registry corruption across parallel tests using prepended method interceptors bound to `Fiber[:crspec_mock_space]`.
11
- - **Rails Connection Leasing & Parallel Testing (`crspec-rails`)**: Leases database connections per example fiber (`ActiveRecord::Base.lease_connection`), handles savepoint transaction strategies (`SAVEPOINT ex_root`), and integrates with Rails parallel worker suites (`Crspec::Rails::Parallel`).
11
+ - **Rails Integration & ActiveSupport Reuse (`crspec-rails`)**: Direct integration with `ActiveSupport::Testing::Assertions`, `ActiveSupport::Testing::TimeHelpers`, and `ActiveSupport::Testing::FileFixtures`. Leases database connections per example fiber (`ActiveRecord::Base.lease_connection`), handles savepoint transaction strategies (`SAVEPOINT ex_root`), and integrates with Rails parallel worker suites (`Crspec::Rails::Parallel`).
12
+ - **Rails Request Specs**: Expressive HTTP request verb helpers (`get`, `post`, `put`, `patch`, `delete`), response status inspection (`response.status`), headers, and `json_response` parsing.
12
13
  - **Prism-Based AST Transpiler (`crspec-transpiler`)**: Automated migration tool using `ruby/prism` C-parser to convert existing RSpec codebases to Crspec syntax and flag thread-unsafe constructs like `before(:all)`.
13
- - **Concurrent Execution Kernel**: Multi-threaded worker pool integrated with non-blocking fiber schedulers (`Async::Scheduler`).
14
+ - **Concurrent Execution Kernel**: Multi-threaded worker pool integrated with real-time progress dot formatting and non-blocking fiber schedulers (`Async::Scheduler`).
14
15
 
15
16
  ---
16
17
 
@@ -30,12 +31,30 @@ mise exec -- bundle install
30
31
 
31
32
  ---
32
33
 
34
+ ## Spec Helpers & Generator
35
+
36
+ Initialize standard `spec_helper.rb` (and `rails_helper.rb` if run within a Rails project):
37
+
38
+ ```bash
39
+ mise exec -- crspec --init
40
+ ```
41
+
42
+ Spec files include helpers explicitly via standard `require` statements:
43
+
44
+ ```ruby
45
+ require "spec_helper"
46
+ # or
47
+ require "rails_helper"
48
+ ```
49
+
50
+ ---
51
+
33
52
  ## Writing Specs
34
53
 
35
54
  `crspec` provides a familiar RSpec-style DSL for structuring example groups and expectations:
36
55
 
37
56
  ```ruby
38
- require "crspec"
57
+ require "spec_helper"
39
58
 
40
59
  Crspec.describe User, type: :model do
41
60
  let(:valid_attributes) { { name: "Jane Doe", email: "jane@example.com" } }
@@ -45,6 +64,12 @@ Crspec.describe User, type: :model do
45
64
  expect(user.valid?).to be(true)
46
65
  end
47
66
 
67
+ it "changes user count when created" do
68
+ expect {
69
+ User.create!(valid_attributes)
70
+ }.to change(User, :count).by(1)
71
+ end
72
+
48
73
  context "when email is omitted" do
49
74
  let(:valid_attributes) { { name: "Jane Doe", email: nil } }
50
75
 
@@ -62,6 +87,7 @@ end
62
87
  - `include(*items)`
63
88
  - `raise_error(ExceptionClass, "message")`
64
89
  - `respond_to(*methods)`
90
+ - `change(receiver, :method).by(amount)` / `change { ... }.by(amount)` / `.from(val).to(val)`
65
91
 
66
92
  ### Advanced: Execution Context & Extensions
67
93
  Regular spec authors do not need to interact with `execution_context` at all—`crspec` automatically isolates `let` memoization, database connections, and stubs invisibly per test.
@@ -90,7 +116,7 @@ Crspec.describe PaymentGateway do
90
116
 
91
117
  it "stubs credit card processing concurrently" do
92
118
  allow(stripe_client).to receive(:create).with(amount: 5000).and_return(status: "succeeded")
93
-
119
+
94
120
  result = stripe_client.create(amount: 5000)
95
121
  expect(result[:status]).to eq("succeeded")
96
122
  end
@@ -99,15 +125,37 @@ end
99
125
 
100
126
  ---
101
127
 
102
- ## Rails Concurrency & Parallel Testing (`crspec-rails`)
128
+ ## Rails Concurrency & Request Specs (`crspec-rails`)
103
129
 
104
130
  ### Database Isolation & Connection Leasing
105
- Wrap examples in connection leasing and transaction savepoints:
131
+ `rails_helper.rb` automatically configures database-independent transaction isolation using your application's `config/database.yml`:
106
132
 
107
133
  ```ruby
108
- Crspec.describe User, type: :model do
109
- around do |example|
110
- Crspec::Rails::DatabaseIsolation.wrap_example(example)
134
+ Crspec.configure do |config|
135
+ config.use_transactional_fixtures = true
136
+ config.infer_spec_type_from_file_location!
137
+ config.include(Crspec::Rails::RequestHelpers)
138
+
139
+ config.around(:each) do |example|
140
+ if defined?(ActiveRecord::Base) && config.use_transactional_fixtures
141
+ Crspec::Rails::DatabaseIsolation.wrap_example(example)
142
+ else
143
+ example.execute!
144
+ end
145
+ end
146
+ end
147
+ ```
148
+
149
+ ### Request Specs
150
+ Perform full REST API controller testing with built-in request helpers:
151
+
152
+ ```ruby
153
+ Crspec.describe "Books API", type: :request do
154
+ it "creates a book via POST" do
155
+ post "/books", params: { name: "Refactoring", author: "Martin Fowler" }
156
+
157
+ expect(response.status).to eq(201)
158
+ expect(json_response[:name]).to eq("Refactoring")
111
159
  end
112
160
  end
113
161
  ```
@@ -118,7 +166,6 @@ Configure worker counts and setup/teardown hooks:
118
166
  ```ruby
119
167
  Crspec::Rails::Parallel.parallelize(workers: 4) do
120
168
  parallelize_setup do |worker_number|
121
- # Prepare worker database, seed data, etc.
122
169
  puts "Worker #{worker_number} starting (TEST_ENV_NUMBER=#{ENV['TEST_ENV_NUMBER']})"
123
170
  end
124
171
 
@@ -128,13 +175,6 @@ Crspec::Rails::Parallel.parallelize(workers: 4) do
128
175
  end
129
176
  ```
130
177
 
131
- ### Multi-Fiber Integration Server
132
- Boot an embedded multi-threaded/multi-fiber Puma server for system/browser specs:
133
-
134
- ```ruby
135
- Crspec::Rails::SystemServer.start_concurrent_server!(Rails.application, 9887)
136
- ```
137
-
138
178
  ---
139
179
 
140
180
  ## Migrating from RSpec (`crspec-transpile`)
@@ -151,6 +191,16 @@ mise exec -- crspec-transpile --write spec/
151
191
 
152
192
  ---
153
193
 
194
+ ## Sample Rails Bookstore App
195
+
196
+ Explore the working sample Rails application in [`samples/book_store`](file:///Users/tachyons/code/crspec/samples/book_store):
197
+
198
+ ```bash
199
+ mise exec -- ./exe/crspec samples/book_store/spec/
200
+ ```
201
+
202
+ ---
203
+
154
204
  ## Running Specs
155
205
 
156
206
  Execute your test suite concurrently using the `crspec` executable:
@@ -0,0 +1,56 @@
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 ADDED
@@ -0,0 +1,72 @@
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
@@ -0,0 +1,15 @@
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
@@ -0,0 +1,16 @@
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
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+
5
+ class UserService
6
+ def self.process_user(id, name, email)
7
+ # Simulate non-blocking I/O query (e.g. database lease / HTTP call)
8
+ sleep 0.001
9
+ token = Digest::SHA256.hexdigest("#{id}-#{name}-#{email}")
10
+ { id: id, name: name, email: email, token: token, valid: true }
11
+ end
12
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest"
4
+ require_relative "../target_service"
5
+
6
+ Minitest.seed ||= srand
7
+
8
+ class UserServiceTest < Minitest::Test
9
+ parallelize_me! if respond_to?(:parallelize_me!)
10
+
11
+ 100.times do |i|
12
+ define_method(:"test_user_#{i}") do
13
+ u = UserService.process_user(i, "User #{i}", "user#{i}@example.com")
14
+ assert u[:valid]
15
+ refute_nil u[:token]
16
+ end
17
+ end
18
+ end
data/demo.rb CHANGED
@@ -6,9 +6,12 @@ gemfile do
6
6
  source "https://rubygems.org"
7
7
  gem "crspec", path: __dir__
8
8
  gem "prism"
9
+ gem "rails", "~> 8.0"
10
+ gem "sqlite3"
9
11
  end
10
12
 
11
13
  require "crspec"
14
+ require "rails"
12
15
 
13
16
  puts "================================================================="
14
17
  puts " Crspec Single-File Inline Demo & Feature Verification "
@@ -86,11 +89,11 @@ end
86
89
 
87
90
  # 3. Rails Parallel Worker Integration (crspec-rails)
88
91
  Crspec::Rails::Parallel.parallelize(workers: 4) do
89
- parallelize_setup do |worker_num|
90
- # Per-worker setup hook (e.g. database setup for worker_num)
92
+ parallelize_setup do |_worker_num|
93
+ # Per-worker setup hook
91
94
  end
92
95
 
93
- parallelize_teardown do |worker_num|
96
+ parallelize_teardown do |_worker_num|
94
97
  # Per-worker teardown hook
95
98
  end
96
99
  end
data/lib/crspec/cli.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "optparse"
4
4
  require_relative "../crspec"
5
+ require_relative "generators/init"
5
6
 
6
7
  module Crspec
7
8
  class CLI
@@ -11,14 +12,30 @@ module Crspec
11
12
 
12
13
  def initialize(args)
13
14
  @args = args
14
- @concurrency = Etc.nprocessors
15
+ @concurrency = Crspec.configuration.concurrency
15
16
  @paths = []
17
+ @requires = []
18
+ @init_mode = false
16
19
  end
17
20
 
18
21
  def run
19
22
  parse_options
23
+
24
+ if @init_mode
25
+ created = Generators::Init.generate
26
+ if created.empty?
27
+ puts "No helper files were created (already exists or not a Rails project)."
28
+ else
29
+ created.each { |f| puts " create #{f}" }
30
+ end
31
+ return true
32
+ end
33
+
34
+ load_requires
20
35
  load_specs
21
- runner = Runner.new(concurrency: @concurrency)
36
+
37
+ concurrency = @concurrency || Crspec.configuration.concurrency
38
+ runner = Runner.new(concurrency: concurrency)
22
39
  groups = Crspec.world.example_groups
23
40
 
24
41
  if groups.empty?
@@ -26,7 +43,7 @@ module Crspec
26
43
  return true
27
44
  end
28
45
 
29
- puts "Running Crspec suite with concurrency #{@concurrency}..."
46
+ puts "Running Crspec suite with concurrency #{concurrency}..."
30
47
  runner.run(groups)
31
48
 
32
49
  puts "\nFinished in #{runner.total_duration.round(4)} seconds"
@@ -54,6 +71,14 @@ module Crspec
54
71
  @concurrency = n
55
72
  end
56
73
 
74
+ opts.on("-r", "--require PATH", String, "Require a file before running specs") do |path|
75
+ @requires << path
76
+ end
77
+
78
+ opts.on("--init", "Initialize spec_helper.rb (and rails_helper.rb if Rails project)") do
79
+ @init_mode = true
80
+ end
81
+
57
82
  opts.on("-h", "--help", "Show help") do
58
83
  puts opts
59
84
  exit 0
@@ -64,6 +89,12 @@ module Crspec
64
89
  @paths = ["spec"] if @paths.empty?
65
90
  end
66
91
 
92
+ def load_requires
93
+ @requires.each do |req|
94
+ require req
95
+ end
96
+ end
97
+
67
98
  def load_specs
68
99
  files = []
69
100
  @paths.each do |p|
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "etc"
4
+
5
+ module Crspec
6
+ class Configuration
7
+ attr_accessor :concurrency, :use_transactional_fixtures, :formatter
8
+ attr_reader :before_hooks, :after_hooks, :around_hooks, :included_modules
9
+
10
+ def initialize
11
+ @concurrency = Etc.nprocessors
12
+ @use_transactional_fixtures = true
13
+ @formatter = nil
14
+ @before_hooks = []
15
+ @after_hooks = []
16
+ @around_hooks = []
17
+ @included_modules = []
18
+ @infer_spec_type = false
19
+ end
20
+
21
+ def before(_scope = :each, &block)
22
+ @before_hooks << block
23
+ end
24
+
25
+ def after(_scope = :each, &block)
26
+ @after_hooks.unshift(block)
27
+ end
28
+
29
+ def around(_scope = :each, &block)
30
+ @around_hooks << block
31
+ end
32
+
33
+ def include(mod)
34
+ @included_modules << mod
35
+ end
36
+
37
+ def infer_spec_type_from_file_location!
38
+ @infer_spec_type = true
39
+ end
40
+
41
+ def infer_spec_type?
42
+ !!@infer_spec_type
43
+ end
44
+ end
45
+
46
+ class << self
47
+ def configuration
48
+ @configuration ||= Configuration.new
49
+ end
50
+
51
+ def configure
52
+ yield configuration if block_given?
53
+ end
54
+
55
+ def reset_configuration!
56
+ @configuration = Configuration.new
57
+ end
58
+ end
59
+ end
@@ -3,6 +3,7 @@
3
3
  require_relative "expectations"
4
4
  require_relative "example"
5
5
  require_relative "mock/double"
6
+ require_relative "configuration"
6
7
 
7
8
  module Crspec
8
9
  class ExampleGroup
@@ -21,6 +22,7 @@ module Crspec
21
22
  @after_hooks = []
22
23
  @let_blocks = {}
23
24
  @eager_lets = []
25
+ @included_modules = []
24
26
  end
25
27
 
26
28
  def self.define(description, metadata = {}, parent = nil, &block)
@@ -43,6 +45,10 @@ module Crspec
43
45
  end
44
46
  alias specify it
45
47
 
48
+ def include(mod)
49
+ @included_modules << mod
50
+ end
51
+
46
52
  def let(name, &block)
47
53
  @let_blocks[name.to_sym] = block
48
54
  end
@@ -71,6 +77,8 @@ module Crspec
71
77
 
72
78
  def ancestor_hooks(type)
73
79
  hooks = []
80
+ hooks.concat(Crspec.configuration.before_hooks) if type == :before
81
+
74
82
  curr = self
75
83
  ancestors = []
76
84
  while curr
@@ -82,6 +90,9 @@ module Crspec
82
90
  list = type == :before ? grp.before_hooks : grp.after_hooks
83
91
  hooks.concat(list)
84
92
  end
93
+
94
+ hooks.concat(Crspec.configuration.after_hooks) if type == :after
95
+
85
96
  hooks
86
97
  end
87
98
 
@@ -109,11 +120,31 @@ module Crspec
109
120
  eager
110
121
  end
111
122
 
123
+ def ancestor_included_modules
124
+ mods = []
125
+ curr = self
126
+ ancestors = []
127
+ while curr
128
+ ancestors.unshift(curr)
129
+ curr = curr.parent
130
+ end
131
+ ancestors.each do |grp|
132
+ mods.concat(grp.instance_variable_get(:@included_modules) || [])
133
+ end
134
+ mods
135
+ end
136
+
112
137
  def create_instance(example)
138
+ modules_to_include = (Crspec.configuration.included_modules + ancestor_included_modules).uniq
139
+
113
140
  klass = Class.new do
114
141
  include Expectations
115
142
  include Mock::DSL
116
143
 
144
+ modules_to_include.each do |mod|
145
+ include mod
146
+ end
147
+
117
148
  attr_reader :__crspec_example__
118
149
 
119
150
  def initialize(example)
@@ -37,7 +37,7 @@ module Crspec
37
37
  @monitor.synchronize do
38
38
  return @memoized_values[key] if @memoized_values.key?(key)
39
39
 
40
- @memoized_values[key] = block.call
40
+ @memoized_values[key] = yield
41
41
  end
42
42
  end
43
43