crspec 0.1.2 → 0.1.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e70183d3b574744a0de1061ba25f99d495bdbaa4d9940cb76f76d37124701328
4
- data.tar.gz: 02f551a7420e923ae7fd5f6357ee6c55256600adac922f6adeab7b3e811cae65
3
+ metadata.gz: c0d7564354ce4a30b067d3786a53d52e414b0d6710e4e50c798bbf47c87309db
4
+ data.tar.gz: 68168ea426d446bcddd2fd4e148efe891cf266854b3d0136a91636d553a3fde0
5
5
  SHA512:
6
- metadata.gz: b9abb32f9cbb3411ce1de2ab806395e5629c42802f83b1cc6f346f164d87c6c487ceea69a64c6650f2a2f2267436eedf06c30b64ba4313c4b757f9bb803bf901
7
- data.tar.gz: b29fba9882c81d8bae86250693e764ed49c17cba74d180e05536e522bc6bf5d9f0faf293340937b39d1a7bf88a4b65a9fe7e617d635749f144548ad7598fe49b
6
+ metadata.gz: b5dfb9faa9089603b0fbb618f24d535de6f618439a2f495579a23ba7eed182a7006072e153d88c95dbcd9a4102cdb09a19d909f5cd4314cc6fde7ed8b4ce0ca2
7
+ data.tar.gz: 70431def1aa0f7e85bc75c114408c3c73d4fbcebe0c3d795102b1007e0c565f8a1549517359d46ff95413a7a2bf3be74627bbd93793e14cf41f0cc933b804ed9
data/CHANGELOG.md CHANGED
@@ -1,5 +1,96 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ### Added (Phases 2–5)
4
+
5
+ - **Fiber tier** (`--fibers M`, `config.fibers`): each worker thread runs up
6
+ to M concurrent example-fibers on the Async reactor; IO-bound examples
7
+ overlap within a thread. `async` is a soft dependency (add it to your
8
+ Gemfile on CRuby); without it `--fibers` falls back to 1.
9
+ - **JRuby support**: full test suite passes on JRuby 10; threads scale
10
+ across all cores (no GVL), so `-c N` is the multi-core tier there.
11
+ `--processes N` raises with guidance (no fork); `--processes auto`
12
+ gracefully falls back to threads.
13
+ - **Process tier** (`--processes P`, `--processes auto` = physical cores):
14
+ forks after spec loading (`Process.warmup`), shards examples by persisted
15
+ timings (LPT bin-packing, round-robin on first run), streams marshalled
16
+ results over binary pipes, creates per-process `db_N` databases via
17
+ `ActiveRecord::TestDatabases`, offsets `SystemServer` ports by
18
+ `TEST_ENV_NUMBER`, and propagates fail-fast via SIGTERM.
19
+ - **Timing persistence** (`example_status_persistence_file_path`, CLI
20
+ default `tmp/crspec_status.json`): slowest-first scheduling,
21
+ `--only-failures`, and deterministic `--seed N` random ordering.
22
+ - **Real pending/skip**: `xit`, `skip`, `pending`, blockless `it`,
23
+ `skip:`/`pending:` metadata, and `xdescribe`/`xcontext` no longer execute;
24
+ reported as pending in the summary.
25
+ - **Verifying `instance_double`**: stubbing a method the doubled class does
26
+ not implement raises `Crspec::Mock::MockError`; string class names are
27
+ resolved.
28
+ - **Matchers**: `be_a`/`be_an`/`be_an_instance_of`/`be_falsey`, `match`,
29
+ `match_array`/`contain_exactly`, `have_attributes`, `all`, `satisfy`,
30
+ `be_within.of/.percent_of`, `start_with`/`end_with`,
31
+ `output.to_stdout/.to_stderr`, `yield_control`/`yield_with_args`/
32
+ `yield_successive_args`, compound `.and`/`.or`, custom-matcher DSL with
33
+ `failure_message`/`failure_message_when_negated`/`description`/`chain`.
34
+ - **Mocks**: `spy` + `have_received` (with count/argument constraints),
35
+ argument matchers (`anything`, `hash_including`, `hash_excluding`,
36
+ `array_including`, `an_instance_of`, `kind_of`, `duck_type`),
37
+ `and_call_original`, `receive_messages`.
38
+ - **Shared groups**: `shared_examples`/`shared_context`/`it_behaves_like`/
39
+ `include_context`/`include_examples` (block re-registration; parallel-safe).
40
+ - **Filtering**: `--tag TAG[:VALUE]`, line-number filters (`spec.rb:42`),
41
+ `fit`/`fdescribe`/`fcontext` focus, `aggregate_failures` backed by a
42
+ fiber-local accumulator (`MultipleExpectationsNotMetError`).
43
+ - **Transpiler**: auto-rewrites `before(:all)`→`before(:each)` (annotated),
44
+ `RSpec::Matchers.define`→`Crspec::Matchers.define`, focus aliases
45
+ normalized; static thread-safety lints (class variables, constant
46
+ mutation, `ENV[]=`, Timecop, `any_instance_of`) with severities;
47
+ `--report` migration report with per-file safety scores; `--diff` dry-run
48
+ and `.bak` backups; removed the dangerous `**/*.rb` fallback glob.
49
+
50
+ ### Changed (breaking, pre-release)
51
+
52
+ - `before`/`after` with `:all`/`:context`/`:suite` scope raise
53
+ `Crspec::MigrationError` at load time (also from `Crspec.configure`).
54
+ - `allow_any_instance_of`/`expect_any_instance_of` raise
55
+ `Crspec::MigrationError` at runtime.
56
+ - Mock interceptor methods are removed after each run
57
+ (`Mock::Interceptor.cleanup!`) instead of accumulating for the process
58
+ lifetime.
59
+
60
+ ### Changed (Phase 1 — concurrency kernel)
61
+
62
+ - **Removed `DatabaseIsolation::MUTEX`**, which serialized all transactional
63
+ examples. Each worker now leases its own connection per writing pool
64
+ (`lease_connection`, `pool.checkout` fallback) with a root non-joinable
65
+ transaction; every example runs in a nested transaction (savepoint) rolled
66
+ back afterwards. Fiber-Storage-keyed connection handoff makes fibers
67
+ spawned inside an example see that example's connection. SQLite pools fall
68
+ back to serialized per-example transactions (single-writer limitation).
69
+ **Breaking:** the `pin_connection!` path was dropped.
70
+ - Runner queue rewritten: `Queue#close` + blocking pop replaces the racy
71
+ `empty?`/`pop(true)`/`rescue` pattern; fail-fast drains the queue instead
72
+ of using an abort flag.
73
+ - Result recording decontended: per-worker result arrays merged at join,
74
+ formatter notifications outside any runner lock, buffered progress-dot
75
+ output with periodic flush.
76
+ - Per-group example classes are built once per group/spec-type and cached
77
+ (previously rebuilt for every example); all `ancestor_*` walks (hooks,
78
+ lets, metadata, modules) are memoized and frozen via `ExampleGroup#finalize!`;
79
+ `World` is frozen at the CLI boundary before the run starts.
80
+ - **Removed the `ENV["TEST_ENV_NUMBER"]` cross-thread race**: worker identity
81
+ now lives in Fiber Storage (`Crspec::Rails::Parallel.current_worker_number`);
82
+ ENV is never mutated from worker threads. Thread-scoped per-worker database
83
+ switching (`db_N`) was removed — it returns at the process tier
84
+ (`--processes`) where the convention is safe.
85
+
86
+ ### Fixed
87
+
88
+ - POST/PUT/PATCH request helpers now send `Content-Type: application/json`
89
+ for JSON bodies, so controllers parse params correctly.
90
+ - `Parallel.teardown_worker` uses `clear_active_connections!` instead of
91
+ `clear_all_connections!`, which destroyed other workers' leased
92
+ connections.
93
+
3
94
  ## [0.1.0] - 2026-07-29
