crspec 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +180 -0
- data/Rakefile +13 -0
- data/demo.rb +126 -0
- data/exe/crspec +9 -0
- data/exe/crspec-transpile +8 -0
- data/lib/crspec/cli.rb +82 -0
- data/lib/crspec/dsl.rb +39 -0
- data/lib/crspec/example.rb +53 -0
- data/lib/crspec/example_group.rb +154 -0
- data/lib/crspec/execution_context.rb +62 -0
- data/lib/crspec/expectations.rb +44 -0
- data/lib/crspec/formatters/progress_formatter.rb +50 -0
- data/lib/crspec/matchers.rb +203 -0
- data/lib/crspec/mock/double.rb +285 -0
- data/lib/crspec/mock/interceptor.rb +35 -0
- data/lib/crspec/mock/space.rb +81 -0
- data/lib/crspec/rails/database_isolation.rb +29 -0
- data/lib/crspec/rails/parallel.rb +98 -0
- data/lib/crspec/rails/system_server.rb +35 -0
- data/lib/crspec/runner.rb +97 -0
- data/lib/crspec/transpiler/cli.rb +108 -0
- data/lib/crspec/transpiler/rewriter.rb +70 -0
- data/lib/crspec/version.rb +5 -0
- data/lib/crspec.rb +23 -0
- data/samples/user_spec.rb +31 -0
- data/sig/crspec.rbs +4 -0
- data/test/crspec/execution_context_test.rb +61 -0
- data/test/crspec/mock_test.rb +55 -0
- data/test/crspec/rails_test.rb +62 -0
- data/test/crspec/runner_test.rb +68 -0
- data/test/crspec/transpiler_test.rb +41 -0
- data/test/test_helper.rb +6 -0
- metadata +96 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 649f2c29f7f22f52d1291691f9c96c2c9718ab907c38e8ea27604fab5ec9ef8f
|
|
4
|
+
data.tar.gz: cfaac3973542770dc81f08c921bb29b054ed64f3dae0913ca75b175d42438242
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 85dc17da65b7d4bb59766cb7a00220f9009fec3b4e869969d3dde17fbafe7c8f63b190b083d2fbd243f3e6b3e02470f0e3076a9142b38b5d5e65ddb7ad595894
|
|
7
|
+
data.tar.gz: '0257282967b6ed5ca718bd01cf7329a920f8fca18266d44a59e196453bbd8db2d0bdaf52e646b9553cf86eed375c2f00c7e33139435d36876ae43bb4ddbd689a'
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aboobacker MK
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# Crspec
|
|
2
|
+
|
|
3
|
+
**Crspec** is a next-generation concurrent, fiber-isolated Ruby testing framework ecosystem. By leveraging Ruby 3.2+ **Fiber Storage** (`Fiber[:key]`), fiber-aware method interceptors, non-blocking fiber schedulers, and `ruby/prism` AST transpilation, Crspec achieves ultra-fast concurrent test execution while maintaining full syntax compatibility with traditional RSpec suites.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Key Features
|
|
8
|
+
|
|
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
|
+
- **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`).
|
|
12
|
+
- **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
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
Add `crspec` to your application's `Gemfile`:
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
gem "crspec", github: "crspec/crspec"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Then install via Bundler (or `mise`):
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
mise exec -- bundle install
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Writing Specs
|
|
34
|
+
|
|
35
|
+
`crspec` provides a familiar RSpec-style DSL for structuring example groups and expectations:
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
require "crspec"
|
|
39
|
+
|
|
40
|
+
Crspec.describe User, type: :model do
|
|
41
|
+
let(:valid_attributes) { { name: "Jane Doe", email: "jane@example.com" } }
|
|
42
|
+
subject(:user) { User.new(valid_attributes) }
|
|
43
|
+
|
|
44
|
+
it "validates primary attributes" do
|
|
45
|
+
expect(user.valid?).to be(true)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
context "when email is omitted" do
|
|
49
|
+
let(:valid_attributes) { { name: "Jane Doe", email: nil } }
|
|
50
|
+
|
|
51
|
+
it "flags validation errors" do
|
|
52
|
+
expect(user.valid?).to be(false)
|
|
53
|
+
expect(user.errors[:email]).to include("can't be blank")
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Supported Matchers
|
|
60
|
+
- `eq(expected)`
|
|
61
|
+
- `be(expected)` / `be_nil` / `be_truthy` / `be_falsy`
|
|
62
|
+
- `include(*items)`
|
|
63
|
+
- `raise_error(ExceptionClass, "message")`
|
|
64
|
+
- `respond_to(*methods)`
|
|
65
|
+
|
|
66
|
+
### Advanced: Execution Context & Extensions
|
|
67
|
+
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.
|
|
68
|
+
|
|
69
|
+
For gem developers, middleware, or custom test extension authors needing request tracing across child fibers, `execution_context` (or `current_context`) provides an encapsulated, thread-safe store:
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
before do
|
|
73
|
+
execution_context[:trace_id] = "TX-12345"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "reads execution context metadata" do
|
|
77
|
+
expect(execution_context[:trace_id]).to eq("TX-12345")
|
|
78
|
+
end
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Fiber-Isolated Mocking (`crspec-mock`)
|
|
84
|
+
|
|
85
|
+
`crspec-mock` routes all doubles and stubs through `Fiber[:crspec_mock_space]`, allowing parallel tests to stub methods on shared target objects without cross-test pollution:
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
Crspec.describe PaymentGateway do
|
|
89
|
+
let(:stripe_client) { instance_double(Stripe::Charge) }
|
|
90
|
+
|
|
91
|
+
it "stubs credit card processing concurrently" do
|
|
92
|
+
allow(stripe_client).to receive(:create).with(amount: 5000).and_return(status: "succeeded")
|
|
93
|
+
|
|
94
|
+
result = stripe_client.create(amount: 5000)
|
|
95
|
+
expect(result[:status]).to eq("succeeded")
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Rails Concurrency & Parallel Testing (`crspec-rails`)
|
|
103
|
+
|
|
104
|
+
### Database Isolation & Connection Leasing
|
|
105
|
+
Wrap examples in connection leasing and transaction savepoints:
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
Crspec.describe User, type: :model do
|
|
109
|
+
around do |example|
|
|
110
|
+
Crspec::Rails::DatabaseIsolation.wrap_example(example)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Rails Parallel Testing Setup
|
|
116
|
+
Configure worker counts and setup/teardown hooks:
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
Crspec::Rails::Parallel.parallelize(workers: 4) do
|
|
120
|
+
parallelize_setup do |worker_number|
|
|
121
|
+
# Prepare worker database, seed data, etc.
|
|
122
|
+
puts "Worker #{worker_number} starting (TEST_ENV_NUMBER=#{ENV['TEST_ENV_NUMBER']})"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
parallelize_teardown do |worker_number|
|
|
126
|
+
puts "Worker #{worker_number} tearing down"
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
```
|
|
130
|
+
|
|
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
|
+
---
|
|
139
|
+
|
|
140
|
+
## Migrating from RSpec (`crspec-transpile`)
|
|
141
|
+
|
|
142
|
+
Use the built-in Prism-powered transpiler CLI to convert legacy RSpec suites to Crspec:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
# 1. Analyze your test directory for thread-unsafe patterns (e.g. before(:all))
|
|
146
|
+
mise exec -- crspec-transpile --analyze spec/
|
|
147
|
+
|
|
148
|
+
# 2. Automatically transpile RSpec code to Crspec syntax in-place
|
|
149
|
+
mise exec -- crspec-transpile --write spec/
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Running Specs
|
|
155
|
+
|
|
156
|
+
Execute your test suite concurrently using the `crspec` executable:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
# Run specs with default concurrency (number of CPU cores)
|
|
160
|
+
mise exec -- crspec spec/
|
|
161
|
+
|
|
162
|
+
# Run specs with specific concurrency
|
|
163
|
+
mise exec -- crspec -c 8 spec/models/
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Development & Testing
|
|
169
|
+
|
|
170
|
+
To run the internal framework unit tests (built with **Minitest**):
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
mise exec -- bundle exec rake test
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## License
|
|
179
|
+
|
|
180
|
+
The gem is available as open source under the terms of the [MIT License](LICENSE.txt).
|
data/Rakefile
ADDED
data/demo.rb
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/inline"
|
|
4
|
+
|
|
5
|
+
gemfile do
|
|
6
|
+
source "https://rubygems.org"
|
|
7
|
+
gem "crspec", path: __dir__
|
|
8
|
+
gem "prism"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
require "crspec"
|
|
12
|
+
|
|
13
|
+
puts "================================================================="
|
|
14
|
+
puts " Crspec Single-File Inline Demo & Feature Verification "
|
|
15
|
+
puts "================================================================="
|
|
16
|
+
|
|
17
|
+
# Reset global state for clean standalone run
|
|
18
|
+
Crspec.reset!
|
|
19
|
+
|
|
20
|
+
# Domain Models
|
|
21
|
+
class User
|
|
22
|
+
attr_accessor :name, :email
|
|
23
|
+
|
|
24
|
+
def initialize(name:, email:)
|
|
25
|
+
@name = name
|
|
26
|
+
@email = email
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def valid?
|
|
30
|
+
return false if name.nil? || email.nil?
|
|
31
|
+
|
|
32
|
+
email.include?("@")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class PaymentProcessor
|
|
37
|
+
def charge(amount)
|
|
38
|
+
"real_charge_#{amount}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# 1. Core DSL, Context Isolation, Lazy Let Memoization, Matchers
|
|
43
|
+
Crspec.describe User, type: :model do
|
|
44
|
+
let(:valid_attributes) { { name: "Jane Doe", email: "jane@example.com" } }
|
|
45
|
+
subject(:user) { User.new(**valid_attributes) }
|
|
46
|
+
|
|
47
|
+
before do
|
|
48
|
+
# Per-example setup hook
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it "validates primary attributes concurrently" do
|
|
52
|
+
expect(user.valid?).to be(true)
|
|
53
|
+
expect(user.name).to eq("Jane Doe")
|
|
54
|
+
expect(user.email).to include("example.com")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
context "when email is invalid" do
|
|
58
|
+
let(:valid_attributes) { { name: "Jane Doe", email: nil } }
|
|
59
|
+
|
|
60
|
+
it "flags validation errors" do
|
|
61
|
+
expect(user.valid?).to eq(false)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
context "when error is expected" do
|
|
66
|
+
it "catches raised exceptions" do
|
|
67
|
+
expect { raise ArgumentError, "Invalid user input" }.to raise_error(ArgumentError, "Invalid user input")
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# 2. Fiber-Aware Mocking Engine (crspec-mock)
|
|
73
|
+
Crspec.describe PaymentProcessor do
|
|
74
|
+
let(:processor) { PaymentProcessor.new }
|
|
75
|
+
|
|
76
|
+
it "supports fiber-isolated method stubs" do
|
|
77
|
+
allow(processor).to receive(:charge).with(5000).and_return("succeeded")
|
|
78
|
+
expect(processor.charge(5000)).to eq("succeeded")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it "supports test doubles and method expectations" do
|
|
82
|
+
gateway_double = double("StripeGateway", process: "ok")
|
|
83
|
+
expect(gateway_double.process).to eq("ok")
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# 3. Rails Parallel Worker Integration (crspec-rails)
|
|
88
|
+
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)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
parallelize_teardown do |worker_num|
|
|
94
|
+
# Per-worker teardown hook
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# 4. Prism AST Transpiler Verification (crspec-transpiler)
|
|
99
|
+
rspec_sample_code = <<~RUBY
|
|
100
|
+
RSpec.describe "Legacy Suite" do
|
|
101
|
+
it "runs" do
|
|
102
|
+
expect(1).to eq(1)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
RUBY
|
|
106
|
+
|
|
107
|
+
transpiled = Crspec::Transpiler::Rewriter.new(rspec_sample_code).transpile
|
|
108
|
+
puts "[Prism Transpiler Check] RSpec -> Crspec: #{transpiled.include?("Crspec.describe") ? "PASSED" : "FAILED"}"
|
|
109
|
+
|
|
110
|
+
# 5. Concurrent Multi-Threaded Execution Kernel
|
|
111
|
+
puts "\n[Executing Specs Concurrently Across 4 Worker Threads]..."
|
|
112
|
+
runner = Crspec::Runner.new(concurrency: 4)
|
|
113
|
+
runner.run(Crspec.world.example_groups)
|
|
114
|
+
|
|
115
|
+
puts "\n---------------- Execution Summary ----------------"
|
|
116
|
+
puts "Total Duration : #{runner.total_duration.round(4)}s"
|
|
117
|
+
puts "Passed Examples: #{runner.passed_examples.size}"
|
|
118
|
+
puts "Failed Examples: #{runner.failed_examples.size}"
|
|
119
|
+
puts "---------------------------------------------------"
|
|
120
|
+
|
|
121
|
+
if runner.success?
|
|
122
|
+
puts "\nSUCCESS: All specs executed concurrently across 4 worker threads and passed cleanly!"
|
|
123
|
+
else
|
|
124
|
+
puts "\nFAILURE: Some specs failed."
|
|
125
|
+
exit 1
|
|
126
|
+
end
|
data/exe/crspec
ADDED
data/lib/crspec/cli.rb
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
require_relative "../crspec"
|
|
5
|
+
|
|
6
|
+
module Crspec
|
|
7
|
+
class CLI
|
|
8
|
+
def self.run(args)
|
|
9
|
+
new(args).run
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(args)
|
|
13
|
+
@args = args
|
|
14
|
+
@concurrency = Etc.nprocessors
|
|
15
|
+
@paths = []
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def run
|
|
19
|
+
parse_options
|
|
20
|
+
load_specs
|
|
21
|
+
runner = Runner.new(concurrency: @concurrency)
|
|
22
|
+
groups = Crspec.world.example_groups
|
|
23
|
+
|
|
24
|
+
if groups.empty?
|
|
25
|
+
puts "No example groups found."
|
|
26
|
+
return true
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
puts "Running Crspec suite with concurrency #{@concurrency}..."
|
|
30
|
+
runner.run(groups)
|
|
31
|
+
|
|
32
|
+
puts "\nFinished in #{runner.total_duration.round(4)} seconds"
|
|
33
|
+
puts "#{runner.passed_examples.size + runner.failed_examples.size} examples, #{runner.failed_examples.size} failures"
|
|
34
|
+
|
|
35
|
+
unless runner.failed_examples.empty?
|
|
36
|
+
puts "\nFailures:"
|
|
37
|
+
runner.failed_examples.each_with_index do |ex, idx|
|
|
38
|
+
puts "#{idx + 1}) #{ex.description}"
|
|
39
|
+
puts " Failure/Error: #{ex.error.message}"
|
|
40
|
+
puts " #{ex.error.backtrace&.first}" if ex.error.backtrace
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
runner.success?
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def parse_options
|
|
50
|
+
parser = OptionParser.new do |opts|
|
|
51
|
+
opts.banner = "Usage: crspec [options] [files or directories]"
|
|
52
|
+
|
|
53
|
+
opts.on("-c", "--concurrency N", Integer, "Number of concurrent worker threads") do |n|
|
|
54
|
+
@concurrency = n
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
opts.on("-h", "--help", "Show help") do
|
|
58
|
+
puts opts
|
|
59
|
+
exit 0
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
@paths = parser.parse(@args)
|
|
64
|
+
@paths = ["spec"] if @paths.empty?
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def load_specs
|
|
68
|
+
files = []
|
|
69
|
+
@paths.each do |p|
|
|
70
|
+
if File.directory?(p)
|
|
71
|
+
files.concat(Dir.glob(File.join(p, "**", "*_spec.rb")))
|
|
72
|
+
elsif File.file?(p)
|
|
73
|
+
files << p
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
files.uniq.each do |file|
|
|
78
|
+
require File.expand_path(file)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
data/lib/crspec/dsl.rb
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "example_group"
|
|
4
|
+
|
|
5
|
+
module Crspec
|
|
6
|
+
class World
|
|
7
|
+
def self.instance
|
|
8
|
+
@instance ||= new
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.reset!
|
|
12
|
+
@instance = new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
attr_reader :example_groups
|
|
16
|
+
|
|
17
|
+
def initialize
|
|
18
|
+
@example_groups = []
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def register(group)
|
|
22
|
+
@example_groups << group
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.describe(description, metadata = {}, &block)
|
|
27
|
+
group = ExampleGroup.define(description, metadata, nil, &block)
|
|
28
|
+
World.instance.register(group)
|
|
29
|
+
group
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.world
|
|
33
|
+
World.instance
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.reset!
|
|
37
|
+
World.reset!
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module Crspec
|
|
6
|
+
class Example
|
|
7
|
+
attr_reader :id, :description, :metadata, :example_group, :block, :status, :error, :execution_time
|
|
8
|
+
|
|
9
|
+
def initialize(description, metadata, example_group, block)
|
|
10
|
+
@id = SecureRandom.uuid
|
|
11
|
+
@description = description
|
|
12
|
+
@metadata = metadata.freeze
|
|
13
|
+
@example_group = example_group
|
|
14
|
+
@block = block
|
|
15
|
+
@status = :pending
|
|
16
|
+
@error = nil
|
|
17
|
+
@execution_time = 0
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def execute!
|
|
21
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
22
|
+
instance = @example_group.create_instance(self)
|
|
23
|
+
|
|
24
|
+
begin
|
|
25
|
+
run_hooks(instance, :before)
|
|
26
|
+
instance.instance_exec(&@block) if @block
|
|
27
|
+
@status = :passed
|
|
28
|
+
rescue StandardError, ExpectationNotMetError => e
|
|
29
|
+
@status = :failed
|
|
30
|
+
@error = e
|
|
31
|
+
ensure
|
|
32
|
+
begin
|
|
33
|
+
run_hooks(instance, :after)
|
|
34
|
+
rescue StandardError => e
|
|
35
|
+
if @status == :passed
|
|
36
|
+
@status = :failed
|
|
37
|
+
@error = e
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
@execution_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def run_hooks(instance, type)
|
|
47
|
+
hooks = @example_group.ancestor_hooks(type)
|
|
48
|
+
hooks.each do |hook|
|
|
49
|
+
instance.instance_exec(&hook)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "expectations"
|
|
4
|
+
require_relative "example"
|
|
5
|
+
require_relative "mock/double"
|
|
6
|
+
|
|
7
|
+
module Crspec
|
|
8
|
+
class ExampleGroup
|
|
9
|
+
include Expectations
|
|
10
|
+
include Mock::DSL
|
|
11
|
+
|
|
12
|
+
attr_reader :description, :metadata, :parent, :examples, :children, :before_hooks, :after_hooks
|
|
13
|
+
|
|
14
|
+
def initialize(description, metadata = {}, parent = nil)
|
|
15
|
+
@description = description
|
|
16
|
+
@metadata = metadata.freeze
|
|
17
|
+
@parent = parent
|
|
18
|
+
@examples = []
|
|
19
|
+
@children = []
|
|
20
|
+
@before_hooks = []
|
|
21
|
+
@after_hooks = []
|
|
22
|
+
@let_blocks = {}
|
|
23
|
+
@eager_lets = []
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.define(description, metadata = {}, parent = nil, &block)
|
|
27
|
+
group = new(description, metadata, parent)
|
|
28
|
+
group.instance_eval(&block) if block
|
|
29
|
+
group
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def describe(description, metadata = {}, &block)
|
|
33
|
+
child = self.class.define(description, metadata, self, &block)
|
|
34
|
+
@children << child
|
|
35
|
+
child
|
|
36
|
+
end
|
|
37
|
+
alias context describe
|
|
38
|
+
|
|
39
|
+
def it(description = nil, metadata = {}, &block)
|
|
40
|
+
example = Example.new(description || "unnamed example", metadata, self, block)
|
|
41
|
+
@examples << example
|
|
42
|
+
example
|
|
43
|
+
end
|
|
44
|
+
alias specify it
|
|
45
|
+
|
|
46
|
+
def let(name, &block)
|
|
47
|
+
@let_blocks[name.to_sym] = block
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def let!(name, &block)
|
|
51
|
+
let(name, &block)
|
|
52
|
+
@eager_lets << name.to_sym
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def subject(name = nil, &block)
|
|
56
|
+
if name
|
|
57
|
+
let(name, &block)
|
|
58
|
+
let(:subject) { send(name) }
|
|
59
|
+
else
|
|
60
|
+
let(:subject, &block)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def before(_scope = :each, &block)
|
|
65
|
+
@before_hooks << block
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def after(_scope = :each, &block)
|
|
69
|
+
@after_hooks.unshift(block)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def ancestor_hooks(type)
|
|
73
|
+
hooks = []
|
|
74
|
+
curr = self
|
|
75
|
+
ancestors = []
|
|
76
|
+
while curr
|
|
77
|
+
ancestors.unshift(curr)
|
|
78
|
+
curr = curr.parent
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
ancestors.each do |grp|
|
|
82
|
+
list = type == :before ? grp.before_hooks : grp.after_hooks
|
|
83
|
+
hooks.concat(list)
|
|
84
|
+
end
|
|
85
|
+
hooks
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def ancestor_let_blocks
|
|
89
|
+
blocks = {}
|
|
90
|
+
curr = self
|
|
91
|
+
ancestors = []
|
|
92
|
+
while curr
|
|
93
|
+
ancestors.unshift(curr)
|
|
94
|
+
curr = curr.parent
|
|
95
|
+
end
|
|
96
|
+
ancestors.each do |grp|
|
|
97
|
+
blocks.merge!(grp.instance_variable_get(:@let_blocks))
|
|
98
|
+
end
|
|
99
|
+
blocks
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def ancestor_eager_lets
|
|
103
|
+
eager = []
|
|
104
|
+
curr = self
|
|
105
|
+
while curr
|
|
106
|
+
eager.concat(curr.instance_variable_get(:@eager_lets))
|
|
107
|
+
curr = curr.parent
|
|
108
|
+
end
|
|
109
|
+
eager
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def create_instance(example)
|
|
113
|
+
klass = Class.new do
|
|
114
|
+
include Expectations
|
|
115
|
+
include Mock::DSL
|
|
116
|
+
|
|
117
|
+
attr_reader :__crspec_example__
|
|
118
|
+
|
|
119
|
+
def initialize(example)
|
|
120
|
+
@__crspec_example__ = example
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def execution_context
|
|
124
|
+
ExecutionContext.current
|
|
125
|
+
end
|
|
126
|
+
alias current_context execution_context
|
|
127
|
+
|
|
128
|
+
def described_class
|
|
129
|
+
nil
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
lets = ancestor_let_blocks
|
|
134
|
+
lets.each do |let_name, let_proc|
|
|
135
|
+
current_proc = let_proc
|
|
136
|
+
klass.define_method(let_name) do
|
|
137
|
+
ExecutionContext.current.fetch_memoized(let_name) do
|
|
138
|
+
instance_exec(¤t_proc)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
inst = klass.new(example)
|
|
144
|
+
|
|
145
|
+
# Evaluate eager lets
|
|
146
|
+
eager_lets = ancestor_eager_lets
|
|
147
|
+
eager_lets.each do |name|
|
|
148
|
+
inst.send(name)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
inst
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|