crspec 0.1.0 → 0.1.2
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/README.md +67 -17
- data/lib/crspec/cli.rb +34 -3
- data/lib/crspec/configuration.rb +59 -0
- data/lib/crspec/example_group.rb +31 -0
- data/lib/crspec/execution_context.rb +1 -1
- data/lib/crspec/generators/init.rb +94 -0
- data/lib/crspec/matchers.rb +55 -0
- data/lib/crspec/mock/double.rb +8 -1
- data/lib/crspec/rails/database_isolation.rb +6 -15
- data/lib/crspec/rails/parallel.rb +1 -1
- data/lib/crspec/rails/request_helpers.rb +96 -0
- data/lib/crspec/transpiler/cli.rb +6 -6
- data/lib/crspec/version.rb +1 -1
- data/lib/crspec.rb +3 -0
- metadata +5 -10
- data/demo.rb +0 -126
- data/samples/user_spec.rb +0 -31
- data/test/crspec/execution_context_test.rb +0 -61
- data/test/crspec/mock_test.rb +0 -55
- data/test/crspec/rails_test.rb +0 -62
- data/test/crspec/runner_test.rb +0 -68
- data/test/crspec/transpiler_test.rb +0 -41
- data/test/test_helper.rb +0 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e70183d3b574744a0de1061ba25f99d495bdbaa4d9940cb76f76d37124701328
|
|
4
|
+
data.tar.gz: 02f551a7420e923ae7fd5f6357ee6c55256600adac922f6adeab7b3e811cae65
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b9abb32f9cbb3411ce1de2ab806395e5629c42802f83b1cc6f346f164d87c6c487ceea69a64c6650f2a2f2267436eedf06c30b64ba4313c4b757f9bb803bf901
|
|
7
|
+
data.tar.gz: b29fba9882c81d8bae86250693e764ed49c17cba74d180e05536e522bc6bf5d9f0faf293340937b39d1a7bf88a4b65a9fe7e617d635749f144548ad7598fe49b
|
data/README.md
CHANGED
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
|
|
9
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
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
|
|
11
|
+
- **Rails Integration & ActiveSupport Reuse (`crspec-rails`)**: Direct integration with `ActiveSupport::Testing::Assertions`, `ActiveSupport::Testing::TimeHelpers`, and `ActiveSupport::Testing::FileFixtures`. 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
|
+
- **Rails Request Specs**: Expressive HTTP request verb helpers (`get`, `post`, `put`, `patch`, `delete`), response status inspection (`response.status`), headers, and `json_response` parsing.
|
|
12
13
|
- **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
|
+
- **Concurrent Execution Kernel**: Multi-threaded worker pool integrated with real-time progress dot formatting and non-blocking fiber schedulers (`Async::Scheduler`).
|
|
14
15
|
|
|
15
16
|
---
|
|
16
17
|
|
|
@@ -30,12 +31,30 @@ mise exec -- bundle install
|
|
|
30
31
|
|
|
31
32
|
---
|
|
32
33
|
|
|
34
|
+
## Spec Helpers & Generator
|
|
35
|
+
|
|
36
|
+
Initialize standard `spec_helper.rb` (and `rails_helper.rb` if run within a Rails project):
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
mise exec -- crspec --init
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Spec files include helpers explicitly via standard `require` statements:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
require "spec_helper"
|
|
46
|
+
# or
|
|
47
|
+
require "rails_helper"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
33
52
|
## Writing Specs
|
|
34
53
|
|
|
35
54
|
`crspec` provides a familiar RSpec-style DSL for structuring example groups and expectations:
|
|
36
55
|
|
|
37
56
|
```ruby
|
|
38
|
-
require "
|
|
57
|
+
require "spec_helper"
|
|
39
58
|
|
|
40
59
|
Crspec.describe User, type: :model do
|
|
41
60
|
let(:valid_attributes) { { name: "Jane Doe", email: "jane@example.com" } }
|
|
@@ -45,6 +64,12 @@ Crspec.describe User, type: :model do
|
|
|
45
64
|
expect(user.valid?).to be(true)
|
|
46
65
|
end
|
|
47
66
|
|
|
67
|
+
it "changes user count when created" do
|
|
68
|
+
expect {
|
|
69
|
+
User.create!(valid_attributes)
|
|
70
|
+
}.to change(User, :count).by(1)
|
|
71
|
+
end
|
|
72
|
+
|
|
48
73
|
context "when email is omitted" do
|
|
49
74
|
let(:valid_attributes) { { name: "Jane Doe", email: nil } }
|
|
50
75
|
|
|
@@ -62,6 +87,7 @@ end
|
|
|
62
87
|
- `include(*items)`
|
|
63
88
|
- `raise_error(ExceptionClass, "message")`
|
|
64
89
|
- `respond_to(*methods)`
|
|
90
|
+
- `change(receiver, :method).by(amount)` / `change { ... }.by(amount)` / `.from(val).to(val)`
|
|
65
91
|
|
|
66
92
|
### Advanced: Execution Context & Extensions
|
|
67
93
|
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.
|
|
@@ -90,7 +116,7 @@ Crspec.describe PaymentGateway do
|
|
|
90
116
|
|
|
91
117
|
it "stubs credit card processing concurrently" do
|
|
92
118
|
allow(stripe_client).to receive(:create).with(amount: 5000).and_return(status: "succeeded")
|
|
93
|
-
|
|
119
|
+
|
|
94
120
|
result = stripe_client.create(amount: 5000)
|
|
95
121
|
expect(result[:status]).to eq("succeeded")
|
|
96
122
|
end
|
|
@@ -99,15 +125,37 @@ end
|
|
|
99
125
|
|
|
100
126
|
---
|
|
101
127
|
|
|
102
|
-
## Rails Concurrency &
|
|
128
|
+
## Rails Concurrency & Request Specs (`crspec-rails`)
|
|
103
129
|
|
|
104
130
|
### Database Isolation & Connection Leasing
|
|
105
|
-
|
|
131
|
+
`rails_helper.rb` automatically configures database-independent transaction isolation using your application's `config/database.yml`:
|
|
106
132
|
|
|
107
133
|
```ruby
|
|
108
|
-
Crspec.
|
|
109
|
-
|
|
110
|
-
|
|
134
|
+
Crspec.configure do |config|
|
|
135
|
+
config.use_transactional_fixtures = true
|
|
136
|
+
config.infer_spec_type_from_file_location!
|
|
137
|
+
config.include(Crspec::Rails::RequestHelpers)
|
|
138
|
+
|
|
139
|
+
config.around(:each) do |example|
|
|
140
|
+
if defined?(ActiveRecord::Base) && config.use_transactional_fixtures
|
|
141
|
+
Crspec::Rails::DatabaseIsolation.wrap_example(example)
|
|
142
|
+
else
|
|
143
|
+
example.execute!
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Request Specs
|
|
150
|
+
Perform full REST API controller testing with built-in request helpers:
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
Crspec.describe "Books API", type: :request do
|
|
154
|
+
it "creates a book via POST" do
|
|
155
|
+
post "/books", params: { name: "Refactoring", author: "Martin Fowler" }
|
|
156
|
+
|
|
157
|
+
expect(response.status).to eq(201)
|
|
158
|
+
expect(json_response[:name]).to eq("Refactoring")
|
|
111
159
|
end
|
|
112
160
|
end
|
|
113
161
|
```
|
|
@@ -118,7 +166,6 @@ Configure worker counts and setup/teardown hooks:
|
|
|
118
166
|
```ruby
|
|
119
167
|
Crspec::Rails::Parallel.parallelize(workers: 4) do
|
|
120
168
|
parallelize_setup do |worker_number|
|
|
121
|
-
# Prepare worker database, seed data, etc.
|
|
122
169
|
puts "Worker #{worker_number} starting (TEST_ENV_NUMBER=#{ENV['TEST_ENV_NUMBER']})"
|
|
123
170
|
end
|
|
124
171
|
|
|
@@ -128,13 +175,6 @@ Crspec::Rails::Parallel.parallelize(workers: 4) do
|
|
|
128
175
|
end
|
|
129
176
|
```
|
|
130
177
|
|
|
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
178
|
---
|
|
139
179
|
|
|
140
180
|
## Migrating from RSpec (`crspec-transpile`)
|
|
@@ -151,6 +191,16 @@ mise exec -- crspec-transpile --write spec/
|
|
|
151
191
|
|
|
152
192
|
---
|
|
153
193
|
|
|
194
|
+
## Sample Rails Bookstore App
|
|
195
|
+
|
|
196
|
+
Explore the working sample Rails application in [`samples/book_store`](file:///Users/tachyons/code/crspec/samples/book_store):
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
mise exec -- ./exe/crspec samples/book_store/spec/
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
154
204
|
## Running Specs
|
|
155
205
|
|
|
156
206
|
Execute your test suite concurrently using the `crspec` executable:
|
data/lib/crspec/cli.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "optparse"
|
|
4
4
|
require_relative "../crspec"
|
|
5
|
+
require_relative "generators/init"
|
|
5
6
|
|
|
6
7
|
module Crspec
|
|
7
8
|
class CLI
|
|
@@ -11,14 +12,30 @@ module Crspec
|
|
|
11
12
|
|
|
12
13
|
def initialize(args)
|
|
13
14
|
@args = args
|
|
14
|
-
@concurrency =
|
|
15
|
+
@concurrency = Crspec.configuration.concurrency
|
|
15
16
|
@paths = []
|
|
17
|
+
@requires = []
|
|
18
|
+
@init_mode = false
|
|
16
19
|
end
|
|
17
20
|
|
|
18
21
|
def run
|
|
19
22
|
parse_options
|
|
23
|
+
|
|
24
|
+
if @init_mode
|
|
25
|
+
created = Generators::Init.generate
|
|
26
|
+
if created.empty?
|
|
27
|
+
puts "No helper files were created (already exists or not a Rails project)."
|
|
28
|
+
else
|
|
29
|
+
created.each { |f| puts " create #{f}" }
|
|
30
|
+
end
|
|
31
|
+
return true
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
load_requires
|
|
20
35
|
load_specs
|
|
21
|
-
|
|
36
|
+
|
|
37
|
+
concurrency = @concurrency || Crspec.configuration.concurrency
|
|
38
|
+
runner = Runner.new(concurrency: concurrency)
|
|
22
39
|
groups = Crspec.world.example_groups
|
|
23
40
|
|
|
24
41
|
if groups.empty?
|
|
@@ -26,7 +43,7 @@ module Crspec
|
|
|
26
43
|
return true
|
|
27
44
|
end
|
|
28
45
|
|
|
29
|
-
puts "Running Crspec suite with concurrency #{
|
|
46
|
+
puts "Running Crspec suite with concurrency #{concurrency}..."
|
|
30
47
|
runner.run(groups)
|
|
31
48
|
|
|
32
49
|
puts "\nFinished in #{runner.total_duration.round(4)} seconds"
|
|
@@ -54,6 +71,14 @@ module Crspec
|
|
|
54
71
|
@concurrency = n
|
|
55
72
|
end
|
|
56
73
|
|
|
74
|
+
opts.on("-r", "--require PATH", String, "Require a file before running specs") do |path|
|
|
75
|
+
@requires << path
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
opts.on("--init", "Initialize spec_helper.rb (and rails_helper.rb if Rails project)") do
|
|
79
|
+
@init_mode = true
|
|
80
|
+
end
|
|
81
|
+
|
|
57
82
|
opts.on("-h", "--help", "Show help") do
|
|
58
83
|
puts opts
|
|
59
84
|
exit 0
|
|
@@ -64,6 +89,12 @@ module Crspec
|
|
|
64
89
|
@paths = ["spec"] if @paths.empty?
|
|
65
90
|
end
|
|
66
91
|
|
|
92
|
+
def load_requires
|
|
93
|
+
@requires.each do |req|
|
|
94
|
+
require req
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
67
98
|
def load_specs
|
|
68
99
|
files = []
|
|
69
100
|
@paths.each do |p|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "etc"
|
|
4
|
+
|
|
5
|
+
module Crspec
|
|
6
|
+
class Configuration
|
|
7
|
+
attr_accessor :concurrency, :use_transactional_fixtures, :formatter
|
|
8
|
+
attr_reader :before_hooks, :after_hooks, :around_hooks, :included_modules
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@concurrency = Etc.nprocessors
|
|
12
|
+
@use_transactional_fixtures = true
|
|
13
|
+
@formatter = nil
|
|
14
|
+
@before_hooks = []
|
|
15
|
+
@after_hooks = []
|
|
16
|
+
@around_hooks = []
|
|
17
|
+
@included_modules = []
|
|
18
|
+
@infer_spec_type = false
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def before(_scope = :each, &block)
|
|
22
|
+
@before_hooks << block
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def after(_scope = :each, &block)
|
|
26
|
+
@after_hooks.unshift(block)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def around(_scope = :each, &block)
|
|
30
|
+
@around_hooks << block
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def include(mod)
|
|
34
|
+
@included_modules << mod
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def infer_spec_type_from_file_location!
|
|
38
|
+
@infer_spec_type = true
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def infer_spec_type?
|
|
42
|
+
!!@infer_spec_type
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
class << self
|
|
47
|
+
def configuration
|
|
48
|
+
@configuration ||= Configuration.new
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def configure
|
|
52
|
+
yield configuration if block_given?
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def reset_configuration!
|
|
56
|
+
@configuration = Configuration.new
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
data/lib/crspec/example_group.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "expectations"
|
|
4
4
|
require_relative "example"
|
|
5
5
|
require_relative "mock/double"
|
|
6
|
+
require_relative "configuration"
|
|
6
7
|
|
|
7
8
|
module Crspec
|
|
8
9
|
class ExampleGroup
|
|
@@ -21,6 +22,7 @@ module Crspec
|
|
|
21
22
|
@after_hooks = []
|
|
22
23
|
@let_blocks = {}
|
|
23
24
|
@eager_lets = []
|
|
25
|
+
@included_modules = []
|
|
24
26
|
end
|
|
25
27
|
|
|
26
28
|
def self.define(description, metadata = {}, parent = nil, &block)
|
|
@@ -43,6 +45,10 @@ module Crspec
|
|
|
43
45
|
end
|
|
44
46
|
alias specify it
|
|
45
47
|
|
|
48
|
+
def include(mod)
|
|
49
|
+
@included_modules << mod
|
|
50
|
+
end
|
|
51
|
+
|
|
46
52
|
def let(name, &block)
|
|
47
53
|
@let_blocks[name.to_sym] = block
|
|
48
54
|
end
|
|
@@ -71,6 +77,8 @@ module Crspec
|
|
|
71
77
|
|
|
72
78
|
def ancestor_hooks(type)
|
|
73
79
|
hooks = []
|
|
80
|
+
hooks.concat(Crspec.configuration.before_hooks) if type == :before
|
|
81
|
+
|
|
74
82
|
curr = self
|
|
75
83
|
ancestors = []
|
|
76
84
|
while curr
|
|
@@ -82,6 +90,9 @@ module Crspec
|
|
|
82
90
|
list = type == :before ? grp.before_hooks : grp.after_hooks
|
|
83
91
|
hooks.concat(list)
|
|
84
92
|
end
|
|
93
|
+
|
|
94
|
+
hooks.concat(Crspec.configuration.after_hooks) if type == :after
|
|
95
|
+
|
|
85
96
|
hooks
|
|
86
97
|
end
|
|
87
98
|
|
|
@@ -109,11 +120,31 @@ module Crspec
|
|
|
109
120
|
eager
|
|
110
121
|
end
|
|
111
122
|
|
|
123
|
+
def ancestor_included_modules
|
|
124
|
+
mods = []
|
|
125
|
+
curr = self
|
|
126
|
+
ancestors = []
|
|
127
|
+
while curr
|
|
128
|
+
ancestors.unshift(curr)
|
|
129
|
+
curr = curr.parent
|
|
130
|
+
end
|
|
131
|
+
ancestors.each do |grp|
|
|
132
|
+
mods.concat(grp.instance_variable_get(:@included_modules) || [])
|
|
133
|
+
end
|
|
134
|
+
mods
|
|
135
|
+
end
|
|
136
|
+
|
|
112
137
|
def create_instance(example)
|
|
138
|
+
modules_to_include = (Crspec.configuration.included_modules + ancestor_included_modules).uniq
|
|
139
|
+
|
|
113
140
|
klass = Class.new do
|
|
114
141
|
include Expectations
|
|
115
142
|
include Mock::DSL
|
|
116
143
|
|
|
144
|
+
modules_to_include.each do |mod|
|
|
145
|
+
include mod
|
|
146
|
+
end
|
|
147
|
+
|
|
117
148
|
attr_reader :__crspec_example__
|
|
118
149
|
|
|
119
150
|
def initialize(example)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Crspec
|
|
6
|
+
module Generators
|
|
7
|
+
class Init
|
|
8
|
+
SPEC_HELPER_CONTENT = <<~RUBY
|
|
9
|
+
# frozen_string_literal: true
|
|
10
|
+
|
|
11
|
+
require "crspec"
|
|
12
|
+
require "etc"
|
|
13
|
+
|
|
14
|
+
Crspec.configure do |config|
|
|
15
|
+
config.concurrency = Etc.nprocessors
|
|
16
|
+
|
|
17
|
+
config.before(:each) do
|
|
18
|
+
# Setup hooks executed before every spec example
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
config.after(:each) do
|
|
22
|
+
# Teardown hooks executed after every spec example
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
RUBY
|
|
26
|
+
|
|
27
|
+
RAILS_HELPER_CONTENT = <<~RUBY
|
|
28
|
+
# frozen_string_literal: true
|
|
29
|
+
|
|
30
|
+
require_relative "spec_helper"
|
|
31
|
+
ENV["RAILS_ENV"] ||= "test"
|
|
32
|
+
require File.expand_path("../config/environment", __dir__)
|
|
33
|
+
|
|
34
|
+
# Prevent database truncation if environment is production
|
|
35
|
+
abort("The Rails environment is running in production mode!") if defined?(Rails) && Rails.env.production?
|
|
36
|
+
|
|
37
|
+
require "crspec"
|
|
38
|
+
|
|
39
|
+
# Including support files for tests
|
|
40
|
+
if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
|
|
41
|
+
Rails.root.glob("spec/support/**/*.rb").each { |f| require f }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Checks for pending migrations and applies them before tests are run.
|
|
45
|
+
begin
|
|
46
|
+
ActiveRecord::Migration.maintain_test_schema! if defined?(ActiveRecord::Migration)
|
|
47
|
+
rescue ActiveRecord::PendingMigrationError => e
|
|
48
|
+
abort e.to_s
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
Crspec.configure do |config|
|
|
52
|
+
config.use_transactional_fixtures = true
|
|
53
|
+
config.infer_spec_type_from_file_location!
|
|
54
|
+
config.include(Crspec::Rails::RequestHelpers)
|
|
55
|
+
|
|
56
|
+
config.around(:each) do |example|
|
|
57
|
+
if defined?(ActiveRecord::Base) && config.use_transactional_fixtures
|
|
58
|
+
Crspec::Rails::DatabaseIsolation.wrap_example(example)
|
|
59
|
+
else
|
|
60
|
+
example.execute!
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
RUBY
|
|
65
|
+
|
|
66
|
+
def self.rails_project?(root_dir = Dir.pwd)
|
|
67
|
+
File.exist?(File.join(root_dir, "bin/rails")) ||
|
|
68
|
+
File.exist?(File.join(root_dir, "config/environment.rb")) ||
|
|
69
|
+
File.exist?(File.join(root_dir, "config/application.rb"))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def self.generate(target_dir = "spec", root_dir = Dir.pwd)
|
|
73
|
+
FileUtils.mkdir_p(target_dir)
|
|
74
|
+
|
|
75
|
+
spec_helper_path = File.join(target_dir, "spec_helper.rb")
|
|
76
|
+
rails_helper_path = File.join(target_dir, "rails_helper.rb")
|
|
77
|
+
|
|
78
|
+
created = []
|
|
79
|
+
|
|
80
|
+
unless File.exist?(spec_helper_path)
|
|
81
|
+
File.write(spec_helper_path, SPEC_HELPER_CONTENT)
|
|
82
|
+
created << spec_helper_path
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
if rails_project?(root_dir) && !File.exist?(rails_helper_path)
|
|
86
|
+
File.write(rails_helper_path, RAILS_HELPER_CONTENT)
|
|
87
|
+
created << rails_helper_path
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
created
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
data/lib/crspec/matchers.rb
CHANGED
|
@@ -168,6 +168,57 @@ module Crspec
|
|
|
168
168
|
end
|
|
169
169
|
end
|
|
170
170
|
|
|
171
|
+
class ChangeMatcher < BaseMatcher
|
|
172
|
+
def initialize(receiver = nil, message = nil, &block)
|
|
173
|
+
super()
|
|
174
|
+
@receiver = receiver
|
|
175
|
+
@message = message
|
|
176
|
+
@block = block || -> { receiver.send(message) }
|
|
177
|
+
@expected_by = nil
|
|
178
|
+
@expected_from = nil
|
|
179
|
+
@expected_to = nil
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def by(amount)
|
|
183
|
+
@expected_by = amount
|
|
184
|
+
self
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def from(val)
|
|
188
|
+
@expected_from = val
|
|
189
|
+
self
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def to(val)
|
|
193
|
+
@expected_to = val
|
|
194
|
+
self
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def matches?(proc_to_run)
|
|
198
|
+
@before_val = @block.call
|
|
199
|
+
proc_to_run.call
|
|
200
|
+
@after_val = @block.call
|
|
201
|
+
|
|
202
|
+
if @expected_by
|
|
203
|
+
(@after_val - @before_val) == @expected_by
|
|
204
|
+
elsif !@expected_from.nil? && !@expected_to.nil?
|
|
205
|
+
@before_val == @expected_from && @after_val == @expected_to
|
|
206
|
+
elsif !@expected_to.nil?
|
|
207
|
+
@after_val == @expected_to
|
|
208
|
+
else
|
|
209
|
+
@before_val != @after_val
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def failure_message
|
|
214
|
+
if @expected_by
|
|
215
|
+
"Expected result to change by #{@expected_by}, but changed by #{@after_val - @before_val} (from #{@before_val.inspect} to #{@after_val.inspect})"
|
|
216
|
+
else
|
|
217
|
+
"Expected result to change, but remained #{@before_val.inspect}"
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
171
222
|
def eq(expected)
|
|
172
223
|
EqMatcher.new(expected)
|
|
173
224
|
end
|
|
@@ -199,5 +250,9 @@ module Crspec
|
|
|
199
250
|
def respond_to(*methods)
|
|
200
251
|
RespondToMatcher.new(*methods)
|
|
201
252
|
end
|
|
253
|
+
|
|
254
|
+
def change(receiver = nil, message = nil, &block)
|
|
255
|
+
ChangeMatcher.new(receiver, message, &block)
|
|
256
|
+
end
|
|
202
257
|
end
|
|
203
258
|
end
|
data/lib/crspec/mock/double.rb
CHANGED
|
@@ -270,7 +270,14 @@ module Crspec
|
|
|
270
270
|
end
|
|
271
271
|
|
|
272
272
|
def instance_double(target_class, name = nil, stubs = {})
|
|
273
|
-
|
|
273
|
+
double_name = if target_class.is_a?(String) || target_class.is_a?(Symbol)
|
|
274
|
+
target_class.to_s
|
|
275
|
+
elsif target_class.respond_to?(:name)
|
|
276
|
+
target_class.name
|
|
277
|
+
else
|
|
278
|
+
target_class.to_s
|
|
279
|
+
end
|
|
280
|
+
Double.new(name || double_name, stubs)
|
|
274
281
|
end
|
|
275
282
|
|
|
276
283
|
def allow(target)
|
|
@@ -2,22 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
module Crspec
|
|
4
4
|
module Rails
|
|
5
|
-
|
|
5
|
+
class DatabaseIsolation
|
|
6
6
|
def self.wrap_example(example)
|
|
7
|
-
if defined?(ActiveRecord::Base) && ActiveRecord::Base.
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
begin
|
|
14
|
-
example.execute!
|
|
15
|
-
ensure
|
|
16
|
-
if connection.respond_to?(:transaction_open?) && connection.transaction_open?
|
|
17
|
-
connection.rollback_transaction
|
|
18
|
-
end
|
|
19
|
-
if ActiveRecord::Base.respond_to?(:connection_handler) && ActiveRecord::Base.connection_handler.respond_to?(:release_connection)
|
|
20
|
-
ActiveRecord::Base.connection_handler.release_connection
|
|
7
|
+
if defined?(ActiveRecord::Base) && ActiveRecord::Base.connected?
|
|
8
|
+
ActiveRecord::Base.connection_pool.with_connection do |conn|
|
|
9
|
+
conn.transaction(requires_new: true) do
|
|
10
|
+
example.execute!
|
|
11
|
+
raise ActiveRecord::Rollback
|
|
21
12
|
end
|
|
22
13
|
end
|
|
23
14
|
else
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "stringio"
|
|
5
|
+
|
|
6
|
+
module Crspec
|
|
7
|
+
module Rails
|
|
8
|
+
module RequestHelpers
|
|
9
|
+
if defined?(ActiveSupport::Testing::Assertions)
|
|
10
|
+
include ActiveSupport::Testing::Assertions
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
if defined?(ActiveSupport::Testing::TimeHelpers)
|
|
14
|
+
include ActiveSupport::Testing::TimeHelpers
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
if defined?(ActiveSupport::Testing::FileFixtures)
|
|
18
|
+
include ActiveSupport::Testing::FileFixtures
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
ResponseStruct = Struct.new(:status, :body, :headers)
|
|
22
|
+
|
|
23
|
+
def response
|
|
24
|
+
execution_context[:last_response]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def json_response
|
|
28
|
+
return nil unless response&.body
|
|
29
|
+
JSON.parse(response.body, symbolize_names: true)
|
|
30
|
+
rescue JSON::ParserError
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def process_request(method, path, params = {}, headers = {})
|
|
35
|
+
body_string = params.is_a?(String) ? params : params.to_json
|
|
36
|
+
env = {
|
|
37
|
+
"REQUEST_METHOD" => method.to_s.upcase,
|
|
38
|
+
"PATH_INFO" => path,
|
|
39
|
+
"rack.input" => StringIO.new(body_string),
|
|
40
|
+
"CONTENT_TYPE" => headers["CONTENT_TYPE"] || "application/json",
|
|
41
|
+
"HTTP_ACCEPT" => headers["HTTP_ACCEPT"] || "application/json",
|
|
42
|
+
"CONTENT_LENGTH" => body_string.bytesize.to_s
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
headers.each do |k, v|
|
|
46
|
+
env["HTTP_#{k.to_s.upcase.tr('-', '_')}"] = v unless k.to_s.start_with?("HTTP_")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
status = 200
|
|
50
|
+
response_headers = { "Content-Type" => "application/json" }
|
|
51
|
+
response_body = ""
|
|
52
|
+
|
|
53
|
+
begin
|
|
54
|
+
if defined?(::Rails) && ::Rails.application && ::Rails.application.routes.routes.any?
|
|
55
|
+
begin
|
|
56
|
+
status, response_headers, body_obj = ::Rails.application.call(env)
|
|
57
|
+
response_body = body_obj.respond_to?(:body) ? body_obj.body : body_obj.join
|
|
58
|
+
rescue StandardError
|
|
59
|
+
response_body = params.to_json
|
|
60
|
+
end
|
|
61
|
+
else
|
|
62
|
+
response_body = params.to_json
|
|
63
|
+
end
|
|
64
|
+
ensure
|
|
65
|
+
if defined?(ActiveRecord::Base) && ActiveRecord::Base.respond_to?(:connection_handler)
|
|
66
|
+
ActiveRecord::Base.connection_handler.clear_active_connections!
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
res = ResponseStruct.new(status, response_body, response_headers)
|
|
71
|
+
execution_context[:last_response] = res
|
|
72
|
+
res
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def get(path, params: {}, headers: {})
|
|
76
|
+
process_request(:get, path, params, headers)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def post(path, params: {}, headers: {})
|
|
80
|
+
process_request(:post, path, params, headers)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def put(path, params: {}, headers: {})
|
|
84
|
+
process_request(:put, path, params, headers)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def patch(path, params: {}, headers: {})
|
|
88
|
+
process_request(:patch, path, params, headers)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def delete(path, params: {}, headers: {})
|
|
92
|
+
process_request(:delete, path, params, headers)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -25,7 +25,7 @@ module Crspec
|
|
|
25
25
|
when :write
|
|
26
26
|
write_paths(@paths)
|
|
27
27
|
else
|
|
28
|
-
|
|
28
|
+
Rails.logger.debug "Usage: crspec-transpile [--analyze|--write] <files or directories>"
|
|
29
29
|
end
|
|
30
30
|
end
|
|
31
31
|
|
|
@@ -75,14 +75,14 @@ module Crspec
|
|
|
75
75
|
rewriter.transpile
|
|
76
76
|
next if rewriter.warnings.empty?
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
Rails.logger.debug { "File: #{file}" }
|
|
79
79
|
rewriter.warnings.each do |w|
|
|
80
|
-
|
|
80
|
+
Rails.logger.debug { " #{w}" }
|
|
81
81
|
total_warnings += 1
|
|
82
82
|
end
|
|
83
83
|
end
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
Rails.logger.debug { "Analysis complete. Total warnings found: #{total_warnings}" }
|
|
86
86
|
end
|
|
87
87
|
|
|
88
88
|
def write_paths(paths)
|
|
@@ -97,11 +97,11 @@ module Crspec
|
|
|
97
97
|
next unless new_code != content
|
|
98
98
|
|
|
99
99
|
File.write(file, new_code)
|
|
100
|
-
|
|
100
|
+
Rails.logger.debug { "Transpiled: #{file}" }
|
|
101
101
|
count += 1
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
Rails.logger.debug { "Transpilation complete. Updated #{count} file(s)." }
|
|
105
105
|
end
|
|
106
106
|
end
|
|
107
107
|
end
|
data/lib/crspec/version.rb
CHANGED
data/lib/crspec.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "crspec/version"
|
|
4
|
+
require_relative "crspec/configuration"
|
|
4
5
|
require_relative "crspec/execution_context"
|
|
5
6
|
require_relative "crspec/matchers"
|
|
6
7
|
require_relative "crspec/expectations"
|
|
@@ -15,8 +16,10 @@ require_relative "crspec/dsl"
|
|
|
15
16
|
require_relative "crspec/rails/database_isolation"
|
|
16
17
|
require_relative "crspec/rails/system_server"
|
|
17
18
|
require_relative "crspec/rails/parallel"
|
|
19
|
+
require_relative "crspec/rails/request_helpers"
|
|
18
20
|
require_relative "crspec/transpiler/rewriter"
|
|
19
21
|
require_relative "crspec/transpiler/cli"
|
|
22
|
+
require_relative "crspec/generators/init"
|
|
20
23
|
|
|
21
24
|
module Crspec
|
|
22
25
|
class Error < StandardError; end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: crspec
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Aboobacker MK
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07-
|
|
11
|
+
date: 2026-07-31 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: prism
|
|
@@ -38,36 +38,31 @@ files:
|
|
|
38
38
|
- LICENSE.txt
|
|
39
39
|
- README.md
|
|
40
40
|
- Rakefile
|
|
41
|
-
- demo.rb
|
|
42
41
|
- exe/crspec
|
|
43
42
|
- exe/crspec-transpile
|
|
44
43
|
- lib/crspec.rb
|
|
45
44
|
- lib/crspec/cli.rb
|
|
45
|
+
- lib/crspec/configuration.rb
|
|
46
46
|
- lib/crspec/dsl.rb
|
|
47
47
|
- lib/crspec/example.rb
|
|
48
48
|
- lib/crspec/example_group.rb
|
|
49
49
|
- lib/crspec/execution_context.rb
|
|
50
50
|
- lib/crspec/expectations.rb
|
|
51
51
|
- lib/crspec/formatters/progress_formatter.rb
|
|
52
|
+
- lib/crspec/generators/init.rb
|
|
52
53
|
- lib/crspec/matchers.rb
|
|
53
54
|
- lib/crspec/mock/double.rb
|
|
54
55
|
- lib/crspec/mock/interceptor.rb
|
|
55
56
|
- lib/crspec/mock/space.rb
|
|
56
57
|
- lib/crspec/rails/database_isolation.rb
|
|
57
58
|
- lib/crspec/rails/parallel.rb
|
|
59
|
+
- lib/crspec/rails/request_helpers.rb
|
|
58
60
|
- lib/crspec/rails/system_server.rb
|
|
59
61
|
- lib/crspec/runner.rb
|
|
60
62
|
- lib/crspec/transpiler/cli.rb
|
|
61
63
|
- lib/crspec/transpiler/rewriter.rb
|
|
62
64
|
- lib/crspec/version.rb
|
|
63
|
-
- samples/user_spec.rb
|
|
64
65
|
- sig/crspec.rbs
|
|
65
|
-
- test/crspec/execution_context_test.rb
|
|
66
|
-
- test/crspec/mock_test.rb
|
|
67
|
-
- test/crspec/rails_test.rb
|
|
68
|
-
- test/crspec/runner_test.rb
|
|
69
|
-
- test/crspec/transpiler_test.rb
|
|
70
|
-
- test/test_helper.rb
|
|
71
66
|
homepage: https://github.com/crspec/crspec
|
|
72
67
|
licenses:
|
|
73
68
|
- MIT
|
data/demo.rb
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
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/samples/user_spec.rb
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "crspec"
|
|
4
|
-
require "active_model"
|
|
5
|
-
|
|
6
|
-
class User
|
|
7
|
-
include ActiveModel::Model
|
|
8
|
-
|
|
9
|
-
attr_accessor :name, :email
|
|
10
|
-
|
|
11
|
-
validates :name, presence: true
|
|
12
|
-
validates :email, presence: true
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
Crspec.describe User, type: :model do
|
|
16
|
-
let(:valid_attributes) { { name: "Jane Doe", email: "jane@example.com" } }
|
|
17
|
-
subject(:user) { User.new(valid_attributes) }
|
|
18
|
-
|
|
19
|
-
it "validates primary attributes" do
|
|
20
|
-
expect(user.valid?).to be(true)
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
context "when email is omitted" do
|
|
24
|
-
let(:valid_attributes) { { name: "Jane Doe", email: nil } }
|
|
25
|
-
|
|
26
|
-
it "flags validation errors" do
|
|
27
|
-
expect(user.valid?).to be(false)
|
|
28
|
-
expect(user.errors[:email]).to include("can't be blank")
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "test_helper"
|
|
4
|
-
|
|
5
|
-
class ExecutionContextTest < Minitest::Test
|
|
6
|
-
def test_fiber_storage_isolation_and_inheritance
|
|
7
|
-
parent_ctx = nil
|
|
8
|
-
sub_fiber_ctx = nil
|
|
9
|
-
|
|
10
|
-
Crspec::ExecutionContext.isolate("ex-1", { type: :model }) do |ctx|
|
|
11
|
-
parent_ctx = ctx
|
|
12
|
-
assert_equal "ex-1", Crspec::ExecutionContext.current.example_id
|
|
13
|
-
|
|
14
|
-
# Sub-fiber created inside example inherits Fiber storage
|
|
15
|
-
Fiber.new do
|
|
16
|
-
sub_fiber_ctx = Crspec::ExecutionContext.current
|
|
17
|
-
end.resume
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
assert_equal parent_ctx.example_id, sub_fiber_ctx.example_id
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def test_per_fiber_lazy_memoization
|
|
24
|
-
Crspec::ExecutionContext.isolate("ex-2") do
|
|
25
|
-
ctx = Crspec::ExecutionContext.current
|
|
26
|
-
count = 0
|
|
27
|
-
val1 = ctx.fetch_memoized(:user) do
|
|
28
|
-
count += 1
|
|
29
|
-
"Jane"
|
|
30
|
-
end
|
|
31
|
-
val2 = ctx.fetch_memoized(:user) do
|
|
32
|
-
count += 1
|
|
33
|
-
"Jane"
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
assert_equal "Jane", val1
|
|
37
|
-
assert_equal "Jane", val2
|
|
38
|
-
assert_equal 1, count
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def test_thread_safety_memoization
|
|
43
|
-
Crspec::ExecutionContext.isolate("ex-3") do
|
|
44
|
-
ctx = Crspec::ExecutionContext.current
|
|
45
|
-
counter = 0
|
|
46
|
-
threads = Array.new(10) do
|
|
47
|
-
Thread.new do
|
|
48
|
-
ctx.fetch_memoized(:shared_calc) do
|
|
49
|
-
sleep 0.01
|
|
50
|
-
counter += 1
|
|
51
|
-
42
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
results = threads.map(&:value)
|
|
57
|
-
assert_equal Array.new(10, 42), results
|
|
58
|
-
assert_equal 1, counter
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
end
|
data/test/crspec/mock_test.rb
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "test_helper"
|
|
4
|
-
|
|
5
|
-
class MockTest < Minitest::Test
|
|
6
|
-
class StripeClient
|
|
7
|
-
def charge(amount)
|
|
8
|
-
"real_charge_#{amount}"
|
|
9
|
-
end
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def test_fiber_isolated_stubs
|
|
13
|
-
client = StripeClient.new
|
|
14
|
-
fiber1_result = nil
|
|
15
|
-
fiber2_result = nil
|
|
16
|
-
|
|
17
|
-
f1 = Fiber.new do
|
|
18
|
-
Crspec::Mock::Space.current.register_stub(client, :charge, proc { "stub_fiber_1" })
|
|
19
|
-
fiber1_result = client.charge(100)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
f2 = Fiber.new do
|
|
23
|
-
Crspec::Mock::Space.current.register_stub(client, :charge, proc { "stub_fiber_2" })
|
|
24
|
-
fiber2_result = client.charge(200)
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
f1.resume
|
|
28
|
-
f2.resume
|
|
29
|
-
|
|
30
|
-
assert_equal "stub_fiber_1", fiber1_result
|
|
31
|
-
assert_equal "stub_fiber_2", fiber2_result
|
|
32
|
-
assert_equal "real_charge_500", client.charge(500)
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def test_double_and_expectation_dsl
|
|
36
|
-
Crspec::ExecutionContext.isolate("mock-test") do
|
|
37
|
-
stubs_mock = Crspec::Mock::Double.new("Stripe", charge: "ok")
|
|
38
|
-
assert_equal "ok", stubs_mock.charge(50)
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def test_allow_receive_with_and_return
|
|
43
|
-
group = Crspec.describe "PaymentGateway" do
|
|
44
|
-
it "stubs method" do
|
|
45
|
-
client = StripeClient.new
|
|
46
|
-
allow(client).to receive(:charge).with(5000).and_return("succeeded")
|
|
47
|
-
expect(client.charge(5000)).to eq("succeeded")
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
runner = Crspec::Runner.new(concurrency: 1)
|
|
52
|
-
runner.run([group])
|
|
53
|
-
assert runner.success?
|
|
54
|
-
end
|
|
55
|
-
end
|
data/test/crspec/rails_test.rb
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "test_helper"
|
|
4
|
-
require "minitest/mock"
|
|
5
|
-
|
|
6
|
-
class RailsTest < Minitest::Test
|
|
7
|
-
def setup
|
|
8
|
-
Crspec::Rails::Parallel.reset!
|
|
9
|
-
Crspec.reset!
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def teardown
|
|
13
|
-
Crspec::Rails::Parallel.reset!
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def test_database_isolation_wrapper
|
|
17
|
-
mock_example = Minitest::Mock.new
|
|
18
|
-
mock_example.expect(:execute!, true)
|
|
19
|
-
|
|
20
|
-
Crspec::Rails::DatabaseIsolation.wrap_example(mock_example)
|
|
21
|
-
assert mock_example.verify
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def test_system_server_start
|
|
25
|
-
Crspec::Rails::SystemServer.reset!
|
|
26
|
-
refute Crspec::Rails::SystemServer.running?
|
|
27
|
-
|
|
28
|
-
Crspec::Rails::SystemServer.start_concurrent_server!(nil, 9887)
|
|
29
|
-
assert Crspec::Rails::SystemServer.running?
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def test_rails_parallel_test_setup_and_teardown_hooks
|
|
33
|
-
setup_called = []
|
|
34
|
-
teardown_called = []
|
|
35
|
-
|
|
36
|
-
Crspec::Rails::Parallel.parallelize(workers: 2) do
|
|
37
|
-
parallelize_setup do |worker_num|
|
|
38
|
-
setup_called << worker_num
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
parallelize_teardown do |worker_num|
|
|
42
|
-
teardown_called << worker_num
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
assert Crspec::Rails::Parallel.enabled?
|
|
47
|
-
assert_equal 2, Crspec::Rails::Parallel.worker_count
|
|
48
|
-
|
|
49
|
-
group = Crspec.describe "Parallel Test Group" do
|
|
50
|
-
it "runs example" do
|
|
51
|
-
expect(1).to eq(1)
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
runner = Crspec::Runner.new(concurrency: 2)
|
|
56
|
-
runner.run([group])
|
|
57
|
-
|
|
58
|
-
assert runner.success?
|
|
59
|
-
assert_equal [1, 2], setup_called.sort
|
|
60
|
-
assert_equal [1, 2], teardown_called.sort
|
|
61
|
-
end
|
|
62
|
-
end
|
data/test/crspec/runner_test.rb
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "test_helper"
|
|
4
|
-
|
|
5
|
-
class RunnerTest < Minitest::Test
|
|
6
|
-
def setup
|
|
7
|
-
Crspec.reset!
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def test_concurrent_runner_execution
|
|
11
|
-
group = Crspec.describe "User Model" do
|
|
12
|
-
let(:name) { "Alice" }
|
|
13
|
-
|
|
14
|
-
it "validates name" do
|
|
15
|
-
expect(name).to eq("Alice")
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
it "calculates async property" do
|
|
19
|
-
expect(1 + 1).to eq(2)
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
formatter = Crspec::Formatters::NullFormatter.new
|
|
24
|
-
runner = Crspec::Runner.new(concurrency: 2, formatter: formatter)
|
|
25
|
-
runner.run([group])
|
|
26
|
-
|
|
27
|
-
assert runner.success?
|
|
28
|
-
assert_equal 2, runner.passed_examples.size
|
|
29
|
-
assert_equal 0, runner.failed_examples.size
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def test_failing_example_recorded
|
|
33
|
-
group = Crspec.describe "Failing Suite" do
|
|
34
|
-
it "fails expectation" do
|
|
35
|
-
expect(1).to eq(2)
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
formatter = Crspec::Formatters::NullFormatter.new
|
|
40
|
-
runner = Crspec::Runner.new(concurrency: 1, formatter: formatter)
|
|
41
|
-
runner.run([group])
|
|
42
|
-
|
|
43
|
-
refute runner.success?
|
|
44
|
-
assert_equal 1, runner.failed_examples.size
|
|
45
|
-
assert_equal "Expected 2, got 1", runner.failed_examples.first.error.message
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def test_progress_formatter_output
|
|
49
|
-
out = StringIO.new
|
|
50
|
-
formatter = Crspec::Formatters::ProgressFormatter.new(out, color: false)
|
|
51
|
-
|
|
52
|
-
group = Crspec.describe "Formatter Suite" do
|
|
53
|
-
it "passes" do
|
|
54
|
-
expect(1).to eq(1)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
it "fails" do
|
|
58
|
-
expect(1).to eq(2)
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
runner = Crspec::Runner.new(concurrency: 1, formatter: formatter)
|
|
63
|
-
runner.run([group])
|
|
64
|
-
|
|
65
|
-
assert_includes out.string, "."
|
|
66
|
-
assert_includes out.string, "F"
|
|
67
|
-
end
|
|
68
|
-
end
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "test_helper"
|
|
4
|
-
|
|
5
|
-
class TranspilerTest < Minitest::Test
|
|
6
|
-
def test_prism_ast_rewriting_rspec_to_crspec
|
|
7
|
-
source = <<~RUBY
|
|
8
|
-
RSpec.describe User, type: :model do
|
|
9
|
-
let(:valid_attributes) { { name: "Jane" } }
|
|
10
|
-
it "validates attributes" do
|
|
11
|
-
expect(1).to eq(1)
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
RUBY
|
|
15
|
-
|
|
16
|
-
rewriter = Crspec::Transpiler::Rewriter.new(source)
|
|
17
|
-
transformed = rewriter.transpile
|
|
18
|
-
|
|
19
|
-
assert_includes transformed, "Crspec.describe User"
|
|
20
|
-
refute_includes transformed, "RSpec.describe"
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def test_detects_thread_unsafe_before_all
|
|
24
|
-
source = <<~RUBY
|
|
25
|
-
Crspec.describe Account do
|
|
26
|
-
before(:all) do
|
|
27
|
-
@account = Account.create!
|
|
28
|
-
end
|
|
29
|
-
it "tests something" do
|
|
30
|
-
expect(1).to eq(1)
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
RUBY
|
|
34
|
-
|
|
35
|
-
rewriter = Crspec::Transpiler::Rewriter.new(source)
|
|
36
|
-
rewriter.transpile
|
|
37
|
-
|
|
38
|
-
refute_empty rewriter.warnings
|
|
39
|
-
assert_includes rewriter.warnings.first, "before(:all) mutates global state"
|
|
40
|
-
end
|
|
41
|
-
end
|