fixture_champagne 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +267 -0
- data/Rakefile +20 -0
- data/lib/fixture_champagne/migration.rb +78 -0
- data/lib/fixture_champagne/migration_context.rb +212 -0
- data/lib/fixture_champagne/migrator.rb +326 -0
- data/lib/fixture_champagne/railtie.rb +12 -0
- data/lib/fixture_champagne/test_fixtures.rb +51 -0
- data/lib/fixture_champagne/version.rb +5 -0
- data/lib/fixture_champagne.rb +35 -0
- data/lib/generators/fixture_champagne/install_generator.rb +22 -0
- data/lib/generators/fixture_champagne/migration_generator.rb +66 -0
- data/lib/generators/fixture_champagne/templates/migration.rb.tt +9 -0
- data/lib/tasks/fixture_champagne_tasks.rake +15 -0
- metadata +76 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 9c999bd5772d62cce811ec251c0a6703a58bd7daf7b31d6d00ed08fe80f9133f
|
4
|
+
data.tar.gz: 1bb147d5724d90fd9341b480b449218e71827ab00e694bc6c10d1ffaa99bda59
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b0a14630348c2e463ca683801cc72be51e9e8b65466a1fb73143072a486979991484dc6d2c80fe081287821d4afa8570c45ec4f33c13f4d2996a22c2202021f4
|
7
|
+
data.tar.gz: 6c6e4d55bf6654cee801ca4545f052351f4ef81505921240e1e007d362e12e9c050f22cc3f9305c566b8946b7fd81831309dce87f00619266c35bd2263cf3bd5
|
data/README.md
ADDED
@@ -0,0 +1,267 @@
|
|
1
|
+
# Fixture Champagne :champagne:
|
2
|
+
|
3
|
+
### Fixture migrations for your Ruby on Rails applications
|
4
|
+
|
5
|
+
|
6
|
+
Fixture Champagne is designed to help you keep your fixtures tidy, applying the data migration pattern to create, update or destroy fixtures.
|
7
|
+
|
8
|
+
It supports label references for `belongs_to` associations, both regular and polymorphic, single table inheritance, enums and all the different data types.
|
9
|
+
|
10
|
+
|
11
|
+
## Installation
|
12
|
+
|
13
|
+
1. Add `fixture-champagne` to the development group of your Rails app's `Gemfile`:
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
group :development do
|
17
|
+
gem 'fixture-champagne'
|
18
|
+
end
|
19
|
+
```
|
20
|
+
|
21
|
+
2. Then, in your project directory:
|
22
|
+
|
23
|
+
```sh
|
24
|
+
# Download and install
|
25
|
+
$ bundle install
|
26
|
+
|
27
|
+
# Generate fixture_migrations folder in your test or spec folder, depending on your test suite
|
28
|
+
$ bin/rails generate fixture_champagne:install
|
29
|
+
```
|
30
|
+
|
31
|
+
|
32
|
+
## Usage
|
33
|
+
|
34
|
+
|
35
|
+
### Sync fixtures with schema
|
36
|
+
|
37
|
+
If your schema version changed and you need to add any new column to the current fixtures, simply run:
|
38
|
+
|
39
|
+
```sh
|
40
|
+
bin/rails fixture_champagne:migrate
|
41
|
+
```
|
42
|
+
|
43
|
+
The migration process will regenarate your `fixtures` folder.
|
44
|
+
|
45
|
+
|
46
|
+
### Add, update or destroy fixtures
|
47
|
+
|
48
|
+
If you need specific values for the any new columns or you want to populate a newly created table, you might find it useful to create a fixture migration. This can be done using the generator:
|
49
|
+
|
50
|
+
```sh
|
51
|
+
bin/rails generate fixture_champagne:migration new_migration_name
|
52
|
+
```
|
53
|
+
|
54
|
+
A new versioned migration file will be created in the `fixture_migrations` folder. If this is your first migration, make sure that folder exists or run the installation command.
|
55
|
+
|
56
|
+
`ActiveRecord` queries and fixture accessors can be used inside the migrations. For example, let's suppose you've just added the `Enemy` model and you need to create a new enemy fixture having the following files:
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
# models/level.rb
|
60
|
+
|
61
|
+
class Level < ApplicationRecord
|
62
|
+
has_many :enemies
|
63
|
+
end
|
64
|
+
|
65
|
+
# models/enemy.rb
|
66
|
+
|
67
|
+
class Enemy < ApplicationRecord
|
68
|
+
belongs_to :level
|
69
|
+
end
|
70
|
+
```
|
71
|
+
|
72
|
+
```yaml
|
73
|
+
# test/fixtures/levels.yml
|
74
|
+
|
75
|
+
first_level:
|
76
|
+
name: Initial
|
77
|
+
```
|
78
|
+
|
79
|
+
You can then generate a new migration:
|
80
|
+
|
81
|
+
```sh
|
82
|
+
bin/rails generate fixture_champagne:migration create_initial_enemy
|
83
|
+
```
|
84
|
+
|
85
|
+
The generator automatically adds a version number to the new migration file, which is important to keep track of executed migrations. Also, the migration filename must correspond with the migration class inside the file. All this should feel similar to the way schema migrations are handled by Rails.
|
86
|
+
|
87
|
+
Add the `up` and `down` logic to the new migration:
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
# 20230126153650_create_initial_enemy.rb
|
91
|
+
|
92
|
+
class CreateInitialEnemy < FixtureChampagne::Migration::Base
|
93
|
+
def up
|
94
|
+
unless Enemy.find_by(name: "Initial Enemy").present?
|
95
|
+
Enemy.create!(name: "Initial Enemy", level: levels(:first_level))
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def down
|
100
|
+
Enemy.find_by(name: "Initial Enemy").destroy!
|
101
|
+
end
|
102
|
+
end
|
103
|
+
```
|
104
|
+
|
105
|
+
Running `bin/rails fixture_champagne:migrate` will execute the `up` method of all pending migrations in ascending version order to update the test database. The `fixtures` folder is regenerated at the end of the process only if all the migrations were successfully executed. In this case, it would generate the following file:
|
106
|
+
|
107
|
+
```yaml
|
108
|
+
# test/fixtures/enemies.yml
|
109
|
+
|
110
|
+
enemies_12345678:
|
111
|
+
level: first_level
|
112
|
+
name: Initial Enemy
|
113
|
+
```
|
114
|
+
|
115
|
+
If the migration is successful, the migrator will take the max version all available migrations and the current schema version and save both numbers in `test/.fixture_champagne_versions.yml` (or `spec/` if using Rspec) to identify future pending migrations.
|
116
|
+
|
117
|
+
The default label for a new fixture is a unique identifier composed of the table name and the record id. However, this label can be configured (keep reading to know how).
|
118
|
+
|
119
|
+
|
120
|
+
### Rollback
|
121
|
+
|
122
|
+
You can optionally complete the `down` method in the migration to allow rollback. Running the following command will rollback the last executed migration:
|
123
|
+
|
124
|
+
```sh
|
125
|
+
bin/rails fixture_champagne:rollback
|
126
|
+
```
|
127
|
+
|
128
|
+
New max version will be set to the next one in descending order. Schema version won't change. Any changes in the configuration apply to both `migrate` or `rollback`.
|
129
|
+
|
130
|
+
|
131
|
+
### Configuration
|
132
|
+
|
133
|
+
You can better control fixture migrations by creating a config YAML file: `test/fixture_champagne.yml` (or, again, `spec/`).
|
134
|
+
|
135
|
+
#### Overwrite current fixtures
|
136
|
+
|
137
|
+
Setting the `overwrite` key to `false` will leave your current fixtures untouched. The generated fixtures will go to `tmp/fixtures`. Default value is set to `true`.
|
138
|
+
|
139
|
+
```yaml
|
140
|
+
# test/fixture_champagne.yml
|
141
|
+
|
142
|
+
overwrite: false
|
143
|
+
```
|
144
|
+
|
145
|
+
#### Fixture labels
|
146
|
+
|
147
|
+
Setting the `label` key will allow you to control the names your fixtures get. It accepts a hash, where keys are table names and values are label templates: strings interpolated with a I18n style syntax. Interpolated keywords must be instance methods or attributes.
|
148
|
+
|
149
|
+
In the previous example, you can configure:
|
150
|
+
|
151
|
+
```yaml
|
152
|
+
# test/fixture_champagne.yml
|
153
|
+
|
154
|
+
label:
|
155
|
+
enemy: "%{name}"
|
156
|
+
```
|
157
|
+
|
158
|
+
To generate:
|
159
|
+
|
160
|
+
```yaml
|
161
|
+
# test/fixtures/enemies.yml
|
162
|
+
|
163
|
+
initial_enemy:
|
164
|
+
level: first_level
|
165
|
+
name: Initial Enemy
|
166
|
+
```
|
167
|
+
|
168
|
+
#### Rename current fixtures
|
169
|
+
|
170
|
+
Setting the `rename` key to `true` will force every fixture label to follow the templates in the configuration. Default value is `false`. If any table is not configured, the default label will be used (something like `%{table_name}_%{id}` if `table_name` was an instance method).
|
171
|
+
|
172
|
+
```yaml
|
173
|
+
# test/fixture_champagne.yml
|
174
|
+
|
175
|
+
rename: true
|
176
|
+
```
|
177
|
+
|
178
|
+
If `rename` is set to `true`, every time you run `migrate` or `rollback` all fixtures will be regenerated in the corresponding folder (depending on `overwrite`), even if there's no pending migrations or schema version is up to date. Keep in mind that a renaming might break the fixture accessors in your tests or previous migrations. It could also break unsupported attachment fixtures.
|
179
|
+
|
180
|
+
#### Ignore tables
|
181
|
+
|
182
|
+
Setting the `ignore` key will allow you to control which tables get saved as fixtures. It accepts an array, where items are table names. Any table ignored by this configuration will disappear from the corresponding fixture folder (depending on `overwrite`).
|
183
|
+
|
184
|
+
Let's say for example that each time a new `Enemy` gets created, it creates an associated `Event` in a callback that runs some processing in the background. If that event belongs to a polymorphic `eventable`, for every single one of those, a new event will be added to your fixtures, making the `events.yml` a big but not very useful file. Or maybe events get incinerated a couple of days after execution and it makes no sense to have fixtures for them. In any of those situations, you could ignore them from fixtures like this:
|
185
|
+
|
186
|
+
```yaml
|
187
|
+
# test/fixture_champagne.yml
|
188
|
+
|
189
|
+
ignore:
|
190
|
+
- events
|
191
|
+
```
|
192
|
+
|
193
|
+
This configuration does not change the shape of the database after the migrations as the database transactions are left untouched (for example, events will be created anyway) but next time fixtures get loaded, no item from ignored tables will be present. This could break the integrity of your database, so make sure everything is working afterwards.
|
194
|
+
|
195
|
+
|
196
|
+
### Manually adding or editing fixtures
|
197
|
+
|
198
|
+
Nothing prevents you from manually editing your fixture files. Take into account that the next time that you run migrations, what's on your fixtures will define the initial state of your migration database, which could break previous migrations or rollbacks (in the rare case that you need to run them again). The next time you run `migrate`, the migrator will tidy the information you added manually.
|
199
|
+
|
200
|
+
|
201
|
+
### Generated fixtures folder structure
|
202
|
+
|
203
|
+
On namespaced models, the migrator will create a folder for each level and a `.yml` file for the last one. For example, `Level::Enemy` fixtures will be saved in `fixtures/level/enemies.yml`.
|
204
|
+
|
205
|
+
If you use single table inheritance, then the file will correspond with the parent model, the owner of the table. For example, `class Weapon::Rocket < Weapon; end` will be saved in `fixtures/weapons.yml`.
|
206
|
+
|
207
|
+
All fixtures files that correspond to attachments will be copied as they are. Those are the ones located in `fixtures/files`, `fixtures/active_storage` and `fixtures/action_text`.
|
208
|
+
|
209
|
+
|
210
|
+
## Features currently not supported
|
211
|
+
|
212
|
+
The following fixture features are not supported:
|
213
|
+
- More than one test suite in the same application
|
214
|
+
- Dynamic ERB fixtures (considered a code smell in the [Rails documentation](https://edgeapi.rubyonrails.org/classes/ActiveRecord/FixtureSet.html))
|
215
|
+
- Explicit `created_at` or `updated_at` timestamps (favoured autofilled ones)
|
216
|
+
- Explicit `id` (favoured label references)
|
217
|
+
- Fixture label interpolation (favoured configuration)
|
218
|
+
- HABTM (`have_and_belong_to_many`) associations as inline lists
|
219
|
+
- Support for YAML defaults (this could be nice)
|
220
|
+
|
221
|
+
As stated before, at least for now, fixtures files that correspond to attachments will be copied as they are. This means:
|
222
|
+
- This fixtures must be generated manually
|
223
|
+
- This fixtures must be updated manually if other fixtures labels change
|
224
|
+
- All this files will be left untouched
|
225
|
+
|
226
|
+
## A few soft recommendations
|
227
|
+
|
228
|
+
|
229
|
+
#### Don't have too many fixtures
|
230
|
+
|
231
|
+
The goal of this gem is to make it easier to keep fixtures tidy and up to date as things start to get complicated, so that factories aren't your only option. But no gem can replace good ol' discipline. If a new fixture gets added for every single small feature or bugfix, maintenance will be hard no matter the tool.
|
232
|
+
|
233
|
+
Reduce repetition, reuse fixtures using helpers to modify them in the tests or use factories for some of your tests.
|
234
|
+
|
235
|
+
#### Make your migrations idempotent
|
236
|
+
|
237
|
+
Versions saved in `.fixture_champagne_versions.yml` are there to ensure that your migrations are only executed once, but it would be a good idea to design your migrations to be idempotent, meaning that executing them more than once does not change the results.
|
238
|
+
|
239
|
+
#### Raise errors
|
240
|
+
|
241
|
+
Raise errors to stop the migration if there are invalid objects. A good way to do that is using `ActiveRecord` bang methods `create!`, `update!` and `destroy!`.
|
242
|
+
|
243
|
+
#### Review changes before git commits
|
244
|
+
|
245
|
+
The safest way to rollback a migration is to revert any changes made to your `fixtures` folder and versions file using git. After migrating, inspect the changes made to the fixture folder and run the whole test suite.
|
246
|
+
|
247
|
+
|
248
|
+
## Contributing
|
249
|
+
|
250
|
+
Feel free to open an issue if you have any doubt, suggestion or find buggy behaviour. If it's a bug, it's always great if you can provide a minimum Rails app that reproduces the issue.
|
251
|
+
|
252
|
+
This project uses [Rubocop](https://github.com/rubocop/rubocop) to format Ruby code. Please make sure to run `rubocop` on your branch before submitting pull requests. You can do that by running `bundle exec rubocop -A`.
|
253
|
+
|
254
|
+
Also run the tests for each supported Rails version with:
|
255
|
+
```sh
|
256
|
+
bundle exec appraisal rake test
|
257
|
+
```
|
258
|
+
|
259
|
+
|
260
|
+
## License
|
261
|
+
|
262
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
263
|
+
|
264
|
+
|
265
|
+
## Code of Conduct
|
266
|
+
|
267
|
+
Everyone interacting in the FixtureChampagne project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/fixture_champagne/blob/master/CODE_OF_CONDUCT.md).
|
data/Rakefile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
|
5
|
+
require "bundler/gem_tasks"
|
6
|
+
|
7
|
+
require "rdoc/task"
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = "rdoc"
|
11
|
+
rdoc.title = "Fixture Champagne"
|
12
|
+
rdoc.options << "--line-numbers"
|
13
|
+
rdoc.rdoc_files.include("README.md")
|
14
|
+
rdoc.rdoc_files.include("lib/**/*.rb")
|
15
|
+
end
|
16
|
+
|
17
|
+
$LOAD_PATH << File.expand_path("test", __dir__)
|
18
|
+
require "rails/plugin/test"
|
19
|
+
|
20
|
+
task default: :test
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FixtureChampagne
|
4
|
+
# Migration contain naming rules for migrations files and objects
|
5
|
+
class Migration
|
6
|
+
MIGRATION_FILENAME_REGEXP = /\A([0-9]+)_([_a-z0-9]*)\.rb\z/.freeze
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def new_migration_version
|
10
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# Migration::Base is the ancestor of generated fixture migrations.
|
15
|
+
#
|
16
|
+
# Inheriting from Base allows migrations to have access to fixture accessors.
|
17
|
+
class Base
|
18
|
+
attr_reader :version, :migrator
|
19
|
+
|
20
|
+
def initialize(version)
|
21
|
+
@version = version
|
22
|
+
end
|
23
|
+
|
24
|
+
def migrate(direction:, migrator:)
|
25
|
+
@migrator = migrator
|
26
|
+
send(direction)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def method_missing(name, *args, **kwargs, &block)
|
32
|
+
if migrator.pre_existing_fixture_accessors.include?(name.to_s)
|
33
|
+
migrator.send(name, *args, **kwargs, &block)
|
34
|
+
else
|
35
|
+
super
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def respond_to_missing?(name, include_private = false)
|
40
|
+
if include_private && migrator.pre_existing_fixture_accessors.include?(name.to_s)
|
41
|
+
true
|
42
|
+
else
|
43
|
+
super
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
Proxy = Struct.new(:name, :version, :filename) do
|
49
|
+
def initialize(name, version, filename)
|
50
|
+
super
|
51
|
+
@migration = nil
|
52
|
+
end
|
53
|
+
|
54
|
+
def basename
|
55
|
+
File.basename(filename)
|
56
|
+
end
|
57
|
+
|
58
|
+
delegate :migrate, to: :migration
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def migration
|
63
|
+
@migration ||= load_migration
|
64
|
+
end
|
65
|
+
|
66
|
+
def load_migration
|
67
|
+
begin
|
68
|
+
Object.send(:remove_const, name)
|
69
|
+
rescue StandardError
|
70
|
+
nil
|
71
|
+
end
|
72
|
+
|
73
|
+
load(File.expand_path(filename))
|
74
|
+
name.constantize.new(version)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,212 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FixtureChampagne
|
4
|
+
# MigrationContext sets the context in which a fixture migration is run.
|
5
|
+
#
|
6
|
+
# A migration context checks where the application files are located, which could be
|
7
|
+
# in /test if the app uses Minitest or /spec if the app uses Rspec, and from that base
|
8
|
+
# it decides where everything else is located:
|
9
|
+
# - Fixture migrations folder
|
10
|
+
# - Configuration YAML file
|
11
|
+
# - Saved versions YAML file
|
12
|
+
# - Fixtures path
|
13
|
+
#
|
14
|
+
# With all that it decides which migrations are pending, which are executed and the target
|
15
|
+
# versions. Depending on the method called, it also decides the direction the Migrator should execute.
|
16
|
+
class MigrationContext
|
17
|
+
class << self
|
18
|
+
def migrate
|
19
|
+
build_context.migrate
|
20
|
+
end
|
21
|
+
|
22
|
+
def rollback
|
23
|
+
build_context.rollback
|
24
|
+
end
|
25
|
+
|
26
|
+
def build_context
|
27
|
+
new(
|
28
|
+
fixture_migrations_path: fixture_migrations_path,
|
29
|
+
schema_current_version: schema_current_version,
|
30
|
+
fixtures_migration_version: fixture_versions["version"]&.to_i || 0,
|
31
|
+
fixtures_schema_version: fixture_versions["schema_version"]&.to_i || 0,
|
32
|
+
configuration: configuration
|
33
|
+
)
|
34
|
+
end
|
35
|
+
|
36
|
+
def fixture_migrations_path
|
37
|
+
raise MissingMigrationsFolderError unless expected_fixture_migrations_path.exist?
|
38
|
+
|
39
|
+
expected_fixture_migrations_path
|
40
|
+
end
|
41
|
+
|
42
|
+
def expected_fixture_migrations_path
|
43
|
+
test_suite_folder_path.join("fixture_migrations")
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_suite_folder_path
|
47
|
+
rspec_path = Rails.root.join("test")
|
48
|
+
minitest_path = Rails.root.join("spec")
|
49
|
+
|
50
|
+
if minitest_path.exist?
|
51
|
+
minitest_path
|
52
|
+
elsif rspec_path.exist?
|
53
|
+
rspec_path
|
54
|
+
else
|
55
|
+
raise "No test nor spec folder found"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def schema_current_version
|
60
|
+
::ActiveRecord::Migrator.current_version
|
61
|
+
end
|
62
|
+
|
63
|
+
def fixture_versions
|
64
|
+
@fixture_versions ||= if fixture_versions_path.exist?
|
65
|
+
YAML.load_file(fixture_versions_path)
|
66
|
+
else
|
67
|
+
{}
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def fixture_versions_path
|
72
|
+
test_suite_folder_path.join(".fixture_champagne_versions.yml")
|
73
|
+
end
|
74
|
+
|
75
|
+
def configuration
|
76
|
+
@configuration ||= if configuration_path.exist?
|
77
|
+
Configuration.new(YAML.load_file(configuration_path))
|
78
|
+
else
|
79
|
+
Configuration.new({})
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def configuration_path
|
84
|
+
test_suite_folder_path.join("fixture_champagne.yml")
|
85
|
+
end
|
86
|
+
|
87
|
+
def fixtures_path
|
88
|
+
test_suite_folder_path.join("fixtures")
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
attr_reader :fixture_migrations_path, :schema_current_version,
|
93
|
+
:fixtures_migration_version, :fixtures_schema_version, :configuration
|
94
|
+
|
95
|
+
def initialize(
|
96
|
+
fixture_migrations_path:, schema_current_version:,
|
97
|
+
fixtures_migration_version:, fixtures_schema_version:,
|
98
|
+
configuration:
|
99
|
+
)
|
100
|
+
@fixture_migrations_path = fixture_migrations_path
|
101
|
+
@schema_current_version = schema_current_version
|
102
|
+
@fixtures_migration_version = fixtures_migration_version
|
103
|
+
@fixtures_schema_version = fixtures_schema_version
|
104
|
+
@configuration = configuration
|
105
|
+
end
|
106
|
+
|
107
|
+
def migrate
|
108
|
+
# If rename_fixtures? is set to true, the migration should run as some label could have changed.
|
109
|
+
if pending_migrations.any? || fixtures_schema_version != schema_current_version || configuration.rename_fixtures?
|
110
|
+
up
|
111
|
+
else
|
112
|
+
puts "No fixture migrations pending."
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def rollback
|
117
|
+
if executed_migrations.any?
|
118
|
+
down
|
119
|
+
else
|
120
|
+
puts "No migration to rollback."
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def up
|
125
|
+
Migrator.new(
|
126
|
+
direction: :up,
|
127
|
+
migrations: pending_migrations,
|
128
|
+
target_migration_version: up_target_fixture_migration_version,
|
129
|
+
target_schema_version: schema_current_version,
|
130
|
+
configuration: configuration
|
131
|
+
).migrate
|
132
|
+
end
|
133
|
+
|
134
|
+
def down
|
135
|
+
Migrator.new(
|
136
|
+
direction: :down,
|
137
|
+
migrations: [executed_migrations.last],
|
138
|
+
target_migration_version: down_target_fixture_migration_version,
|
139
|
+
target_schema_version: schema_current_version,
|
140
|
+
configuration: configuration
|
141
|
+
).migrate
|
142
|
+
end
|
143
|
+
|
144
|
+
def up_target_fixture_migration_version
|
145
|
+
return fixtures_migration_version if pending_migrations.empty?
|
146
|
+
|
147
|
+
pending_migrations.map(&:version).max
|
148
|
+
end
|
149
|
+
|
150
|
+
def down_target_fixture_migration_version
|
151
|
+
return 0 if executed_migrations.one?
|
152
|
+
|
153
|
+
executed_migrations.last(2).first.version
|
154
|
+
end
|
155
|
+
|
156
|
+
def pending_migrations
|
157
|
+
migrations.select { |m| m.version > fixtures_migration_version }
|
158
|
+
end
|
159
|
+
|
160
|
+
def executed_migrations
|
161
|
+
migrations.select { |m| m.version <= fixtures_migration_version }
|
162
|
+
end
|
163
|
+
|
164
|
+
def migrations
|
165
|
+
@migrations ||= set_migrations
|
166
|
+
end
|
167
|
+
|
168
|
+
def set_migrations
|
169
|
+
migrations = migration_files.map do |file|
|
170
|
+
version, name = parse_migration_filename(file)
|
171
|
+
raise IllegalMigrationNameError, file unless version
|
172
|
+
|
173
|
+
Migration::Proxy.new(name.camelize, version.to_i, file)
|
174
|
+
end
|
175
|
+
|
176
|
+
migrations.sort_by(&:version)
|
177
|
+
end
|
178
|
+
|
179
|
+
def migration_files
|
180
|
+
Dir["#{fixture_migrations_path}/**/[0-9]*_*.rb"]
|
181
|
+
end
|
182
|
+
|
183
|
+
def parse_migration_filename(filename)
|
184
|
+
File.basename(filename).scan(Migration::MIGRATION_FILENAME_REGEXP).first
|
185
|
+
end
|
186
|
+
|
187
|
+
Configuration = Struct.new(:configuration_data) do
|
188
|
+
def initialize(configuration_data)
|
189
|
+
super
|
190
|
+
@configuration_data = configuration_data.with_indifferent_access
|
191
|
+
end
|
192
|
+
|
193
|
+
def label_templates
|
194
|
+
@configuration_data["label"] || {}
|
195
|
+
end
|
196
|
+
|
197
|
+
def overwrite_fixtures?
|
198
|
+
return true unless @configuration_data.key?("overwrite")
|
199
|
+
|
200
|
+
@configuration_data["overwrite"]
|
201
|
+
end
|
202
|
+
|
203
|
+
def rename_fixtures?
|
204
|
+
@configuration_data["rename"]
|
205
|
+
end
|
206
|
+
|
207
|
+
def ignored_tables
|
208
|
+
@configuration_data["ignore"].to_a || []
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
@@ -0,0 +1,326 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "test_fixtures"
|
4
|
+
|
5
|
+
module FixtureChampagne
|
6
|
+
# Migrator parses pre existing fixtures, executes given migrations in the given direction and regenerates fixtures.
|
7
|
+
#
|
8
|
+
# The migrator implements the TestFixtures module to have access to a test database with pre existing fixtures
|
9
|
+
# already loaded and fixture accessors. It allows migrations access to all this.
|
10
|
+
# It contains the rules on how to avoid repeated fixtures, how to handle attached files, where to put temporary
|
11
|
+
# fixtures before overwritting (if necessary) and how to structure the fixtures folder tree.
|
12
|
+
class Migrator
|
13
|
+
include TestFixtures
|
14
|
+
|
15
|
+
attr_reader :direction, :migrations, :target_migration_version, :target_schema_version, :configuration
|
16
|
+
|
17
|
+
class << self
|
18
|
+
def fixture_unique_id(table_name:, id:)
|
19
|
+
"#{table_name}_#{id}"
|
20
|
+
end
|
21
|
+
|
22
|
+
def tmp_fixture_path
|
23
|
+
Rails.root.join("tmp", "fixtures")
|
24
|
+
end
|
25
|
+
|
26
|
+
def fixture_attachment_folders
|
27
|
+
%w[files active_storage action_text].map { |f| fixture_path.join(f) }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def initialize(
|
32
|
+
direction:, migrations:,
|
33
|
+
target_migration_version:, target_schema_version:, configuration:
|
34
|
+
)
|
35
|
+
@direction = direction
|
36
|
+
@migrations = migrations
|
37
|
+
@target_migration_version = target_migration_version
|
38
|
+
@target_schema_version = target_schema_version
|
39
|
+
@configuration = configuration
|
40
|
+
end
|
41
|
+
|
42
|
+
def migrate
|
43
|
+
before_migrate
|
44
|
+
|
45
|
+
process_pre_existing_fixtures
|
46
|
+
run_migrations
|
47
|
+
create_fixture_files_from_test_db
|
48
|
+
|
49
|
+
after_migrate
|
50
|
+
end
|
51
|
+
|
52
|
+
def process_pre_existing_fixtures
|
53
|
+
@pre_existing_fixtures_label_mapping = pre_existing_fixture_sets.each_with_object({}) do |fixture_set, mapping|
|
54
|
+
fixture_set.fixtures.each do |label, fixture|
|
55
|
+
unique_id = fixture_unique_id(fixture, fixture_set)
|
56
|
+
if mapping.key?(unique_id)
|
57
|
+
raise RepeatedFixtureError,
|
58
|
+
"repeated fixture in preprocess for label #{label}, unique id #{unique_id}"
|
59
|
+
end
|
60
|
+
mapping[unique_id] = label.to_s
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def run_migrations
|
66
|
+
migrations.each { |m| m.migrate(direction: direction, migrator: self) }
|
67
|
+
end
|
68
|
+
|
69
|
+
def create_fixture_files_from_test_db
|
70
|
+
klasses = fixtureable_models
|
71
|
+
fixtures_data = build_fixture_data(klasses)
|
72
|
+
create_new_fixture_files(klasses, fixtures_data)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Only generate fixture files for application models that own a table to avoid several files
|
76
|
+
# in the case of STI.
|
77
|
+
def fixtureable_models
|
78
|
+
descendants = ApplicationRecord.descendants
|
79
|
+
descendants.filter(&:table_exists?).map do |descendant|
|
80
|
+
next if configuration.ignored_tables.include?(descendant.table_name.to_s)
|
81
|
+
|
82
|
+
table_ancestor = descendant
|
83
|
+
table_ancestor = table_ancestor.superclass while table_ancestor.superclass.table_exists?
|
84
|
+
table_ancestor
|
85
|
+
end.compact.uniq
|
86
|
+
end
|
87
|
+
|
88
|
+
def build_fixture_data(klasses)
|
89
|
+
data = serialize_database_records(klasses)
|
90
|
+
|
91
|
+
data.each_with_object({}) do |(table, table_data), sorted_data|
|
92
|
+
next if table_data.empty?
|
93
|
+
|
94
|
+
sorted_data[table] = table_data.sort.to_h
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def serialize_database_records(klasses)
|
99
|
+
klasses.each_with_object({}) do |klass, hash|
|
100
|
+
table_name = klass.table_name.to_s
|
101
|
+
if hash.key?(table_name)
|
102
|
+
raise RepeatedFixtureError,
|
103
|
+
"repeated table key for new fixtures, table #{table_name}, class: #{klass}"
|
104
|
+
end
|
105
|
+
|
106
|
+
hash[table_name] = serialize_table_records(klass)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def serialize_table_records(klass)
|
111
|
+
table_data = {}
|
112
|
+
|
113
|
+
klass.all.each do |record|
|
114
|
+
label = fixture_label(record)
|
115
|
+
if table_data.key?(label)
|
116
|
+
raise RepeatedFixtureError,
|
117
|
+
"repeated fixture in serialization for label #{label}, class #{klass}"
|
118
|
+
end
|
119
|
+
|
120
|
+
table_data[label] = fixture_serialized_attributes(record)
|
121
|
+
end
|
122
|
+
|
123
|
+
table_data
|
124
|
+
end
|
125
|
+
|
126
|
+
# Record id is built from fixture label via ActiveRecord::FixtureSet.identify
|
127
|
+
def fixture_unique_id(fixture, fixture_set)
|
128
|
+
self.class.fixture_unique_id(table_name: fixture_set.table_name, id: fixture.find.id)
|
129
|
+
end
|
130
|
+
|
131
|
+
def fixture_label(record)
|
132
|
+
labeler.label_for(record: record)
|
133
|
+
end
|
134
|
+
|
135
|
+
def labeler
|
136
|
+
@labeler ||= Labeler.new(
|
137
|
+
pre_existing_fixtures_labels: @pre_existing_fixtures_label_mapping,
|
138
|
+
templates: configuration.label_templates,
|
139
|
+
rename: configuration.rename_fixtures?
|
140
|
+
)
|
141
|
+
end
|
142
|
+
|
143
|
+
def fixture_serialized_attributes(record)
|
144
|
+
serializer.serialized_attributes_for(record: record)
|
145
|
+
end
|
146
|
+
|
147
|
+
def serializer
|
148
|
+
@serializer ||= Serializer.new(labeler: labeler)
|
149
|
+
end
|
150
|
+
|
151
|
+
def create_new_fixture_files(klasses, fixtures_data)
|
152
|
+
setup_temporary_fixtures_dir
|
153
|
+
copy_fixture_attachments
|
154
|
+
klasses.each do |klass|
|
155
|
+
data = fixtures_data[klass.table_name]
|
156
|
+
filename = temporary_fixture_filename(klass)
|
157
|
+
create_temporary_fixture_file(data, filename)
|
158
|
+
end
|
159
|
+
return unless configuration.overwrite_fixtures?
|
160
|
+
|
161
|
+
overwrite_fixtures
|
162
|
+
remember_new_fixture_versions
|
163
|
+
end
|
164
|
+
|
165
|
+
def setup_temporary_fixtures_dir
|
166
|
+
FileUtils.rm_r(self.class.tmp_fixture_path, secure: true) if self.class.tmp_fixture_path.exist?
|
167
|
+
FileUtils.mkdir(self.class.tmp_fixture_path)
|
168
|
+
end
|
169
|
+
|
170
|
+
def copy_fixture_attachments
|
171
|
+
self.class.fixture_attachment_folders.each do |folder|
|
172
|
+
FileUtils.cp_r(folder, self.class.tmp_fixture_path) if folder.exist?
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def temporary_fixture_filename(klass)
|
177
|
+
parts = klass.to_s.split("::").map(&:underscore)
|
178
|
+
parts << parts.pop.pluralize.concat(".yml")
|
179
|
+
self.class.tmp_fixture_path.join(*parts)
|
180
|
+
end
|
181
|
+
|
182
|
+
def create_temporary_fixture_file(data, filename)
|
183
|
+
FileUtils.mkdir_p(filename.dirname)
|
184
|
+
File.open(filename, "w") do |file|
|
185
|
+
yaml = YAML.dump(data).gsub(/\n(?=[^\s])/, "\n\n").delete_prefix("---\n\n")
|
186
|
+
file.write(yaml)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def overwrite_fixtures
|
191
|
+
removable_fixture_path = self.class.fixture_path.dirname.join("old_fixtures")
|
192
|
+
FileUtils.mv(self.class.fixture_path, removable_fixture_path)
|
193
|
+
FileUtils.mv(self.class.tmp_fixture_path, self.class.fixture_path)
|
194
|
+
FileUtils.rm_r(removable_fixture_path, secure: true)
|
195
|
+
end
|
196
|
+
|
197
|
+
def remember_new_fixture_versions
|
198
|
+
File.open(MigrationContext.fixture_versions_path, "w") do |file|
|
199
|
+
yaml = YAML.dump({ "version" => target_migration_version, "schema_version" => target_schema_version })
|
200
|
+
file.write(yaml)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
# Labeler decides how a fixture should be labeled based on the config file and interpolation rules.
|
205
|
+
class Labeler
|
206
|
+
INTERPOLATION_PATTERN = /%\{([\w|]+)\}/.freeze
|
207
|
+
|
208
|
+
def initialize(pre_existing_fixtures_labels:, templates:, rename:)
|
209
|
+
@pre_existing_fixtures_labels = pre_existing_fixtures_labels
|
210
|
+
@templates = templates
|
211
|
+
@rename = rename
|
212
|
+
end
|
213
|
+
|
214
|
+
def label_for(record:)
|
215
|
+
if @rename
|
216
|
+
build_label_for(record)
|
217
|
+
else
|
218
|
+
find_label_for(record) || build_label_for(record)
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
private
|
223
|
+
|
224
|
+
def find_label_for(record)
|
225
|
+
@pre_existing_fixtures_labels[record_unique_id(record)]
|
226
|
+
end
|
227
|
+
|
228
|
+
def build_label_for(record)
|
229
|
+
template = @templates[record.class.table_name]
|
230
|
+
|
231
|
+
if template.nil? || template == "DEFAULT"
|
232
|
+
default_label(record)
|
233
|
+
else
|
234
|
+
interpolate_template(template, record)
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
def default_label(record)
|
239
|
+
record_unique_id(record)
|
240
|
+
end
|
241
|
+
|
242
|
+
def record_unique_id(record)
|
243
|
+
Migrator.fixture_unique_id(table_name: record.class.table_name, id: record.id)
|
244
|
+
end
|
245
|
+
|
246
|
+
def interpolate_template(template, record)
|
247
|
+
template.gsub(INTERPOLATION_PATTERN) do
|
248
|
+
attribute = ::Regexp.last_match(1).to_sym
|
249
|
+
value = if record.respond_to?(attribute)
|
250
|
+
record.send(attribute)
|
251
|
+
else
|
252
|
+
raise WrongFixtureLabelInterpolationError, attribute: attribute, klass: record.class
|
253
|
+
end
|
254
|
+
value
|
255
|
+
end.parameterize(separator: "_")
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
# Serializer decides how instance attributes translate to a hash, later saved as a fixture in a YAML file.
|
260
|
+
class Serializer
|
261
|
+
attr_reader :labeler
|
262
|
+
|
263
|
+
def initialize(labeler:)
|
264
|
+
@labeler = labeler
|
265
|
+
end
|
266
|
+
|
267
|
+
def serialized_attributes_for(record:)
|
268
|
+
column_attributes = record.attributes.select { |a| record.class.column_names.include?(a) }
|
269
|
+
|
270
|
+
# Favour fixtures autofilled timestamps and autogenerated ids
|
271
|
+
filtered_attributes = %w[id created_at updated_at]
|
272
|
+
|
273
|
+
serialized_attributes = column_attributes.map do |attribute, value|
|
274
|
+
serialize_attribute(record, attribute, value, filtered_attributes)
|
275
|
+
end
|
276
|
+
|
277
|
+
serialized_attributes.sort_by(&:first).to_h.except(*filtered_attributes)
|
278
|
+
end
|
279
|
+
|
280
|
+
def serialize_attribute(record, attribute, value, filtered_attributes)
|
281
|
+
belongs_to_association = record.class.reflect_on_all_associations.filter(&:belongs_to?).find do |a|
|
282
|
+
a.foreign_key.to_s == attribute
|
283
|
+
end
|
284
|
+
type = record.class.type_for_attribute(attribute)
|
285
|
+
|
286
|
+
if belongs_to_association.present?
|
287
|
+
filter_belongs_to_columns(belongs_to_association, filtered_attributes)
|
288
|
+
serialize_belongs_to(record, belongs_to_association)
|
289
|
+
else
|
290
|
+
serialize_type(record, attribute, value, type)
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
def serialize_belongs_to(record, belongs_to_association)
|
295
|
+
associated_record = record.send(belongs_to_association.name)
|
296
|
+
|
297
|
+
reference_label = if belongs_to_association.polymorphic?
|
298
|
+
foreign_type = record.send(belongs_to_association.foreign_type)
|
299
|
+
"#{labeler.label_for(record: associated_record)} (#{foreign_type})"
|
300
|
+
else
|
301
|
+
labeler.label_for(record: associated_record)
|
302
|
+
end
|
303
|
+
|
304
|
+
[belongs_to_association.name.to_s, reference_label]
|
305
|
+
end
|
306
|
+
|
307
|
+
def serialize_type(record, attribute, value, type)
|
308
|
+
if type.type == :datetime
|
309
|
+
# ActiveRecord::Type::DateTime#serialize returns a TimeWithZone object that makes the YAML dump less clear
|
310
|
+
[attribute, record.read_attribute_before_type_cast(attribute)]
|
311
|
+
elsif type.respond_to?(:serialize)
|
312
|
+
[attribute, type.serialize(value)]
|
313
|
+
else
|
314
|
+
[attribute, value.to_s]
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
def filter_belongs_to_columns(belongs_to_association, filtered_attributes)
|
319
|
+
filtered_attributes << belongs_to_association.foreign_key.to_s
|
320
|
+
return unless belongs_to_association.polymorphic?
|
321
|
+
|
322
|
+
filtered_attributes << belongs_to_association.foreign_type.to_s
|
323
|
+
end
|
324
|
+
end
|
325
|
+
end
|
326
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FixtureChampagne
|
4
|
+
class Railtie < Rails::Railtie # :nodoc:
|
5
|
+
railtie_name :fixture_champagne
|
6
|
+
|
7
|
+
rake_tasks do
|
8
|
+
path = File.expand_path(__dir__)
|
9
|
+
Dir.glob("#{path}/../tasks/**/*.rake").each { |f| load f }
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FixtureChampagne
|
4
|
+
# TestFixtures allows access to a test database with fixtures preloaded and all fixture accessors.
|
5
|
+
#
|
6
|
+
# TestFixtures calls the ActiveRecord::TestDatabases to regenerate a test db and adapts the
|
7
|
+
# ActiveRecord::TestFixtures module to parse all pre existing fixtures and generate the required
|
8
|
+
# accessors.
|
9
|
+
module TestFixtures
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
include ActiveRecord::TestFixtures
|
12
|
+
|
13
|
+
included do
|
14
|
+
self.fixture_path = MigrationContext.fixtures_path
|
15
|
+
fixtures :all
|
16
|
+
end
|
17
|
+
|
18
|
+
def name
|
19
|
+
self.class.name
|
20
|
+
end
|
21
|
+
|
22
|
+
def before_migrate
|
23
|
+
# Database is already prepared
|
24
|
+
ActiveRecord::TestDatabases.create_and_load_schema(0, env_name: "test") unless Rails.env.test?
|
25
|
+
|
26
|
+
setup_fixtures
|
27
|
+
end
|
28
|
+
|
29
|
+
def after_migrate
|
30
|
+
teardown_fixtures
|
31
|
+
end
|
32
|
+
|
33
|
+
def pre_existing_fixtures
|
34
|
+
@loaded_fixtures
|
35
|
+
end
|
36
|
+
|
37
|
+
def pre_existing_fixture_sets
|
38
|
+
pre_existing_fixtures.map { |_k, v| v }
|
39
|
+
end
|
40
|
+
|
41
|
+
def pre_existing_fixture_accessors
|
42
|
+
# fixture_sets implementation: https://github.com/rails/rails/commit/05d80fc24f03ca5310931eacefdc247a393dd861
|
43
|
+
# Still not released
|
44
|
+
return fixture_sets.keys if respond_to?(:fixture_sets) && fixture_sets.keys.any?
|
45
|
+
|
46
|
+
fixture_table_names.map do |t_name|
|
47
|
+
t_name.to_s.include?("/") ? t_name.to_s.tr("/", "_") : t_name.to_s
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "fixture_champagne/version"
|
4
|
+
|
5
|
+
module FixtureChampagne # :nodoc:
|
6
|
+
require "fixture_champagne/railtie" if defined?(Rails)
|
7
|
+
|
8
|
+
autoload :MigrationContext, "fixture_champagne/migration_context"
|
9
|
+
autoload :Migrator, "fixture_champagne/migrator"
|
10
|
+
autoload :Migration, "fixture_champagne/migration"
|
11
|
+
|
12
|
+
class MissingMigrationsFolderError < StandardError # :nodoc:
|
13
|
+
def initialize
|
14
|
+
super("No fixture_migrations folder found in test suite folder")
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class RepeatedFixtureError < StandardError; end # :nodoc:
|
19
|
+
|
20
|
+
class WrongFixtureLabelInterpolationError < StandardError # :nodoc:
|
21
|
+
def initialize(error_data = {})
|
22
|
+
super("Missing attribute or method #{error_data[:attribute]} for record class #{error_data[:klass]}")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class IllegalMigrationNameError < StandardError # :nodoc:
|
27
|
+
def initialize(name = nil)
|
28
|
+
if name
|
29
|
+
super("Illegal name for migration file: #{name}\n\t(only lower case letters, numbers, and '_' allowed).")
|
30
|
+
else
|
31
|
+
super("Illegal name for migration.")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FixtureChampagne
|
4
|
+
module Generators
|
5
|
+
# InstallGenerator generates a the boilerplate necessary to use the gem.
|
6
|
+
#
|
7
|
+
# From your app directory you can run:
|
8
|
+
# bin/rails generate fixture_champagne:install
|
9
|
+
#
|
10
|
+
# This will create the folder set at FixtureChampagne::MigrationContext.fixture_migrations_path
|
11
|
+
# if it does not already exist.
|
12
|
+
class InstallGenerator < Rails::Generators::Base
|
13
|
+
desc "Setup fixture_champagne required files and folders."
|
14
|
+
|
15
|
+
def create_migrations_folder
|
16
|
+
return if FixtureChampagne::MigrationContext.expected_fixture_migrations_path.exist?
|
17
|
+
|
18
|
+
empty_directory FixtureChampagne::MigrationContext.expected_fixture_migrations_path
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FixtureChampagne
|
4
|
+
module Generators
|
5
|
+
# MigrationGenerator generates a new migration using the migration.rb.tt template.
|
6
|
+
#
|
7
|
+
# Ensure that a fixture_migrations folder exists in your test suite folder or create one
|
8
|
+
# running the install generator. This folder is set at FixtureChampagne::MigrationContext.fixture_migrations_path
|
9
|
+
# Then from your app directory you can run:
|
10
|
+
#
|
11
|
+
# bin/rails generate fixture_champagne:migration <new_migration_name>
|
12
|
+
#
|
13
|
+
# Where <new_migration_name> should be the name of your new migration. For example:
|
14
|
+
# bin/rails generate fixture_champagne:migration add_new_user
|
15
|
+
#
|
16
|
+
# Will generate fixture_migrations/20230126153650_add_new_user.rb with the following content:
|
17
|
+
# class AddTurtle < FixtureChampagne::Migration::Base
|
18
|
+
# def up
|
19
|
+
# # Create, update or destroy records here
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# def down
|
23
|
+
# # Optionally, reverse changes made by the :up method
|
24
|
+
# end
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# The generator automatically adds a version number to the new migration file, which is important
|
28
|
+
# to keep track of executed migrations. Also, the migration filename must correspond with the migration
|
29
|
+
# class inside the file.
|
30
|
+
class MigrationGenerator < Rails::Generators::NamedBase
|
31
|
+
include Rails::Generators::ResourceHelpers
|
32
|
+
|
33
|
+
source_root File.expand_path("templates", __dir__)
|
34
|
+
|
35
|
+
desc "Generates a migration with the given NAME and a version."
|
36
|
+
|
37
|
+
def generate_migration
|
38
|
+
validate_new_migration_file_name!
|
39
|
+
template "migration.rb", new_migration_file_path
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def validate_new_migration_file_name!
|
45
|
+
return if FixtureChampagne::Migration::MIGRATION_FILENAME_REGEXP.match?(File.basename(new_migration_file_path))
|
46
|
+
|
47
|
+
raise IllegalMigrationNameError, new_migration_file_path
|
48
|
+
end
|
49
|
+
|
50
|
+
def new_migration_file_path
|
51
|
+
fixture_migrations_path = FixtureChampagne::MigrationContext.fixture_migrations_path
|
52
|
+
"#{fixture_migrations_path}/#{new_migration_filename}.rb"
|
53
|
+
end
|
54
|
+
|
55
|
+
def new_migration_filename
|
56
|
+
@new_migration_filename ||= build_new_migration_filename
|
57
|
+
end
|
58
|
+
|
59
|
+
def build_new_migration_filename
|
60
|
+
new_migration_version = FixtureChampagne::Migration.new_migration_version
|
61
|
+
given_name = file_name
|
62
|
+
"#{new_migration_version}_#{given_name}"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fixture_champagne"
|
4
|
+
|
5
|
+
namespace :fixture_champagne do
|
6
|
+
desc "Run Fixture Champagne migrations to update fixtures"
|
7
|
+
task migrate: :environment do
|
8
|
+
FixtureChampagne::MigrationContext.migrate
|
9
|
+
end
|
10
|
+
|
11
|
+
desc "Rollback Fixture Champagne last executed migration"
|
12
|
+
task rollback: :environment do
|
13
|
+
FixtureChampagne::MigrationContext.rollback
|
14
|
+
end
|
15
|
+
end
|
metadata
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fixture_champagne
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Juan Gueçaimburu
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-02-07 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 6.1.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 6.1.0
|
27
|
+
description: |
|
28
|
+
Data migration pattern applied to fixtures.
|
29
|
+
Create, update and keep fixtures synced with db schema.
|
30
|
+
email:
|
31
|
+
- guecaimburu.j@gmail.com
|
32
|
+
executables: []
|
33
|
+
extensions: []
|
34
|
+
extra_rdoc_files: []
|
35
|
+
files:
|
36
|
+
- README.md
|
37
|
+
- Rakefile
|
38
|
+
- lib/fixture_champagne.rb
|
39
|
+
- lib/fixture_champagne/migration.rb
|
40
|
+
- lib/fixture_champagne/migration_context.rb
|
41
|
+
- lib/fixture_champagne/migrator.rb
|
42
|
+
- lib/fixture_champagne/railtie.rb
|
43
|
+
- lib/fixture_champagne/test_fixtures.rb
|
44
|
+
- lib/fixture_champagne/version.rb
|
45
|
+
- lib/generators/fixture_champagne/install_generator.rb
|
46
|
+
- lib/generators/fixture_champagne/migration_generator.rb
|
47
|
+
- lib/generators/fixture_champagne/templates/migration.rb.tt
|
48
|
+
- lib/tasks/fixture_champagne_tasks.rake
|
49
|
+
homepage: https://github.com/jguecaimburu/fixture_champagne
|
50
|
+
licenses:
|
51
|
+
- MIT
|
52
|
+
metadata:
|
53
|
+
homepage_uri: https://github.com/jguecaimburu/fixture_champagne
|
54
|
+
source_code_uri: https://github.com/jguecaimburu/fixture_champagne
|
55
|
+
changelog_uri: https://github.com/jguecaimburu/fixture_champagne/CHANGELOG.md
|
56
|
+
rubygems_mfa_required: 'true'
|
57
|
+
post_install_message:
|
58
|
+
rdoc_options: []
|
59
|
+
require_paths:
|
60
|
+
- lib
|
61
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
62
|
+
requirements:
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: 2.7.0
|
66
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: '0'
|
71
|
+
requirements: []
|
72
|
+
rubygems_version: 3.3.26
|
73
|
+
signing_key:
|
74
|
+
specification_version: 4
|
75
|
+
summary: Fixture migrations for Ruby on Rails applications
|
76
|
+
test_files: []
|