fixpoints 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/.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: []
|