heroku_hatchet 0.2.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7f6fe23a7e5a091233694be4bc546232d45b13fe
4
- data.tar.gz: 4c161fbcb5e7b5140b6025caa6a1b511635617c1
3
+ metadata.gz: cbe186a27b0018f41b0ab9338925c09acdf9a52b
4
+ data.tar.gz: b7dcdd7fdf884f4e6773e6ae5c34fc731b615876
5
5
  SHA512:
6
- metadata.gz: 22409adecd80f6761593dd2f4cfb68696722903294cc08e7b5fb45183661d12e7b16ed1f67dd51a726c57e6249a9105a331ab9f0e1e3541a90c381384a1454ec
7
- data.tar.gz: 1c95c1a1777355e891760054b1b75ec3cf4c70963271bd492cfc1beb9496e6e811e3e288650041a0ec0583a5c358266ac17b3060cce4f292efa0f0fb7580855b
6
+ metadata.gz: 66ae54f148dfde98f77515fedc1401aee42820084de8846d0dae3c1a350f0b3e19393033ecac347e14096b945adc5c7f84d3f0c26d1e72cbde78fbfd0b3b114a
7
+ data.tar.gz: 8f2fc16b6a0aaa668eed1efd53b1807bbbceb15afc4a298bf8a081086bcb53c7e2afe3bb1539d605d943368ff7926d46dfa4678e36b292366e0f06f3949e41c2
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  ## HEAD
2
2
 
3
+ ## 1.0.0
4
+
5
+ - Move remote console running code to https://github.com/schneems/repl_runner
6
+ This changes the API for running interactive code. README has been updated
3
7
 
4
8
  ## 0.2.0
5
9
 
@@ -26,4 +30,4 @@
26
30
 
27
31
  ## 0.0.1
28
32
 
29
- - Initial Release
33
+ - Initial Release
data/README.md CHANGED
@@ -32,160 +32,231 @@ it will be pulled automatically via shelling out, but this is slower.
32
32
 
33
33
  $ bundle exec rake test
34
34
 
35
+ ## Why Test a Buildpack?
35
36
 
36
- ## Writing Tests
37
+ To prevent regressions and to make pushing out new features faster and easier.
37
38
 
38
- Hatchet is meant for running integration tests, which means we actually have to deploy a real live honest to goodness app on Heroku and see how it behaves.
39
+ ## What can Hatchet Test?
39
40
 
40
- First you'll need a repo to an app you know works on Heroku, add it to the proper folder in repos. Such as `repos/rails3/`. Once you've done that make a corresponding test file in the `test` dir such as `test/repos/rails3`. I've already got a project called "codetriage" and the test is "triage_test.rb".
41
+ Hatchet can easily test deployment of buildpacks, getting the build output, and running arbitrary interactive processes such as `heroku run bash`.
41
42
 
42
- Now that you have an app, we'll need to create a heroku instance, and deploy our code to that instance you can do that with this code:
43
+ ## Testing a Buildpack
43
44
 
44
- Hatchet::App.new("repos/rails3/codetriage").deploy do |app|
45
- ##
46
- end
45
+ Hatchet was built for testing the Ruby buildpack, but you can use it to test any buildpack you desire provided you don't mind writing your tests written in Ruby.
47
46
 
48
- The first argument to the app is the directory where you can find the code. Once your test is done, the app will automatically be destroyed.
47
+ You will need copies of applications that can be deployed by your buildpack. You can see the ones for the Hatchet unit tests (and the Ruby buildpack) https://github.com/sharpstone. Hatchet does not require that you keep these apps checked into your git repo which would make fetching your buildpack slow instead declare them in a `hatchet.json` file (see below).
49
48
 
50
- Now that you've deployed your app you'll want to make sure that the deploy worked correctly. Since we're using `test/unit` you can use regular assertions such as `assert` and `refute`. Since we're yielding to an `app` variable we can check the `deployed?` status of the app:
49
+ Hatchet will automate retrieving these files `$ hatchet install`, as well as deploying them using your local copy of the buildpack, retrieving the build output and running commands against deploying applications.
51
50
 
52
- Hatchet::App.new("repos/rails3/codetriage").deploy do |app|
53
- assert app.deployed?
54
- end
55
51
 
56
- The primary purpose of the buildpack is configuring and deploying apps, so if it deployed chances are the buildpack is working correctly, but sometimes you may want more information. You can run arbitrary commands such as `heroku bash` and then check for the existence of a file.
52
+ ## Hatchet.json
57
53
 
58
- Hatchet::App.new("repos/rails3/codetriage").deploy do |app|
59
- app.run("bash") do |cmd|
60
- assert cmd.run("ls public/assets").include?("application.css")
61
- end
62
- end
54
+ Hatchet expects a json file in the root of your buildpack called `hatchet.json`. You can configure install options using the `"hatchet"` key. In this example we're telling hatchet to install the given repos to our `test/fixtures` directory instead of the default current directory.
63
55
 
