cypress-rails 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +40 -3
- data/CHANGELOG.md +19 -0
- data/README.md +83 -32
- data/cypress-rails.gemspec +4 -2
- data/lib/cypress-rails/config.rb +4 -1
- data/lib/cypress-rails/initializer_hooks.rb +6 -2
- data/lib/cypress-rails/launches_cypress.rb +14 -1
- data/lib/cypress-rails/manages_transactions.rb +76 -0
- data/lib/cypress-rails/resets_state.rb +32 -3
- data/lib/cypress-rails/server.rb +9 -4
- data/lib/cypress-rails/starts_rails_server.rb +11 -13
- data/lib/cypress-rails/version.rb +1 -1
- metadata +32 -4
- data/lib/cypress-rails/tracks_resets.rb +0 -26
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c08ef4f9087d66f0f04df9831568414e757aae3fb83baf6bfd85c0d7a3509312
|
|
4
|
+
data.tar.gz: aa4baeebd6d8035ca9b9fc6ba91b0c487e9cf3a385a25ee9da92c96e2fd4ce2c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 15cf818dd04eb1eb89853d2a14839dd54d25ed4c39b21a705fcae507c7cecb12813477913fab43e486a7f80f0bcc4df8cb18cafa0252d3a38c33608b8441c6b7
|
|
7
|
+
data.tar.gz: 2fa87db917ccc275d14c8dc28f67a55c13a375e13d6a9f25887fc9ad87989d18bb515b2eabab19ef58dcee6da9b098cf1ccabf97ee6a057649468d8f4f740245
|
data/.github/workflows/main.yml
CHANGED
|
@@ -7,14 +7,17 @@ jobs:
|
|
|
7
7
|
runs-on: ubuntu-latest
|
|
8
8
|
|
|
9
9
|
strategy:
|
|
10
|
+
fail-fast: false
|
|
10
11
|
matrix:
|
|
11
12
|
ruby-version: ['3.0', '3.1', '3.2', '3.3', '3.4']
|
|
12
13
|
|
|
13
14
|
steps:
|
|
14
|
-
- uses: actions/checkout@
|
|
15
|
+
- uses: actions/checkout@v7
|
|
15
16
|
- uses: ruby/setup-ruby@v1
|
|
16
17
|
with:
|
|
17
18
|
ruby-version: ${{ matrix.ruby-version }}
|
|
19
|
+
rubygems: latest
|
|
20
|
+
bundler: latest
|
|
18
21
|
bundler-cache: true
|
|
19
22
|
|
|
20
23
|
- name: Run root unit tests and lint
|
|
@@ -23,26 +26,60 @@ jobs:
|
|
|
23
26
|
bundle exec rake
|
|
24
27
|
|
|
25
28
|
example-app-tests:
|
|
29
|
+
name: example-app-tests (${{ matrix.ruby-version }}, ${{ matrix.rails-version }}, transactional ${{ matrix.transactional-server }})
|
|
26
30
|
runs-on: ubuntu-latest
|
|
27
31
|
|
|
28
32
|
strategy:
|
|
29
33
|
fail-fast: false
|
|
30
34
|
matrix:
|
|
31
35
|
ruby-version: ['3.0', '3.1', '3.2', '3.3', '3.4']
|
|
32
|
-
rails-version: ['7.1', '7.2', '8.0']
|
|
36
|
+
rails-version: ['7.1', '7.2', '8.0', '8.1']
|
|
37
|
+
transactional-server: ['true']
|
|
33
38
|
exclude:
|
|
34
39
|
- ruby-version: '3.0'
|
|
35
40
|
rails-version: '7.2'
|
|
36
41
|
- ruby-version: '3.0'
|
|
37
42
|
rails-version: '8.0'
|
|
43
|
+
- ruby-version: '3.0'
|
|
44
|
+
rails-version: '8.1'
|
|
45
|
+
- ruby-version: '3.1'
|
|
46
|
+
rails-version: '8.0'
|
|
38
47
|
- ruby-version: '3.1'
|
|
48
|
+
rails-version: '8.1'
|
|
49
|
+
include:
|
|
50
|
+
- ruby-version: '3.4'
|
|
51
|
+
rails-version: '7.1'
|
|
52
|
+
transactional-server: 'false'
|
|
53
|
+
- ruby-version: '3.4'
|
|
54
|
+
rails-version: '7.2'
|
|
55
|
+
transactional-server: 'false'
|
|
56
|
+
- ruby-version: '3.4'
|
|
39
57
|
rails-version: '8.0'
|
|
58
|
+
transactional-server: 'false'
|
|
59
|
+
|
|
60
|
+
services:
|
|
61
|
+
postgres:
|
|
62
|
+
image: postgres:18
|
|
63
|
+
env:
|
|
64
|
+
POSTGRES_PASSWORD: postgres
|
|
65
|
+
ports:
|
|
66
|
+
- 5432:5432
|
|
67
|
+
options: >-
|
|
68
|
+
--health-cmd pg_isready
|
|
69
|
+
--health-interval 10s
|
|
70
|
+
--health-timeout 5s
|
|
71
|
+
--health-retries 5
|
|
40
72
|
|
|
41
73
|
env:
|
|
42
74
|
BUNDLE_GEMFILE: ${{ github.workspace }}/example/gemfiles/rails_${{ matrix.rails-version }}.gemfile
|
|
75
|
+
CYPRESS_RAILS_TRANSACTIONAL_SERVER: ${{ matrix.transactional-server }}
|
|
76
|
+
PGHOST: localhost
|
|
77
|
+
PGUSER: postgres
|
|
78
|
+
PGPASSWORD: postgres
|
|
43
79
|
|
|
44
80
|
steps:
|
|
45
|
-
- uses: actions/checkout@
|
|
81
|
+
- uses: actions/checkout@v7
|
|
82
|
+
- uses: actions/setup-node@v7
|
|
46
83
|
- uses: ruby/setup-ruby@v1
|
|
47
84
|
with:
|
|
48
85
|
ruby-version: ${{ matrix.ruby-version }}
|
data/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,26 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 0.9.0
|
|
4
|
+
|
|
5
|
+
* **[Breaking]** Require Rails 7.1 or newer
|
|
6
|
+
* **[Breaking]** (relative to 0.8.0) Rename `after_reset_requested` back to
|
|
7
|
+
`after_state_reset`, and restore the transactional server as the default
|
|
8
|
+
behavior
|
|
9
|
+
* Bring back the transactional server (and `CYPRESS_RAILS_TRANSACTIONAL_SERVER`),
|
|
10
|
+
which 0.8.0 had removed. It didn't work reliably to have apps reset their own
|
|
11
|
+
state under cypress-rails' multi-threaded Puma server
|
|
12
|
+
* `/cypress_rails_reset_state` now resets before responding (returning `200`
|
|
13
|
+
instead of `202`), rather than deferring the reset to the next request
|
|
14
|
+
|
|
15
|
+
See [docs/changes-in-0.9.0.md](docs/changes-in-0.9.0.md) for a full comparison
|
|
16
|
+
with both 0.7.1 and 0.8.0.
|
|
17
|
+
|
|
3
18
|
## 0.8.0
|
|
4
19
|
|
|
20
|
+
(Avoid this version due to issues with resetting data during threaded requests.
|
|
21
|
+
0.9.0 is almost entirely compatible with 0.7.1 while still bringing Rails 7.2+
|
|
22
|
+
compatibility, so the breaking changes in this version are for nothing.)
|
|
23
|
+
|
|
5
24
|
* **[Breaking]** Remove `CYPRESS_RAILS_TRANSACTIONAL_SERVER`. cypress-rails no
|
|
6
25
|
longer wraps the server in a transaction or resets it between tests -
|
|
7
26
|
automatic Rails-version-specific transaction management proved unsustainable
|
data/README.md
CHANGED
|
@@ -93,39 +93,35 @@ is a JavaScript-based tool and can't easily manipulate your Rails app directly,
|
|
|
93
93
|
cypress-rails provides a number of hooks that you can use to manage your test
|
|
94
94
|
data.
|
|
95
95
|
|
|
96
|
-
cypress-rails
|
|
97
|
-
tests
|
|
98
|
-
|
|
99
|
-
|
|
96
|
+
By default, cypress-rails runs your app inside a database transaction and rolls
|
|
97
|
+
it back between tests, much like a Rails system test. Every thread serving your
|
|
98
|
+
app shares that one transaction, so whatever your tests create is undone on the
|
|
99
|
+
next reset.
|
|
100
100
|
|
|
101
101
|
Here's what a `config/initializers/cypress_rails.rb` initializer might look
|
|
102
|
-
like
|
|
102
|
+
like:
|
|
103
103
|
|
|
104
104
|
```ruby
|
|
105
105
|
return unless Rails.env.test?
|
|
106
106
|
|
|
107
|
-
require "database_cleaner/active_record"
|
|
108
|
-
DatabaseCleaner.strategy = :transaction
|
|
109
|
-
|
|
110
107
|
CypressRails.hooks.before_server_start do
|
|
111
|
-
# Called once, before the server is started
|
|
112
|
-
|
|
108
|
+
# Called once, before either the transaction or the server is started
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
CypressRails.hooks.after_transaction_start do
|
|
112
|
+
# Called after the transaction is started (at launch and after each reset)
|
|
113
113
|
end
|
|
114
114
|
|
|
115
115
|
CypressRails.hooks.after_server_start do
|
|
116
116
|
# Called once, after the server has booted
|
|
117
117
|
end
|
|
118
118
|
|
|
119
|
-
CypressRails.hooks.
|
|
120
|
-
# Triggered after `/cypress_rails_reset_state` is called
|
|
121
|
-
# chance to actually reset the database
|
|
122
|
-
DatabaseCleaner.clean
|
|
123
|
-
DatabaseCleaner.start
|
|
119
|
+
CypressRails.hooks.after_state_reset do
|
|
120
|
+
# Triggered after `/cypress_rails_reset_state` is called
|
|
124
121
|
end
|
|
125
122
|
|
|
126
123
|
CypressRails.hooks.before_server_stop do
|
|
127
124
|
# Called once, at_exit
|
|
128
|
-
DatabaseCleaner.clean
|
|
129
125
|
end
|
|
130
126
|
```
|
|
131
127
|
|
|
@@ -135,9 +131,17 @@ initializer](/example/config/initializers/cypress_rails_initializer.rb)
|
|
|
135
131
|
in this repo.)
|
|
136
132
|
|
|
137
133
|
The gem also provides a special route on the test server:
|
|
138
|
-
`/cypress_rails_reset_state`. Each time it's called, cypress-rails
|
|
139
|
-
|
|
140
|
-
|
|
134
|
+
`/cypress_rails_reset_state`. Each time it's called, cypress-rails does two
|
|
135
|
+
things before responding:
|
|
136
|
+
|
|
137
|
+
* If `CYPRESS_RAILS_TRANSACTIONAL_SERVER` is enabled, roll back the transaction
|
|
138
|
+
and start a new one, resetting the database to how it was when the test run
|
|
139
|
+
started, then trigger any `after_transaction_start` hooks
|
|
140
|
+
* Trigger any `after_state_reset` hooks you've configured (regardless of the
|
|
141
|
+
transactional server setting)
|
|
142
|
+
|
|
143
|
+
If a hook raises an error, the request fails with a 500 status and the error
|
|
144
|
+
message, so the `cy.request` that triggered the reset fails too.
|
|
141
145
|
|
|
142
146
|
This way, you can easily instruct the server to reset its test state from your
|
|
143
147
|
Cypress tests like so:
|
|
@@ -151,6 +155,39 @@ beforeEach(() => {
|
|
|
151
155
|
(Remember, in Cypress, `before` is a before-all hook and `beforeEach` is run
|
|
152
156
|
between each test case!)
|
|
153
157
|
|
|
158
|
+
### Turning off the transactional server
|
|
159
|
+
|
|
160
|
+
The transactional server funnels all of your app's database queries through a
|
|
161
|
+
single connection, one at a time, just like a Rails system test. If your tests
|
|
162
|
+
need your app to access the database concurrently, or your app does work that
|
|
163
|
+
can't happen inside a transaction, set `CYPRESS_RAILS_TRANSACTIONAL_SERVER=false`
|
|
164
|
+
and reset your data yourself in an `after_state_reset` hook. For example, with
|
|
165
|
+
[database_cleaner](https://github.com/DatabaseCleaner/database_cleaner) and
|
|
166
|
+
Rails fixtures:
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
CypressRails.hooks.after_state_reset do
|
|
170
|
+
DatabaseCleaner[:active_record].clean_with(:truncation)
|
|
171
|
+
ActiveRecord::FixtureSet.reset_cache
|
|
172
|
+
ActiveRecord::FixtureSet.create_fixtures(Rails.root.join("test/fixtures"), ["users", "posts"])
|
|
173
|
+
end
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
A few things to watch out for:
|
|
177
|
+
|
|
178
|
+
* Don't use database_cleaner's `:transaction` strategy. It only opens a
|
|
179
|
+
transaction on one of the server's database connections, so requests served
|
|
180
|
+
on other connections save their changes for real, and those changes survive
|
|
181
|
+
the reset. (On SQLite, the open transaction also makes other connections'
|
|
182
|
+
writes fail with "database is locked" errors.)
|
|
183
|
+
* Don't set `DatabaseCleaner.strategy = ...` at the top of an initializer.
|
|
184
|
+
Before ActiveRecord has loaded, that assignment is silently ignored and
|
|
185
|
+
database_cleaner's default `:transaction` strategy is used instead.
|
|
186
|
+
* Truncation removes everything, including data loaded in
|
|
187
|
+
`before_server_start`, so rebuild whatever your tests need in the hook. Call
|
|
188
|
+
`ActiveRecord::FixtureSet.reset_cache` before reloading fixtures, or they'll
|
|
189
|
+
be skipped as already loaded.
|
|
190
|
+
|
|
154
191
|
## Configuration
|
|
155
192
|
|
|
156
193
|
### Environment variables
|
|
@@ -171,6 +208,12 @@ preferred environment variables project-wide using a tool like
|
|
|
171
208
|
requests to the app (e.g. via `cy.visit()`). If you've customized your
|
|
172
209
|
`baseUrl` setting (e.g. in `cypress.config.js`), you'll need to duplicate it with
|
|
173
210
|
this environment variable
|
|
211
|
+
* **CYPRESS_RAILS_TRANSACTIONAL_SERVER** (default: `true`) when true, starts a
|
|
212
|
+
transaction before launching the server and rolls it back on every reset and
|
|
213
|
+
on exit, so anything done during `cypress open` or `cypress run` is undone
|
|
214
|
+
(similar to running a Rails system test). While it's enabled, all of the
|
|
215
|
+
server's database access shares one connection. See [Turning off the
|
|
216
|
+
transactional server](#turning-off-the-transactional-server)
|
|
174
217
|
* **CYPRESS_RAILS_CYPRESS_OPTS** (default: _none_) any options you want to
|
|
175
218
|
forward to the Cypress CLI when running its `open` or `run` commands.
|
|
176
219
|
|
|
@@ -196,32 +239,40 @@ $ CYPRESS_RAILS_CYPRESS_OPTS="--browser chromium" bin/rake cypress:run
|
|
|
196
239
|
### before_server_start
|
|
197
240
|
|
|
198
241
|
Pass a block to `CypressRails.hooks.before_server_start` to register a hook that
|
|
199
|
-
will execute before the server has been started. If you use
|
|
200
|
-
may make sense to load them here, so they
|
|
201
|
-
|
|
202
|
-
you're using (e.g. `DatabaseCleaner.start`).
|
|
242
|
+
will execute before the server or any transaction has been started. If you use
|
|
243
|
+
Rails fixtures, it may make sense to load them here, so they persist across
|
|
244
|
+
resets instead of being re-inserted each time.
|
|
203
245
|
|
|
204
246
|
### after_server_start
|
|
205
247
|
|
|
206
248
|
Pass a block to `CypressRails.hooks.after_server_start` to register a hook that
|
|
207
|
-
will execute after the server has booted
|
|
249
|
+
will execute after the server has booted (inside the transaction, if
|
|
250
|
+
`CYPRESS_RAILS_TRANSACTIONAL_SERVER` is enabled).
|
|
251
|
+
|
|
252
|
+
### after_transaction_start
|
|
253
|
+
|
|
254
|
+
If there's any custom behavior or state management you want to do inside the
|
|
255
|
+
transaction (so that it's also rolled back each time a reset is triggered),
|
|
256
|
+
pass a block to `CypressRails.hooks.after_transaction_start`. It runs when the
|
|
257
|
+
server launches and after every reset, and only when
|
|
258
|
+
`CYPRESS_RAILS_TRANSACTIONAL_SERVER` is enabled.
|
|
208
259
|
|
|
209
|
-
###
|
|
260
|
+
### after_state_reset
|
|
210
261
|
|
|
211
262
|
Every time the test server receives an HTTP request at
|
|
212
|
-
`/cypress_rails_reset_state`, the
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
To set up the hook, pass a block to `CypressRails.hooks.after_reset_requested`.
|
|
263
|
+
`/cypress_rails_reset_state`, the transaction will be rolled back and restarted
|
|
264
|
+
(if `CYPRESS_RAILS_TRANSACTIONAL_SERVER` is enabled) and then the
|
|
265
|
+
`after_state_reset` hook will be triggered, all before the request responds. To
|
|
266
|
+
set up the hook, pass a block to `CypressRails.hooks.after_state_reset`.
|
|
217
267
|
|
|
218
268
|
### before_server_stop
|
|
219
269
|
|
|
220
270
|
In case you've made any permanent changes to your test database that could
|
|
221
271
|
pollute other test suites or scripts, you can use the `before_server_stop` to
|
|
222
272
|
(assuming everything exits gracefully) clean things up and restore the state
|
|
223
|
-
of your test database.
|
|
224
|
-
|
|
273
|
+
of your test database. If `CYPRESS_RAILS_TRANSACTIONAL_SERVER` is enabled, this
|
|
274
|
+
runs after the transaction has been rolled back. To set up the hook, pass a
|
|
275
|
+
block to `CypressRails.hooks.before_server_stop`.
|
|
225
276
|
|
|
226
277
|
## Configuring Rails
|
|
227
278
|
|
data/cypress-rails.gemspec
CHANGED
|
@@ -16,17 +16,19 @@ Gem::Specification.new do |spec|
|
|
|
16
16
|
# Specify which files should be added to the gem when it is released.
|
|
17
17
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
18
18
|
spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do
|
|
19
|
-
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|example)/}) }
|
|
19
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|example|docs)/}) }
|
|
20
20
|
end
|
|
21
21
|
spec.bindir = "exe"
|
|
22
22
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
23
23
|
spec.require_paths = ["lib"]
|
|
24
24
|
|
|
25
|
-
spec.add_dependency "railties", ">=
|
|
25
|
+
spec.add_dependency "railties", ">= 7.1"
|
|
26
26
|
spec.add_dependency "puma", ">= 3.8.0"
|
|
27
27
|
|
|
28
28
|
spec.add_development_dependency "bundler"
|
|
29
29
|
spec.add_development_dependency "rake", "~> 13.0"
|
|
30
30
|
spec.add_development_dependency "minitest", "~> 5.0"
|
|
31
31
|
spec.add_development_dependency "standard", ">= 0.2.0"
|
|
32
|
+
spec.add_development_dependency "activerecord"
|
|
33
|
+
spec.add_development_dependency "sqlite3"
|
|
32
34
|
end
|
data/lib/cypress-rails/config.rb
CHANGED
|
@@ -2,7 +2,7 @@ require_relative "env"
|
|
|
2
2
|
|
|
3
3
|
module CypressRails
|
|
4
4
|
class Config
|
|
5
|
-
attr_accessor :rails_dir, :cypress_dir, :host, :port, :base_path, :cypress_cli_opts
|
|
5
|
+
attr_accessor :rails_dir, :cypress_dir, :host, :port, :base_path, :transactional_server, :cypress_cli_opts
|
|
6
6
|
|
|
7
7
|
def initialize(
|
|
8
8
|
rails_dir: Env.fetch("CYPRESS_RAILS_DIR", default: Dir.pwd),
|
|
@@ -10,6 +10,7 @@ module CypressRails
|
|
|
10
10
|
host: Env.fetch("CYPRESS_RAILS_HOST", default: "127.0.0.1"),
|
|
11
11
|
port: Env.fetch("CYPRESS_RAILS_PORT"),
|
|
12
12
|
base_path: Env.fetch("CYPRESS_RAILS_BASE_PATH", default: "/"),
|
|
13
|
+
transactional_server: Env.fetch("CYPRESS_RAILS_TRANSACTIONAL_SERVER", type: :boolean, default: true),
|
|
13
14
|
cypress_cli_opts: Env.fetch("CYPRESS_RAILS_CYPRESS_OPTS", default: "")
|
|
14
15
|
)
|
|
15
16
|
@rails_dir = rails_dir
|
|
@@ -17,6 +18,7 @@ module CypressRails
|
|
|
17
18
|
@host = host
|
|
18
19
|
@port = port
|
|
19
20
|
@base_path = base_path
|
|
21
|
+
@transactional_server = transactional_server
|
|
20
22
|
@cypress_cli_opts = cypress_cli_opts
|
|
21
23
|
end
|
|
22
24
|
|
|
@@ -30,6 +32,7 @@ module CypressRails
|
|
|
30
32
|
CYPRESS_RAILS_HOST....................#{host.inspect}
|
|
31
33
|
CYPRESS_RAILS_PORT....................#{port.inspect}
|
|
32
34
|
CYPRESS_RAILS_BASE_PATH...............#{base_path.inspect}
|
|
35
|
+
CYPRESS_RAILS_TRANSACTIONAL_SERVER....#{transactional_server.inspect}
|
|
33
36
|
CYPRESS_RAILS_CYPRESS_OPTS............#{cypress_cli_opts.inspect}
|
|
34
37
|
|
|
35
38
|
DESC
|
|
@@ -16,8 +16,12 @@ module CypressRails
|
|
|
16
16
|
register(:after_server_start, blk)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
def
|
|
20
|
-
register(:
|
|
19
|
+
def after_transaction_start(&blk)
|
|
20
|
+
register(:after_transaction_start, blk)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def after_state_reset(&blk)
|
|
24
|
+
register(:after_state_reset, blk)
|
|
21
25
|
end
|
|
22
26
|
|
|
23
27
|
def before_server_stop(&blk)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require_relative "finds_bin"
|
|
2
2
|
require_relative "config"
|
|
3
3
|
require_relative "initializer_hooks"
|
|
4
|
+
require_relative "manages_transactions"
|
|
4
5
|
require_relative "starts_rails_server"
|
|
5
6
|
|
|
6
7
|
module CypressRails
|
|
@@ -14,9 +15,14 @@ module CypressRails
|
|
|
14
15
|
def call(command, config)
|
|
15
16
|
puts config
|
|
16
17
|
@initializer_hooks.run(:before_server_start)
|
|
18
|
+
if config.transactional_server
|
|
19
|
+
ManagesTransactions.instance.begin_transaction
|
|
20
|
+
@initializer_hooks.run(:after_transaction_start)
|
|
21
|
+
end
|
|
17
22
|
server = @starts_rails_server.call(
|
|
18
23
|
host: config.host,
|
|
19
|
-
port: config.port
|
|
24
|
+
port: config.port,
|
|
25
|
+
transactional_server: config.transactional_server
|
|
20
26
|
)
|
|
21
27
|
bin = @finds_bin.call(config.cypress_dir)
|
|
22
28
|
|
|
@@ -46,6 +52,13 @@ module CypressRails
|
|
|
46
52
|
@at_exit_hooks_have_fired ||= false # avoid warning
|
|
47
53
|
return if @at_exit_hooks_have_fired
|
|
48
54
|
|
|
55
|
+
if config.transactional_server
|
|
56
|
+
begin
|
|
57
|
+
ManagesTransactions.instance.rollback_transaction
|
|
58
|
+
rescue => e
|
|
59
|
+
warn e.full_message
|
|
60
|
+
end
|
|
61
|
+
end
|
|
49
62
|
@initializer_hooks.run(:before_server_stop)
|
|
50
63
|
|
|
51
64
|
@at_exit_hooks_have_fired = true
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
module CypressRails
|
|
2
|
+
class ManagesTransactions
|
|
3
|
+
def self.instance
|
|
4
|
+
@instance ||= new
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
module MinitestLifecycle
|
|
8
|
+
def before_setup
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def after_teardown
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def begin_transaction
|
|
16
|
+
return unless defined?(ActiveRecord::TestFixtures)
|
|
17
|
+
|
|
18
|
+
# A connection this thread already holds wouldn't be the pinned one
|
|
19
|
+
release_connections_held_by_this_thread
|
|
20
|
+
on_owner_thread { fixtures.before_setup }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def rollback_transaction
|
|
24
|
+
return unless defined?(ActiveRecord::TestFixtures)
|
|
25
|
+
|
|
26
|
+
begin
|
|
27
|
+
on_owner_thread { fixtures.after_teardown }
|
|
28
|
+
ensure
|
|
29
|
+
release_connections_held_by_this_thread
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def initialize
|
|
36
|
+
@jobs = Queue.new
|
|
37
|
+
# Rails 7.1 keys a pinned connection to the thread that pinned it, and a
|
|
38
|
+
# Puma thread's end-of-request cleanup would check it back in, so a
|
|
39
|
+
# dedicated thread that never serves requests owns the transaction.
|
|
40
|
+
@owner = Thread.new do
|
|
41
|
+
loop do
|
|
42
|
+
job, result = @jobs.pop
|
|
43
|
+
begin
|
|
44
|
+
result << [:ok, job.call]
|
|
45
|
+
rescue Exception => e # standard:disable Lint/RescueException
|
|
46
|
+
result << [:error, e]
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def on_owner_thread(&job)
|
|
53
|
+
result = Queue.new
|
|
54
|
+
@jobs << [job, result]
|
|
55
|
+
status, value = result.pop
|
|
56
|
+
raise value if status == :error
|
|
57
|
+
value
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def release_connections_held_by_this_thread
|
|
61
|
+
ActiveRecord::Base.connection_handler.clear_active_connections!(:all)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def fixtures
|
|
65
|
+
@fixtures ||= Class.new {
|
|
66
|
+
# Must be included before TestFixtures, or its hooks never run
|
|
67
|
+
include MinitestLifecycle
|
|
68
|
+
include ActiveRecord::TestFixtures
|
|
69
|
+
|
|
70
|
+
def name
|
|
71
|
+
"cypress-rails"
|
|
72
|
+
end
|
|
73
|
+
}.new
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -1,14 +1,43 @@
|
|
|
1
1
|
require_relative "config"
|
|
2
2
|
require_relative "initializer_hooks"
|
|
3
|
+
require_relative "manages_transactions"
|
|
3
4
|
|
|
4
5
|
module CypressRails
|
|
5
6
|
class ResetsState
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
LOCK = Mutex.new
|
|
8
|
+
|
|
9
|
+
def initialize(
|
|
10
|
+
transactional_server:,
|
|
11
|
+
manages_transactions: nil,
|
|
12
|
+
initializer_hooks: InitializerHooks.instance,
|
|
13
|
+
wrap: ->(&block) { Rails.application.reloader.wrap(&block) }
|
|
14
|
+
)
|
|
15
|
+
@transactional_server = transactional_server
|
|
16
|
+
@manages_transactions = manages_transactions
|
|
17
|
+
@initializer_hooks = initializer_hooks
|
|
18
|
+
@wrap = wrap
|
|
8
19
|
end
|
|
9
20
|
|
|
10
21
|
def call
|
|
11
|
-
|
|
22
|
+
LOCK.synchronize do
|
|
23
|
+
if @transactional_server
|
|
24
|
+
# Outside the reloader: waiting on the owner thread while holding a
|
|
25
|
+
# reloader share could deadlock against a code reload
|
|
26
|
+
manages_transactions.rollback_transaction
|
|
27
|
+
manages_transactions.begin_transaction
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
@wrap.call do
|
|
31
|
+
@initializer_hooks.run(:after_transaction_start) if @transactional_server
|
|
32
|
+
@initializer_hooks.run(:after_state_reset)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def manages_transactions
|
|
40
|
+
@manages_transactions ||= ManagesTransactions.instance
|
|
12
41
|
end
|
|
13
42
|
end
|
|
14
43
|
end
|
data/lib/cypress-rails/server.rb
CHANGED
|
@@ -23,7 +23,9 @@ module CypressRails
|
|
|
23
23
|
host:,
|
|
24
24
|
port:,
|
|
25
25
|
reportable_errors: [Exception],
|
|
26
|
-
extra_middleware: []
|
|
26
|
+
extra_middleware: [],
|
|
27
|
+
initializer_hooks: InitializerHooks.instance,
|
|
28
|
+
puma: Puma)
|
|
27
29
|
@app = app
|
|
28
30
|
@extra_middleware = extra_middleware
|
|
29
31
|
@server_thread = nil # suppress warnings
|
|
@@ -33,7 +35,8 @@ module CypressRails
|
|
|
33
35
|
@port ||= Server.ports[port_key]
|
|
34
36
|
@port ||= find_available_port(host)
|
|
35
37
|
@checker = Checker.new(@host, @port)
|
|
36
|
-
@initializer_hooks =
|
|
38
|
+
@initializer_hooks = initializer_hooks
|
|
39
|
+
@puma = puma
|
|
37
40
|
end
|
|
38
41
|
|
|
39
42
|
def reset_error!
|
|
@@ -72,7 +75,7 @@ module CypressRails
|
|
|
72
75
|
Server.ports[port_key] = port
|
|
73
76
|
|
|
74
77
|
@server_thread = Thread.new {
|
|
75
|
-
|
|
78
|
+
@puma.create(middleware, port, host)
|
|
76
79
|
}
|
|
77
80
|
|
|
78
81
|
timer = Timer.new(60)
|
|
@@ -80,8 +83,10 @@ module CypressRails
|
|
|
80
83
|
raise "Rack application timed out during boot" if timer.expired?
|
|
81
84
|
|
|
82
85
|
@server_thread.join(0.1)
|
|
83
|
-
@initializer_hooks.run(:after_server_start)
|
|
84
86
|
end
|
|
87
|
+
|
|
88
|
+
# Once the server is up, so slow hooks don't count against the timeout
|
|
89
|
+
@initializer_hooks.run(:after_server_start)
|
|
85
90
|
end
|
|
86
91
|
|
|
87
92
|
self
|
|
@@ -1,28 +1,26 @@
|
|
|
1
|
-
require_relative "
|
|
1
|
+
require_relative "resets_state"
|
|
2
2
|
require_relative "server"
|
|
3
3
|
|
|
4
4
|
module CypressRails
|
|
5
5
|
class StartsRailsServer
|
|
6
|
-
def call(host:, port:)
|
|
7
|
-
|
|
8
|
-
app = create_rack_app
|
|
6
|
+
def call(host:, port:, transactional_server:)
|
|
7
|
+
app = create_rack_app(transactional_server)
|
|
9
8
|
Server.new(app, host: host, port: port).tap do |server|
|
|
10
9
|
server.boot
|
|
11
10
|
end
|
|
12
11
|
end
|
|
13
12
|
|
|
14
|
-
def
|
|
15
|
-
Rails.application.executor.to_run do
|
|
16
|
-
TracksResets.instance.reset_state_if_needed
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def create_rack_app
|
|
13
|
+
def create_rack_app(transactional_server)
|
|
21
14
|
Rack::Builder.new do
|
|
22
15
|
map "/cypress_rails_reset_state" do
|
|
23
16
|
run lambda { |env|
|
|
24
|
-
|
|
25
|
-
|
|
17
|
+
begin
|
|
18
|
+
ResetsState.new(transactional_server: transactional_server).call
|
|
19
|
+
[200, {"content-type" => "text/plain"}, ["Reset"]]
|
|
20
|
+
rescue => e
|
|
21
|
+
warn e.full_message
|
|
22
|
+
[500, {"content-type" => "text/plain"}, ["#{e.class}: #{e.message}"]]
|
|
23
|
+
end
|
|
26
24
|
}
|
|
27
25
|
end
|
|
28
26
|
map "/" do
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: cypress-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Justin Searls
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version:
|
|
18
|
+
version: '7.1'
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version:
|
|
25
|
+
version: '7.1'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: puma
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -93,6 +93,34 @@ dependencies:
|
|
|
93
93
|
- - ">="
|
|
94
94
|
- !ruby/object:Gem::Version
|
|
95
95
|
version: 0.2.0
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: activerecord
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - ">="
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '0'
|
|
103
|
+
type: :development
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - ">="
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '0'
|
|
110
|
+
- !ruby/object:Gem::Dependency
|
|
111
|
+
name: sqlite3
|
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - ">="
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '0'
|
|
117
|
+
type: :development
|
|
118
|
+
prerelease: false
|
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - ">="
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '0'
|
|
96
124
|
email:
|
|
97
125
|
- searls@gmail.com
|
|
98
126
|
executables:
|
|
@@ -120,6 +148,7 @@ files:
|
|
|
120
148
|
- lib/cypress-rails/init.rb
|
|
121
149
|
- lib/cypress-rails/initializer_hooks.rb
|
|
122
150
|
- lib/cypress-rails/launches_cypress.rb
|
|
151
|
+
- lib/cypress-rails/manages_transactions.rb
|
|
123
152
|
- lib/cypress-rails/open.rb
|
|
124
153
|
- lib/cypress-rails/railtie.rb
|
|
125
154
|
- lib/cypress-rails/rake.rb
|
|
@@ -131,7 +160,6 @@ files:
|
|
|
131
160
|
- lib/cypress-rails/server/puma.rb
|
|
132
161
|
- lib/cypress-rails/server/timer.rb
|
|
133
162
|
- lib/cypress-rails/starts_rails_server.rb
|
|
134
|
-
- lib/cypress-rails/tracks_resets.rb
|
|
135
163
|
- lib/cypress-rails/version.rb
|
|
136
164
|
- script/test
|
|
137
165
|
- script/test_example_app
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
require_relative "resets_state"
|
|
2
|
-
|
|
3
|
-
module CypressRails
|
|
4
|
-
class TracksResets
|
|
5
|
-
def self.instance
|
|
6
|
-
@instance ||= new
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
def reset_needed!
|
|
10
|
-
@reset_needed = true
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def reset_state_if_needed
|
|
14
|
-
if @reset_needed
|
|
15
|
-
ResetsState.new.call
|
|
16
|
-
@reset_needed = false
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
private
|
|
21
|
-
|
|
22
|
-
def initialize
|
|
23
|
-
@reset_needed = false
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|