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 +4 -4
- data/CHANGELOG.md +86 -0
- data/README.md +1 -1
- data/lib/crspec/cli.rb +86 -5
- data/lib/crspec/configuration.rb +62 -9
- data/lib/crspec/dsl.rb +66 -2
- data/lib/crspec/example.rb +60 -6
- data/lib/crspec/example_group.rb +363 -63
- data/lib/crspec/execution_context.rb +10 -6
- data/lib/crspec/expectations.rb +62 -2
- data/lib/crspec/file_fixtures.rb +18 -0
- data/lib/crspec/formatters/progress_formatter.rb +37 -14
- data/lib/crspec/matchers.rb +642 -0
- data/lib/crspec/mock/argument_matchers.rb +157 -0
- data/lib/crspec/mock/double.rb +246 -13
- data/lib/crspec/mock/interceptor.rb +32 -9
- data/lib/crspec/mock/space.rb +14 -0
- data/lib/crspec/process_runner.rb +332 -0
- data/lib/crspec/rails/assets_shim.rb +33 -0
- data/lib/crspec/rails/database_isolation.rb +179 -8
- data/lib/crspec/rails/parallel.rb +29 -29
- data/lib/crspec/rails/request_helpers.rb +70 -14
- data/lib/crspec/rails/system_server.rb +7 -0
- data/lib/crspec/rails/warden_shim.rb +18 -0
- data/lib/crspec/runner.rb +143 -29
- data/lib/crspec/shared_examples.rb +70 -0
- data/lib/crspec/status_persistence.rb +46 -0
- data/lib/crspec/transpiler/cli.rb +106 -15
- data/lib/crspec/transpiler/rewriter.rb +179 -25
- data/lib/crspec/version.rb +1 -1
- data/lib/crspec.rb +6 -0
- data/lib/rspec/core.rb +2 -0
- data/lib/rspec/expectations.rb +2 -0
- data/lib/rspec/mocks.rb +2 -0
- data/lib/rspec/rails.rb +2 -0
- data/lib/rspec.rb +2 -0
- metadata +27 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3fcb8bb24535f599678015c5468d0745300977ccc90fbd6a3231a17a10301247
|
|
4
|
+
data.tar.gz: 5def220500d988066bca912c12c39d730b8c114aab8c296cc46f9453a347845f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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)
|
data/lib/crspec/configuration.rb
CHANGED
|
@@ -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(
|
|
22
|
-
|
|
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(
|
|
26
|
-
|
|
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(
|
|
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(
|
|
27
|
-
|
|
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
|
data/lib/crspec/example.rb
CHANGED
|
@@ -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
|
-
|
|
26
|
-
instance
|
|
27
|
-
|
|
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
|