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.
- checksums.yaml +4 -4
- data/README.md +67 -17
- data/benchmark/README.md +56 -0
- data/benchmark/run.rb +72 -0
- data/benchmark/spec/user_service_crspec_spec.rb +15 -0
- data/benchmark/spec/user_service_rspec_spec.rb +16 -0
- data/benchmark/target_service.rb +12 -0
- data/benchmark/test/user_service_test.rb +18 -0
- data/demo.rb +6 -3
- data/lib/crspec/cli.rb +34 -3
- data/lib/crspec/configuration.rb +59 -0
- data/lib/crspec/example_group.rb +31 -0
- data/lib/crspec/execution_context.rb +1 -1
- data/lib/crspec/generators/init.rb +94 -0
- data/lib/crspec/matchers.rb +55 -0
- data/lib/crspec/mock/double.rb +8 -1
- data/lib/crspec/rails/database_isolation.rb +6 -15
- data/lib/crspec/rails/parallel.rb +1 -1
- data/lib/crspec/rails/request_helpers.rb +96 -0
- data/lib/crspec/transpiler/cli.rb +6 -6
- data/lib/crspec/version.rb +1 -1
- data/lib/crspec.rb +3 -0
- data/samples/book_store/.dockerignore +51 -0
- data/samples/book_store/.gitattributes +9 -0
- data/samples/book_store/.github/dependabot.yml +12 -0
- data/samples/book_store/.github/workflows/ci.yml +130 -0
- data/samples/book_store/.gitignore +35 -0
- data/samples/book_store/.kamal/hooks/docker-setup.sample +3 -0
- data/samples/book_store/.kamal/hooks/post-app-boot.sample +3 -0
- data/samples/book_store/.kamal/hooks/post-deploy.sample +14 -0
- data/samples/book_store/.kamal/hooks/post-proxy-reboot.sample +3 -0
- data/samples/book_store/.kamal/hooks/pre-app-boot.sample +3 -0
- data/samples/book_store/.kamal/hooks/pre-build.sample +51 -0
- data/samples/book_store/.kamal/hooks/pre-connect.sample +47 -0
- data/samples/book_store/.kamal/hooks/pre-deploy.sample +122 -0
- data/samples/book_store/.kamal/hooks/pre-proxy-reboot.sample +3 -0
- data/samples/book_store/.kamal/secrets +20 -0
- data/samples/book_store/.rubocop.yml +8 -0
- data/samples/book_store/.ruby-version +1 -0
- data/samples/book_store/Dockerfile +77 -0
- data/samples/book_store/Gemfile +67 -0
- data/samples/book_store/Gemfile.lock +572 -0
- data/samples/book_store/README.md +24 -0
- data/samples/book_store/Rakefile +6 -0
- data/samples/book_store/app/assets/images/.keep +0 -0
- data/samples/book_store/app/assets/stylesheets/application.css +10 -0
- data/samples/book_store/app/controllers/application_controller.rb +2 -0
- data/samples/book_store/app/controllers/books_controller.rb +30 -0
- data/samples/book_store/app/controllers/comments_controller.rb +21 -0
- data/samples/book_store/app/controllers/concerns/.keep +0 -0
- data/samples/book_store/app/controllers/users_controller.rb +21 -0
- data/samples/book_store/app/helpers/application_helper.rb +2 -0
- data/samples/book_store/app/helpers/books_helper.rb +2 -0
- data/samples/book_store/app/helpers/comments_helper.rb +2 -0
- data/samples/book_store/app/helpers/users_helper.rb +2 -0
- data/samples/book_store/app/javascript/application.js +3 -0
- data/samples/book_store/app/javascript/controllers/application.js +9 -0
- data/samples/book_store/app/javascript/controllers/hello_controller.js +7 -0
- data/samples/book_store/app/javascript/controllers/index.js +4 -0
- data/samples/book_store/app/jobs/application_job.rb +7 -0
- data/samples/book_store/app/mailers/application_mailer.rb +4 -0
- data/samples/book_store/app/models/application_record.rb +3 -0
- data/samples/book_store/app/models/book.rb +10 -0
- data/samples/book_store/app/models/comment.rb +6 -0
- data/samples/book_store/app/models/concerns/.keep +0 -0
- data/samples/book_store/app/models/user.rb +4 -0
- data/samples/book_store/app/views/layouts/application.html.erb +29 -0
- data/samples/book_store/app/views/layouts/mailer.html.erb +13 -0
- data/samples/book_store/app/views/layouts/mailer.text.erb +1 -0
- data/samples/book_store/app/views/pwa/manifest.json.erb +22 -0
- data/samples/book_store/app/views/pwa/service-worker.js +26 -0
- data/samples/book_store/bin/brakeman +7 -0
- data/samples/book_store/bin/bundler-audit +6 -0
- data/samples/book_store/bin/ci +6 -0
- data/samples/book_store/bin/dev +2 -0
- data/samples/book_store/bin/docker-entrypoint +8 -0
- data/samples/book_store/bin/importmap +4 -0
- data/samples/book_store/bin/jobs +6 -0
- data/samples/book_store/bin/kamal +16 -0
- data/samples/book_store/bin/rails +4 -0
- data/samples/book_store/bin/rake +4 -0
- data/samples/book_store/bin/rubocop +8 -0
- data/samples/book_store/bin/setup +35 -0
- data/samples/book_store/bin/thrust +5 -0
- data/samples/book_store/config/application.rb +27 -0
- data/samples/book_store/config/boot.rb +4 -0
- data/samples/book_store/config/bundler-audit.yml +5 -0
- data/samples/book_store/config/cable.yml +17 -0
- data/samples/book_store/config/cache.yml +16 -0
- data/samples/book_store/config/ci.rb +24 -0
- data/samples/book_store/config/credentials.yml.enc +1 -0
- data/samples/book_store/config/database.yml +45 -0
- data/samples/book_store/config/deploy.yml +119 -0
- data/samples/book_store/config/environment.rb +5 -0
- data/samples/book_store/config/environments/development.rb +78 -0
- data/samples/book_store/config/environments/production.rb +90 -0
- data/samples/book_store/config/environments/test.rb +53 -0
- data/samples/book_store/config/importmap.rb +7 -0
- data/samples/book_store/config/initializers/assets.rb +7 -0
- data/samples/book_store/config/initializers/content_security_policy.rb +29 -0
- data/samples/book_store/config/initializers/filter_parameter_logging.rb +8 -0
- data/samples/book_store/config/initializers/inflections.rb +16 -0
- data/samples/book_store/config/locales/en.yml +31 -0
- data/samples/book_store/config/puma.rb +42 -0
- data/samples/book_store/config/queue.yml +18 -0
- data/samples/book_store/config/recurring.yml +15 -0
- data/samples/book_store/config/routes.rb +17 -0
- data/samples/book_store/config/storage.yml +27 -0
- data/samples/book_store/config.ru +6 -0
- data/samples/book_store/db/cable_schema.rb +11 -0
- data/samples/book_store/db/cache_schema.rb +12 -0
- data/samples/book_store/db/migrate/20260731035441_create_users.rb +10 -0
- data/samples/book_store/db/migrate/20260731035506_create_books.rb +11 -0
- data/samples/book_store/db/migrate/20260731035528_create_comments.rb +11 -0
- data/samples/book_store/db/queue_schema.rb +129 -0
- data/samples/book_store/db/schema.rb +39 -0
- data/samples/book_store/db/seeds.rb +9 -0
- data/samples/book_store/lib/tasks/.keep +0 -0
- data/samples/book_store/log/.keep +0 -0
- data/samples/book_store/public/400.html +135 -0
- data/samples/book_store/public/404.html +135 -0
- data/samples/book_store/public/406-unsupported-browser.html +135 -0
- data/samples/book_store/public/422.html +135 -0
- data/samples/book_store/public/500.html +135 -0
- data/samples/book_store/public/icon.png +0 -0
- data/samples/book_store/public/icon.svg +3 -0
- data/samples/book_store/public/robots.txt +1 -0
- data/samples/book_store/script/.keep +0 -0
- data/samples/book_store/spec/models/book_spec.rb +42 -0
- data/samples/book_store/spec/models/comment_spec.rb +22 -0
- data/samples/book_store/spec/models/user_advanced_spec.rb +27 -0
- data/samples/book_store/spec/models/user_spec.rb +30 -0
- data/samples/book_store/spec/rails_helper.rb +36 -0
- data/samples/book_store/spec/rails_parallel_spec.rb +23 -0
- data/samples/book_store/spec/requests/books_spec.rb +21 -0
- data/samples/book_store/spec/requests/users_spec.rb +14 -0
- data/samples/book_store/spec/services/inventory_service_spec.rb +28 -0
- data/samples/book_store/spec/services/time_dependent_service_spec.rb +26 -0
- data/samples/book_store/spec/spec_helper.rb +16 -0
- data/samples/book_store/spec/system/book_store_system_spec.rb +18 -0
- data/samples/book_store/storage/.keep +0 -0
- data/samples/book_store/test/controllers/.keep +0 -0
- data/samples/book_store/test/controllers/books_controller_test.rb +7 -0
- data/samples/book_store/test/controllers/comments_controller_test.rb +7 -0
- data/samples/book_store/test/controllers/users_controller_test.rb +7 -0
- data/samples/book_store/test/fixtures/books.yml +11 -0
- data/samples/book_store/test/fixtures/comments.yml +11 -0
- data/samples/book_store/test/fixtures/files/.keep +0 -0
- data/samples/book_store/test/fixtures/users.yml +9 -0
- data/samples/book_store/test/helpers/.keep +0 -0
- data/samples/book_store/test/integration/.keep +0 -0
- data/samples/book_store/test/mailers/.keep +0 -0
- data/samples/book_store/test/models/.keep +0 -0
- data/samples/book_store/test/models/book_test.rb +7 -0
- data/samples/book_store/test/models/comment_test.rb +7 -0
- data/samples/book_store/test/models/user_test.rb +7 -0
- data/samples/book_store/test/test_helper.rb +15 -0
- data/samples/book_store/tmp/.keep +0 -0
- data/samples/book_store/tmp/pids/.keep +0 -0
- data/samples/book_store/tmp/storage/.keep +0 -0
- data/samples/book_store/vendor/.keep +0 -0
- data/samples/book_store/vendor/javascript/.keep +0 -0
- data/test/crspec/configuration_test.rb +76 -0
- metadata +152 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7abc092d66324287e2b4c429b9961025c131dfb3f02dfd40e2d61d8342a9db45
|
|
4
|
+
data.tar.gz: c5a3573fb6acddcea79ee61b34f861b0b6f781ef3d810d7e384faedb4f0bc650
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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 "
|
|
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 &
|
|
128
|
+
## Rails Concurrency & Request Specs (`crspec-rails`)
|
|
103
129
|
|
|
104
130
|
### Database Isolation & Connection Leasing
|
|
105
|
-
|
|
131
|
+
`rails_helper.rb` automatically configures database-independent transaction isolation using your application's `config/database.yml`:
|
|
106
132
|
|
|
107
133
|
```ruby
|
|
108
|
-
Crspec.
|
|
109
|
-
|
|
110
|
-
|
|
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:
|
data/benchmark/README.md
ADDED
|
@@ -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 |
|
|
90
|
-
# Per-worker setup hook
|
|
92
|
+
parallelize_setup do |_worker_num|
|
|
93
|
+
# Per-worker setup hook
|
|
91
94
|
end
|
|
92
95
|
|
|
93
|
-
parallelize_teardown do |
|
|
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 =
|
|
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
|
-
|
|
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 #{
|
|
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
|
data/lib/crspec/example_group.rb
CHANGED
|
@@ -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)
|