64
- Anything you put in `cmd.run` at this point will be executed from with in the app that you are in.
56
+ ```
57
+ {
58
+ "hatchet": {"directory": "test/fixtures"},
59
+ "rails3": ["sharpstone/rails3_mri_193"],
60
+ "rails2": ["sharpstone/rails2blog"],
61
+ "bundler": ["sharpstone/no_lockfile"]
62
+ }
63
+ ```
65
64
 
66
- cmd.run("cat")
67
- cmd.run("cd")
68
- cmd.run("cd .. ; ls | grep foo")
65
+ When you run `$ hatchet install` it will grab the git repos from github and place them on your local machine in a file structure that looks like this:
69
66
 
70
- It behaves exactly as if you were in a remote shell. If you really wanted you could even run the tests:
67
+ ```
68
+ test/
69
+ fixtures/
70
+ repos/
71
+ rails3/
72
+ rails3_mri_193/
73
+ rails2/
74
+ rails2blog/
75
+ bundler/
76
+ no_lockfile/
77
+ ```
71
78
 
72
- cmd.run("rake test")
79
+ Now in your test you can reference one of these applications by using it's git name:
73
80
 
74
- But since cmd.run doesn't return the exit status now, that wouldn't be
75
- so useful (also there is a default timeout to all commands). If you want
76
- you can configure the timeout by passing in a second parameter
81
+ ```ruby
82
+ Hatchet::AnvilApp.new('no_lockfile')
83
+ ```
77
84
 
78
- cmd.run("rake test", 180.seconds)
85
+ If you have conflicting names, use full paths.
79
86
 
87
+ A word of warning on including repos inside of your test
88
+ directory, if you're using a runner that looks for patterns such as
89
+ `*_test.rb` to run your hatchet tests, it may incorrectly think you want
90
+ to run the tests inside of the repos. To get rid of this
91
+ problem move your repos direcory out of `test/` or be more specific
92
+ with your tests such as moving them to a `test/hatchet` directory and
93
+ changing your pattern if you are using `Rake::TestTask` it might look like this:
80
94
 
81
- ## Running One off Commands
95
+ t.pattern = 'test/hatchet/**/*_test.rb'
82
96
 
83
- If you only want to run one command you can call `app.run` without
84
- passing a block
97
+ A note on external repos: since you're basing tests on these repos, it
98
+ is in your best interest to not change them or your tests may
99
+ spontaneously fail. In the future we may create a hatchet.lockfile or
100
+ something to declare the commit
85
101
 
86
- Hatchet::AnvilApp.new("/codetriage").deploy do |app|
87
- assert_match "1.9.3", app.run("ruby -v")
88
- end
102
+ ## Deploying apps
89
103
 
104
+ Now that you've got your apps locally you can have hatchet deploy them for you. Hatchet can deploy using one of two ways Anvil and Git. A `Hatchet::AnvilApp` will deploy using Anvil against the current directory. This means that the buildpack you have locally will be used when deploying, due to this we recommend using Anvil to run your tests.
90
105
 
91
- ## Testing A Different Buildpack
106
+ A `Hatchet::GitApp` will deploy using the standard `git push heroku master`, if you use this option you need to have a publicly accessible copy of your buildpack. Using Git to test your buildpack may be slow and require you to frequently push your buildpack to a public git repo. For this reason we recommend using Anvil to run your tests:
92
107
 
93
- You can specify buildpack to deploy with like so:
108
+ ```ruby
109
+ Hatchet::AnvilApp.new("rails3_mri_193").deploy do |app|
94
110
 
95
- Hatchet::App.new("repos/rails3/codetriage", buildpack: "https://github.com/schneems/heroku-buildpack-ruby.git").deploy do |app|
111
+ end
112
+ ```
96
113
 
97
- ## Hatchet Config
114
+ Deploys are expected to work, if the `ENV['HATCHET_RETRIES']` is set, then deploys will be automatically retried that number of times. Due to testing using a network and random Anvil failures, setting this value to `3` retries seems to work well. If an app cannot be deployed within its allotted number of retries an error will be raised.
98
115
 
99
- Hatchet is designed to test buildpacks, and requires full repositories
100
- to deploy to Heroku. Web application repos, especially Rails repos, aren't known for
101
- being small, if you're testing a custom buildpack and have
102
- `BUILDPACK_URL` set in your app config, it needs to be cloned each time
103
- you deploy your app. If you've `git add`-ed a bunch of repos then this
104
- clone would be pretty slow, we're not going to do this. Do not commit
105
- your repos to git.
116
+ If you are testing an app that is supposed to fail deployment you can set the `allow_failure: true` flag when creating the app:
106
117
 
107
- Instead we will keep a structured file called
108
- inventively `hatchet.json` at the root of your project. This file will
109
- describe the structure of your repos, have the name of the repo, and a
110
- git url. We will use it to sync remote git repos with your local
111
- project. It might look something like this
118
+ ```ruby
119
+ Hatchet::AnvilApp.new("no_lockfile", allow_failure: true).deploy do |app|
120
+ ```
112
121
 
