crspec 0.1.2 → 0.1.3

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: 3fcb8bb24535f599678015c5468d0745300977ccc90fbd6a3231a17a10301247
4
+ data.tar.gz: 5def220500d988066bca912c12c39d730b8c114aab8c296cc46f9453a347845f
5
5
  SHA512:
6
- metadata.gz: b9abb32f9cbb3411ce1de2ab806395e5629c42802f83b1cc6f346f164d87c6c487ceea69a64c6650f2a2f2267436eedf06c30b64ba4313c4b757f9bb803bf901
7
- data.tar.gz: b29fba9882c81d8bae86250693e764ed49c17cba74d180e05536e522bc6bf5d9f0faf293340937b39d1a7bf88a4b65a9fe7e617d635749f144548ad7598fe49b
6
+ metadata.gz: f2ff2613f438e6819e0a0045127ee79e9890e233f6cc4c77a0319d3076356aee27d6c603587ffd496125e0798caf0fafbd60d14b8eaedb362f5d2a95063a37f5
7
+ data.tar.gz: 332947c34cdf729079fab7b006c80ffc63d1130886832571582e82b5e1f06e1d5a19a47577f911a159f57eb88593c37b6557f13944320a09e5ef08020289afe4
data/CHANGELOG.md CHANGED
@@ -1,5 +1,91 @@
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 now a runtime dependency.
8
+ - **Process tier** (`--processes P`, `--processes auto` = physical cores):
9
+ forks after spec loading (`Process.warmup`), shards examples by persisted
10
+ timings (LPT bin-packing, round-robin on first run), streams marshalled
11
+ results over binary pipes, creates per-process `db_N` databases via
12
+ `ActiveRecord::TestDatabases`, offsets `SystemServer` ports by
13
+ `TEST_ENV_NUMBER`, and propagates fail-fast via SIGTERM.
14
+ - **Timing persistence** (`example_status_persistence_file_path`, CLI
15
+ default `tmp/crspec_status.json`): slowest-first scheduling,
16
+ `--only-failures`, and deterministic `--seed N` random ordering.
17
+ - **Real pending/skip**: `xit`, `skip`, `pending`, blockless `it`,
18
+ `skip:`/`pending:` metadata, and `xdescribe`/`xcontext` no longer execute;
19
+ reported as pending in the summary.
20
+ - **Verifying `instance_double`**: stubbing a method the doubled class does
21
+ not implement raises `Crspec::Mock::MockError`; string class names are
22
+ resolved.
23
+ - **Matchers**: `be_a`/`be_an`/`be_an_instance_of`/`be_falsey`, `match`,
24
+ `match_array`/`contain_exactly`, `have_attributes`, `all`, `satisfy`,
25
+ `be_within.of/.percent_of`, `start_with`/`end_with`,
26
+ `output.to_stdout/.to_stderr`, `yield_control`/`yield_with_args`/
27
+ `yield_successive_args`, compound `.and`/`.or`, custom-matcher DSL with
28
+ `failure_message`/`failure_message_when_negated`/`description`/`chain`.
29
+ - **Mocks**: `spy` + `have_received` (with count/argument constraints),
30
+ argument matchers (`anything`, `hash_including`, `hash_excluding`,
31
+ `array_including`, `an_instance_of`, `kind_of`, `duck_type`),
32
+ `and_call_original`, `receive_messages`.
33
+ - **Shared groups**: `shared_examples`/`shared_context`/`it_behaves_like`/
34
+ `include_context`/`include_examples` (block re-registration; parallel-safe).
35
+ - **Filtering**: `--tag TAG[:VALUE]`, line-number filters (`spec.rb:42`),
36
+ `fit`/`fdescribe`/`fcontext` focus, `aggregate_failures` backed by a
37
+ fiber-local accumulator (`MultipleExpectationsNotMetError`).
38
+ - **Transpiler**: auto-rewrites `before(:all)`→`before(:each)` (annotated),
39
+ `RSpec::Matchers.define`→`Crspec::Matchers.define`, focus aliases
40
+ normalized; static thread-safety lints (class variables, constant
41
+ mutation, `ENV[]=`, Timecop, `any_instance_of`) with severities;
42
+ `--report` migration report with per-file safety scores; `--diff` dry-run
43
+ and `.bak` backups; removed the dangerous `**/*.rb` fallback glob.
44
+
45
+ ### Changed (breaking, pre-release)
46
+
47
+ - `before`/`after` with `:all`/`:context`/`:suite` scope raise
48
+ `Crspec::MigrationError` at load time (also from `Crspec.configure`).
49
+ - `allow_any_instance_of`/`expect_any_instance_of` raise
50
+ `Crspec::MigrationError` at runtime.
51
+ - Mock interceptor methods are removed after each run
52
+ (`Mock::Interceptor.cleanup!`) instead of accumulating for the process
53
+ lifetime.
54
+
55
+ ### Changed (Phase 1 — concurrency kernel)
56
+
57
+ - **Removed `DatabaseIsolation::MUTEX`**, which serialized all transactional
58
+ examples. Each worker now leases its own connection per writing pool
59
+ (`lease_connection`, `pool.checkout` fallback) with a root non-joinable
60
+ transaction; every example runs in a nested transaction (savepoint) rolled
61
+ back afterwards. Fiber-Storage-keyed connection handoff makes fibers
62
+ spawned inside an example see that example's connection. SQLite pools fall
63
+ back to serialized per-example transactions (single-writer limitation).
64
+ **Breaking:** the `pin_connection!` path was dropped.
65
+ - Runner queue rewritten: `Queue#close` + blocking pop replaces the racy
66
+ `empty?`/`pop(true)`/`rescue` pattern; fail-fast drains the queue instead
67
+ of using an abort flag.
68
+ - Result recording decontended: per-worker result arrays merged at join,
69
+ formatter notifications outside any runner lock, buffered progress-dot
70
+ output with periodic flush.
71
+ - Per-group example classes are built once per group/spec-type and cached
72
+ (previously rebuilt for every example); all `ancestor_*` walks (hooks,
73
+ lets, metadata, modules) are memoized and frozen via `ExampleGroup#finalize!`;
74
+ `World` is frozen at the CLI boundary before the run starts.
75
+ - **Removed the `ENV["TEST_ENV_NUMBER"]` cross-thread race**: worker identity
76
+ now lives in Fiber Storage (`Crspec::Rails::Parallel.current_worker_number`);
77
+ ENV is never mutated from worker threads. Thread-scoped per-worker database
78
+ switching (`db_N`) was removed — it returns at the process tier
79
+ (`--processes`) where the convention is safe.
80
+
81
+ ### Fixed
82
+
83
+ - POST/PUT/PATCH request helpers now send `Content-Type: application/json`
84
+ for JSON bodies, so controllers parse params correctly.
85
+ - `Parallel.teardown_worker` uses `clear_active_connections!` instead of
86
+ `clear_all_connections!`, which destroyed other workers' leased
87
+ connections.
88
+
3
89
  ## [0.1.0] - 2026-07-29
