fixpoints 0.1.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 +7 -0
- data/.gitignore +8 -0
- data/.rspec +3 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +61 -0
- data/LICENSE.txt +21 -0
- data/README.md +67 -0
- data/fixpoints.gemspec +27 -0
- data/lib/fixpoint.rb +171 -0
- data/lib/fixpoint_diff.rb +58 -0
- data/lib/fixpoints.rb +9 -0
- data/lib/fixpoints/version.rb +3 -0
- data/lib/incremental_fixpoint.rb +109 -0
- metadata +86 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: accfd17194594b7b1f83144d68a15930991a6aa0369fe695bd4ac769d247abbe
|
4
|
+
data.tar.gz: 831f6c9c694d96173175b7b1082600947575f54864161fac52cf5dabdd039e7c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5684b274d7118189c284c483816e6f18059529f08ce92585fd8c94dec3360106a9a77238cc6348d9b25f8daa4f4d9f2de227ef0daffe8fd511b7eb0ceddee05a
|
7
|
+
data.tar.gz: 626a9395a2472992c02c18c0e6bf51a5502b29d460023034ef13501221b0312b5e35fd493b7d9661a0f8ddccee13c1755345e5ca4e0521c14b37bb9f3ba1f828
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
fixpoints (0.1.0)
|
5
|
+
activerecord (>= 5.0.0)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
activemodel (5.2.4.4)
|
11
|
+
activesupport (= 5.2.4.4)
|
12
|
+
activerecord (5.2.4.4)
|
13
|
+
activemodel (= 5.2.4.4)
|
14
|
+
activesupport (= 5.2.4.4)
|
15
|
+
arel (>= 9.0)
|
16
|
+
activesupport (5.2.4.4)
|
17
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
18
|
+
i18n (>= 0.7, < 2)
|
19
|
+
minitest (~> 5.1)
|
20
|
+
tzinfo (~> 1.1)
|
21
|
+
arel (9.0.0)
|
22
|
+
coderay (1.1.3)
|
23
|
+
concurrent-ruby (1.1.7)
|
24
|
+
diff-lcs (1.4.4)
|
25
|
+
i18n (1.8.5)
|
26
|
+
concurrent-ruby (~> 1.0)
|
27
|
+
method_source (1.0.0)
|
28
|
+
minitest (5.14.2)
|
29
|
+
pry (0.13.1)
|
30
|
+
coderay (~> 1.1)
|
31
|
+
method_source (~> 1.0)
|
32
|
+
rspec (3.9.0)
|
33
|
+
rspec-core (~> 3.9.0)
|
34
|
+
rspec-expectations (~> 3.9.0)
|
35
|
+
rspec-mocks (~> 3.9.0)
|
36
|
+
rspec-core (3.9.2)
|
37
|
+
rspec-support (~> 3.9.3)
|
38
|
+
rspec-expectations (3.9.2)
|
39
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
40
|
+
rspec-support (~> 3.9.0)
|
41
|
+
rspec-mocks (3.9.1)
|
42
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
43
|
+
rspec-support (~> 3.9.0)
|
44
|
+
rspec-support (3.9.3)
|
45
|
+
sqlite3 (1.4.2)
|
46
|
+
thread_safe (0.3.6)
|
47
|
+
tzinfo (1.2.7)
|
48
|
+
thread_safe (~> 0.1)
|
49
|
+
|
50
|
+
PLATFORMS
|
51
|
+
ruby
|
52
|
+
|
53
|
+
DEPENDENCIES
|
54
|
+
activerecord (>= 5.0.0)
|
55
|
+
fixpoints!
|
56
|
+
pry
|
57
|
+
rspec
|
58
|
+
sqlite3
|
59
|
+
|
60
|
+
BUNDLED WITH
|
61
|
+
2.1.4
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2020 Tom Rothe
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
# Fixpoints
|
2
|
+
|
3
|
+
Fixpoints enables saving, restoring and comparing the database state before & after tests.
|
4
|
+
|
5
|
+
## Motivation
|
6
|
+
|
7
|
+
TODO
|
8
|
+
|
9
|
+
Link to `https://tomrothe.de/posts/behaviour-driven-test-data.html`
|
10
|
+
|
11
|
+
## Usage
|
12
|
+
|
13
|
+
Add this line to your application's Gemfile: `gem 'fixpoints'`
|
14
|
+
|
15
|
+
TODO: Write usage instructions here
|
16
|
+
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
# TODO: update this code
|
20
|
+
it 'registers a user' do
|
21
|
+
visit new_user_path
|
22
|
+
fill_in 'Name', with: 'Hans'
|
23
|
+
click_on 'Save'
|
24
|
+
|
25
|
+
store_fixpoint :registred_user
|
26
|
+
# creates YAML files containing all records (/spec/fixpoints/[table_name].yml)
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'posts an item' do
|
30
|
+
restore_fixpoint :registered_user
|
31
|
+
|
32
|
+
user = User.find_by(name: 'Hans')
|
33
|
+
visit new_item_path(user)
|
34
|
+
fill_in 'Item', with: '...'
|
35
|
+
click_on 'Post'
|
36
|
+
|
37
|
+
compare_fixpoint(:posted_item, ignore_columns: [:release_date], store_fixpoint_and_fail: true)
|
38
|
+
# compares the database state with the previously saved fixpoint and
|
39
|
+
# raises if there is a difference. when there is no previous fixpoint,
|
40
|
+
# it writes it and fails the test (so it can be re-run)
|
41
|
+
end
|
42
|
+
```
|
43
|
+
|
44
|
+
## Development
|
45
|
+
|
46
|
+
```bash
|
47
|
+
docker run --rm -ti -v (pwd):/app -w /app ruby:2.7 bash
|
48
|
+
bundle install
|
49
|
+
rspec
|
50
|
+
pry # require_relative 'lib/fixpoints.rb'
|
51
|
+
|
52
|
+
gem build
|
53
|
+
gem install fixpoints-0.1.0.gem
|
54
|
+
pry -r fixpoints
|
55
|
+
gem uninstall fixpoints
|
56
|
+
```
|
57
|
+
|
58
|
+
|
59
|
+
## Development
|
60
|
+
|
61
|
+
## Contributing
|
62
|
+
|
63
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/motine/fixpoints.
|
64
|
+
|
65
|
+
## License
|
66
|
+
|
67
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/fixpoints.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require_relative 'lib/fixpoints/version'
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.name = "fixpoints"
|
5
|
+
spec.version = Fixpoints::VERSION
|
6
|
+
spec.authors = ["Tom Rothe"]
|
7
|
+
spec.email = ["info@tomrothe.de"]
|
8
|
+
|
9
|
+
spec.summary = "Don't discard the database state at the end of your test. Use it!"
|
10
|
+
spec.description = 'Fixpoints enables saving, restoring and comparing the database state before & after tests'
|
11
|
+
spec.homepage = "https://github.com/motine/fixpoints"
|
12
|
+
spec.license = "MIT"
|
13
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
|
14
|
+
|
15
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
16
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
17
|
+
|
18
|
+
spec.add_runtime_dependency "activerecord", ">=5.0.0"
|
19
|
+
spec.add_runtime_dependency "rspec"
|
20
|
+
|
21
|
+
# Specify which files should be added to the gem when it is released.
|
22
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
23
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
24
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
25
|
+
end
|
26
|
+
spec.require_paths = ["lib"]
|
27
|
+
end
|
data/lib/fixpoint.rb
ADDED
@@ -0,0 +1,171 @@
|
|
1
|
+
# A fixpoint is a snapshot of the database contents.
|
2
|
+
# It is saved to the +spec/fixpoints+ folder.
|
3
|
+
# A fixpoint (file) contains a mapping of table names to a list if their records.
|
4
|
+
#
|
5
|
+
# Empty tables are stripped from files.
|
6
|
+
#
|
7
|
+
# Make sure to run the tests in the right order: In a single RSpec file, you can use the order in which the tests are defined (`RSpec.describe 'MyFeature', order: :defined do`).
|
8
|
+
# However, tests in groups might follow a slightly different order (see https://relishapp.com/rspec/rspec-core/docs/configuration/overriding-global-ordering)
|
9
|
+
#
|
10
|
+
# If you did a lot of changes to a test, you can remove a fixpoint file from its directory.
|
11
|
+
# It will be recreated when the test producing it runs again.
|
12
|
+
# Don't forget re-running the tests _based on_ it because their fixpoints might have to change too.
|
13
|
+
# Example: You need to add something to the database's seeds.rb. All subsequent fixpoints are missing the required entry.
|
14
|
+
# To update all fixpoints, just remove the whole `spec/fixpoints` folder and re-run all tests. Now all fixpoints should be updated.
|
15
|
+
# Be careful though, don't just remove the fixpoints if you are not sure what is going on.
|
16
|
+
# A change in a fixpoint might point to an unintended change in code.
|
17
|
+
#
|
18
|
+
# We need to be be careful to use +let+ and +let!+ with factories.
|
19
|
+
# Records might be created twice when using create in there (once by the fixpoint and once by the factory).
|
20
|
+
#
|
21
|
+
# KNOWN ISSUES
|
22
|
+
# Under certain conditions you may get `duplicate key value violates unique constraint` because the primary key sequences are not updated correctly.
|
23
|
+
# If this happens, just add a Fixpoint.reset_pk_sequences! at the beginning of your test. We need to dig a little deeper here at some point...
|
24
|
+
#
|
25
|
+
# LIMITATIONS
|
26
|
+
# The records in tables are ordered by their id.
|
27
|
+
# If there is no id for a table, we use database's order (what the SELECT query returns).
|
28
|
+
# This order may be instable.
|
29
|
+
class Fixpoint
|
30
|
+
class Error < StandardError; end
|
31
|
+
|
32
|
+
FIXPOINT_FOLDER = 'fixpoints'
|
33
|
+
TABLES_TO_SKIP = %w[ar_internal_metadata delayed_jobs schema_info schema_migrations].freeze
|
34
|
+
|
35
|
+
class << self
|
36
|
+
def exists?(fixname)
|
37
|
+
File.exist?(fixpoint_path(fixname))
|
38
|
+
end
|
39
|
+
|
40
|
+
def from_file(fixname)
|
41
|
+
raise Fixpoint::Error, "The requested fixpoint (\"#{fixname}\") could not be found. Re-run the test which stores the fixpoint." unless exists?(fixname)
|
42
|
+
|
43
|
+
file_path = fixpoint_path(fixname)
|
44
|
+
new(YAML.load_file(file_path))
|
45
|
+
end
|
46
|
+
|
47
|
+
# Creates a Fixpoint from the database contents. Empty tables are skipped.
|
48
|
+
def from_database
|
49
|
+
new(read_database_records)
|
50
|
+
end
|
51
|
+
|
52
|
+
def remove(fixname)
|
53
|
+
FileUtils.rm_f(fixpoint_path(fixname))
|
54
|
+
end
|
55
|
+
|
56
|
+
# reset primary key sequences for all tables
|
57
|
+
# useful when tests sometimes run before the storing the first fixpoint.
|
58
|
+
# these test might have incremented the id sequence already, so the ids in the fixpoints chance (which leads to differences).
|
59
|
+
def reset_pk_sequences!
|
60
|
+
return unless conn.respond_to?(:reset_pk_sequence!)
|
61
|
+
conn.tables.each { |table_name| conn.reset_pk_sequence!(table_name) }
|
62
|
+
end
|
63
|
+
|
64
|
+
def fixpoint_path(fixname)
|
65
|
+
fspath = self.fixpoints_path
|
66
|
+
raise Fixpoint::Error, 'Can not automatically infer the base path for the specs, please set `rspec_config.fixpoints_path` explicitly' if fspath.nil?
|
67
|
+
raise Fixpoint::Error, "Please create the fixpoints folder (and maybe create a .gitkeep): #{fspath}" if !File.exist?(fspath)
|
68
|
+
|
69
|
+
File.join(fspath, "#{fixname}.yml")
|
70
|
+
end
|
71
|
+
|
72
|
+
def conn
|
73
|
+
ActiveRecord::Base.connection
|
74
|
+
end
|
75
|
+
|
76
|
+
protected
|
77
|
+
|
78
|
+
def fixpoints_path
|
79
|
+
return RSpec.configuration.fixpoints_path unless RSpec.configuration.fixpoints_path.nil?
|
80
|
+
return Rails.root.join(RSpec.configuration.default_path, FIXPOINT_FOLDER) if defined?(Rails)
|
81
|
+
# now this is ugly, but necessary. we go up from the current example's path until we find the spec folder...
|
82
|
+
return nil if RSpec.current_example.nil?
|
83
|
+
spec_path = Pathname.new(RSpec.current_example.file_path).ascend.find { |pn| pn.basename.to_s == RSpec.configuration.default_path }.expand_path
|
84
|
+
|
85
|
+
File.join(spec_path, FIXPOINT_FOLDER)
|
86
|
+
end
|
87
|
+
|
88
|
+
def read_database_records
|
89
|
+
# adapted from: https://yizeng.me/2017/07/16/generate-rails-test-fixtures-yaml-from-database-dump/
|
90
|
+
tables = conn.tables
|
91
|
+
tables.reject! { |table_name| TABLES_TO_SKIP.include?(table_name) }
|
92
|
+
|
93
|
+
tables.each_with_object({}) do |table_name, acc|
|
94
|
+
result = conn.select_all("SELECT * FROM #{table_name}")
|
95
|
+
next if result.count.zero?
|
96
|
+
|
97
|
+
rows = result.to_a
|
98
|
+
rows.sort_by! { |row| row['id'] } if result.columns.include?('id') # let's make the order of items stable
|
99
|
+
acc[table_name] = rows
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
attr_reader :records_in_tables # the complete records in the tables
|
105
|
+
|
106
|
+
def initialize(records_in_tables)
|
107
|
+
@records_in_tables = records_in_tables
|
108
|
+
end
|
109
|
+
|
110
|
+
def load_into_database
|
111
|
+
# Here some more pointers on implementation details of fixtures:
|
112
|
+
# - https://github.com/rails/rails/blob/2998672fc22f0d5e1a79a29ccb60d0d0e627a430/activerecord/lib/active_record/fixtures.rb#L612
|
113
|
+
# - http://api.rubyonrails.org/v5.2.4/classes/ActiveRecord/FixtureSet.html#method-c-create_fixtures
|
114
|
+
# - https://github.com/rails/rails/blob/67feba0c822d64741d574dfea808c1a2feedbcfc/activerecord/test/cases/fixtures_test.rb
|
115
|
+
#
|
116
|
+
# Note from the past (useful if we want to get back to using Rails' +create_fixtures+ method)
|
117
|
+
# we used to do: ActiveRecord::FixtureSet.create_fixtures(folder_path, filename_without_extension) # this will also clear the table
|
118
|
+
# but we abandoned this approach because we want to one file per fixpoint (not one file per table)
|
119
|
+
# ActiveRecord::FixtureSet.reset_cache # create_fixtures does use only the table name as cache key. we always invalidate the cache because we may want to read different fixpoints but with the same table names
|
120
|
+
|
121
|
+
# let's remove all data
|
122
|
+
conn.tables.each { |table| conn.select_all("DELETE FROM #{conn.quote_table_name(table)}") }
|
123
|
+
|
124
|
+
# actually insert
|
125
|
+
conn.insert_fixtures_set(@records_in_tables)
|
126
|
+
self.class.reset_pk_sequences!
|
127
|
+
end
|
128
|
+
|
129
|
+
def save_to_file(fixname)
|
130
|
+
file_path = self.class.fixpoint_path(fixname)
|
131
|
+
FileUtils.mkdir_p(File.dirname(file_path))
|
132
|
+
File.write(file_path, contents_for_file)
|
133
|
+
end
|
134
|
+
|
135
|
+
def table_names
|
136
|
+
@records_in_tables.keys
|
137
|
+
end
|
138
|
+
|
139
|
+
# Returns the records for the given +table_name+ as a list of Hashes.
|
140
|
+
# +ignore_columns+ array of columns to remove from each record Hash.
|
141
|
+
# Aside from having the form <tt>[:created_at, :updated_at]</tt>,
|
142
|
+
# it can contain attributes scoped by a table name <tt>[:created_at, :updated_at, users: [:password_hash]]</tt>
|
143
|
+
def records_for_table(table_name, ignore_columns = [])
|
144
|
+
strip_columns_from_records(@records_in_tables[table_name], table_name, ignore_columns)
|
145
|
+
end
|
146
|
+
|
147
|
+
protected
|
148
|
+
|
149
|
+
delegate :conn, to: :class
|
150
|
+
|
151
|
+
def contents_for_file
|
152
|
+
YAML.dump(@records_in_tables)
|
153
|
+
end
|
154
|
+
|
155
|
+
# see #records_for_table
|
156
|
+
def strip_columns_from_records(records, table_name, columns)
|
157
|
+
return nil if records.nil?
|
158
|
+
|
159
|
+
if columns.last.is_a?(Hash) # columns has the a table names at the end (e.g. [:created_at, :updated_at, users: [:password_hash]])
|
160
|
+
columns = columns.dup
|
161
|
+
all_table_scoped = columns.pop.stringify_keys
|
162
|
+
table_scoped = all_table_scoped[table_name]
|
163
|
+
columns += table_scoped if table_scoped
|
164
|
+
end
|
165
|
+
columns = columns.collect(&:to_s)
|
166
|
+
|
167
|
+
records.collect do |attributes|
|
168
|
+
attributes.reject { |col, _value| columns.include?(col) }
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# Helper module which implements diff-ing for fixpoints
|
2
|
+
module FixpointDiff
|
3
|
+
DELETED_KEY = '++DELETED++'
|
4
|
+
IGNORE_ATTRIBUTES = ['updated_at']
|
5
|
+
|
6
|
+
|
7
|
+
def apply_changes(parent_records_in_tables, changes_in_tables)
|
8
|
+
tables = (parent_records_in_tables.keys + changes_in_tables.keys).uniq
|
9
|
+
|
10
|
+
tables.each_with_object({}) do |table, records|
|
11
|
+
records[table] = apply_records_changes(parent_records_in_tables[table], changes_in_tables[table])
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def extract_changes(parent_records_in_tables, records_in_tables)
|
16
|
+
tables = (parent_records_in_tables.keys + records_in_tables.keys).uniq
|
17
|
+
|
18
|
+
tables.each_with_object({}) do |table, changes_in_tables|
|
19
|
+
changes_in_tables[table] = extract_records_changes(parent_records_in_tables[table], records_in_tables[table])
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
module_function :apply_changes, :extract_changes
|
24
|
+
|
25
|
+
protected
|
26
|
+
|
27
|
+
def apply_records_changes(parent_records, changes)
|
28
|
+
return parent_records if changes.blank?
|
29
|
+
|
30
|
+
parent_records ||= [] # the table was not part of an earlier fixpoint
|
31
|
+
changes.zip(parent_records).collect do |change, parent_record| # we can rely on the fact that changes has always more entries than parent_records
|
32
|
+
next change if parent_record.nil? # we do have a new record
|
33
|
+
next nil if change[DELETED_KEY]
|
34
|
+
|
35
|
+
parent_record.merge(change)
|
36
|
+
end.compact
|
37
|
+
end
|
38
|
+
|
39
|
+
def extract_records_changes(parent_records, records)
|
40
|
+
return records if parent_records.blank?
|
41
|
+
records ||= []
|
42
|
+
|
43
|
+
# we pad parent_records with nil values so we can zip them together
|
44
|
+
parent_records = parent_records + [nil] * (records.count - parent_records.count) if parent_records.count < records.count
|
45
|
+
|
46
|
+
parent_records.zip(records).collect do |parent, record|
|
47
|
+
next { DELETED_KEY => true } if record.nil?
|
48
|
+
next record if parent.nil? # newly added record
|
49
|
+
|
50
|
+
parent.each_with_object({}) do |(parent_key, parent_value), changes| # we can rely on the fact that both hashes have the same attributes
|
51
|
+
record_value = record[parent_key]
|
52
|
+
changes[parent_key] = record_value unless record_value == parent_value || IGNORE_ATTRIBUTES.include?(parent_key)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
module_function :apply_records_changes, :extract_records_changes
|
58
|
+
end
|
data/lib/fixpoints.rb
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
# Enhances Fixpoint to only save incremental changes.
|
2
|
+
#
|
3
|
+
# A fixpoint can be saved fully, where all records are saved to the file or one can give a parent fixpoint.
|
4
|
+
# When doing so, only the difference (aka. changes) to the parent is saved in the file.
|
5
|
+
# Yet, if a record does not change parent an empty hash is saved. This is done, so removals from the database can be tracked.
|
6
|
+
#
|
7
|
+
# LIMITATIONS
|
8
|
+
# Assume you remove a record at the end of a table and then add another one.
|
9
|
+
# Then the fixpoint diff will complain that an entry has changed instead of noticing the addition/removal.
|
10
|
+
class IncrementalFixpoint < Fixpoint
|
11
|
+
PARENT_YAML_KEY = '++parent_fixpoint++'
|
12
|
+
|
13
|
+
attr_reader :changes_in_tables # only the difference to the parent (in tables)
|
14
|
+
|
15
|
+
def initialize(changes_in_tables, parent_fixname=nil)
|
16
|
+
@parent_fixname = parent_fixname
|
17
|
+
@changes_in_tables = changes_in_tables
|
18
|
+
if parent_fixname.nil?
|
19
|
+
super(changes_in_tables)
|
20
|
+
else
|
21
|
+
parent = self.class.from_file(parent_fixname)
|
22
|
+
super(FixpointDiff.apply_changes(parent.records_in_tables, @changes_in_tables))
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.from_file(fixname)
|
27
|
+
raise Fixpoint::Error, "The requested fixpoint (\"#{fixname}\") could not be found. Re-run the test which stores the fixpoint." unless exists?(fixname)
|
28
|
+
|
29
|
+
file_path = fixpoint_path(fixname)
|
30
|
+
changes_in_tables = YAML.load_file(file_path)
|
31
|
+
parent_fixname = changes_in_tables.delete(PARENT_YAML_KEY)
|
32
|
+
new(changes_in_tables, parent_fixname)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Creates a Fixpoint from the database contents. Empty tables are skipped.
|
36
|
+
def self.from_database(parent_fixname=nil)
|
37
|
+
return super() if parent_fixname.nil?
|
38
|
+
|
39
|
+
parent = from_file(parent_fixname)
|
40
|
+
changes_in_tables = FixpointDiff.extract_changes(parent.records_in_tables, read_database_records)
|
41
|
+
new(changes_in_tables, parent_fixname)
|
42
|
+
end
|
43
|
+
|
44
|
+
protected
|
45
|
+
|
46
|
+
def contents_for_file
|
47
|
+
file_contents = @changes_in_tables.dup
|
48
|
+
file_contents[PARENT_YAML_KEY] = @parent_fixname unless @parent_fixname.nil?
|
49
|
+
return YAML.dump(file_contents)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Helper methods to be included into RSpec
|
54
|
+
module FixpointTestHelpers
|
55
|
+
def restore_fixpoint(fixname)
|
56
|
+
IncrementalFixpoint.from_file(fixname).load_into_database
|
57
|
+
end
|
58
|
+
|
59
|
+
# Compares the fixpoint with the records in the database.
|
60
|
+
# If there is no such fixpoint yet, it will write a new one to the file system.
|
61
|
+
# The latter is useful if the fixpoint was deleted to accommodate changes to it (see example in class description).
|
62
|
+
#
|
63
|
+
# +tables_to_compare+ can either be +:all+ or a list of table names (e.g. ['users', 'posts'])
|
64
|
+
# +ignored_columns+ see Fixnum#records_for_table
|
65
|
+
# +not_exists_handler+ when given and the fixpoint does not exists, it will be called with the fixname as argument
|
66
|
+
#
|
67
|
+
# ---
|
68
|
+
# If we refactor this to a gem, we should rely on rspec (e.g. use minitest or move comparison logic to Fixpoint class).
|
69
|
+
# Anyhow, we keep it like this for now, because the expectations give much nicer output than the minitest assertions.
|
70
|
+
def compare_fixpoint(fixname, ignored_columns=[:updated_at, :created_at], tables_to_compare=:all, ¬_exists_handler)
|
71
|
+
if !IncrementalFixpoint.exists?(fixname)
|
72
|
+
not_exists_handler.call(fixname) if not_exists_handler
|
73
|
+
return
|
74
|
+
end
|
75
|
+
|
76
|
+
database_fp = IncrementalFixpoint.from_database
|
77
|
+
fixpoint_fp = IncrementalFixpoint.from_file(fixname)
|
78
|
+
|
79
|
+
tables_to_compare = (database_fp.table_names + fixpoint_fp.table_names).uniq if tables_to_compare == :all
|
80
|
+
tables_to_compare.each do |table_name|
|
81
|
+
db_records = database_fp.records_for_table(table_name, ignored_columns)
|
82
|
+
fp_records = fixpoint_fp.records_for_table(table_name, ignored_columns)
|
83
|
+
|
84
|
+
# if a table is present in a fixpoint, there must be records in it because empty tables are stripped from fixpoints
|
85
|
+
expect(db_records).not_to be_empty, "#{table_name} not in database, but in fixpoint"
|
86
|
+
expect(fp_records).not_to be_empty, "#{table_name} not in fixpoint, but in database"
|
87
|
+
# we assume that the order of records returned by SELECT is stable (so we do not do any sorting)
|
88
|
+
expect(db_records).to eq(fp_records), "Database records for table \"#{table_name}\" did not match fixpoint \"#{fixname}\". Consider removing the fixpoint and re-running the test if the change is intended."
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def store_fixpoint_and_fail(fixname, parent_fixname = nil)
|
93
|
+
store_fixpoint(fixname, parent_fixname)
|
94
|
+
pending("Fixpoint \"#{fixname}\" did not exist yet. Skipping comparison, but created fixpoint from database")
|
95
|
+
fail
|
96
|
+
end
|
97
|
+
|
98
|
+
# it is not a good idea to overwrite the fixpoint each time because timestamps may change (which then shows up in version control).
|
99
|
+
# Hence we only provide a method to write to it if it does not exist.
|
100
|
+
def store_fixpoint_unless_present(fixname, parent_fixname = nil)
|
101
|
+
store_fixpoint(fixname, parent_fixname) unless IncrementalFixpoint.exists?(fixname)
|
102
|
+
end
|
103
|
+
|
104
|
+
# +parent_fixname+ when given, only the (incremental) changes to the parent are saved
|
105
|
+
# please see store_fixpoint_unless_present for note on why not to use this method
|
106
|
+
def store_fixpoint(fixname, parent_fixname = nil)
|
107
|
+
IncrementalFixpoint.from_database(parent_fixname).save_to_file(fixname)
|
108
|
+
end
|
109
|
+
end
|
metadata
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fixpoints
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Tom Rothe
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-09-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 5.0.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 5.0.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
description: Fixpoints enables saving, restoring and comparing the database state
|
42
|
+
before & after tests
|
43
|
+
email:
|
44
|
+
- info@tomrothe.de
|
45
|
+
executables: []
|
46
|
+
extensions: []
|
47
|
+
extra_rdoc_files: []
|
48
|
+
files:
|
49
|
+
- ".gitignore"
|
50
|
+
- ".rspec"
|
51
|
+
- Gemfile
|
52
|
+
- Gemfile.lock
|
53
|
+
- LICENSE.txt
|
54
|
+
- README.md
|
55
|
+
- fixpoints.gemspec
|
56
|
+
- lib/fixpoint.rb
|
57
|
+
- lib/fixpoint_diff.rb
|
58
|
+
- lib/fixpoints.rb
|
59
|
+
- lib/fixpoints/version.rb
|
60
|
+
- lib/incremental_fixpoint.rb
|
61
|
+
homepage: https://github.com/motine/fixpoints
|
62
|
+
licenses:
|
63
|
+
- MIT
|
64
|
+
metadata:
|
65
|
+
homepage_uri: https://github.com/motine/fixpoints
|
66
|
+
source_code_uri: https://github.com/motine/fixpoints
|
67
|
+
post_install_message:
|
68
|
+
rdoc_options: []
|
69
|
+
require_paths:
|
70
|
+
- lib
|
71
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 2.3.0
|
76
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - ">="
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: '0'
|
81
|
+
requirements: []
|
82
|
+
rubygems_version: 3.1.2
|
83
|
+
signing_key:
|
84
|
+
specification_version: 4
|
85
|
+
summary: Don't discard the database state at the end of your test. Use it!
|
86
|
+
test_files: []
|