monkey_butler 1.2.2

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.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/.bundle/config +2 -0
  3. data/.gitignore +3 -0
  4. data/.ruby-version +1 -0
  5. data/CHANGELOG.md +28 -0
  6. data/Gemfile +4 -0
  7. data/Gemfile.lock +122 -0
  8. data/Guardfile +8 -0
  9. data/LICENSE +202 -0
  10. data/README.md +221 -0
  11. data/Rakefile +16 -0
  12. data/bin/mb +6 -0
  13. data/lib/monkey_butler.rb +1 -0
  14. data/lib/monkey_butler/actions.rb +38 -0
  15. data/lib/monkey_butler/cli.rb +354 -0
  16. data/lib/monkey_butler/databases/abstract_database.rb +49 -0
  17. data/lib/monkey_butler/databases/cassandra_database.rb +69 -0
  18. data/lib/monkey_butler/databases/sqlite_database.rb +105 -0
  19. data/lib/monkey_butler/migrations.rb +52 -0
  20. data/lib/monkey_butler/project.rb +90 -0
  21. data/lib/monkey_butler/targets/base.rb +85 -0
  22. data/lib/monkey_butler/targets/cassandra/cassandra_target.rb +72 -0
  23. data/lib/monkey_butler/targets/cassandra/create_schema_migrations.cql.erb +16 -0
  24. data/lib/monkey_butler/targets/cassandra/migration.cql.erb +0 -0
  25. data/lib/monkey_butler/targets/cocoapods/cocoapods_target.rb +43 -0
  26. data/lib/monkey_butler/targets/cocoapods/podspec.erb +12 -0
  27. data/lib/monkey_butler/targets/maven/maven_target.rb +60 -0
  28. data/lib/monkey_butler/targets/maven/project/.gitignore +6 -0
  29. data/lib/monkey_butler/targets/maven/project/MonkeyButler.iml +19 -0
  30. data/lib/monkey_butler/targets/maven/project/build.gradle +54 -0
  31. data/lib/monkey_butler/targets/sqlite/create_monkey_butler_tables.sql.erb +15 -0
  32. data/lib/monkey_butler/targets/sqlite/migration.sql.erb +11 -0
  33. data/lib/monkey_butler/targets/sqlite/sqlite_target.rb +91 -0
  34. data/lib/monkey_butler/templates/Gemfile.erb +4 -0
  35. data/lib/monkey_butler/templates/gitignore.erb +1 -0
  36. data/lib/monkey_butler/util.rb +71 -0
  37. data/lib/monkey_butler/version.rb +3 -0
  38. data/logo.jpg +0 -0
  39. data/monkey_butler.gemspec +33 -0
  40. data/spec/cli_spec.rb +700 -0
  41. data/spec/databases/cassandra_database_spec.rb +241 -0
  42. data/spec/databases/sqlite_database_spec.rb +181 -0
  43. data/spec/migrations_spec.rb +4 -0
  44. data/spec/project_spec.rb +128 -0
  45. data/spec/sandbox/cassandra/.gitignore +2 -0
  46. data/spec/sandbox/cassandra/.monkey_butler.yml +7 -0
  47. data/spec/sandbox/cassandra/migrations/20140523123443021_create_sandbox.cql.sql +14 -0
  48. data/spec/sandbox/cassandra/sandbox.cql +0 -0
  49. data/spec/sandbox/sqlite/.gitignore +2 -0
  50. data/spec/sandbox/sqlite/.monkey_butler.yml +7 -0
  51. data/spec/sandbox/sqlite/migrations/20140523123443021_create_sandbox.sql +14 -0
  52. data/spec/sandbox/sqlite/sandbox.sql +0 -0
  53. data/spec/spec_helper.rb +103 -0
  54. data/spec/targets/cassandra_target_spec.rb +191 -0
  55. data/spec/targets/cocoapods_target_spec.rb +197 -0
  56. data/spec/targets/maven_target_spec.rb +156 -0
  57. data/spec/targets/sqlite_target_spec.rb +103 -0
  58. data/spec/util_spec.rb +13 -0
  59. metadata +260 -0