113
- {
114
- "hatchet": {},
115
- "rails3": ["git@github.com:codetriage/codetriage.git"],
116
- "rails2": ["git@github.com:heroku/rails2blog.git"]
117
- }
122
+ After the block finishes your app will be removed from heroku. If you are investigating a deploy, you can add the `debug: true` flag to your app:
118
123
 
119
- the 'hatchet' object accessor is reserved for hatchet settings.
120
- . To copy each repo in your `hatchet.json`
121
- run the command:
124
+ ```ruby
125
+ Hatchet::AnvilApp.new("rails3_mri_193", debug: true).deploy do |app|
126
+ ```
122
127
 
123
- $ hatchet install
128
+ Now after Hatchet is done deploying your app it will remain on Heroku. It will also output the name of the app into your test logs so that you can `heroku run bash` into it for detailed postmortem.
124
129
 
125
- The above `hatchet.json` will produce a directory structure like this:
130
+ If you are wanting to run a test against a specific app without deploying to it, you can set the app name like this:
126
131
 
127
- repos/
128
- rails3/
129
- codetriage/
130
- #...
131
- rails2/
132
- rails2blog/
133
- # ...
132
+ ```ruby
133
+ app = Hatchet::AnvilApp.new("rails3_mri_193", name: "testapp")
134
+ ```
134
135
 
135
- While you are running your tests if you reference a repo that isn't
136
- synced locally Hatchet will raise an error. Since you're using a
137
- standard file for your repos, you can now reference the name of the git
138
- repo, provided you don't have conflicting names:
136
+ Deploying the app takes a few minutes, so you may want to skip that part to make debugging a problem easier since you're iterating much faster.
139
137
 
140
- Hatchet::App.new("codetriage").deploy do |app|
141
138
 
142
- If you do have conflicting names, use full paths.
139
+ If you need to deploy using a buildpack that is not in the root of your directory you can specify a path in the `buildpack` option:
143
140
 
144
- A word of warning on including rails/ruby repos inside of your test
145
- directory, if you're using a runner that looks for patterns such as
146
- `*_test.rb` to run your hatchet tests, it may incorrectly think you want
147
- to run the tests inside of the rails repositories. To get rid of this
148
- problem move your repos direcory out of `test/` or be more specific
149
- with your tests such as moving them to a `test/hatchet` directory and
150
- changing your pattern if you are using `Rake::TestTask` it might look like this:
151
141
 
152
- t.pattern = 'test/hatchet/**/*_test.rb'
142
+ ```ruby
143
+ buildpack_path = File.expand_path 'test/fixtures/buildpacks/heroku-buildpack-ruby'
153
144
 
154
- A note on external repos: since you're basing tests on these repos, it
155
- is in your best interest to not change them or your tests may
156
- spontaneously fail. In the future we may create a hatchet.lockfile or
157
- something to declare the commit
145
+ def test_deploy
146
+ Hatchet::AnvilApp.new("rails3_mri_193", buildpack: buildpack_path).deploy do |app|
147
+ # ...
148
+ ```
158
149
 
159
- ## Hatchet CLI
150
+ If you are using a `Hatchet::GitApp` this is where you specify the publicly avaialble location of your buildpack, such as `https://github.com/heroku/heroku-buildpack-ruby.git#mybranch`
160
151
 
161
- Hatchet has a CLI for installing and maintaining external repos you're
162
- using to test against. If you have Hatchet installed as a gem run
163
152
 
164
- $ hatchet --help
153
+ ## Getting Deploy Output
165
154
 
166
- For more info on commands. If you're using the source code you can run
167
- the command by going to the source code directory and running:
155
+ After Hatchet deploys your app you can get the output by using `app.output`
168
156
 
