snowglobe 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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