@@ -0,0 +1,2 @@
1
+ .DS_Store
2
+ sandbox.sqlite
@@ -0,0 +1,7 @@
1
+ name: sandbox
2
+ database_url: cassandra://localhost:9042/sandbox
3
+ targets:
4
+ - cocoapods
5
+ config:
6
+ cocoapods.repo: example_specs_repo
7
+ maven.url: "http://maven.layer.com:1234/nexus"
@@ -0,0 +1,14 @@
1
+ /* Create Monkey Butler Tables */
2
+
3
+ -- Maintains list of applied migrations
4
+ CREATE TABLE schema_migrations(
5
+ version INTEGER UNIQUE NOT NULL
6
+ );
7
+
8
+ /* Create application tables */
9
+ /*
10
+ CREATE TABLE table_name (
11
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
12
+ ...
13
+ );
14
+ */
File without changes
@@ -0,0 +1,2 @@
1
+ .DS_Store
2
+ sandbox.sqlite
@@ -0,0 +1,7 @@
1
+ name: sandbox
2
+ database_url: sqlite:sandbox.sqlite
3
+ targets:
4
+ - cocoapods
5
+ config:
6
+ cocoapods.repo: example_specs_repo
7
+ maven.url: "http://maven.layer.com:1234/nexus"
@@ -0,0 +1,14 @@
1
+ /* Create Monkey Butler Tables */
2
+
3
+ -- Maintains list of applied migrations
4
+ CREATE TABLE schema_migrations(
5
+ version INTEGER UNIQUE NOT NULL
6
+ );
7
+
8
+ /* Create application tables */
9
+ /*
10
+ CREATE TABLE table_name (
11
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
12
+ ...
13
+ );
14
+ */
File without changes
@@ -0,0 +1,103 @@
1
+ require 'bundler/setup'
2
+ Bundler.setup
3
+
4
+ require 'monkey_butler'
5
+
6
+ require 'rspec/core/shared_context'
7
+ require 'tempfile'
8
+ require 'digest'
9
+ require 'byebug'
10
+
11
+ module GlobalContext
12
+ extend RSpec::Core::SharedContext
13
+
14
+ # Assume that most specs will describe a Thor subclass
15
+ let(:thor_class) { subject.class }
16
+ end
17
+
18
+ RSpec.configure do |config|
19
+ config.before do
20
+ ARGV.replace []
21
+ MonkeyButler::Project.clear
22
+ end
23
+
24
+ config.include GlobalContext
25
+
26
+ def capture(stream)
27
+ begin
28
+ stream = stream.to_s
29
+ eval "$#{stream} = StringIO.new"
30
+ yield
31
+ result = eval("$#{stream}").string
32
+ ensure
33
+ eval("$#{stream} = #{stream.upcase}")
34
+ end
35
+
36
+ result
37
+ end
38
+
39
+ def capture_output(proc, &block)
40
+ error = nil
41
+ content = capture(:stdout) do
42
+ error = capture(:stderr) do
43
+ proc.call
44
+ end
45
+ end
46
+ yield content, error if block_given?
47
+ { stdout: content, stderr: error }
48
+ end
49
+
50
+ # def source_root
51
+ # File.join(File.dirname(__FILE__), "fixtures")
52
+ # end
53
+
54
+ def sandbox_root
55
+ Pathname.new File.join(File.dirname(__FILE__), "sandbox")
56
+ end
57
+
58
+ def clone_temp_sandbox(database = :sqlite)
59
+ Dir.mktmpdir.tap do |path|
60
+ FileUtils.cp_r Dir.glob(sandbox_root + "#{database}/."), path
61
+ Dir.chdir(path) do
62
+ system("git init -q .")
63
+ system("git remote add origin git@github.com:layerhq/monkey_butler_sandbox.git")
64
+ end
65
+ end
66
+ end
67
+
68
+ def random_migration_name
69
+ timestamp = MonkeyButler::Util.migration_timestamp + rand(1..1000)
70
+ MonkeyButler::Util.migration_named(Digest::SHA256.hexdigest(Time.now.to_s), timestamp)
71
+ end
72
+
73
+ # Requires `thor_class` and `project_root`
74
+ def invoke!(args = [], options = {:capture => true})
75
+ output = nil
76
+ # Some commands work with a directory that doesn't yet exist
77
+ dir = File.exists?(project_root) ? project_root : '.'
78
+ Dir.chdir(dir) do
79
+ if options[:capture]
80
+ output = capture_output(proc { thor_class.start(args) })
81
+ else
82
+ thor_class.start(args)
83
+ end
84
+ end
85
+ end
86
+
87
+ def invoke_target!(command, options = {})
88
+ output = nil
89
+ # Some commands work with a directory that doesn't yet exist
90
+ dir = File.exists?(project_root) ? project_root : '.'
91
+ Dir.chdir(dir) do
92
+ if options.delete(:capture)
93
+ output = capture_output proc do
94
+ target = thor_class.new([], options)
95
+ target.invoke(command)
96
+ end
97
+ else
98
+ target = thor_class.new([], options)
99
+ target.invoke(command)
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,191 @@
1
+ require 'spec_helper'
2
+ require 'monkey_butler/targets/cassandra/cassandra_target'
3
+ require 'monkey_butler/databases/cassandra_database'
4
+
5
+ describe MonkeyButler::Targets::CassandraTarget do
6
+ let(:thor_class) { MonkeyButler::CLI }
7
+ let!(:project_root) { clone_temp_sandbox(:cassandra) }
8
+ let(:project) { MonkeyButler::Project.load(project_root) }
9
+ let(:database) { MonkeyButler::Databases::CassandraDatabase.new(project.database_url) }
10
+
11
+ before(:each) do
12
+ database.drop
13
+ end
14
+
15
+ describe "#init" do
16
+ let(:path) do
17
+ Dir.mktmpdir.tap { |path| FileUtils.remove_entry(path) }
18
+ end
19
+
20
+ it "generates a new migration with the given name" do
21
+ invoke!(["init", "-d", "cassandra://localhost:9170/sandbox", path])
22
+ migration = Dir.entries(File.join(path, 'migrations')).detect { |f| f =~ /\d{15}_create_.+\.cql/ }
23
+ migration.should_not be_nil
24
+ end
25
+ end
26
+
27
+ describe "#new" do
28
+ it "generates a new migration with the given name" do
29
+ invoke!(%w{new add_column_to_table})
30
+ migration = Dir.entries(File.join(project_root, 'migrations')).detect { |f| f =~ /add_column_to_table\.cql/ }
31
+ migration.should_not be_nil
32
+ end
33
+ end
34
+
35
+ describe "#dump" do
36
+ before(:each) do
37
+ database = MonkeyButler::Databases::CassandraDatabase.new(project.database_url)
38
+ database.create_migrations_table
39
+ database.insert_version(123)
40
+ end
41
+
42
+ it "dumps the database" do
43
+ invoke!(%w{dump})
44
+ content = File.read(File.join(project_root, project.schema_path))
45
+ content.should =~ /CREATE KEYSPACE sandbox/
46
+ end
47
+
48
+ it "dumps the schema_migrations rows" do
49
+ invoke!(%w{dump})
50
+ content = File.read(File.join(project_root, project.schema_path))
51
+ content.should =~ /INSERT INTO schema_migrations\(partition_key, version\) VALUES \(0, 123\);/
52
+ end
53
+
54
+ context "when there are extra keyspaces specified" do
55
+ before(:each) do
56
+ database.client.execute "CREATE KEYSPACE IF NOT EXISTS extra1 WITH replication = {'class' : 'SimpleStrategy', 'replication_factor' : 1};"
57
+ database.client.execute "CREATE KEYSPACE IF NOT EXISTS extra2 WITH replication = {'class' : 'SimpleStrategy', 'replication_factor' : 1};"
58
+ end
59
+
60
+ it "dumps the additional keyspaces" do
61
+ project.config['cassandra.keyspaces'] = %w{extra1 extra2}
62
+ project.save!(project_root)
63
+ invoke!(%w{dump})
64
+ content = File.read(File.join(project_root, project.schema_path))
65
+ content.should =~ /CREATE KEYSPACE extra1/
66
+ content.should =~ /CREATE KEYSPACE extra2/
67
+ end
68
+ end
69
+ end
70
+
71
+ describe "#load" do
72
+ before(:each) do
73
+ @database = MonkeyButler::Databases::CassandraDatabase.new(project.database_url)
74
+ @database.create_migrations_table
75
+ invoke!(%w{dump})
76
+ @database.drop
77
+ end
78
+
79
+ it "loads the database" do
80
+ expect(@database.migrations_table?).to be_false
81
+ invoke!(%w{load})
82
+ expect(@database.migrations_table?).to be_true
83
+ end
84
+ end
85
+
86
+ describe "#status" do
87
+ context "when the database is empty" do
88
+ before(:each) do
89
+ database.drop
90
+ end
91
+
92
+ it "shows status" do
93
+ output = invoke!(%w{status})
94
+ output[:stdout].should =~ /New database/
95
+ output[:stdout].should =~ /The database at 'cassandra:\/\/localhost:9042\/sandbox' does not have a 'schema_migrations' table./
96
+ end
97
+ end
98
+
99
+ context "when the database is up to date" do
100
+ before(:each) do
101
+ database.create_migrations_table
102
+ database.insert_version(12345)
103
+ end
104
+
105
+ it "tells the user the database is up to date" do
106
+ output = invoke!(%w{status})
107
+ output[:stdout].should =~ /Current version: 12345/
108
+ output[:stdout].should =~ /Database is up to date./
109
+ end
110
+ end
111
+
112
+ context "when there are migrations to apply" do
113
+ before(:each) do
114
+ File.open(File.join(project_root, 'migrations/20140523123443021_add_another_table.cql'), 'w') do |f|
115
+ f << "CREATE TABLE IF NOT EXISTS sandbox.another_table (some_column VARINT, PRIMARY KEY (some_column));"
116
+ end
117
+ database.create_migrations_table
118
+ database.insert_version(12345)
119
+ end
120
+
121
+ it "tells the user there is a migration to apply" do
122
+ output = invoke!(%w{status})
123
+ output[:stdout].should =~ /The database at 'cassandra:\/\/localhost:9042\/sandbox' is 1 version behind 20140523123443021/
124
+ output[:stdout].should =~ /pending migration: migrations\/20140523123443021_add_another_table.cql/
125
+ end
126
+ end
127
+ end
128
+
129
+ describe "#migrate" do
130
+ context "when the database is empty" do
131
+ before(:each) do
132
+ database.drop
133
+ end
134
+
135
+ it "shows migrate" do
136
+ output = invoke!(%w{migrate})
137
+ output[:stdout].should =~ /Database is up to date./
138
+ end
139
+ end
140
+
141
+ context "when the database is up to date" do
142
+ before(:each) do
143
+ database.create_migrations_table
144
+ database.insert_version(12345)
145
+ end
146
+
147
+ it "tells the user the database is up to date" do
148
+ output = invoke!(%w{migrate})
149
+ output[:stdout].should =~ /Database is up to date./
150
+ end
151
+ end
152
+
153
+ context "when there are migrations to apply" do
154
+ before(:each) do
155
+ File.open(File.join(project_root, 'migrations/20140523123443021_add_another_table.cql'), 'w') do |f|
156
+ f << "CREATE TABLE IF NOT EXISTS sandbox.another_table (some_column VARINT, PRIMARY KEY (some_column));"
157
+ end
158
+ database.create_migrations_table
159
+ database.insert_version(12345)
160
+ end
161
+
162
+ it "tells the user there is a migration to apply" do
163
+ output = invoke!(%w{migrate})
164
+ output[:stdout].should =~ /applying migration: migrations\/20140523123443021_add_another_table.cql/
165
+ output[:stdout].should =~ /Migration to version 20140523123443021 complete./
166
+ end
167
+ end
168
+ end
169
+
170
+ describe "#drop" do
171
+ before(:each) do
172
+ database.create_migrations_table
173
+ database.insert_version(123)
174
+ database.client.execute "CREATE KEYSPACE IF NOT EXISTS extra1 WITH replication = {'class' : 'SimpleStrategy', 'replication_factor' : 1};"
175
+ database.client.execute "CREATE KEYSPACE IF NOT EXISTS extra2 WITH replication = {'class' : 'SimpleStrategy', 'replication_factor' : 1};"
176
+ project.config['cassandra.keyspaces'] = %w{extra1 extra2}
177
+ project.save!(project_root)
178
+ end
179
+
180
+ it "drops all keyspaces" do
181
+ invoke!(%w{drop})
182
+ database.client.use(:system)
183
+ rows = database.client.execute('SELECT keyspace_name FROM schema_columnfamilies')
184
+ keyspaces = []
185
+ rows.each { |row| keyspaces << row['keyspace_name'] }
186
+ keyspaces.should_not include('sandbox')
187
+ keyspaces.should_not include('extra1')
188
+ keyspaces.should_not include('extra2')
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,197 @@
1
+ require 'spec_helper'
2
+ require 'monkey_butler/targets/cocoapods/cocoapods_target'
3
+
4
+ module Pod
5
+ class Spec
6
+ attr_accessor :name, :version, :summary, :homepage, :author, :source, :license, :resource_bundles, :requires_arc
7
+
8
+ def initialize(hash = {})
9
+ yield self if block_given?
10
+ end
11
+ end
12
+ end
13
+
14
+ describe MonkeyButler::Targets::CocoapodsTarget do
15
+ let(:thor_class) { MonkeyButler::Targets::CocoapodsTarget }
16
+ let!(:project_root) { clone_temp_sandbox }
17
+
18
+ before(:each) do
19
+ Dir.chdir(project_root) do
20
+ system("git config user.email developer@layer.com")
21
+ system("git config user.name 'Layer Developer'")
22
+ end
23
+ end
24
+
25
+ describe "#init" do
26
+ it "asks for the name of the cocoapods repository" do
27
+ remove_cocoapods_repo_from_config
28
+ expect(Thor::LineEditor).to receive(:readline).with("What is the name of your Cocoapods specs repo? ", {}).and_return("layerhq")
29
+ invoke!(['init'])
30
+ end
31
+ end
32
+
33
+ describe '#generate' do
34
+ let!(:podspec) do
35
+ invoke!(['generate'])
36
+ eval(File.read(File.join(project_root, 'sandbox.podspec')))
37
+ end
38
+
39
+ it "has name" do
40
+ expect(podspec.name).to eq('sandbox')
41
+ end
42
+
43
+ it "has version" do
44
+ expect(podspec.version).to eq('20140523123443021')
45
+ end
46
+
47
+ it "has summary" do
48
+ expect(podspec.summary).to eq('Packages the database schema and migrations for sandbox')
49
+ end
50
+
51
+ it "has homepage" do
52
+ expect(podspec.homepage).to eq('http://github.com/layerhq')
53
+ end
54
+
55
+ it "has author" do
56
+ expect(podspec.author).to eq({"Layer Developer"=>"developer@layer.com"})
57
+ end
58
+
59
+ it "has source" do
60
+ expect(podspec.source).to eq({:git=>"git@github.com:layerhq/monkey_butler_sandbox.git", :tag=>"20140523123443021"})
61
+ end
62
+
63
+ it "has license" do
64
+ expect(podspec.license).to eq('Commercial')
65
+ end
66
+
67
+ it "has resource_bundles" do
68
+ expect(podspec.resource_bundles).to eq({"sandbox"=>["migrations/*", "sandbox.sql"]})
69
+ end
70
+ end
71
+
72
+ describe "#push" do
73
+ context "when cocoapods.repo is not configured" do
74
+ it "fails with an error" do
75
+ remove_cocoapods_repo_from_config
76
+ output = invoke!(['push'])
77
+ output[:stderr].should =~ /Invalid configuration: cocoapods.repo is not configured./
78
+ end
79
+ end
80
+
81
+ context "when cocoapods.repo is configured" do
82
+ it "invokes pod push" do
83
+ output = invoke!(['push', '--pretend'])
84
+ output[:stdout].should =~ /run\s+pod repo push --allow-warnings example_specs_repo sandbox.podspec/
85
+ end
86
+ end
87
+ end
88
+
89
+ describe "#validate" do
90
+ context "when the cocoapods repo is not configured" do
91
+ before(:each) do
92
+ remove_cocoapods_repo_from_config
93
+ end
94
+
95
+ it "fails with error" do
96
+ output = invoke!(['validate'])
97
+ output[:stderr].should =~ /Invalid configuration: cocoapods.repo is not configured./
98
+ end
99
+ end
100
+ end
101
+
102
+ describe "#push" do
103
+ context "when there is no repo configured" do
104
+ before(:each) do
105
+ remove_cocoapods_repo_from_config
106
+ end
107
+
108
+ it "fails validation" do
109
+ output = invoke!(['push'])
110
+ output[:stderr].should =~ /Invalid configuration: cocoapods.repo is not configured./
111
+ end
112
+ end
113
+
114
+ context "when there is a repo configured" do
115
+ def with_temporary_cocoapods_repo
116
+ path = Dir.mktmpdir
117
+ temp_specs_repo_at_path(File.join(path, 'master'))
118
+ temp_specs_repo_at_path(File.join(path, 'example_specs_repo'))
119
+
120
+ ENV['CP_REPOS_DIR'] = path
121
+ yield path
122
+ ENV.delete('CP_REPOS_DIR')
123
+ end
124
+
125
+ it "pushes to cocoapods" do
126
+ git_repo_path = path_for_temp_bare_git_repo
127
+ Dir.chdir(project_root) do
128
+ `git remote set-url origin file://#{git_repo_path}`
129
+ end
130
+
131
+ # Generate the podspec
132
+ invoke!(['generate'])
133
+
134
+ Dir.chdir(project_root) do
135
+ `git remote set-url origin file://#{git_repo_path}`
136
+ `git add .`
137
+ `git commit --no-status -m 'Adding files' .`
138
+ `git tag 20140523123443021`
139
+ `git push -q origin master --tags`
140
+ end
141
+
142
+ with_temporary_cocoapods_repo do |repo_path|
143
+ output = invoke!(%w{push --quiet})
144
+
145
+ Dir.chdir(repo_path) do
146
+ pushed_podspec_path = File.join(repo_path, 'example_specs_repo', 'sandbox', '20140523123443021', 'sandbox.podspec')
147
+ File.exists?(pushed_podspec_path).should be_true
148
+ content = File.read(pushed_podspec_path)
149
+ content.should =~ /20140523123443021/
150
+ content.should =~ /resource_bundles/
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
156
+
157
+ def remove_cocoapods_repo_from_config
158
+ yaml_path = File.join(project_root, '.monkey_butler.yml')
159
+ project = YAML.load(File.read(yaml_path))
160
+ project['config'].delete 'cocoapods.repo'
161
+ File.open(yaml_path, 'w') { |f| f << YAML.dump(project) }
162
+ end
163
+
164
+ def path_for_temp_bare_git_repo
165
+ git_repo_path = Dir.mktmpdir
166
+ Dir.chdir(git_repo_path) do
167
+ `git init --bare`
168
+ end
169
+ git_repo_path
170
+ end
171
+
172
+ def temp_specs_repo_at_path(path)
173
+ FileUtils.mkdir_p path
174
+ Dir.chdir(path) do
175
+ run("git init -q .")
176
+ raise "Failed to init git repo" unless $?.exitstatus.zero?
177
+ run("echo '' > README.md")
178
+ run("git add README.md")
179
+ run("git commit --no-status -m 'Add README.md' .")
180
+ raise "Failed to touch README.md" unless $?.exitstatus.zero?
181
+ remote_url = "file://#{path_for_temp_bare_git_repo}"
182
+ run("git remote add origin #{remote_url}")
183
+ raise "Failed to set origin remote url" unless $?.exitstatus.zero?
184
+ run("git push -q -u origin master")
185
+ raise "Failed to push to temp repo" unless $?.exitstatus.zero?
186
+ end
187
+ end
188
+
189
+ def run(command, echo = false)
190
+ if echo
191
+ puts "Executing `#{command}`"
192
+ system(command)
193
+ else
194
+ `#{command}`
195
+ end
196
+ end
197
+ end