4
90
 
5
91
  - 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,54 @@ 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
+ runner = if multi_process
50
+ ProcessRunner.new(processes: @processes, concurrency: concurrency,
51
+ fibers: fibers, fail_fast: @fail_fast, seed: @seed,
52
+ only_failures: @only_failures,
53
+ persistence_path: persistence_path)
54
+ else
55
+ Runner.new(concurrency: concurrency, fibers: fibers, fail_fast: @fail_fast,
56
+ seed: @seed, only_failures: @only_failures,
57
+ persistence_path: persistence_path,
58
+ tags: @tags.empty? ? nil : @tags,
59
+ locations: @locations.empty? ? nil : @locations)
60
+ end
39
61
  groups = Crspec.world.example_groups
62
+ Crspec.world.freeze!
40
63
 
41
64
  if groups.empty?
42
65
  puts "No example groups found."
43
66
  return true
44
67
  end
45
68
 
46
- puts "Running Crspec suite with concurrency #{concurrency}..."
69
+ banner = "Running Crspec suite with concurrency #{concurrency}"
70
+ banner += ", #{@processes == :auto ? 'auto' : @processes} processes" if multi_process
71
+ banner += ", #{fibers} fibers/worker" if fibers && fibers > 1
72
+ banner += ", seed #{@seed}" if @seed
73
+ puts "#{banner}..."
47
74
  runner.run(groups)