169
- $ ./bin/hatchet --help
157
+ ```ruby
158
+ Hatchet::AnvilApp.new("rails3_mri_193").deploy do |app|
159
+ puts app.output
160
+ end
161
+ ```
162
+
163
+ If you told Hatchet to `allow_failure: true` then the full output of the failed build will be in `app.output` even though the app was not deployed. It is a good idea to test against the output for text that should be present. Using a testing framework such as `Test::Unit` a failed test output may look like this
164
+
165
+ ```ruby
166
+ Hatchet::AnvilApp.new("no_lockfile", allow_failure: true).deploy do |app|
167
+ assert_match "Gemfile.lock required", app.output
168
+ end
169
+ ```
170
+
171
+ Since an error will be raised on failed deploys you don't need to check for a deployed status (the error will automatically fail the test for you).
172
+
173
+ ## Running Processes
174
+
175
+ Often times asserting output of a build can only get you so far, and you will need to actually run a task on the dyno. To run a non-interactive command such as `heroku run ls` you can do this using the `app.run()` command and do not pass it a block
176
+
177
+ ```ruby
178
+ Hatchet::AnvilApp.new("rails3_mri_193").deploy do |app|
179
+ assert_match "applications.css", app.run("ls public/assets")
180
+ ```
181
+
182
+ This is useful for checking the existence of generated files such as assets. If you need to run an interactive session such as `heroku run bash` or `heroku run rails console` you can use the run command and pass a block:
183
+
184
+ ```ruby
185
+ Hatchet::AnvilApp.new("rails3_mri_193").deploy do |app|
186
+ app.run("bash") do |bash|
187
+ bash.run("ls") {|result| assert_match "Gemfile.lock", result }
188
+ bash.run("cat Procfile") {|result| assert_match "web:", result }
189
+ end
190
+ end
191
+ ```
192
+
193
+ or
194
+
195
+ ```ruby
196
+ Hatchet::AnvilApp.new("rails3_mri_193").deploy do |app|
197
+ app.run("rails console") do |console|
198
+ console.run("a = 1 + 2") {|result| assert_match "3", result }
199
+ console.run("'foo' * a") {|result| assert_match "foofoofoo", result }
200
+ end
201
+ end
202
+ ```
203
+
204
+ This functionality is provided by [repl_runner](http://github.com/schneems/repl_runner). Please read the docs on that readme for more info. The only interactive commands that are supported out of the box are `rails console`, `bash`, and `irb` it is fairly easy to add your own though:
205
+
206
+ ```
207
+ ReplRunner.register_commands(:python) do |config|
208
+ config.terminate_command "exit()" # the command you use to end the 'python' console
209
+ config.startup_timeout 60 # seconds to boot
210
+ config.return_char "\n" # the character that submits the command
211
+ end
212
+ ```
213
+
214
+ If you have questions on setting running other interactive commands message [@schneems](http://twitter.com/schneems)
215
+
216
+ ## Writing Tests
170
217
 
218
+ Hatchet is test framework agnostic. [This project](https://github.com/heroku/hatchet) uses `Test::Unit` to run it's own tests. While the [heroku-ruby-buildpack](https://github.com/heroku/heroku-buildpack-ruby) uses rspec.
171
219
 
172
- ## Retries
220
+ Rspec has a number of advantages, the ability to run `focused: true` to only run the exact test you want as well as the ability to tag tests. Rspec also has a number of useful plugins, one especialy useful one is `gem 'rspec-retry'` which will re-run any failed tests a given number of times (I recommend setting this to at least 2) this decrease the number of false negatives your tests will have.
173
221
 
174
- Phantom errors happen. To auto retry deploy failures set the environment variable `HATCHET_RETRIES=3` which will auto retry deploys 3 times. By default deploys will not be retried. Once the number of retries has occurred the last exception will be raised.
222
+ Whatever testing framework you chose, we recommend using a parallel test runner when running the full suite [parallel_tests](https://github.com/grosser/parallel_tests) works with rspec and test::unit and is amazing.
175
223
 
176
- ## The Future
224
+ If you're unfamiliar with the ruby testing eco-system or want some help with boilerplate and work for Heroku: [@schneems](http://twitter.com/schneems) can help you get started. Looking at existing projects is a good place to get started
177
225
 
178
- ### Speed
226
+ ## Testing on Travis
179
227
 
180
- Efforts may be spent optimizing / parallelizing the process, almost all of the time of the test is spent waiting for IO, so hopefully we should be able to parallelize many tests / deploys at the same time. The hardest part of this (i believe) would be splitting out the different runs into different log streams so that the output wouldn't be completely useless.
228
+ Once you've got your tests working locally, you'll likely want to get them running on Travis because a) CI is awesome, and b) you can use pull requests to run your all your tests in parallel without having to kill your network connection.
181
229
 
182
- Right now running 1 deploy test takes about 3 min on my machine.
230
+ To run on travis you will need to configure your `.travis.yml` to run the appropriate commands and to set up encrypted data so you can run tests against a valid heroku user.
183
231
 
184
- ## Git Based Deploys
232
+ For reference see the `.travis.yml` from [hatchet](https://github.com/heroku/hatchet/blob/master/.travis.yml) and the [heroku-ruby-buildpack](https://github.com/heroku/heroku-buildpack-ruby/blob/master/.travis.yml). To make running on travis easier there is a rake task in Hatchet that can be run before your tests are executed
233
+
234
+ ```
235
+ before_script: bundle exec rake hatchet:setup_travis
236
+ ```
237
+
238
+ I recommend signing up for a new heroku account for running your tests on travis, otherwise you will quickly excede your API limit. Once you have the new api token you can use this technique to [securely send travis the data](http://about.travis-ci.org/docs/user/build-configuration/#Secure-environment-variables).
239
+
240
+
241
+ ## Extra App Commands
242
+
243
+ ```
244
+ app.add_database # adds a database to specified app
245
+ app.heroku # returns a Herou Api client https://github.com/heroku/heroku.rb
246
+ ```
247
+
248
+ ## Hatchet CLI
249
+
250
+ Hatchet has a CLI for installing and maintaining external repos you're
251
+ using to test against. If you have Hatchet installed as a gem run
252
+
253
+ $ hatchet --help
254
+
255
+ For more info on commands. If you're using the source code you can run
256
+ the command by going to the source code directory and running:
257
+
258
+ $ ./bin/hatchet --help
185
259
 
186
- It would be great to allow hatchet to deploy apps off of git url, however if we do that we could open ourselves up to false negatives, if we are pointing at an external repo that gets broken.
187
260
 
188
261
 
189
- ## Features?
190
262
 
191
- What else do we want to test? Config vars, addons, etc. Let's write some tests.
data/hatchet.gemspec CHANGED
@@ -24,6 +24,8 @@ Gem::Specification.new do |gem|
24
24
  gem.add_dependency "anvil-cli"
25
25
  gem.add_dependency "excon"
26
26
  gem.add_dependency "thor"
27
+ gem.add_dependency 'repl_runner'
28
+
27
29
 
28
30
  gem.add_development_dependency "rake"
29
31
  gem.add_development_dependency "mocha"
data/lib/hatchet/app.rb CHANGED
@@ -39,7 +39,10 @@ module Hatchet
39
39
  # runs a command on heroku similar to `$ heroku run #foo`
40
40
  # but programatically and with more control
41
41
  def run(command, timeout = nil, &block)
42
- ProcessSpawn.new(command, self, timeout).run(&block)
42
+ heroku_command = "heroku run #{command} -a #{name}"
43
+ return `#{heroku_command}` if block.blank?
44
+
45
+ ReplRunner.new(command, heroku_command, startup_timeout: timeout).run(&block)
43
46
  end
44
47
 
45
48
  # set debug: true when creating app if you don't want it to be
@@ -115,14 +118,13 @@ module Hatchet
115
118
  @output
116
119
  end
117
120
 
118
- private
119
- def api_key
120
- @api_key ||= ENV['HEROKU_API_KEY'] || `heroku auth:token`.chomp
121
- end
121
+ def api_key
122
+ @api_key ||= ENV['HEROKU_API_KEY'] || `heroku auth:token`.chomp
123
+ end
122
124
 
123
- def heroku
124
- @heroku ||= Heroku::API.new(api_key: api_key)
125
- end
125
+ def heroku
126
+ @heroku ||= Heroku::API.new(api_key: api_key)
127
+ end
126
128
  end
127
129
  end
128
130
 
@@ -1,3 +1,3 @@
1
1
  module Hatchet
2
- VERSION = "0.2.0"
2
+ VERSION = "1.0.0"
3
3
  end
data/lib/hatchet.rb CHANGED
@@ -2,6 +2,7 @@ require 'heroku/api'
2
2
  require 'anvil/engine'
3
3
  require 'active_support/core_ext/object/blank'
4
4
  require 'rrrretry'
5
+ require 'repl_runner'
5
6
 
6
7
  require 'json'
7
8
  require 'stringio'
@@ -20,8 +21,4 @@ require 'hatchet/version'
20
21
  require 'hatchet/app'
21
22
  require 'hatchet/anvil_app'
22
23
  require 'hatchet/git_app'
23
- require 'hatchet/command_parser'
24
- require 'hatchet/stream_exec'
25
- require 'hatchet/repl_runner'
26
- require 'hatchet/process_spawn'
27
24
  require 'hatchet/config'
@@ -11,8 +11,8 @@ class AnvilTest < Test::Unit::TestCase
11
11
 
12
12
  assert_match '1.9.3', app.run("ruby -v")
13
13
  app.run("bash") do |cmd|
14
- assert cmd.run("cat Gemfile").include?("gem 'pg'")
15
- assert cmd.run("ls public/assets").include?("application.css")
14
+ cmd.run("cat Gemfile") {|r| assert_match "gem 'pg'", r}
15
+ cmd.run("ls public/assets") {|r| assert_match "application.css", r}
16
16
  end
17
17
  end
18
18
  end
@@ -8,7 +8,7 @@ class GitAppTest < Test::Unit::TestCase
8
8
 
9
9
  app.run("bash") do |cmd|
10
10
  # cmd.run("cd public/assets")
11
- assert cmd.run("ls public/assets").include?("application.css")
11
+ cmd.run("ls public/assets") {|r| assert_match "application.css", r}
12
12
  end
13
13
  end
14
14
  end
@@ -11,18 +11,26 @@ class MultiCmdRunnerTest < Test::Unit::TestCase
11
11
  Hatchet::AnvilApp.new("rails3_mri_193", buildpack: @buildpack_path).deploy do |app|
12
12
  app.add_database
13
13
 
14
- rand(3..7).times do
15
- app.run("bash") do |bash|
16
- assert_match /Gemfile/, bash.run("ls")
14
+ assert_raise ReplRunner::UnregisteredCommand do
15
+ app.run("ls", 2) do |ls| # will return right away, should raise error
16
+ ls.run("cat")
17
17
  end
18
18
  end
19
19
 
20
20
  rand(3..7).times do
21
21
  app.run("rails console") do |console|
22
- assert_match /foofoofoofoofoo/, console.run("'foo' * 5")
23
- assert_match /hello world/, console.run("'hello ' + 'world'")
22
+ console.run("`ls`")
23
+ console.run("'foo' * 5") {|r| assert_match "foofoofoofoofoo", r }
24
+ console.run("'hello ' + 'world'") {|r| assert_match "hello world", r }
25
+ end
26
+ end
27
+
28
+ rand(3..7).times do
29
+ app.run("bash") do |bash|
30
+ bash.run("ls") { |r| assert_match "Gemfile", r }
24
31
  end
25
32
  end
33
+
26
34
  end
27
35
  end
28
36
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: heroku_hatchet
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Richard Schneeman
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-06-22 00:00:00.000000000 Z
11
+ date: 2013-06-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: heroku-api
@@ -94,6 +94,20 @@ dependencies:
94
94
  - - '>='
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: repl_runner
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
97
111
  - !ruby/object:Gem::Dependency
98
112
  name: rake
99
113
  requirement: !ruby/object:Gem::Requirement
@@ -157,12 +171,8 @@ files:
157
171
  - lib/hatchet.rb
158
172
  - lib/hatchet/anvil_app.rb
159
173
  - lib/hatchet/app.rb
160
- - lib/hatchet/command_parser.rb
161
174
  - lib/hatchet/config.rb
162
175
  - lib/hatchet/git_app.rb
163
- - lib/hatchet/process_spawn.rb
164
- - lib/hatchet/repl_runner.rb
165
- - lib/hatchet/stream_exec.rb
166
176
  - lib/hatchet/tasks.rb
167
177
  - lib/hatchet/version.rb
168
178
  - test/fixtures/buildpacks/heroku-buildpack-ruby/.gitignore
@@ -204,12 +214,9 @@ files:
204
214
  - test/hatchet/allow_failure_anvil_test.rb
205
215
  - test/hatchet/allow_failure_git_test.rb
206
216
  - test/hatchet/anvil_test.rb
207
- - test/hatchet/command_parser_test.rb
208
217
  - test/hatchet/config_test.rb
209
218
  - test/hatchet/git_test.rb
210
219
  - test/hatchet/multi_cmd_runner_test.rb
211
- - test/hatchet/repl_runner_test.rb
212
- - test/hatchet/stream_exec_test.rb
213
220
  - test/test_helper.rb
214
221
  homepage: https://github.com/heroku/hatchet
215
222
  licenses:
@@ -275,10 +282,7 @@ test_files:
275
282
  - test/hatchet/allow_failure_anvil_test.rb
276
283
  - test/hatchet/allow_failure_git_test.rb
277
284
  - test/hatchet/anvil_test.rb
278
- - test/hatchet/command_parser_test.rb
279
285
  - test/hatchet/config_test.rb
280
286
  - test/hatchet/git_test.rb
281
287
  - test/hatchet/multi_cmd_runner_test.rb
282
- - test/hatchet/repl_runner_test.rb
283
- - test/hatchet/stream_exec_test.rb
284
288
  - test/test_helper.rb
@@ -1,39 +0,0 @@
1
- # removes the commands from strings retrieved from stuff like `heroku run bash`
2
- # since likely you care about the output, not the input
3
- # this is especially useful for seeing if a given input command has finished running
4
- # if we cannot find a valid input command and output command return the full unparsed string
5
- module Hatchet
6
- class CommandParser
7
- attr_accessor :command
8
-
9
- def initialize(command)
10
- @command = command
11
- @parsed_string = ""
12
- @raw_string = ""
13
- end
14
-
15
- def regex
16
- /#{Regexp.quote(command)}\r*\n+/
17
- end
18
-
19
- def parse(string)
20
- @raw_string = string
21
- @parsed_string = string.split(regex).last
22
- return self
23
- end
24
-
25
- def to_s
26
- @parsed_string || @raw_string
27
- end
28
-
29
- def missing_valid_output?
30
- !has_valid_output?
31
- end
32
-
33
- def has_valid_output?
34
- return false unless @raw_string.match(regex)
35
- return false if @parsed_string.blank? || @parsed_string.strip.blank?
36
- true
37
- end
38
- end
39
- end
@@ -1,67 +0,0 @@
1
- require 'pty'
2
- module Hatchet
3
- # spawns a process on Heroku, and keeps it open for writing
4
- # like `heroku run bash`
5
- class ProcessSpawn
6
- attr_reader :command, :app, :timeout, :pid
7
- TIMEOUT = 60 # seconds to bring up a heroku command like `heroku run bash`
8
-
9
- def initialize(command, app, timeout = nil)
10
- raise "need command" unless command.present?
11
- raise "need app" unless app.present?
12
- @command = "heroku run #{command} -a #{app.name}"
13
- @ready_regex = "^run.*up.*#{command}"
14
- @app = app
15
- @timeout = timeout || TIMEOUT
16
- end
17
-
18
- def ready?
19
- @ready ||= `heroku ps -a #{app.name}`.match(/#{@ready_regex}/).present?
20
- end
21
-
22
- def not_ready?
23
- !ready?
24
- end
25
-
26
- def wait_for_spawn!
27
- while not_ready?
28
- sleep 1
29
- end
30
- return true
31
- end
32
-
33
- # some REPL's don't sync standard out by default
34
- # try to do it auto-magically
35
- def repl_magic(repl)
36
- case command
37
- when /rails\s*console/, /\sirb\s/
38
- # puts "magic for: '#{command}'"
39
- repl.run("STDOUT.sync = true")
40
- end
41
- end
42
-
43
- # Open up PTY (pseudo terminal) to command like `heroku run bash`
44
- # Wait for the dyno to deploy, then allow user to run arbitrary commands
45
- def spawn_repl
46
- output, input, pid = PTY.spawn(command)
47
- stream = StreamExec.new(output, input, pid)
48
- repl = ReplRunner.new(stream)
49
- stream.timeout("waiting for spawn", timeout) do
50
- wait_for_spawn!
51
- end
52
- raise "Could not run: '#{command}', command took longer than #{timeout} seconds" unless self.ready?
53
-
54
- repl_magic(repl)
55
- repl.wait_for_boot(5) # important to get rid of startup info i.e. "booting rails console ..."
56
- return repl
57
- end
58
-
59
- def run(&block)
60
- return `#{command}` if block.blank? # one off command, no block given
61
-
62
- yield repl = spawn_repl
63
- ensure
64
- repl.close if repl.present?
65
- end
66
- end
67
- end
@@ -1,61 +0,0 @@
1
- # takes a StringExec class and attempts to parse commands out of it
2
- module Hatchet
3
- class ReplRunner
4
- TIMEOUT = 1
5
- RETRIES = 10
6
-
7
- attr_accessor :repl
8
-
9
- def initialize(repl, command_parser_klass = CommandParser)
10
- @repl = repl
11
- @command_parser_klass = command_parser_klass
12
- end
13
-
14
- def command_parser_klass
15
- @command_parser_klass
16
- end
17
-
18
- # adds a newline cause thats what most repl-s need to run command
19
- def write(cmd)
20
- repl.write("#{cmd}\n")
21
- end
22
-
23
- def run(cmd, options = {})
24
- timeout = options[:timeout] || TIMEOUT
25
- retries = options[:retries] || RETRIES
26
-
27
- write(cmd)
28
- read(cmd, timeout, retries)
29
- end
30
-
31
- def wait_for_boot(timeout = 5)
32
- repl.read(timeout)
33
- end
34
-
35
- def close
36
- repl.close
37
- end
38
-
39
- # take in a command like "ls", and tries to find it in the output
40
- # of the repl (StreamExec)
41
- # Example
42
- # output, input, pid = PTY.spawn('sh')
43
- # stream = StreamExec.new(output, input, pid)
44
- # repl_runner = ReplRunner.new(stream)
45
- # repl_runner.write("ls\n")
46
- # repl_runner.read
47
- # # => "app\tconfig.ru Gemfile\t LICENSE.txt public\t script vendor\r\r\nbin\tdb\t Gemfile.lock log\t Rakefile\t test\r\r\nconfig\tdoc\t lib\t\t Procfile README.md tmp\r\r\n"
48
- #
49
- # if the command "ls" is not found, repl runner will continue to retry grabbing more output
50
- def read(cmd, timeout = TIMEOUT, retries = RETRIES)
51
- str = ""
52
- command_parser = command_parser_klass.new(cmd)
53
- retries.times.each do
54
- next if command_parser.has_valid_output?
55
- str << repl.read(timeout)
56
- command_parser.parse(str)
57
- end
58
- return command_parser.to_s
59
- end
60
- end
61
- end
@@ -1,62 +0,0 @@
1
- require 'timeout'
2
-
3
- module Hatchet
4
- # runs arbitrary commands within a Heroku process
5
- class StreamExec
6
- attr_reader :input, :output, :pid
7
- TIMEOUT = 1 # seconds to run an arbitrary command on a heroku process like `$ls`
8
-
9
- def initialize(output, input, pid)
10
- @input = input
11
- @output = output
12
- @pid = pid
13
- end
14
-
15
- def write(cmd)
16
- input.write(cmd)
17
- rescue Errno::EIO => e
18
- raise e, "#{e.message} | trying to write '#{cmd}'"
19
- end
20
-
21
- def run(cmd, timeout = TIMEOUT)
22
- write(cmd)
23
- return read(timeout)
24
- end
25
-
26
- def close
27
- timeout("closing stream") do
28
- input.close
29
- output.close
30
- end
31
- ensure
32
- Process.kill('TERM', pid) if pid.present?
33
- end
34
-
35
- # There be dragons - (You're playing with process deadlock)
36
- #
37
- # We want to read the whole output of the command
38
- # First pull all contents from stdout (except we don't know how many there are)
39
- # So we have to go until our process deadlocks, then we timeout and return the string
40
- #
41
- def read(timeout = TIMEOUT)
42
- str = ""
43
- while true
44
- Timeout::timeout(timeout) do
45
- str << output.readline
46
- end
47
- end
48
-
49
- return str
50
- rescue Timeout::Error, EOFError
51
- return str
52
- end
53
-
54
- def timeout(msg = nil, val = TIMEOUT, &block)
55
- Timeout::timeout(val) do
56
- yield
57
- end
58
- rescue Timeout::Error
59
- puts "timeout #{msg}" if msg
60
- end
61
- end
62
- end
@@ -1,54 +0,0 @@
1
- require 'test_helper'
2
-
3
- class CommandParserTest < Test::Unit::TestCase
4
- def test_removes_command_from_string
5
- hash = {command: "1+1",
6
- string: "1+1\r\r\n=> 2\r\r\n",
7
- expect: "=> 2\r\r\n"
8
- }
9
- cp = Hatchet::CommandParser.new(hash[:command]).parse(hash[:string])
10
- assert cp.has_valid_output?
11
- assert_equal hash[:expect], cp.to_s
12
-
13
-
14
- hash = {command: "ls",
15
- string: "Running `bash` attached to terminal... up, run.8041\r\n\e[01;34m~\e[00m \e[01;32m$ \e[00mls\r\r\napp config\tdb Gemfile\t lib\tProcfile Rakefile script tmp\r\r\nbin config.ru\tdoc Gemfile.lock log\tpublic\t README.rdoc test vendor\r\r\n",
16
- expect: "app config\tdb Gemfile\t lib\tProcfile Rakefile script tmp\r\r\nbin config.ru\tdoc Gemfile.lock log\tpublic\t README.rdoc test vendor\r\r\n"
17
- }
18
- cp = Hatchet::CommandParser.new(hash[:command]).parse(hash[:string])
19
- assert cp.has_valid_output?
20
- assert_equal hash[:expect], cp.to_s
21
- end
22
-
23
- def test_returns_result_if_no_command_in_result
24
- hash = {command: "ls",
25
- string: "1+1\r\r\n=> 2\r\r\n",
26
- expect: "1+1\r\r\n=> 2\r\r\n"
27
- }
28
- cp = Hatchet::CommandParser.new(hash[:command]).parse(hash[:string])
29
- refute cp.has_valid_output?
30
- assert_equal hash[:expect], cp.to_s
31
- end
32
-
33
- def test_empty_string
34
- hash = {command: "ls",
35
- string: "",
36
- expect: ""
37
- }
38
- cp = Hatchet::CommandParser.new(hash[:command]).parse(hash[:string])
39
- refute cp.has_valid_output?
40
- assert_equal hash[:expect], cp.to_s
41
- end
42
-
43
-
44
- def test_partial_command_no_result
45
- hash = {command: "1+1",
46
- string: "1+1\r\r\n",
47
- expect: "1+1\r\r\n"
48
- }
49
- cp = Hatchet::CommandParser.new(hash[:command]).parse(hash[:string])
50
- assert_equal hash[:expect], cp.to_s
51
- refute cp.has_valid_output?
52
- end
53
- end
54
-
@@ -1,20 +0,0 @@
1
- require 'test_helper'
2
- require 'stringio'
3
-
4
- class ReplRunnerTest < Test::Unit::TestCase
5
-
6
- def test_returns_full_output_if_command_not_found
7
- command = "irb"
8
- input = StringIO.new("bar")
9
- bogus_output = StringIO.new("foo")
10
- stream = Hatchet::StreamExec.new(bogus_output, input, 1)
11
- repl = Hatchet::ReplRunner.new(stream)
12
- repl.write("1+1")
13
- assert_equal bogus_output.string, repl.read("1+1")
14
-
15
- Hatchet::CommandParser.any_instance.expects(:parse).times(Hatchet::ReplRunner::RETRIES)
16
- Hatchet::CommandParser.any_instance.stubs(:to_s)
17
- repl.write("1+1")
18
- repl.read("1+1")
19
- end
20
- end
@@ -1,12 +0,0 @@
1
- require 'test_helper'
2
-
3
- class StreamExecTest < Test::Unit::TestCase
4
- def test_local_irb_stream
5
- command = "irb"
6
- output, input, pid = PTY.spawn(command)
7
- stream = Hatchet::StreamExec.new(output, input, pid)
8
- stream.run("STDOUT.sync = true\n")
9
- assert_equal "1+1\r\n => 2 \r\n", stream.run("1+1\n")
10
- end
11
- end
12
-