fixation 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4e7265052364d7efeb5f95dee6a3aafb4f10d9d0
4
+ data.tar.gz: 54e03637bb7923633bf96030fc4a8a75e3ceaf08
5
+ SHA512:
6
+ metadata.gz: c88eba27cc793e2589ec689afac25d0e064c15e3837055eb4d516bfbdf32f30a84673bdf05030b116b9d0e8aa396611bec48f2985cb7f3698d8e77f665236980
7
+ data.tar.gz: 8c66e7f0dafa4350e5d9a0b1e396d264ba42de3b15de3b2e4c5100cfdb3a784ccd0a627f6b691fbdb06a2fd1090ac25d3000ac235d92a7b62a60bd905162f635
data/.gitignore ADDED
@@ -0,0 +1,36 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /spec/examples.txt
9
+ /test/tmp/
10
+ /test/version_tmp/
11
+ /tmp/
12
+
13
+ ## Specific to RubyMotion:
14
+ .dat*
15
+ .repl_history
16
+ build/
17
+
18
+ ## Documentation cache and generated files:
19
+ /.yardoc/
20
+ /_yardoc/
21
+ /doc/
22
+ /rdoc/
23
+
24
+ ## Environment normalization:
25
+ /.bundle/
26
+ /vendor/bundle
27
+ /lib/bundler/man/
28
+
29
+ # for a library or gem, you might want to ignore these files since the code is
30
+ # intended to run in multiple environments; otherwise, check them in:
31
+ # Gemfile.lock
32
+ # .ruby-version
33
+ # .ruby-gemset
34
+
35
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
36
+ .rvmrc
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in fixation.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Will Bryant
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 all
13
+ 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 THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,67 @@
1
+ # Fixation
2
+
3
+ This gem will precompile the SQL statements needed to clear and repopulate your test tables with fixtures when the app boots under spring, so that spec startup just needs to run a small number of multi-row SQL statements to prepare for run. This takes around 1/10th the time as a normal fixture load.
4
+
5
+ To avoid any problems when you change your model classes, Fixation avoids loading model classes when it reads your fixture files. This creates some incompatibilities with normal ActiveRecord fixtures for certain use cases - see Limitations below.
6
+
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ ```ruby
13
+ gem 'fixation'
14
+ ```
15
+
16
+ And then execute:
17
+
18
+ $ bundle
19
+
20
+ Or install it yourself as:
21
+
22
+ $ gem install fixation
23
+
24
+ Then, make an initializer:
25
+
26
+ if ENV['PRELOAD_FIXTURES'].to_i > 0 && Rails.env.test?
27
+ Rails.application.config.after_initialize do
28
+ require 'spec/fixture_helper'
29
+ Fixation.build_fixtures
30
+ end
31
+ end
32
+
33
+ And run `spring stop` so this gets picked up.
34
+
35
+ Finally, open up your spec_helper.rb, and find the current `global_fixtures` setting:
36
+
37
+ config.global_fixtures = :all
38
+
39
+ Change that to [] so that ActiveRecord doesn't load the fixtures again itself, and load the Fixation module:
40
+
41
+ config.global_fixtures = []
42
+ config.include Fixation.fixture_methods
43
+
44
+ ## Usage
45
+
46
+ Add the PRECOMPILE_FIXTURES=1 option to your spring test commands:
47
+
48
+ PRECOMPILE_FIXTURES=1 bundle exec spring spec/models/my_spec.rb
49
+
50
+ ## Limitations
51
+
52
+ PRECOMPILE_FIXTURES is not on by default in our suggested initializer above, because you don't want it set when running rake tasks like `db:create` and `db:test:prepare` - the initializer above will asplode if you do. This is one of the biggest drawbacks of this version of the gem.
53
+
54
+ Because Fixation wants to avoid loading your model classes when the app initializes, not all features of regular ActiveRecord fixtures are supported:
55
+ * the fixture filenames must match the database table names directly - whereas ActiveRecord actually assumes the filenames are underscored versions of the model class names, and it uses that class's configured table name
56
+ * when using the identify syntax to set association foreign keys, you must use the name that corresponds to the foreign key attribute (for example, `parent: sue` if you want to set the `parent_id` field to the appropriate value for the fixture called `sue`) - whereas ActiveRecord accepts the name of the association (`lead_carer: sue`) even if it uses a different foreign key name (`belongs_to :lead_carer, :foreign_key => :parent_id`)
57
+ * HABTM support has not been implemented (does anyone use HABTM these days?)
58
+ * enums are not known
59
+
60
+ ## Contributing
61
+
62
+ Bug reports and pull requests are welcome on GitHub at https://github.com/willbryant/fixation.
63
+
64
+ ## License
65
+
66
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
67
+
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "fixation"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
data/fixation.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'fixation/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "fixation"
8
+ spec.version = Fixation::VERSION
9
+ spec.authors = ["Will Bryant"]
10
+ spec.email = ["will.bryant@gmail.com"]
11
+
12
+ spec.summary = %q{10x faster fixture startup under spring.}
13
+ spec.description = %q{This gem will precompile the SQL statements needed to clear and repopulate your test tables with fixtures when the app boots under spring, so that spec startup just needs to run a small number of multi-row SQL statements to prepare for run. This takes around 1/10th the time as a normal fixture load.}
14
+ spec.homepage = "https://github.com/willbryant/fixation"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_development_dependency "bundler", "~> 1.10"
23
+ spec.add_development_dependency "rake", "~> 10.0"
24
+ end
@@ -0,0 +1,3 @@
1
+ module Fixation
2
+ VERSION = "0.1.0"
3
+ end
data/lib/fixation.rb ADDED
@@ -0,0 +1,262 @@
1
+ require 'erb'
2
+ require 'yaml'
3
+ require 'set'
4
+ require 'active_support/dependencies'
5
+ require 'active_record/errors'
6
+ require "fixation/version"
7
+
8
+ module Fixation
9
+ class FixtureTable
10
+ attr_reader :filename, :table_name, :connection, :now
11
+
12
+ def initialize(filename, table_name, connection, now)
13
+ @filename = filename
14
+ @table_name = table_name
15
+ @connection = connection
16
+ @now = now
17
+ end
18
+
19
+ def columns_hash
20
+ @columns_hash ||= connection.columns(table_name).index_by(&:name)
21
+ end
22
+
23
+ def content
24
+ template = File.read(filename)
25
+ render_context = ActiveRecord::FixtureSet::RenderContext.create_subclass.new.get_binding
26
+ ERB.new(template).result(render_context)
27
+ end
28
+
29
+ def parsed_rows
30
+ result = YAML.load(content)
31
+ result ||= {} # for completely empty files
32
+
33
+ unless (result.is_a?(Hash) || result.is_a?(YAML::Omap)) && result.all? { |name, attributes| name.is_a?(String) && attributes.is_a?(Hash) }
34
+ raise ActiveRecord::Fixture::FormatError, "#{filename} needs to contain a hash of fixtures"
35
+ end
36
+
37
+ result.delete('DEFAULTS')
38
+ result
39
+ rescue ArgumentError, Psych::SyntaxError => error
40
+ # we use exactly the same error class and message as ActiveRecord::FixtureSet in case anyone was depending on it
41
+ raise ActiveRecord::Fixture::FormatError, "a YAML error occurred parsing #{filename}. Please note that YAML must be consistently indented using spaces. Tabs are not allowed. Please have a look at http://www.yaml.org/faq.html\nThe exact error was:\n #{error.class}: #{error}", error.backtrace
42
+ end
43
+
44
+ def embellished_rows
45
+ @embellished_rows ||= parsed_rows.each do |name, attributes|
46
+ embellish_fixture(name, attributes, columns_hash)
47
+ end
48
+ end
49
+
50
+ def embellish_fixture(name, attributes, columns_hash)
51
+ # populate the primary key column, if not already set
52
+ if columns_hash['id'] && !attributes.has_key?('id')
53
+ attributes['id'] = Fixation.identify(name, columns_hash['id'].type)
54
+ end
55
+
56
+ if columns_hash['uuid'] && !attributes.has_key?('uuid')
57
+ attributes['uuid'] = Fixation.identify(name, columns_hash['uuid'].type)
58
+ end
59
+
60
+ # substitute $LABEL into all string values
61
+ attributes.each do |column_name, value|
62
+ attributes[column_name] = value.gsub("$LABEL", name) if value.is_a?(String)
63
+ end
64
+
65
+ # populate any timestamp columns, if not already set
66
+ %w(created_at updated_at).each do |column_name|
67
+ attributes[column_name] = now if columns_hash[column_name] && !attributes.has_key?(column_name)
68
+ end
69
+ %w(created_at updated_at).each do |column_name|
70
+ attributes[column_name] = now.to_date if columns_hash[column_name] && !attributes.has_key?(column_name)
71
+ end
72
+
73
+ # convert any association names into the identity column equivalent
74
+ nonexistant_columns = attributes.keys - columns_hash.keys
75
+ nonexistant_columns.each do |column_name|
76
+ if columns_hash["#{column_name}_id"]
77
+ value = attributes.delete(column_name)
78
+
79
+ if columns_hash["#{column_name}_type"] && value.is_a?(String) && value.sub!(/\s*\(([^\)]*)\)\s*$/, "")
80
+ # support polymorphic belongs_to as "label (Type)"
81
+ attributes["#{column_name}_type"] = $1
82
+ end
83
+
84
+ attributes["#{column_name}_id"] = value ? Fixation.identify(value) : value
85
+ else
86
+ raise ActiveRecord::Fixture::FormatError, "No column named #{column_name} found in table #{table_name}"
87
+ end
88
+ end
89
+ end
90
+
91
+ def fixture_ids
92
+ embellished_rows.each_with_object({}) do |(name, attributes), ids|
93
+ ids[name] = attributes['id'] || attributes['uuid']
94
+ end
95
+ end
96
+
97
+ def statements
98
+ statements = ["DELETE FROM #{connection.quote_table_name table_name}"]
99
+
100
+ unless embellished_rows.empty?
101
+ # first figure out what columns we have to insert into; we're going to need to use the same names for
102
+ # all rows so we can use the multi-line INSERT syntax
103
+ columns_to_include = Set.new
104
+ embellished_rows.each do |name, attributes|
105
+ attributes.each do |column_name, value|
106
+ columns_to_include.add(columns_hash[column_name])
107
+ end
108
+ end
109
+
110
+ # now build the INSERT statement
111
+ quoted_column_names = columns_to_include.collect { |column| connection.quote_column_name(column.name) }.join(', ')
112
+ statements <<
113
+ "INSERT INTO #{connection.quote_table_name table_name} (#{quoted_column_names}) VALUES " +
114
+ embellished_rows.collect do |name, attributes|
115
+ '(' + columns_to_include.collect do |column|
116
+ if attributes.has_key?(column.name)
117
+ quote_value(column, attributes[column.name])
118
+ else
119
+ column.default_function || quote_value(column, column.default)
120
+ end
121
+ end.join(', ') + ')'
122
+ end.join(', ')
123
+ end
124
+
125
+ statements
126
+ end
127
+
128
+ def quote_value(column, value)
129
+ connection.quote(value)
130
+ rescue TypeError
131
+ connection.quote(YAML.dump(value))
132
+ end
133
+ end
134
+
135
+ class Fixtures
136
+ def initialize
137
+ @class_names = {}
138
+ @fixture_ids = {}
139
+ @statements = {}
140
+
141
+ compile_fixture_files
142
+ end
143
+
144
+ def compile_fixture_files(connection = ActiveRecord::Base.connection)
145
+ puts "#{Time.now} building fixtures" if Fixation.trace
146
+
147
+ now = ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now
148
+ Fixation.paths.each do |path|
149
+ Dir["#{path}/{**,*}/*.yml"].each do |pathname|
150
+ basename = pathname[path.size + 1..-5]
151
+ compile_fixture_file(pathname, basename.gsub('/', '_'), basename.classify, connection, now) if ::File.file?(pathname)
152
+ end
153
+ end
154
+
155
+ puts "#{Time.now} built fixtures for #{@fixture_ids} tables" if Fixation.trace
156
+ end
157
+
158
+ def compile_fixture_file(filename, table_name, class_name, connection, now)
159
+ fixture_table = FixtureTable.new(filename, table_name, connection, now)
160
+ @fixture_ids[table_name] = fixture_table.fixture_ids
161
+ @statements[table_name] = fixture_table.statements
162
+ @class_names[table_name] = class_name
163
+ end
164
+
165
+ def apply_fixtures(connection = ActiveRecord::Base.connection)
166
+ @statements.each do |table_name, table_statements|
167
+ table_statements.each do |statement|
168
+ connection.execute(statement)
169
+ end
170
+ end
171
+ end
172
+
173
+ def fixture_methods
174
+ fixture_ids = @fixture_ids
175
+ class_names = @class_names
176
+
177
+ methods = Module.new do
178
+ def setup_fixtures(config = ActiveRecord::Base)
179
+ if run_in_transaction?
180
+ @@fixated_fixtures_applied ||= false
181
+ unless @@fixated_fixtures_applied
182
+ puts "#{Time.now} applying fixtures" if Fixation.trace
183
+ Fixation.apply_fixtures
184
+ @@fixated_fixtures_applied = true
185
+ puts "#{Time.now} applied fixtures" if Fixation.trace
186
+ end
187
+ else
188
+ @@fixated_fixtures_applied = false
189
+ end
190
+ super
191
+ end
192
+
193
+ fixture_ids.each do |table_name, fixtures|
194
+ begin
195
+ klass = class_names[table_name].constantize
196
+ rescue NameError
197
+ next
198
+ end
199
+
200
+ accessor_name = table_name
201
+ define_method(accessor_name) do |*fixture_names|
202
+ force_reload = fixture_names.pop if fixture_names.last == true || fixture_names.last == :reload
203
+
204
+ @fixture_cache[table_name] ||= {}
205
+
206
+ instances = fixture_names.map do |name|
207
+ id = fixtures[name.to_s]
208
+ raise StandardError, "No fixture named '#{name}' found for fixture set '#{table_name}'" if id.nil?
209
+
210
+ @fixture_cache[table_name].delete(name) if force_reload
211
+ @fixture_cache[table_name][name] ||= klass.find(id)
212
+ end
213
+
214
+ instances.size == 1 ? instances.first : instances
215
+ end
216
+ private accessor_name
217
+ end
218
+ end
219
+ end
220
+ end
221
+
222
+ cattr_accessor :trace
223
+ cattr_accessor :paths
224
+ self.paths = %w(test/fixtures spec/fixtures)
225
+
226
+ def self.build_fixtures
227
+ subclasses_before = ActiveRecord::Base.subclasses
228
+
229
+ @fixtures = Fixtures.new
230
+
231
+ subclasses_after = ActiveRecord::Base.subclasses
232
+
233
+ unless subclasses_after.size == subclasses_before.size
234
+ new_subclasses = subclasses_after - subclasses_before
235
+ puts "warning: #{new_subclasses.to_sentence} #{new_subclasses.size == 1 ? 'was' : 'were'} auto-loaded while loading fixtures. #{new_subclasses.size == 1 ? 'this class' : 'these classes'} may not reload properly."
236
+ end
237
+ end
238
+
239
+ def self.apply_fixtures
240
+ build_fixtures unless @fixtures
241
+ @fixtures.apply_fixtures
242
+ end
243
+
244
+ def self.fixture_methods
245
+ build_fixtures unless @fixtures
246
+ @fixtures.fixture_methods
247
+ end
248
+
249
+ # Returns a consistent, platform-independent identifier for +label+.
250
+ # Integer identifiers are values less than 2^30. UUIDs are RFC 4122 version 5 SHA-1 hashes.
251
+ #
252
+ # Uses the ActiveRecord fixtures method for compatibility.
253
+ if ActiveRecord::FixtureSet.method(:identify).arity == 1
254
+ def self.identify(label, _column_type = :integer)
255
+ ActiveRecord::FixtureSet.identify(label)
256
+ end
257
+ else
258
+ def self.identify(label, column_type = :integer)
259
+ ActiveRecord::FixtureSet.identify(label, column_type)
260
+ end
261
+ end
262
+ end
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fixation
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Will Bryant
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-03-24 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.10'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.10'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ description: This gem will precompile the SQL statements needed to clear and repopulate
42
+ your test tables with fixtures when the app boots under spring, so that spec startup
43
+ just needs to run a small number of multi-row SQL statements to prepare for run. This
44
+ takes around 1/10th the time as a normal fixture load.
45
+ email:
46
+ - will.bryant@gmail.com
47
+ executables: []
48
+ extensions: []
49
+ extra_rdoc_files: []
50
+ files:
51
+ - ".gitignore"
52
+ - Gemfile
53
+ - LICENSE
54
+ - README.md
55
+ - Rakefile
56
+ - bin/console
57
+ - bin/setup
58
+ - fixation.gemspec
59
+ - lib/fixation.rb
60
+ - lib/fixation/version.rb
61
+ homepage: https://github.com/willbryant/fixation
62
+ licenses:
63
+ - MIT
64
+ metadata: {}
65
+ post_install_message:
66
+ rdoc_options: []
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ requirements: []
80
+ rubyforge_project:
81
+ rubygems_version: 2.2.5
82
+ signing_key:
83
+ specification_version: 4
84
+ summary: 10x faster fixture startup under spring.
85
+ test_files: []