48
75
 
49
76
  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"
77
+ total = runner.passed_examples.size + runner.failed_examples.size + runner.pending_examples.size
78
+ summary = "#{total} examples, #{runner.failed_examples.size} failures"
79
+ summary += ", #{runner.pending_examples.size} pending" unless runner.pending_examples.empty?
80
+ puts summary
51
81
 
52
82
  unless runner.failed_examples.empty?
53
83
  puts "\nFailures:"
54
84
  runner.failed_examples.each_with_index do |ex, idx|
55
85
  puts "#{idx + 1}) #{ex.description}"
56
- puts " Failure/Error: #{ex.error.message}"
57
- puts " #{ex.error.backtrace&.first}" if ex.error.backtrace
86
+ if ex.respond_to?(:error) && ex.error
87
+ puts " Failure/Error: #{ex.error.message}"
88
+ puts " #{ex.error.backtrace&.first}" if ex.error.backtrace
89
+ elsif ex.respond_to?(:error_message)
90
+ puts " Failure/Error: #{ex.error_message}"
91
+ puts " #{ex.error_backtrace&.first}" if ex.error_backtrace
92
+ end
58
93
  end
59
94
  end
60
95
 
@@ -71,6 +106,10 @@ module Crspec
71
106
  @concurrency = n
72
107
  end
73
108
 
109
+ opts.on("--fibers M", Integer, "Concurrent example fibers per worker thread (default 1)") do |m|
110
+ @fibers = m
111
+ end
112
+
74
113
  opts.on("-r", "--require PATH", String, "Require a file before running specs") do |path|
75
114
  @requires << path
76
115
  end
@@ -79,6 +118,27 @@ module Crspec
79
118
  @init_mode = true
80
119
  end
81
120
 
121
+ opts.on("--fail-fast [N]", Integer, "Abort the run after N failures (default 1)") do |n|
122
+ @fail_fast = n || 1
123
+ end
124
+
125
+ opts.on("--seed N", Integer, "Randomize example order with the given seed") do |n|
126
+ @seed = n
127
+ end
128
+
129
+ opts.on("--only-failures", "Run only examples that failed in the previous run") do
130
+ @only_failures = true
131
+ end
132
+
133
+ opts.on("--processes P", String, "Fork P worker processes ('auto' = physical cores)") do |p|
134
+ @processes = p == "auto" ? :auto : Integer(p)
135
+ end
136
+
137
+ opts.on("--tag TAG", String, "Run only examples with the given tag (TAG or TAG:VALUE)") do |t|
138
+ key, value = t.split(":", 2)
139
+ @tags[key.to_sym] = value.nil? ? true : parse_tag_value(value)
140
+ end
141
+
82
142
  opts.on("-h", "--help", "Show help") do
83
143
  puts opts
84
144
  exit 0
@@ -87,6 +147,25 @@ module Crspec
87
147
 
88
148
  @paths = parser.parse(@args)
89
149
  @paths = ["spec"] if @paths.empty?
150
+
151
+ # Extract path:42 line-number filters.
152
+ @paths = @paths.map do |p|
153
+ if p =~ /\A(.+\.rb):(\d+)\z/
154
+ @locations << [Regexp.last_match(1), Regexp.last_match(2).to_i]
155
+ Regexp.last_match(1)
156
+ else
157
+ p
158
+ end
159
+ end
160
+ end
161
+
162
+ def parse_tag_value(value)
163
+ case value
164
+ when "true" then true
165
+ when "false" then false
166
+ when /\A\d+\z/ then value.to_i
167
+ else value.to_sym
168
+ end
90
169
  end
91
170
 
92
171
  def load_requires
@@ -96,6 +175,8 @@ module Crspec
96
175
  end
97
176
 
98
177
  def load_specs
178
+ spec_dir = File.expand_path("spec")
179
+ $LOAD_PATH.unshift(spec_dir) if File.directory?(spec_dir) && !$LOAD_PATH.include?(spec_dir)
99
180
  files = []
100
181
  @paths.each do |p|
101
182
  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