snowglobe 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.
@@ -0,0 +1,25 @@
1
+ module Snowglobe
2
+ module DatabaseAdapters
3
+ class PostgreSQL
4
+ def self.name
5
+ :postgresql
6
+ end
7
+
8
+ attr_reader :database
9
+
10
+ def initialize(database)
11
+ @database = database
12
+ end
13
+
14
+ def adapter
15
+ self.class.name
16
+ end
17
+
18
+ def require_dependencies
19
+ require "pg"
20
+ end
21
+ end
22
+
23
+ DatabaseConfigurationRegistry.instance.register(PostgreSQL)
24
+ end
25
+ end
@@ -0,0 +1,26 @@
1
+ module Snowglobe
2
+ module DatabaseAdapters
3
+ class SQLite3
4
+ def self.name
5
+ :sqlite3
6
+ end
7
+
8
+ def initialize(_database)
9
+ end
10
+
11
+ def adapter
12
+ self.class.name
13
+ end
14
+
15
+ def database
16
+ "db/db.sqlite3"
17
+ end
18
+
19
+ def require_dependencies
20
+ require "sqlite3"
21
+ end
22
+ end
23
+
24
+ DatabaseConfigurationRegistry.instance.register(SQLite3)
25
+ end
26
+ end
@@ -0,0 +1,33 @@
1
+ require_relative "database_configuration_registry"
2
+ require "delegate"
3
+
4
+ module Snowglobe
5
+ class DatabaseConfiguration < SimpleDelegator
6
+ ENVIRONMENTS = %w(development test production).freeze
7
+
8
+ attr_reader :adapter_class
9
+
10
+ def self.for(database_name, adapter_name)
11
+ config_class = DatabaseConfigurationRegistry.instance.get(adapter_name)
12
+ config = config_class.new(database_name)
13
+ new(config)
14
+ end
15
+
16
+ def initialize(config)
17
+ @adapter_class = config.class.to_s.split("::").last
18
+ super(config)
19
+ end
20
+
21
+ def to_hash
22
+ ENVIRONMENTS.each_with_object({}) do |env, config_as_hash|
23
+ config_as_hash[env] = inner_config_as_hash
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def inner_config_as_hash
30
+ { "adapter" => adapter.to_s, "database" => database.to_s }
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,28 @@
1
+ require "singleton"
2
+
3
+ module Snowglobe
4
+ class DatabaseConfigurationRegistry
5
+ include Singleton
6
+
7
+ def initialize
8
+ @registry = {}
9
+ end
10
+
11
+ def register(config_class)
12
+ registry[config_class.name] = config_class
13
+ end
14
+
15
+ def get(name)
16
+ registry.fetch(name) do
17
+ raise KeyError, "No such adapter registered: #{name}"
18
+ end
19
+ end
20
+
21
+ protected
22
+
23
+ attr_reader :registry
24
+ end
25
+ end
26
+
27
+ require_relative "database_adapters/postgresql"
28
+ require_relative "database_adapters/sqlite3"
@@ -0,0 +1,95 @@
1
+ require "fileutils"
2
+
3
+ module Snowglobe
4
+ class Filesystem
5
+ def clean
6
+ if root_directory.exist?
7
+ root_directory.rmtree
8
+ end
9
+ end
10
+
11
+ def project_directory
12
+ root_directory.join(Snowglobe.configuration.project_name)
13
+ end
14
+
15
+ def create_project
16
+ project_directory.mkpath
17
+ end
18
+
19
+ def within_project(&block)
20
+ Dir.chdir(project_directory, &block)
21
+ end
22
+
23
+ def find_in_project(path)
24
+ project_directory.join(path)
25
+ end
26
+
27
+ def open_file(path, *args, &block)
28
+ wrap_file(path).open(*args, &block)
29
+ end
30
+
31
+ def comment_lines_matching_in_file(path, pattern)
32
+ transform_file(path) do |lines|
33
+ lines.map do |line|
34
+ if line && line =~ pattern
35
+ "###{line}"
36
+ else
37
+ line
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ def transform_file(path)
44
+ content = read_file(path)
45
+ lines = content.split(/\n/)
46
+ transformed_lines = yield lines
47
+ write_file(path, transformed_lines.join("\n") + "\n")
48
+ end
49
+
50
+ def read_file(path)
51
+ wrap_file(path).read
52
+ end
53
+
54
+ def write_file(path, content)
55
+ pathname = wrap_file(path)
56
+ create_parents_of(pathname)
57
+ pathname.open("w") { |f| f.write(content) }
58
+ pathname
59
+ end
60
+
61
+ def append_to_file(path, content, _options = {})
62
+ pathname = wrap_file(path)
63
+ create_parents_of(pathname)
64
+ pathname.open("a") { |f| f.puts(content + "\n") }
65
+ end
66
+
67
+ def remove_from_file(path, pattern)
68
+ unless pattern.is_a?(Regexp)
69
+ pattern = Regexp.new("^" + Regexp.escape(pattern) + "$")
70
+ end
71
+
72
+ transform(path) do |lines|
73
+ lines.reject { |line| line =~ pattern }
74
+ end
75
+ end
76
+
77
+ private
78
+
79
+ def root_directory
80
+ Snowglobe.configuration.temporary_directory
81
+ end
82
+
83
+ def wrap_file(path)
84
+ if path.is_a?(Pathname)
85
+ path
86
+ else
87
+ find_in_project(path)
88
+ end
89
+ end
90
+
91
+ def create_parents_of(pathname)
92
+ pathname.dirname.mkpath
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,55 @@
1
+ module Snowglobe
2
+ class GemVersion
3
+ def initialize(version)
4
+ @version = Gem::Version.new(version.to_s + "")
5
+ end
6
+
7
+ def major
8
+ segments[0]
9
+ end
10
+
11
+ def minor
12
+ segments[1]
13
+ end
14
+
15
+ def <(other)
16
+ compare?(:<, other)
17
+ end
18
+
19
+ def <=(other)
20
+ compare?(:<=, other)
21
+ end
22
+
23
+ def ==(other)
24
+ compare?(:==, other)
25
+ end
26
+
27
+ def >=(other)
28
+ compare?(:>=, other)
29
+ end
30
+
31
+ def >(other)
32
+ compare?(:>, other)
33
+ end
34
+
35
+ def =~(other)
36
+ Gem::Requirement.new(other).satisfied_by?(version)
37
+ end
38
+
39
+ def to_s
40
+ version.to_s
41
+ end
42
+
43
+ private
44
+
45
+ attr_reader :version
46
+
47
+ def segments
48
+ @_segments ||= version.to_s.split(".")
49
+ end
50
+
51
+ def compare?(op, other)
52
+ Gem::Requirement.new("#{op} #{other}").satisfied_by?(version)
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,69 @@
1
+ module Snowglobe
2
+ class ProjectCommandRunner
3
+ def initialize(fs)
4
+ @fs = fs
5
+ end
6
+
7
+ def run_migrations!
8
+ run_rake_tasks!(["db:drop", "db:create", "db:migrate"])
9
+ end
10
+
11
+ def run_n_unit_tests(*paths)
12
+ run_command_within_bundle("ruby -I lib -I test", *paths)
13
+ end
14
+
15
+ def run_n_unit_test_suite
16
+ run_rake_tasks("test", env: { TESTOPTS: "-v" })
17
+ end
18
+
19
+ def run_rake_tasks!(*args, **options, &block)
20
+ run_rake_tasks(
21
+ *args,
22
+ **options,
23
+ run_successfully: true,
24
+ &block
25
+ )
26
+ end
27
+
28
+ def run_rake_tasks(*tasks)
29
+ options = tasks.last.is_a?(Hash) ? tasks.pop : {}
30
+ args = ["bundle", "exec", "rake", *tasks, "--trace"] + [options]
31
+ run(*args)
32
+ end
33
+
34
+ def run_within_bundle(*args)
35
+ run(*args) do |runner|
36
+ runner.command_prefix = "bundle exec"
37
+ runner.env["BUNDLE_GEMFILE"] = fs.find_in_project("Gemfile").to_s
38
+
39
+ runner.around_command do |run_command|
40
+ Bundler.with_clean_env(&run_command)
41
+ end
42
+
43
+ yield runner if block_given?
44
+ end
45
+ end
46
+
47
+ def run!(*args, **options, &block)
48
+ CommandRunner.run!(
49
+ *args,
50
+ directory: fs.project_directory,
51
+ **options,
52
+ &block
53
+ )
54
+ end
55
+
56
+ def run(*args, **options, &block)
57
+ CommandRunner.run(
58
+ *args,
59
+ directory: fs.project_directory,
60
+ **options,
61
+ &block
62
+ )
63
+ end
64
+
65
+ private
66
+
67
+ attr_reader :fs
68
+ end
69
+ end
@@ -0,0 +1,194 @@
1
+ require "forwardable"
2
+ require "yaml"
3
+
4
+ require_relative "bundle"
5
+ require_relative "database"
6
+ require_relative "filesystem"
7
+ require_relative "project_command_runner"
8
+
9
+ module Snowglobe
10
+ class RailsApplication
11
+ extend Forwardable
12
+
13
+ def_delegators :bundle, :add_gem
14
+ def_delegators :fs, :append_to_file, :write_file
15
+ def_delegators :command_runner, :run_migrations!, :run_n_unit_test_suite
16
+
17
+ def initialize
18
+ @fs = Filesystem.new
19
+ @command_runner = ProjectCommandRunner.new(fs)
20
+ @bundle = Bundle.new(fs: fs, command_runner: command_runner)
21
+ @database = Database.new
22
+ end
23
+
24
+ def create
25
+ fs.clean
26
+ generate
27
+
28
+ fs.within_project do
29
+ # install_gems
30
+ remove_unwanted_gems
31
+ end
32
+ end
33
+
34
+ # def load
35
+ # load_environment
36
+ # run_migrations
37
+ # end
38
+
39
+ def evaluate!(code)
40
+ command_runner = nil
41
+
42
+ Dir::Tmpname.create(
43
+ ["", ".rb"],
44
+ fs.find_in_project("tmp"),
45
+ ) do |path, _, _, _|
46
+ tempfile = fs.write_file(path, code)
47
+
48
+ command_runner = run_command!(
49
+ "ruby",
50
+ tempfile.to_s,
51
+ err: nil,
52
+ env: { "RUBYOPT" => "" },
53
+ )
54
+ end
55
+
56
+ command_runner.output
57
+ end
58
+
59
+ def run_command!(*args, &block)
60
+ command_runner.run!(*args, &block)
61
+ end
62
+
63
+ # def gemfile_path
64
+ # fs.find("Gemfile")
65
+ # end
66
+
67
+ # def temp_views_directory
68
+ # fs.find_in_project("tmp/views")
69
+ # end
70
+
71
+ # def create_temp_view(path, contents)
72
+ # full_path = temp_view_path_for(path)
73
+ # full_path.dirname.mkpath
74
+ # full_path.open("w") { |f| f.write(contents) }
75
+ # end
76
+
77
+ # def delete_temp_views
78
+ # if temp_views_directory.exist?
79
+ # temp_views_directory.rmtree
80
+ # end
81
+ # end
82
+
83
+ # def draw_routes(&block)
84
+ # Rails.application.routes.draw(&block)
85
+ # Rails.application.routes
86
+ # end
87
+
88
+ def migration_class_name
89
+ if rails_version > 5
90
+ number = [rails_version.major, rails_version.minor].join(".").to_f
91
+ "ActiveRecord::Migration[#{number}]"
92
+ else
93
+ "ActiveRecord::Migration"
94
+ end
95
+ end
96
+
97
+ private
98
+
99
+ attr_reader :fs, :command_runner, :bundle, :database
100
+
101
+ def rails_version
102
+ @_rails_version ||= bundle.version_of("rails")
103
+ end
104
+
105
+ def migrations_directory
106
+ fs.find_in_project("db/migrate")
107
+ end
108
+
109
+ def temp_view_path_for(path)
110
+ temp_views_directory.join(path)
111
+ end
112
+
113
+ def generate
114
+ rails_new
115
+ fix_available_locales_warning
116
+ remove_bootsnap
117
+ write_database_configuration
118
+
119
+ if bundle.version_of("rails") >= 5
120
+ add_initializer_for_time_zone_aware_types
121
+ end
122
+ end
123
+
124
+ def rails_new
125
+ CommandRunner.run!(
126
+ %W(rails new #{fs.project_directory} --skip-bundle --no-rc),
127
+ )
128
+ end
129
+
130
+ def fix_available_locales_warning
131
+ # See here for more on this:
132
+ # http://stackoverflow.com/questions/20361428/rails-i18n-validation-deprecation-warning
133
+ fs.transform_file("config/application.rb") do |lines|
134
+ lines.insert(-3, <<-TEXT)
135
+ if I18n.respond_to?(:enforce_available_locales=)
136
+ I18n.enforce_available_locales = false
137
+ end
138
+ TEXT
139
+ end
140
+ end
141
+
142
+ def remove_bootsnap
143
+ # Rails 5.2 introduced bootsnap, which is helpful when you're developing
144
+ # or deploying an app, but we don't really need it (and it messes with
145
+ # Zeus anyhow)
146
+ fs.comment_lines_matching_in_file(
147
+ "config/boot.rb",
148
+ %r{\Arequire 'bootsnap/setup'},
149
+ )
150
+ end
151
+
152
+ def write_database_configuration
153
+ fs.open_file("config/database.yml", "w") do |f|
154
+ YAML.dump(database.config.to_hash, f)
155
+ end
156
+ end
157
+
158
+ def add_initializer_for_time_zone_aware_types
159
+ path = "config/initializers/configure_time_zone_aware_types.rb"
160
+ fs.write_file(path, <<-TEXT)
161
+ Rails.application.configure do
162
+ config.active_record.time_zone_aware_types = [:datetime, :time]
163
+ end
164
+ TEXT
165
+ end
166
+
167
+ # def load_environment
168
+ # require environment_file_path
169
+ # end
170
+
171
+ # def environment_file_path
172
+ # fs.find_in_project("config/environment")
173
+ # end
174
+
175
+ # def run_migrations
176
+ # fs.within_project do
177
+ # run_command! "bundle exec rake db:drop db:create db:migrate"
178
+ # end
179
+ # end
180
+
181
+ # def install_gems
182
+ # bundle.install_gems
183
+ # end
184
+
185
+ def remove_unwanted_gems
186
+ bundle.updating do
187
+ bundle.remove_gem "debugger"
188
+ bundle.remove_gem "byebug"
189
+ bundle.add_gem "pry-byebug"
190
+ bundle.remove_gem "web-console"
191
+ end
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,19 @@
1
+ module Snowglobe
2
+ module TestHelpers
3
+ def rails_app
4
+ @_rails_app ||= RailsApplication.new
5
+ end
6
+
7
+ def fs
8
+ rails_app.fs
9
+ end
10
+
11
+ def bundle
12
+ rails_app.bundle
13
+ end
14
+
15
+ def database
16
+ rails_app.database
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,3 @@
1
+ module Snowglobe
2
+ VERSION = "0.1.0"
3
+ end
data/lib/snowglobe.rb ADDED
@@ -0,0 +1,13 @@
1
+ require_relative "snowglobe/configuration"
2
+ require_relative "snowglobe/rails_application"
3
+ require_relative "snowglobe/version"
4
+
5
+ module Snowglobe
6
+ def self.configure(&block)
7
+ configuration.update!(&block)
8
+ end
9
+
10
+ def self.configuration
11
+ @_configuration ||= Configuration.new
12
+ end
13
+ end
data/snowglobe.gemspec ADDED
@@ -0,0 +1,41 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "snowglobe/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "snowglobe"
8
+ spec.version = Snowglobe::VERSION
9
+ spec.authors = ["Elliot Winkler"]
10
+ spec.email = ["elliot.winkler@gmail.com"]
11
+
12
+ spec.summary = %q{Create temporary Rails applications for use in testing}
13
+ spec.description = %q{Snowglobe is a gem that helps erect and destroy Rails applications for use in tests.}
14
+ spec.homepage = "https://github.com/mcmire/snowglobe"
15
+ spec.license = "MIT"
16
+
17
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
18
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
19
+ if spec.respond_to?(:metadata)
20
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
21
+
22
+ spec.metadata["homepage_uri"] = spec.homepage
23
+ spec.metadata["source_code_uri"] = spec.homepage
24
+ # spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
25
+ else
26
+ raise "RubyGems 2.0 or newer is required to protect against " \
27
+ "public gem pushes."
28
+ end
29
+
30
+ # Specify which files should be added to the gem when it is released.
31
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
32
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
33
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
34
+ end
35
+ spec.bindir = "exe"
36
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
37
+ spec.require_paths = ["lib"]
38
+
39
+ spec.add_development_dependency "bundler", "~> 2.0"
40
+ spec.add_development_dependency "rake", "~> 10.0"
41
+ end