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 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
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2026-07-29
4
+
5
+ - Initial release
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
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "lib"
8
+ t.libs << "test"
9
+ t.pattern = "test/**/*_test.rb"
10
+ t.verbose = true
11
+ end
12
+
13
+ task default: :test
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
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
5
+ require "crspec"
6
+ require "crspec/cli"
7
+
8
+ success = Crspec::CLI.run(ARGV)
9
+ exit(success ? 0 : 1)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
5
+ require "crspec"
6
+ require "crspec/transpiler/cli"
7
+
8
+ Crspec::Transpiler::CLI.run(ARGV)
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(&current_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