4
95
 
5
96
  - Initial release
data/README.md CHANGED
@@ -166,7 +166,7 @@ Configure worker counts and setup/teardown hooks:
166
166
  ```ruby
167
167
  Crspec::Rails::Parallel.parallelize(workers: 4) do
168
168
  parallelize_setup do |worker_number|
169
- puts "Worker #{worker_number} starting (TEST_ENV_NUMBER=#{ENV['TEST_ENV_NUMBER']})"
169
+ puts "Worker #{worker_number} starting"
170
170
  end
171
171
 
172
172
  parallelize_teardown do |worker_number|
data/lib/crspec/cli.rb CHANGED
@@ -13,9 +13,16 @@ module Crspec
13
13
  def initialize(args)
14
14
  @args = args
15
15
  @concurrency = Crspec.configuration.concurrency
16
+ @fibers = nil
16
17
  @paths = []
17
18
  @requires = []
18
19
  @init_mode = false
20
+ @fail_fast = false
21
+ @seed = nil
22
+ @only_failures = false
23
+ @processes = 1
24
+ @tags = {}
25
+ @locations = []
19
26
  end
20
27
 
21
28
  def run
@@ -35,26 +42,66 @@ module Crspec
35
42
  load_specs
36
43
 
37
44
  concurrency = @concurrency || Crspec.configuration.concurrency
38
- runner = Runner.new(concurrency: concurrency)
45
+ fibers = @fibers || Crspec.configuration.fibers
46
+ persistence_path = Crspec.configuration.example_status_persistence_file_path ||
47
+ File.join("tmp", "crspec_status.json")
48
+ multi_process = @processes == :auto || (@processes.is_a?(Integer) && @processes > 1)
49
+ if multi_process && !ProcessRunner.fork_supported?
50
+ if @processes == :auto
51
+ # auto = "use the best multi-core strategy"; on engines without a
52
+ # GVL, threads already are that strategy.
53
+ puts "#{RUBY_ENGINE} has no fork; --processes auto falling back to #{concurrency} threads."
54
+ multi_process = false
55
+ else
56
+ warn "Error: --processes requires fork, which #{RUBY_ENGINE} does not support."
57
+ warn "On #{RUBY_ENGINE} threads use all cores; use -c/--concurrency instead."
58
+ return false
59
+ end
60
+ end
61
+ runner = if multi_process
62
+ ProcessRunner.new(processes: @processes, concurrency: concurrency,
63
+ fibers: fibers, fail_fast: @fail_fast, seed: @seed,
64
+ only_failures: @only_failures,
65
+ persistence_path: persistence_path)
66
+ else
67
+ Runner.new(concurrency: concurrency, fibers: fibers, fail_fast: @fail_fast,
68
+ seed: @seed, only_failures: @only_failures,
69
+ persistence_path: persistence_path,
70
+ tags: @tags.empty? ? nil : @tags,
71
+ locations: @locations.empty? ? nil : @locations)
72
+ end
39
73
  groups = Crspec.world.example_groups
74
+ Crspec.world.freeze!
40
75
 
41
76
  if groups.empty?
42
77
  puts "No example groups found."
43
78
  return true
44
79
  end
45
80
 
46
- puts "Running Crspec suite with concurrency #{concurrency}..."
81
+ banner = "Running Crspec suite with concurrency #{concurrency}"
82
+ banner += ", #{@processes == :auto ? 'auto' : @processes} processes" if multi_process
83
+ banner += ", #{fibers} fibers/worker" if fibers && fibers > 1
84
+ banner += ", seed #{@seed}" if @seed
85
+ puts "#{banner}..."
47
86
  runner.run(groups)
48
87
 
49
88
  puts "\nFinished in #{runner.total_duration.round(4)} seconds"
50
- puts "#{runner.passed_examples.size + runner.failed_examples.size} examples, #{runner.failed_examples.size} failures"
89
+ total = runner.passed_examples.size + runner.failed_examples.size + runner.pending_examples.size
90
+ summary = "#{total} examples, #{runner.failed_examples.size} failures"
91
+ summary += ", #{runner.pending_examples.size} pending" unless runner.pending_examples.empty?
92
+ puts summary
51
93
 
52
94
  unless runner.failed_examples.empty?
53
95
  puts "\nFailures:"
54
96
  runner.failed_examples.each_with_index do |ex, idx|
55
97
  puts "#{idx + 1}) #{ex.description}"
56
- puts " Failure/Error: #{ex.error.message}"
57
- puts " #{ex.error.backtrace&.first}" if ex.error.backtrace
98
+ if ex.respond_to?(:error) && ex.error
99
+ puts " Failure/Error: #{ex.error.message}"
100
+ puts " #{ex.error.backtrace&.first}" if ex.error.backtrace
101
+ elsif ex.respond_to?(:error_message)
102
+ puts " Failure/Error: #{ex.error_message}"
103
+ puts " #{ex.error_backtrace&.first}" if ex.error_backtrace
104
+ end
58
105
  end
59
106
  end
60
107
 
@@ -71,6 +118,10 @@ module Crspec
71
118
  @concurrency = n
72
119
  end
73
120
 
121
+ opts.on("--fibers M", Integer, "Concurrent example fibers per worker thread (default 1)") do |m|
122
+ @fibers = m
123
+ end
124
+
74
125
  opts.on("-r", "--require PATH", String, "Require a file before running specs") do |path|
75
126
  @requires << path
76
127
  end
@@ -79,6 +130,27 @@ module Crspec
79
130
  @init_mode = true
80
131
  end
81
132
 
133
+ opts.on("--fail-fast [N]", Integer, "Abort the run after N failures (default 1)") do |n|
134
+ @fail_fast = n || 1
135
+ end
136
+
137
+ opts.on("--seed N", Integer, "Randomize example order with the given seed") do |n|
138
+ @seed = n
139
+ end
140
+
141
+ opts.on("--only-failures", "Run only examples that failed in the previous run") do
142
+ @only_failures = true
143
+ end
144
+
145
+ opts.on("--processes P", String, "Fork P worker processes ('auto' = physical cores)") do |p|
146
+ @processes = p == "auto" ? :auto : Integer(p)
147
+ end
148
+
149
+ opts.on("--tag TAG", String, "Run only examples with the given tag (TAG or TAG:VALUE)") do |t|
150
+ key, value = t.split(":", 2)
151
+ @tags[key.to_sym] = value.nil? ? true : parse_tag_value(value)
152
+ end
153
+
82
154
  opts.on("-h", "--help", "Show help") do
83
155
  puts opts
84
156
  exit 0
@@ -87,6 +159,25 @@ module Crspec
87
159
 
88
160
  @paths = parser.parse(@args)
89
161
  @paths = ["spec"] if @paths.empty?
162
+
163
+ # Extract path:42 line-number filters.
164
+ @paths = @paths.map do |p|
165
+ if p =~ /\A(.+\.rb):(\d+)\z/
166
+ @locations << [Regexp.last_match(1), Regexp.last_match(2).to_i]
167
+ Regexp.last_match(1)
168
+ else
169
+ p
170
+ end
171
+ end
172
+ end
173
+
174
+ def parse_tag_value(value)
175
+ case value
176
+ when "true" then true
177
+ when "false" then false
178
+ when /\A\d+\z/ then value.to_i
179
+ else value.to_sym
180
+ end
90
181
  end
91
182
 
92
183
  def load_requires
@@ -96,6 +187,8 @@ module Crspec
96
187
  end
97
188
 
98
189
  def load_specs
190
+ spec_dir = File.expand_path("spec")
191
+ $LOAD_PATH.unshift(spec_dir) if File.directory?(spec_dir) && !$LOAD_PATH.include?(spec_dir)
99
192
  files = []
100
193
  @paths.each do |p|
101
194
  if File.directory?(p)
@@ -3,12 +3,16 @@
3
3
  require "etc"
4
4
 
5
5
  module Crspec
6
+ class MigrationError < StandardError; end
7
+
6
8
  class Configuration
7
- attr_accessor :concurrency, :use_transactional_fixtures, :formatter
9
+ attr_accessor :concurrency, :fibers, :use_transactional_fixtures, :formatter, :example_status_persistence_file_path, :fixture_paths
10
+ attr_writer :file_fixture_path
8
11
  attr_reader :before_hooks, :after_hooks, :around_hooks, :included_modules
9
12
 
10
13
  def initialize
11
14
  @concurrency = Etc.nprocessors
15
+ @fibers = 1
12
16
  @use_transactional_fixtures = true
13
17
  @formatter = nil
14
18
  @before_hooks = []
@@ -16,22 +20,30 @@ module Crspec
16
20
  @around_hooks = []
17
21
  @included_modules = []
18
22
  @infer_spec_type = false
23
+ @fixture_paths = []
24
+ @file_fixture_path = nil
25
+ end
26
+
27
+ def file_fixture_path
28
+ @file_fixture_path || File.join("spec", "fixtures", "files")
19
29
  end
20
30
 
21
- def before(_scope = :each, &block)
22
- @before_hooks << block
31
+ def before(*args, **kwargs, &block)
32
+ reject_serial_scope!(:before, args)
33
+ @before_hooks << { block: block, filters: args, kwfilters: kwargs }
23
34
  end
24
35
 
25
- def after(_scope = :each, &block)
26
- @after_hooks.unshift(block)
36
+ def after(*args, **kwargs, &block)
37
+ reject_serial_scope!(:after, args)
38
+ @after_hooks.unshift({ block: block, filters: args, kwfilters: kwargs })
27
39
  end
28
40
 
29
- def around(_scope = :each, &block)
30
- @around_hooks << block
41
+ def around(*args, **kwargs, &block)
42
+ @around_hooks << { block: block, filters: args, kwfilters: kwargs }
31
43
  end
32
44
 
33
- def include(mod)
34
- @included_modules << mod
45
+ def include(mod, *filters, **kwfilters)
46
+ @included_modules << { module: mod, filters: filters, kwfilters: kwfilters }
35
47
  end
36
48
 
37
49
  def infer_spec_type_from_file_location!
@@ -41,6 +53,47 @@ module Crspec
41
53
  def infer_spec_type?
42
54
  !!@infer_spec_type
43
55
  end
56
+
57
+ def infer_type_for_file_path(file_path)
58
+ return nil unless @infer_spec_type
59
+ case file_path.to_s
60
+ when %r{spec/controllers/} then :controller
61
+ when %r{spec/requests/}, %r{spec/api/} then :request
62
+ when %r{spec/models/} then :model
63
+ when %r{spec/helpers/} then :helper
64
+ when %r{spec/system/} then :system
65
+ when %r{spec/views/} then :view
66
+ when %r{spec/components/} then :component
67
+ when %r{spec/mailers/} then :mailer
68
+ when %r{spec/jobs/} then :job
69
+ when %r{spec/services/} then :service
70
+ end
71
+ end
72
+
73
+ def filter_rails_from_backtrace!
74
+ end
75
+
76
+ def define_derived_metadata(*args)
77
+ end
78
+
79
+ def expect_with(*args)
80
+ end
81
+
82
+ def mock_with(*args)
83
+ end
84
+
85
+ private
86
+
87
+ def reject_serial_scope!(hook_name, args)
88
+ scope = args.first
89
+ return unless scope.is_a?(Symbol) && %i[all context suite].include?(scope)
90
+
91
+ raise MigrationError, <<~MSG
92
+ config.#{hook_name}(:#{scope}) is not supported by Crspec: shared state
93
+ across examples is inherently serial and breaks the concurrent
94
+ execution model. Use config.#{hook_name}(:each) or let/let! instead.
95
+ MSG
96
+ end
44
97
  end
45
98
 
46
99
  class << self
data/lib/crspec/dsl.rb CHANGED
@@ -16,15 +16,41 @@ module Crspec
16
16
 
17
17
  def initialize
18
18
  @example_groups = []
19
+ @frozen = false
19
20
  end
20
21
 
21
22
  def register(group)
23
+ raise Error, "Cannot register example groups after the run has started" if @frozen
24
+
22
25
  @example_groups << group
23
26
  end
27
+
28
+ def freeze!
29
+ @frozen = true
30
+ @example_groups.freeze
31
+ self
32
+ end
33
+
34
+ def frozen?
35
+ @frozen
36
+ end
24
37
  end
25
38
 
26
- def self.describe(description, metadata = {}, &block)
27
- group = ExampleGroup.define(description, metadata, nil, &block)
39
+ def self.describe(*args, **kwargs, &block)
40
+ metadata = kwargs.dup
41
+ metadata.merge!(args.pop) if args.last.is_a?(Hash)
42
+ if metadata[:type].nil? && Crspec.configuration.infer_spec_type?
43
+ loc = caller_locations(1, 10).find { |l| l.path.include?("spec/") }
44
+ if loc
45
+ inferred = Crspec.configuration.infer_type_for_file_path(loc.path)
46
+ metadata[:type] = inferred if inferred
47
+ end
48
+ end
49
+ described = args.first.is_a?(Module) ? args.first : nil
50
+ description = described && args.size == 1 ? described : args.map(&:to_s).join(" ")
51
+ group = ExampleGroup.new(description, metadata, nil)
52
+ group.described_module = described
53
+ group.instance_eval(&block) if block
28
54
  World.instance.register(group)
29
55
  group
30
56
  end
@@ -36,4 +62,42 @@ module Crspec
36
62
  def self.reset!
37
63
  World.reset!
38
64
  end
65
+
66
+ def self.freeze_world!
67
+ World.instance.freeze!
68
+ end
69
+
70
+ def self.xdescribe(*args, **kwargs, &block)
71
+ describe(*args, **kwargs, &block).mark_pending!
72
+ end
73
+
74
+ def self.fdescribe(*args, **kwargs, &block)
75
+ describe(*args, **kwargs, &block).mark_focused!
76
+ end
77
+
78
+ module DSL
79
+ def shared_examples(name, &block)
80
+ Crspec.shared_examples(name, &block)
81
+ end
82
+ alias shared_examples_for shared_examples
83
+ alias shared_context shared_examples
84
+
85
+ def describe(*args, **kwargs, &block)
86
+ Crspec.describe(*args, **kwargs, &block)
87
+ end
88
+
89
+ def xdescribe(*args, **kwargs, &block)
90
+ Crspec.xdescribe(*args, **kwargs, &block)
91
+ end
92
+ alias xcontext xdescribe
93
+
94
+ def fdescribe(*args, **kwargs, &block)
95
+ Crspec.fdescribe(*args, **kwargs, &block)
96
+ end
97
+ alias fcontext fdescribe
98
+ end
39
99
  end
100
+
101
+ main = TOPLEVEL_BINDING.eval("self")
102
+ main.extend(Crspec::DSL)
103
+ include Crspec::DSL
@@ -4,27 +4,71 @@ require "securerandom"
4
4
 
5
5
  module Crspec
6
6
  class Example
7
- attr_reader :id, :description, :metadata, :example_group, :block, :status, :error, :execution_time
7
+ attr_reader :id, :description, :metadata, :example_group, :block, :status, :error, :execution_time, :file_path, :line_number
8
+ attr_accessor :focused
8
9
 
9
- def initialize(description, metadata, example_group, block)
10
+ def initialize(description, metadata, example_group, block, pending: false, location: nil)
10
11
  @id = SecureRandom.uuid
11
12
  @description = description
12
13
  @metadata = metadata.freeze
13
14
  @example_group = example_group
14
15
  @block = block
15
16
  @status = :pending
17
+ @pending = pending || block.nil? || metadata[:skip] || metadata[:pending] ? true : false
18
+ @focused = metadata[:focus] ? true : false
16
19
  @error = nil
17
20
  @execution_time = 0
21
+ location ||= block&.source_location
22
+ @file_path, @line_number = location if location
23
+ end
24
+
25
+ def focused?
26
+ @focused
27
+ end
28
+
29
+ def pending?
30
+ @pending
31
+ end
32
+
33
+ def persistence_key
34
+ @persistence_key ||= begin
35
+ parts = []
36
+ group = @example_group
37
+ while group
38
+ parts.unshift(group.description.to_s)
39
+ group = group.parent
40
+ end
41
+ parts << @description.to_s
42
+ parts.join(" > ")
43
+ end
44
+ end
45
+
46
+ class ExampleInvocation
47
+ def initialize(runner_proc)
48
+ @runner_proc = runner_proc
49
+ end
50
+
51
+ def execute!
52
+ @runner_proc.call
53
+ end
54
+ alias run execute!
55
+ alias call execute!
18
56
  end
19
57
 
20
58
  def execute!
59
+ return if @pending
60
+
21
61
  start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
22
62
  instance = @example_group.create_instance(self)
23
63
 
24
64
  begin
25
- run_hooks(instance, :before)
26
- instance.instance_exec(&@block) if @block
27
- @status = :passed
65
+ around_hooks = @example_group.ancestor_hooks(:around, self)
66
+ run_around_hooks(instance, around_hooks) do
67
+ @example_group.run_eager_lets(instance)
68
+ run_hooks(instance, :before)
69
+ instance.instance_exec(&@block) if @block
70
+ @status = :passed
71
+ end
28
72
  rescue StandardError, ExpectationNotMetError => e
29
73
  @status = :failed
30
74
  @error = e
@@ -43,8 +87,18 @@ module Crspec
43
87
 
44
88
  private
45
89
 
90
+ def run_around_hooks(instance, hooks, &final_block)
91
+ if hooks.empty?
92
+ final_block.call
93
+ else
94
+ head, *tail = hooks
95
+ inv = ExampleInvocation.new(-> { run_around_hooks(instance, tail, &final_block) })
96
+ instance.instance_exec(inv, &head)
97
+ end
98
+ end
99
+
46
100
  def run_hooks(instance, type)
47
- hooks = @example_group.ancestor_hooks(type)
101
+ hooks = @example_group.ancestor_hooks(type, self)
48
102
  hooks.each do |hook|
49
103
  instance.instance_exec(&hook)
50
104
  end