heroku_hatchet 5.0.1 → 7.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +21 -1
  3. data/.gitignore +2 -0
  4. data/CHANGELOG.md +38 -1
  5. data/README.md +790 -164
  6. data/bin/hatchet +19 -7
  7. data/etc/ci_setup.rb +16 -12
  8. data/etc/setup_heroku.sh +0 -2
  9. data/hatchet.gemspec +4 -6
  10. data/hatchet.json +6 -2
  11. data/hatchet.lock +12 -8
  12. data/lib/hatchet.rb +1 -2
  13. data/lib/hatchet/api_rate_limit.rb +13 -24
  14. data/lib/hatchet/app.rb +170 -62
  15. data/lib/hatchet/config.rb +1 -1
  16. data/lib/hatchet/git_app.rb +28 -1
  17. data/lib/hatchet/reaper.rb +159 -56
  18. data/lib/hatchet/reaper/app_age.rb +49 -0
  19. data/lib/hatchet/reaper/reaper_throttle.rb +55 -0
  20. data/lib/hatchet/shell_throttle.rb +71 -0
  21. data/lib/hatchet/test_run.rb +16 -9
  22. data/lib/hatchet/version.rb +1 -1
  23. data/{test → repo_fixtures}/different-folder-for-checked-in-repos/default_ruby/Gemfile +0 -0
  24. data/spec/hatchet/allow_failure_git_spec.rb +55 -0
  25. data/spec/hatchet/app_spec.rb +226 -0
  26. data/spec/hatchet/ci_spec.rb +67 -0
  27. data/spec/hatchet/config_spec.rb +34 -0
  28. data/spec/hatchet/edit_repo_spec.rb +17 -0
  29. data/spec/hatchet/git_spec.rb +9 -0
  30. data/spec/hatchet/heroku_api_spec.rb +30 -0
  31. data/spec/hatchet/local_repo_spec.rb +26 -0
  32. data/spec/hatchet/lock_spec.rb +81 -0
  33. data/spec/spec_helper.rb +25 -0
  34. data/spec/unit/reaper_spec.rb +153 -0
  35. data/spec/unit/shell_throttle.rb +28 -0
  36. metadata +43 -87
  37. data/.travis.yml +0 -16
  38. data/test/fixtures/buildpacks/null-buildpack/bin/compile +0 -4
  39. data/test/fixtures/buildpacks/null-buildpack/bin/detect +0 -5
  40. data/test/fixtures/buildpacks/null-buildpack/bin/release +0 -3
  41. data/test/fixtures/buildpacks/null-buildpack/hatchet.json +0 -4
  42. data/test/fixtures/buildpacks/null-buildpack/readme.md +0 -41
  43. data/test/hatchet/allow_failure_git_test.rb +0 -16
  44. data/test/hatchet/app_test.rb +0 -96
  45. data/test/hatchet/ci_four_test.rb +0 -19
  46. data/test/hatchet/ci_test.rb +0 -11
  47. data/test/hatchet/ci_three_test.rb +0 -9
  48. data/test/hatchet/ci_too_test.rb +0 -19
  49. data/test/hatchet/config_test.rb +0 -51
  50. data/test/hatchet/edit_repo_test.rb +0 -20
  51. data/test/hatchet/git_test.rb +0 -16
  52. data/test/hatchet/heroku_api_test.rb +0 -30
  53. data/test/hatchet/labs_test.rb +0 -20
  54. data/test/hatchet/local_repo_test.rb +0 -26
  55. data/test/hatchet/lock_test.rb +0 -9
  56. data/test/hatchet/multi_cmd_runner_test.rb +0 -30
  57. data/test/test_helper.rb +0 -28
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3c903b4979a0e4002e76f54d76a91f4651d5dfb2f20701909fefa5ac48954298
4
- data.tar.gz: 5f54f9ebf2ec4b23a12109638aee44cc841d244c5e3f6f76994841eb47995950
3
+ metadata.gz: f8c7a94962a255edf58024ed43c7102fc7eb97e07a3d1127ccec6f1cb16c560b
4
+ data.tar.gz: 36c1999581499ba71e09c93376281bb57105be30d7c42637b1e38a29decdd602
5
5
  SHA512:
6
- metadata.gz: 7aed83bd81ea797e0626184d82d0cc0d9582db9b94741e2388020196600c0d4061ddcc17d913593c85098d2f3654979be81eb643f6710e640a9bf2d74f193893
7
- data.tar.gz: 34fe3e85dc3531538d84fcdfecdc204af64f3699cae3e77d89a361c999ddd1a75d1fd736a0772bcd5727a0605971176ca7cecb8ed1c9eae53dceee37e3ae9aa5
6
+ metadata.gz: bbc833ba77cc7ce963795cc90ec282489b35a235e5c161e85704b15d2cd430a1a426906d26bdee3f00045a637b56eb31b11778beb220b459c7e79d2d87f1306a
7
+ data.tar.gz: a7c48cc4ce7103313cdfa8dcd5a27d519ade4fa02d328b837f1cd6c0d4f02af377ec2df764235af0d66316b535ea145716ff0974c797ee32b243ad415472db86
@@ -3,7 +3,18 @@ references:
3
3
  unit: &unit
4
4
  run:
5
5
  name: Run test suite
6
- command: bundle exec parallel_test test/hatchet -n 11
6
+ command: PARALLEL_SPLIT_TEST_PROCESSES=25 bundle exec parallel_split_test spec/
7
+ environment:
8
+ HATCHET_EXPENSIVE_MODE: 1 # !!!! WARNING !!!! ONLY RUN THIS IF YOU WORK FOR HEROKU !!!! WARNING !!!!
9
+ restore: &restore
10
+ restore_cache:
11
+ keys:
12
+ - v1_bundler_deps-{{ .Environment.CIRCLE_JOB }}
13
+ save: &save
14
+ save_cache:
15
+ paths:
16
+ - ./vendor/bundle
17
+ key: v1_bundler_deps-{{ .Environment.CIRCLE_JOB }} # CIRCLE_JOB e.g. "ruby-2.5"
7
18
  hatchet_setup: &hatchet_setup
8
19
  run:
9
20
  name: Hatchet setup
@@ -14,31 +25,40 @@ references:
14
25
  name: install dependencies
15
26
  command: |
16
27
  bundle install --jobs=4 --retry=3 --path vendor/bundle
28
+ bundle update
29
+ bundle clean
30
+
17
31
  jobs:
18
32
  "ruby-2.5":
19
33
  docker:
20
34
  - image: circleci/ruby:2.5
21
35
  steps:
22
36
  - checkout
37
+ - <<: *restore
23
38
  - <<: *bundle
24
39
  - <<: *hatchet_setup
25
40
  - <<: *unit
41
+ - <<: *save
26
42
  "ruby-2.6":
27
43
  docker:
28
44
  - image: circleci/ruby:2.6
29
45
  steps:
30
46
  - checkout
47
+ - <<: *restore
31
48
  - <<: *bundle
32
49
  - <<: *hatchet_setup
33
50
  - <<: *unit
51
+ - <<: *save
34
52
  "ruby-2.7":
35
53
  docker:
36
54
  - image: circleci/ruby:2.7
37
55
  steps:
38
56
  - checkout
57
+ - <<: *restore
39
58
  - <<: *bundle
40
59
  - <<: *hatchet_setup
41
60
  - <<: *unit
61
+ - <<: *save
42
62
 
43
63
  workflows:
44
64
  version: 2
data/.gitignore CHANGED
@@ -1,8 +1,10 @@
1
1
  .DS_Store
2
2
  test/fixtures/repos/*
3
+ repo_fixtures/repos/*
3
4
  *.gem
4
5
 
5
6
 
6
7
  Gemfile.lock
7
8
  debug.rb
8
9
  .ruby-version
10
+ .rspec_status
@@ -1,5 +1,42 @@
1
1
  ## HEAD
2
2
 
3
+ ## 7.1.0
4
+
5
+ - Initializing an `App` can now take a `retries` key to overload the global hatchet env var (https://github.com/heroku/hatchet/pull/119)
6
+ - Calling `App#commit!` now adds an empty commit if there is no changes on disk (https://github.com/heroku/hatchet/pull/119)
7
+ - Bugfix: Failed release phase in deploys can now be re-run (https://github.com/heroku/hatchet/pull/119)
8
+ - Bugfix: Allow `hatchet lock` to be run against new projects (https://github.com/heroku/hatchet/pull/118)
9
+ - Bugfix: Allow `hatchet.lock` file to lock a repo to a different branch than what is specified as default for GitHub (https://github.com/heroku/hatchet/pull/118)
10
+
11
+ ## 7.0.0
12
+
13
+ - ActiveSupport's Object#blank? and Object#present? are no longer provided by default (https://github.com/heroku/hatchet/pull/107)
14
+ - Remove deprecated support for passing a block to `App#run` (https://github.com/heroku/hatchet/pull/105)
15
+ - Ignore 403 on app delete due to race condition (https://github.com/heroku/hatchet/pull/101)
16
+ - The hatchet.lock file can now be locked to "main" in addition to "master" (https://github.com/heroku/hatchet/pull/86)
17
+ - Allow concurrent one-off dyno runs with the `run_multi: true` flag on apps (https://github.com/heroku/hatchet/pull/94)
18
+ - Apps are now marked as being "finished" by enabling maintenance mode on them when `teardown!` is called. Finished apps can be reaped immediately (https://github.com/heroku/hatchet/pull/97)
19
+ - Applications that are not marked as "finished" will be allowed to live for a HATCHET_ALIVE_TTL_MINUTES duration before they're deleted by the reaper to protect against deleting an app mid-deploy, default is seven minutes (https://github.com/heroku/hatchet/pull/97)
20
+ - The HEROKU_APP_LIMIT env var no longer does anything, instead hatchet application reaping is manually executed if an app cannot be created (https://github.com/heroku/hatchet/pull/97)
21
+ - App#deploy without a block will no longer run `teardown!` automatically (https://github.com/heroku/hatchet/pull/97)
22
+ - Calls to `git push heroku` are now rate throttled (https://github.com/heroku/hatchet/pull/98)
23
+ - Calls to `app.run` are now rate throttled (https://github.com/heroku/hatchet/pull/99)
24
+ - Deployment now raises and error when the release failed (https://github.com/heroku/hatchet/pull/93)
25
+
26
+ ## 6.0.0
27
+
28
+ - Rate throttling is now provided directly by `platform-api` (https://github.com/heroku/hatchet/pull/82)
29
+
30
+ ## 5.0.3
31
+
32
+ - Allow repos to be "locked" to master instead of a specific commit (https://github.com/heroku/hatchet/pull/80)
33
+
34
+ ## 5.0.2
35
+
36
+ - Fix `before_deploy` use with ci test runs (https://github.com/heroku/hatchet/pull/78)
37
+
38
+ ## 5.0.1
39
+
3
40
  - Circle CI support in `ci:setup` command (https://github.com/heroku/hatchet/pull/76)
4
41
 
5
42
  ## 5.0.0
@@ -234,7 +271,7 @@
234
271
 
235
272
  ## 1.1.1
236
273
 
237
- - Ensure external commands run inside of `bundle_exec` block are run with propper environment variables set.
274
+ - Ensure external commands run inside of `bundle_exec` block are run with proper environment variables set.
238
275
 
239
276
  ## 1.1.0
240
277
 
data/README.md CHANGED
@@ -2,52 +2,130 @@
2
2
 
3
3
  ![](http://f.cl.ly/items/2M2O2Q2I2x0e1M1P2936/Screen%20Shot%202013-01-06%20at%209.59.38%20PM.png)
4
4
 
5
- Hatchet is a an integration testing library for developing Heroku buildpacks.
5
+ Hatchet is an integration testing library for developing Heroku buildpacks.
6
6
 
7
7
  ## Install
8
8
 
9
- First run:
9
+ To get started, add this gem to your Gemfile:
10
10
 
11
- $ bundle install
11
+ ```
12
+ gem "heroku_hatchet"
13
+ ```
14
+
15
+ Then run:
12
16
 
13
- This library uses the heroku CLI and API. You will need to make your API key available to the system. If you're running on a CI platform, you'll need to generate an OAuth token and make it available on the system you're running on see the "CI" section below.
17
+ ```
18
+ $ bundle install
19
+ ```
14
20
 
15
- ## Run the Tests
21
+ This library uses the Heroku CLI and API. You will need to make your API key available to the system (`$ heroku login`). If you're running on a CI platform, you'll need to generate an OAuth token and make it available on the system you're running on see the "CI" section below.
16
22
 
17
- $ bundle exec rake test
23
+ ## About Hatchet
18
24
 
19
- ## Why Test a Buildpack?
25
+ ### Why Test a Buildpack?
20
26
 
21
27
  Testing a buildpack prevents regressions, and pushes out new features faster and easier.
22
28
 
23
- ## What can Hatchet Test?
29
+ ### What can Hatchet test?
30
+
31
+ Hatchet can easily test certain operations: deployment of buildpacks, getting the build output and running arbitrary interactive processes (e.g. `heroku run bash`). Hatchet can also test running Heroku CI against an app.
32
+
33
+ ### How does Hatchet test a buildpack?
34
+
35
+ To be able to check the behavior of a buildpack, you have to execute it. Hatchet does this by creating new Heroku apps `heroku create`, setting them to use your branch of the buildpack (must be available publicly) `heroku buildpacks:set https://github.com/your/buildpack-url#branch-name`, then deploying the app `git push heroku master`. It has built-in features such as API rate throttling (so your deploys slow down instead of fail) and internal retry mechanisms. Once deployed, it can `heroku run <command>` for you to allow you to assert behavior.
36
+
37
+ ### Can I use Hatchet to test my buildpack locally?
38
+
39
+ Yes, but the workflow is less than ideal since Heroku (and by extension, Hatchet) need your work to be available at a public URL. Let's say you're doing TDD and have already written a single failing test. You are developing on a branch and have already committed the test to that branch. To test your new code, you'll need to commit what you've got, push it to your public source repository.
40
+
41
+ ```
42
+ $ git add -P
43
+ $ git commit -m "[ci skip] WIP"
44
+ $ git push origin <current-branchname>
45
+ $ bundle exec rspec spec/path-to-your-test.rb:5 # This syntax focus runs a single test on line number 5
46
+ ```
47
+
48
+ Now when the tests execute Hatchet will use your code on your public branch. If you don't like a bunch of ugly "wip" commits you can keep amending the same commit over and over while you're iterating, alternatively you can [rebase your commits when you're done](https://www.codetriage.com/rebase).
49
+
50
+ ### Isn't deploying an app to Heroku overkill for testing? I want to go faster.
51
+
52
+ Hatchet is for integration testing. You can also unit test your code if you want your tests to execute much quicker. If your buildpack is written in bash, there is [shUnit2](https://github.com/kward/shunit2/), for example. It is recommended that you use both integration and unit tests.
53
+
54
+ But can't you integration test the buildpack by calling `bin/compile` directly without having to jump through deploying a Heroku app? It is possible to call your `bin/compile` script from your machine locally without Hatchet, but you'll not have access to config vars, addons, release phase, `heroku run`, and many more features. Also, calling `bin/compile` is very slow, and a medium to large buildpack can have upwards of 70 different integration test cases. If each were to take 1 minute optimistically, it would take over an hour to run your whole suite. Since Hatchet can be safely run via a parallel runner, it can execute most of these builds in parallel, and the whole suite would take roughly 5 minutes when running on CI.
55
+
56
+ In addition to speed, Hatchet provides isolation. Suppose you're executing `bin/compile` locally. In that case, you need to be very careful not to pollute the environment or local disk between runs, or you'll end up with odd failures that are seemingly impossible to hunt down.
57
+
58
+ ## Quicklinks
59
+
60
+ - Concepts
61
+ - [Tell Hatchet how to find your buildpack](#specify-buildpack)
62
+ - [Give Hatchet some example apps to deploy](#example-apps)
63
+ - [Use Hatchet to deploy app](#deploying-apps)
64
+ - [Use Hatchet to test runtime behavior and environment](#build-versus-run-testing)
65
+ - [How to update or modify test app files safely in parallel](#modifying-apps-on-disk-before-deploy)
66
+ - [Understand how Hatchet (does and does not) clean up apps](#app-reaping)
67
+ - [How to re-deploy the same app](#deploying-multiple-times)
68
+ - [How to test your buildpack on Heroku CI](#testing-ci)
69
+ - [How to safely test locally without modifying disk or your environment](#testing-on-local-disk-without-deploying)
70
+ - [How to set up your buildpack on a Continuous Integration (CI) service](#running-your-buildpack-tests-on-a-ci-service)
71
+
72
+ - Reference Docs:
73
+ - Method arguments to `Hatchet::Runner.new` [docs](#init-options)
74
+ - Method documentation for `Hatchet::Runner` and `TestRun` objects [docs](#app-methods)
75
+ - All ENV vars and what they do [docs](#env-vars)
76
+
77
+ - Ruby language and ecosystem basics
78
+ - [Introduction to the Rspec testing framework for non-rubyists](#basic-rspec)
79
+ - [Introduction to Ruby for non-rubyists](#basic-ruby)
24
80
 
25
- Hatchet can easily test certain operations: deployment of buildpacks, getting the build output, and running arbitrary interactive processes (e.g. `heroku run bash`). Hatchet can also test running CI against an app.
81
+ ## Concepts
26
82
 
27
- ## Writing Tests
83
+ ### Specify buildpack
28
84
 
29
- Hatchet assumes a test framework doesn't exist. [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.
85
+ Tell Hatchet what buildpack you want to use by default by setting environment variables, this is commonly done in the `spec_helper.rb` file:
30
86
 
31
- Running `focused: true` in rspec allows you to choose which test to run and to tag tests. Rspec has useful plugins, such as `gem 'rspec-retry'` which will re-run any failed tests a given number of times (I recommend setting this to at least 2) to decrease false negatives in your tests.
87
+ ```ruby
88
+ ENV["HATCHET_BUILDPACK_BASE"] = "https://github.com/path-to-your/buildpack"
89
+ require 'hatchet'`
90
+ ```
91
+
92
+ If you do not specify `HATCHET_BUILDPACK_URL` the default Ruby buildpack will be used. If you do not specify a `HATCHET_BUILDPACK_BRANCH` the current branch you are on will be used. This is how the Ruby buildpack runs tests on branches on CI (by leaving `HATCHET_BUILDPACK_BRANCH` blank).
93
+
94
+ The workflow generally looks like this:
95
+
96
+ 1. Make a change to the codebase
97
+ 2. Commit it and push to GitHub so it's publicly available
98
+ 3. Execute your test suite or individual test
99
+ 4. Repeat until you're happy
100
+ 5. Be happy
32
101
 
33
- 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.
102
+ ### Example apps:
34
103
 
35
- If you're unfamiliar with the ruby testing eco-system or want some help, start by looking at existing projects.
104
+ Hatchet works by deploying example apps to Heroku's production service first you'll need an app to deploy that works with the buildpack you want to test. This method is preferred if you've got a very small app that might only need one or two files. There are two ways to give Hatchet a test app, you can either specify a remote app or a local directory.
105
+
106
+ - **Local directory use of Hatchet:**
107
+
108
+ ```ruby
109
+ Hatchet::Runner.new("path/to/local/directory").deploy do |app|
110
+ end
111
+ ```
36
112
 
37
- *Spoilers*: There is a section below on getting Hatchet to work on CI
113
+ An example of this is the [heroku/nodejs buildpack tests](https://github.com/heroku/heroku-buildpack-nodejs/blob/9898b875f45639d9fe0fd6959f42aea5214504db/spec/ci/node_10_spec.rb#L6).
38
114
 
39
- ## Testing a Buildpack
115
+ You can either check in your apps to your source control or, you can use code to generate them, for example:
40
116
 
41
- Hatchet was built for testing the Ruby buildpack, but Hatchet can test any buildpack provided your tests are written in Ruby.
117
+ - [Generating an example app](https://github.com/sharpstone/force_absolute_paths_buildpack/blob/53c3cffb039fd366b5abb4524fb32983c11f9344/spec/hatchet/buildpack_spec.rb#L5-L20)
118
+ - [Source code for `generate_fixture_app`](https://github.com/sharpstone/force_absolute_paths_buildpack/blob/53c3cffb039fd366b5abb4524fb32983c11f9344/spec/spec_helper.rb#L34-L64)
42
119
 
43
- 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).
120
+ If you generate example apps programmatically, then add the folder you put them in to your `.gitignore`.
44
121
 
45
- Hatchet will automate retrieving these files `$ hatchet install`, deploy these files using your local copy of the buildpack, retrieve the build output and run commands against deploying applications.
122
+ > Note: If you're not using the `hatchet.json` you'll still need an empty one in your project with contents `{}`
46
123
 
124
+ - **Github app use of Hatchet:**
47
125
 
48
- ## Hatchet.json
126
+ Instead of storing your apps locally or generating them, you can point Hatchet at a remote github repo. This method of storing apps on GitHub is preferred is you have an app that is large or has many files (for example, a Rails app).
49
127
 
50
- 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.
128
+ Hatchet expects a json file in the root of your buildpack called `hatchet.json`. You can configure the 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 current default directory.
51
129
 
52
130
  ```
53
131
  {
@@ -78,302 +156,828 @@ You can reference one of these applications in your test by using it's git name:
78
156
  Hatchet::Runner.new('no_lockfile')
79
157
  ```
80
158
 
81
- If you have conflicting names, use full paths.
159
+ If you have conflicting names, use full paths like `Hatchet::RUnner.new("sharpstone/no_lockfile")`.
82
160
 
83
- To test with fixtures that are checked in locally, add the fixture directory to the path and skip the `hatchet install`:
161
+ When you run `hatchet install` it will lock all the Repos to a specific commit. This is done so that if a repo changes upstream that introduces an error the test suite won't automatically pick it up. For example in https://github.com/sharpstone/lock_fail/commit/e61ba47043fbae131abb74fd74added7e6e504df an error is added, but this will only cause a failure if your project intentionally locks to commit `e61ba47043fbae131abb74fd74added7e6e504df` or later.
162
+
163
+ You can re-lock your projects by running `hatchet lock`. This modifies the `hatchet.lock` file. For example:
84
164
 
85
165
  ```
86
- Hatchet::Runner.new("spec/fixtures/repos/node-10-metrics")
166
+ ---
167
+ - - test/fixtures/repos/bundler/no_lockfile
168
+ - 1947ce9a9c276d5df1c323b2ad78d1d85c7ab4c0
169
+ - - test/fixtures/repos/ci/rails5_ci_fails_no_database
170
+ - 3044f05febdfbbe656f0f5113cf5968ca07e34fd
171
+ - - test/fixtures/repos/ci/rails5_ruby_schema_format
172
+ - 3e63c3e13f435cf4ab11265e9abd161cc28cc552
173
+ - - test/fixtures/repos/default/default_ruby
174
+ - 6e642963acec0ff64af51bd6fba8db3c4176ed6e
175
+ - - test/fixtures/repos/lock/lock_fail
176
+ - da748a59340be8b950e7bbbfb32077eb67d70c3c
177
+ - - test/fixtures/repos/lock/lock_fail_main
178
+ - main
179
+ - - test/fixtures/repos/rails2/rails2blog
180
+ - b37357a498ae5e8429f5601c5ab9524021dc2aaa
181
+ - - test/fixtures/repos/rails3/rails3_mri_193
182
+ - 88c5d0d067cfd11e4452633994a85b04627ae8c7
87
183
  ```
88
184
 
89
- Be careful when including repos inside of your test directory. If you're using a runner that looks for patterns such as `*_test.rb` to run your hatchet tests, it may run the tests inside of the repos. To prevent this problem, move your repos directory out of `test/` or into specific directories such as `test/hatchet`. Then change your pattern. If you are using `Rake::TestTask`, it might look like this:
185
+ > Note: If you don't want to lock to a specific commit, you can always use the latest commit by specifying `main` manually as seen above. This will always give you the latest commit on the `main` branch. The `master` keyword is supported as well.
90
186
 
91
- t.pattern = 'test/hatchet/**/*_test.rb'
187
+ ### Deploying apps
92
188
 
93
- When basing tests on external repos, do not change the tests or they may spontaneously fail. We may create a hatchet.lockfile or something to declare the commit in the future.
189
+ Once you've got an app and have set up your buildpack, you can deploy an app and assert based on the output (all examples use rspec for testing framework).
94
190
 
191
+ ```ruby
192
+ Hatchet::Runner.new("default_ruby").deploy do |app|
193
+ expect(app.output).to match("Installing dependencies using bundler")
194
+ end
195
+ ```
95
196
 
96
- ## Deploying apps
197
+ By default, an error will be raised if the deploy doesn't work, which forces the test to fail. If you're trying to test failing behavior (for example you want to test that an app without a `Gemfile.lock` fails to build), you can manually allow failures:
97
198
 
199
+ ```ruby
200
+ Hatchet::Runner.new("no_lockfile", allow_failure: true).deploy do |app|
201
+ expect(app).not_to be_deployed
202
+ expect(app.output).to include("Gemfile.lock required")
203
+ end
204
+ ```
98
205
 
99
- You can specify the location of your public buildpack url in an environment variable:
206
+ ### Build versus run testing
100
207
 
101
- ```sh
102
- HATCHET_BUILDPACK_BASE=https://github.com/heroku/heroku-buildpack-ruby.git
103
- HATCHET_BUILDPACK_BRANCH=master
208
+ In addition to testing what the build output was, the next most common thing to assert is that behavior at runtime produces expected results. Hatchet provides a helper for calling `heroku run <cmd>` and asserting against it. For example:
209
+
210
+ ```ruby
211
+ Hatchet::Runner.new("minimal_webpacker", buildpacks: buildpacks).deploy do |app, heroku|
212
+ expect(app.run("which node")).to match("/app/bin/node")
213
+ end
104
214
  ```
105
215
 
106
- If you do not specify `HATCHET_BUILDPACK_URL` the default Ruby buildpack will be used. If you do not specify a `HATCHET_BUILDPACK_BRANCH` the current branch you are on will be used. This is how the Ruby buildpack runs tests on branches on CI (by leaving `HATCHET_BUILDPACK_BRANCH` blank).
216
+ In this example, Hatchet is calling `heroku run which node` and passing the results back to the test so we can assert against it.
107
217
 
108
- If the `ENV['HATCHET_RETRIES']` is set to a number, deploys are expected to work and automatically retry that number of times. Due to testing using a network and random 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.
218
+ - **Asserting exit status:**
109
219
 
110
- If you are testing an app that is supposed to fail deployment, you can set the `allow_failure: true` flag when creating the app:
220
+ In ruby the way you assert a command you ran on the shell was successful or not is by using the `$?` "magic object". By default calling `app.run` will set this variable which can be used in your tests:
111
221
 
112
222
  ```ruby
113
- Hatchet::Runner.new("no_lockfile", allow_failure: true).deploy do |app|
223
+ Hatchet::Runner.new("minimal_webpacker", buildpacks: buildpacks).deploy do |app, heroku|
224
+ expect(app.run("which node")).to match("/app/bin/node")
225
+ expect($?.exitstatus).to eq(0)
226
+ expect($?.success?).to be_truthy
227
+
228
+ # In Ruby all objects except `nil` and `false` are "truthy" in this case it could also be tested using `be_true` but
229
+ # it's best practice to use this test helper in rspec
230
+ end
114
231
  ```
115
232
 
116
- After the block finishes, your app will be queued to be removed from heroku. If you are investigating a deploy, you can add the `debug: true` flag to your app:
233
+ You can disable this behavior [see how to do it in the reference tests](https://github.com/heroku/hatchet/blob/master/spec/hatchet/app_spec.rb)
234
+
235
+ - **Escaping and raw mode:**
236
+
237
+ By default `app.run()` will escape the input so you can safely call `app.run("cmd && cmd")` and it works as expected. But if you want to do something custom, you can enable raw mode by passing in `raw: true` [see how to do it in the reference tests](https://github.com/heroku/hatchet/blob/master/spec/hatchet/app_spec.rb)
238
+
239
+ - **Heroku options:**
240
+
241
+ You can use all the options available to `heroku run bash` such as `heroku run bash --env FOO=bar` [see how to do it in the reference tests](https://github.com/heroku/hatchet/blob/master/spec/hatchet/app_spec.rb)
242
+
243
+ ### Modifying apps on disk before deploy
244
+
245
+ Hatchet is designed to play nicely with running tests in parallel via threads or processes. To support this the code that is executed in the `deploy` block is being run in a new directory. This allows you to modify files on disk safely without having to worry about race conditions. Still, it introduces the unexpected behavior that changes might not work like you think they will.
246
+
247
+ One typical pattern is to have a minimal example app, and then to modify it as needed before your tests. You can do this safely using the `before_deploy` block.
117
248
 
118
249
  ```ruby
119
- Hatchet::Runner.new("rails3_mri_193", debug: true).deploy do |app|
250
+ Hatchet::Runner.new("default_ruby").tap do |app|
251
+ app.before_deploy do
252
+ out = `echo 'ruby "2.7.1"' >> Gemfile`
253
+ raise "Echo command failed: #{out}" unless $?.success?
254
+ end
255
+ app.deploy do |app|
256
+ expect(app.output).to include("Using Ruby version: ruby-2.6.6")
257
+ end
258
+ end
120
259
  ```
121
260
 
122
- 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.
261
+ This example will add the string `ruby "2.7.1"` to the end of the Gemfile on disk. It accomplishes this by shelling out to `echo`. If you prefer, you can directly use `File.open` to write contents to disk.
262
+
263
+ > Note: The above [tap method in ruby](https://ruby-doc.org/core-2.4.0/Object.html#method-i-tap) returns itself in a block, it makes this example cleaner.
123
264
 
124
- If you are wanting to run a test against a specific app without deploying to it, you can specify the app name like this:
265
+ > Note: that we're checking the status code of the shell command we're running (shell commands are executed via backticks in ruby), a common pattern is to write a simple helper function to automate this:
125
266
 
126
267
  ```ruby
127
- app = Hatchet::Runner.new("rails3_mri_193", name: "testapp")
268
+ # spec_helper.rb
269
+
270
+ def run!(cmd)
271
+ out = `#{cmd}`
272
+ raise "Command #{cmd} failed with output #{out}" unless $?.success?
273
+ out
274
+ end
128
275
  ```
129
276
 
130
- Deploying the app takes a few minutes. You may want to skip deployment to make debugging faster.
277
+ Then you can use it in your tests:
131
278
 
132
- If you need to deploy using a different buildpack you can specify one manually:
279
+ ```ruby
280
+ Hatchet::Runner.new("default_ruby").tap do |app|
281
+ app.before_deploy do
282
+ run!(%Q{echo 'ruby "2.7.1"'})
283
+ end
284
+ app.deploy do |app|
285
+ expect(app.output).to include("Using Ruby version: ruby-2.6.6")
286
+ end
287
+ end
288
+ ```
289
+
290
+ > Note: that `%Q{}` is a method of creating a string in Ruby if we didn't use it here we could escape the quotes:
133
291
 
134
292
  ```ruby
293
+ run!("echo 'ruby \"2.7.1\"'")
294
+ ```
135
295
 
136
- def test_deploy
137
- Hatchet::Runner.new("rails3_mri_193", buildpack: "https://github.com/heroku/heroku-buildpack-ruby.git").deploy do |app|
138
- # ...
296
+ In Ruby double quotes allow for the insert operator in strings, but single quotes do not:
297
+
298
+ ```ruby
299
+ name = "schneems"
300
+ puts "Hello #{name}" # => Hello schneems
301
+ puts 'Hello #{name}' # => Hello #{name}
302
+ puts "Hello '#{name}'" # => Hello 'schneems'
303
+ puts %Q{Hello "#{name}"} # => Hello "schneems"
139
304
  ```
140
305
 
141
- You can specify multiple buildpacks by passing in an array. When you do that you also need to tell hatchet where to place your buildpack. Since hatchet needs to build your buildpack from a branch you should not hardcode a path like `heroku/ruby` instead Hatchet has a replacement mechanism. Use the `:default` symbol where you want your buildpack to execute. For example:
306
+ ### App reaping
307
+
308
+ When your tests are running you'll see hatchet output some details about what it's doing:
142
309
 
143
310
  ```
144
- Hatchet::Runner.new("default_ruby", buildpacks: [:default, "https://github.com/pgbouncer/pgbouncer"])
311
+ Hatchet setup: "hatchet-t-bed73940a6" for "rails51_webpacker"
145
312
  ```
146
313
 
147
- That will expand your buildpack and branch. For example if you're on the `update_readme` branch of the `heroku-buildpack-ruby` buildpack it would expand to:
314
+ And later:
148
315
 
149
316
  ```
150
- Hatchet::Runner.new("default_ruby", buildpacks: ["https://github.com/heroku/heroku-buildpack-ruby#update_readme", "https://github.com/pgbouncer/pgbouncer"])
317
+ Destroying "hatchet-t-fd25e3626b". Hatchet app limit: 80
151
318
  ```
152
319
 
153
- You can also specify a stack:
320
+ By default, Hatchet does not destroy your app at the end of the test run, that way if your test failed unexpectedly if it's not destroyed yet, you can:
154
321
 
155
322
  ```
156
- Hatchet::Runner.new("rails3_mri_193", stack: "cedar-14").deploy do |app|
323
+ $ heroku run bash -a hatchet-t-bed73940a6
157
324
  ```
158
325
 
159
- ## Getting Deploy Output
326
+ And use that to debug. Hatchet deletes old apps on demand. You tell it what your limits are and it will stay within those limits:
327
+
328
+ ```
329
+ HATCHET_APP_LIMIT=20
330
+ ```
160
331
 
161
- After Hatchet deploys your app you can get the output by using `app.output`
332
+ With these env vars, Hatchet will "reap" older hatchet apps when it sees there are 20 or more hatchet apps. For CI, it's recommended that you increase the `HATCHET_APP_LIMIT` to 80-100. Hatchet will mark apps as safe for deletion once they've finished, and the `teardown!` method has been called on them (it tracks this by enabling maintenance mode on apps). Hatchet only tracks its apps. Hatchet uses a regex pattern on the name of apps to see which ones it can manage. If your account has reached the maximum number of global Heroku apps, you'll need to remove some manually.
333
+
334
+ If an app is not marked as being in maintenance mode for some reason, it can be deleted, but only after it has been allowed to live for some time. This behavior is configured by the `HATCHET_ALIVE_TTL_MINUTES` env var. For example, if you set it for `7`, Hatchet will ensure that any apps that are not marked as being in maintenance mode are allowed to live for at least seven minutes. This should give the app time to finish the test's execution, so it is not deleted mid-deploy. When this deletion happens, you'll see a warning in your output. It could indicate you're not properly cleaning up and calling `teardown!` on some of your apps, or it could mean that you're attempting to execute more tests concurrently than your `HATCHET_APP_LIMIT` allows. This deletion-mid-test behavior might otherwise be triggered if you have multiple CI runs executing at the same time.
335
+
336
+ It's recommended you don't use your personal Heroku API key for running tests on a CI server since the hatchet apps count against your account maximum limits. Running tests using your account locally is fine for debugging one or two tests.
337
+
338
+ If you find your local account has hit your maximum app limit, one handy trick is to get rid of any old "default" Heroku apps you've created. This plugin (https://github.com/hunterloftis/heroku-destroy-temp) can help:
339
+
340
+ ```
341
+ $ heroku plugins:install heroku-destroy-temp
342
+ $ heroku apps:destroy-temp
343
+ ```
344
+
345
+ > This won't detect hatchet apps, but it's still handy for cleaning up other unused apps.
346
+
347
+ ### Deploying multiple times
348
+
349
+ If your buildpack uses the cache, you'll likely want to deploy multiple times against the same app to assert the cache was used. Here's an example of how to do that:
162
350
 
163
351
  ```ruby
164
- Hatchet::Runner.new("rails3_mri_193").deploy do |app|
165
- puts app.output
352
+ Hatchet::Runner.new("python_default").deploy do |app|
353
+ expect(app.output).to match(/Installing pip/)
354
+
355
+ # Redeploy with changed requirements file
356
+ run!(%Q{echo "pygments" >> requirements.txt})
357
+ app.commit!
358
+
359
+ app.push! # <======= HERE
360
+
361
+ expect(app.output).to match("Requirements file has been changed, clearing cached dependencies")
166
362
  end
167
363
  ```
168
364
 
169
- 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:
365
+ ### Testing CI
366
+
367
+ You can run an app against CI using the `run_ci` command (instead of `deploy`). You can re-run tests against the same app with the `run_again` command.
170
368
 
171
369
  ```ruby
172
- Hatchet::Runner.new("no_lockfile", allow_failure: true).deploy do |app|
173
- assert_match "Gemfile.lock required", app.output
370
+ Hatchet::Runner.new("python_default").run_ci do |test_run|
371
+ expect(test_run.output).to match("Downloading nose")
372
+ expect(test_run.status).to eq(:succeeded)
373
+
374
+ test_run.run_again
375
+
376
+ expect(test_run.output).to match("installing from cache")
377
+ expect(test_run.output).to_not match("Downloading nose")
174
378
  end
175
379
  ```
176
380
 
177
- 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).
381
+ > Note: That thing returned by the `run_ci` command is not an "app" object but rather a `test_run` object.
178
382
 
179
- ## Running Processes
383
+ - `test_run.output` will have the setup and test output of your tests.
384
+ - `test_run.app` has a reference to the "app" you're testing against, however currently no `heroku create` is run (as it's not needed to run tests, only a pipeline and a blob of code).
180
385
 
181
- 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 use the `app.run()` command without passing a block:
386
+ An exception will be raised if either the test times out or a status of `:errored` or `:failed` is returned. If you expect your test to fail, you can pass in `allow_failure: true` when creating your hatchet runner. If you do that, you'll also get access to different statuses:
182
387
 
183
- ```ruby
184
- Hatchet::Runner.new("rails3_mri_193").deploy do |app|
185
- assert_match "applications.css", app.run("ls public/assets")
186
- ```
388
+ - `test_run.status` will return a symbol of the status of your test. Statuses include, but are not limited to `:pending`, `:building`, `:errored`, `:creating`, `:succeeded`, and `:failed`
187
389
 
188
- This is useful for checking the existence of generated files such as assets. To run an interactive session such as `heroku run bash` or `heroku run rails console`, run the command and pass a block:
390
+ You can pass in a different timeout to the `run_ci` method `run_ci(timeout: 300)`.
189
391
 
392
+ You probably need an `app.json` in the root directory of the app you're deploying. For example:
393
+
394
+ ```json
395
+ {
396
+ "environments": {
397
+ "test": {
398
+ "addons":[
399
+ "heroku-postgresql"
400
+ ]
401
+ }
402
+ }
403
+ }
190
404
  ```
191
- Hatchet::Runner.new("rails3_mri_193").deploy do |app|
192
- app.run("cat Procfile")
405
+
406
+ This is on [a Rails5 test app](https://github.com/sharpstone/rails5_ruby_schema_format/blob/master/app.json) that needs the database to run.
407
+
408
+ Do **NOT** specify a `buildpacks` key in the `app.json` because Hatchet will automatically do this for you. If you need to set buildpacks, you can pass them into the `buildpacks:` keyword argument:
409
+
410
+ ```ruby
411
+ buildpacks = [
412
+ "https://github.com/heroku/heroku-buildpack-pgbouncer.git",
413
+ :default
414
+ ]
415
+
416
+ Hatchet::Runner.new("rails5_ruby_schema_format", buildpacks: buildpacks).run_ci do |test_run|
417
+ # ...
193
418
  end
194
419
  ```
195
420
 
196
- By default commands will be shell escaped (to prevent commands from escaping the `heroku run` command), if you want to manage your own quoting you can use the `raw: true` option:
421
+ > Note that the `:default` symbol (like a singleton string object in Ruby) can be used for where you want your buildpack inserted, it will be replaced with your app's repo and git branch you're testing against.
197
422
 
423
+ ### Testing on local disk without deploying
424
+
425
+ Sometimes you might want to assert something against a test app without deploying. This modification is tricky if you're modifying files or the environment in your test. To help out there's a helper `in_directory_fork`:
426
+
427
+ ```ruby
428
+ Hatchet::App.new('rails6-basic').in_directory_fork do
429
+ require 'language_pack/rails5'
430
+ require 'language_pack/rails6'
431
+
432
+ expect(LanguagePack::Rails5.use?).to eq(false)
433
+ expect(LanguagePack::Rails6.use?).to eq(true)
434
+ end
198
435
  ```
199
- app.run('echo \$HELLO \$NAME', raw: true)
436
+
437
+ ## Running your buildpack tests on a CI service
438
+
439
+ Once you've got your tests working locally, you'll likely want to get them running on CI. For reference, see the [Circle CI config from this repo](https://github.com/heroku/hatchet/blob/master/.circleci/config.yml) and the [Heroku CI config from the ruby buildpack](https://github.com/heroku/heroku-buildpack-ruby/blob/master/app.json).
440
+
441
+ To make running on CI easier, there is a setup script in Hatchet that can be run on your CI server each time before your tests are executed:
442
+
443
+ ```yml
444
+ bundle exec hatchet ci:setup
200
445
  ```
201
446
 
202
- You can specify Heroku flags to the `heroku run` command by passing in the `heroku:` key along with a hash.
447
+ If you're a Heroku employee, see [private instructions for setting up test users](https://github.com/heroku/languages-team/blob/master/guides/create_test_users_for_ci.md) to generate a user a grab the API token.
448
+
449
+ Once you have an API token you'll want to set up these env vars with your CI provider:
203
450
 
204
451
  ```
205
- app.run("nproc", heroku: { "size" => "performance-l" })
206
- # => 8
452
+ HATCHET_APP_LIMIT=100
453
+ HATCHET_RETRIES=2
454
+ HEROKU_API_KEY=<redacted>
455
+ HEROKU_API_USER=<redacted@example.com>
207
456
  ```
208
457
 
209
- You can see a list of Heroku flags by running:
458
+ You can reference this PR for getting a buildpack set up from scratch with tests to see what kinds of files you might need: https://github.com/sharpstone/force_absolute_paths_buildpack/pull/2.
459
+
460
+ ## Reference docs
210
461
 
462
+ The `Hatchet::Runner.new` takes several arguments.
463
+
464
+ ### Init options
465
+
466
+ - stack (String): The stack you want to deploy to on Heroku.
467
+
468
+ ```ruby
469
+ Hatchet::Runner.new("default_ruby", stack: "heroku-16").deploy do |app|
470
+ # ...
471
+ end
211
472
  ```
212
- $ heroku run --help
213
- run a one-off process inside a heroku dyno
214
473
 
215
- USAGE
216
- $ heroku run
474
+ - name (String): The name of an app you want to use. If you choose to provide your own app name, then Hatchet will not reap it, you'll have to delete it manually.
475
+ - allow_failure (Boolean): If set to a truthy value then the test won't error if the deploy fails
476
+ - labs (Array): Heroku has "labs" that are essentially features that are not enabled by default, one of the most popular ones is "preboot" https://devcenter.heroku.com/articles/preboot.
477
+ - buildpacks (Array): Pass in the buildpacks you want to use against your app
217
478
 
218
- OPTIONS
219
- -a, --app=app (required) app to run command against
220
- -e, --env=env environment variables to set (use ';' to split multiple vars)
221
- -r, --remote=remote git remote of app to use
222
- -s, --size=size dyno size
223
- -x, --exit-code passthrough the exit code of the remote command
224
- --no-notify disables notification when dyno is up (alternatively use HEROKU_NOTIFICATIONS=0)
225
- --no-tty force the command to not run in a tty
226
- --type=type process type
479
+ ```ruby
480
+ Hatchet::Runner.new("default_ruby", buildpacks: ["heroku/nodejs", :default]).deploy do |app|
481
+ # ...
482
+ end
227
483
  ```
228
484
 
229
- By default Hatchet will set the app name and the exit code
485
+ In this example, the app would use the nodejs buildpack, and then `:default` gets replaced by your Git url and branch name.
230
486
 
487
+ - before_deploy (Block): Instead of using the `tap` syntax you can provide a block directly to hatchet app initialization:
231
488
 
489
+ ```ruby
490
+ Hatchet::Runner.new("default_ruby", before_deploy: ->{ FileUtils.touch("foo.txt")}).deploy do
491
+ # Assert stuff
492
+ end
232
493
  ```
233
- app.run("exit 127")
234
- puts $?.exitcode
235
- # => 127
494
+
495
+ A block in ruby is essentially an un-named method. Think of it as code to be executed later. See docs below for more info on blocks, procs and lambdas.
496
+
497
+ - config (Hash): You can set config vars against your app:
498
+
499
+ ```ruby
500
+ config = { "DEPLOY_TASKS" => "run:bloop", "FOO" => "bar" }
501
+ Hatchet::Runner.new('default_ruby', config: config).deploy do |app|
502
+ expect(app.run("echo $DEPLOY_TASKS").to match("run:bloop")
503
+ end
236
504
  ```
237
505
 
238
- To skip a value you can use the constant:
506
+ > A hash in Ruby is like a dict in python. It is a set of key/value pairs. The syntax `=>` is called a "hashrocket" and is an alternative syntax to "json" syntax for hashes. It is used to allow for string keys instead of symbol keys.
239
507
 
508
+ - `run_multi` (Boolean): Allows you to run more than a single "one-off" dyno at a time (the `HATCHET_EXPENSIVE_MODE` env var must be set to use this feature). By default, "free" Heroku apps are restricted to only allowing one dyno to run at a time. You can increase this limit by scaling an application to paid application, but it will incur charges against your application:
509
+
510
+ ```ruby
511
+ Hatchet::Runner.new("default_ruby", run_multi: true).deploy do |app|
512
+ # This code runs in the background
513
+ app.run_multi("ls") do |out, status|
514
+ expect(status.success?).to be_truthy
515
+ expect(out).to include("Gemfile")
516
+ end
517
+
518
+ # This code runs in the background in parallel
519
+ app.run_multi("ruby -v") do |out, status|
520
+ expect(status.success?).to be_truthy
521
+ expect(out).to include("ruby")
522
+ end
523
+
524
+ # This line will be reached before either of the above blocks finish
525
+ end
240
526
  ```
241
- app.run("exit 127", heroku: { "exit-code" => Hatchet::App::SkipDefaultOption})
242
- puts $?.exitcode
243
- # => 0
527
+
528
+ In this example, the `heroku run ls` and `heroku run ruby -v` will be executed concurrently. The order that the `run_multi` blocks execute is not guaranteed. You can toggle this `run_multi` setting on globally by using `HATCHET_RUN_MULTI=1`. Without this setting enabled, you might need to add a `sleep` between multiple `app.run` invocations.
529
+
530
+ WARNING: Enabling `run_multi` setting on an app will charge your Heroku account 🤑.
531
+ WARNING: Do not use `run_multi` if you're not using the `deploy` block syntax or manually call `teardown!` inside the text context [more info about how behavior does not work with the `after` block syntax in rspec](https://github.com/heroku/hatchet/issues/110).
532
+ WARNING: To work, `run_multi` requires your application to have a `web` process associated with it.
533
+
534
+ - `retries` (Integer): When passed in, this value will be used insead of the global `HATCHET_RETRIES` set via environment variable. When `allow_failures: true` is set as well as a retries value, then the application will not retry pushing to Heroku.
535
+
536
+ ### App methods:
537
+
538
+ - `app.set_config()`: Updates the configuration on your app taking in a hash
539
+
540
+ You can also update your config using the `set_config` method:
541
+
542
+ ```ruby
543
+ app = Hatchet::Runner.new("default_ruby")
544
+ app.set_config({"DEPLOY_TASKS" => "run:bloop", "FOO" => "bar"})
545
+ app.deploy do
546
+ expect(app.run("echo $DEPLOY_TASKS").to match("run:bloop")
547
+ end
244
548
  ```
245
549
 
246
- To specify a flag that has no value (such as `--no-notify`, `no-tty`, or `--exit-code`) pass a `nil` value:
550
+ - `app.get_config()`: returns the Heroku value for a specific env var:
247
551
 
552
+ ```ruby
553
+ app = Hatchet::Runner.new("default_ruby")
554
+ app.set_config({"DEPLOY_TASKS" => "run:bloop", "FOO" => "bar"})
555
+ app.get_config("DEPLOY_TASKS") # => "run:bloop"
556
+ ```
557
+
558
+ - `app.set_lab()`: Enables the specified lab/feature on the app
559
+ - `app.add_database()`: adds a database to the app, defaults to the "dev" database
560
+ - `app.run()`: Runs a `heroku run bash` session with the arguments, covered above.
561
+ - `app.run_multi()`: Runs a `heroku run bash` session in the background and yields the results. This requires the `run_multi` flag of the app to be set to `true`, which will charge your application (the `HATCHET_EXPENSIVE_MODE` env var must also be set to use this feature). Example above.
562
+ - `app.create_app`: Can be used to manually create the app without deploying it (You probably want `setup!` though)
563
+ - `app.setup!`: Gets the application in a state ready for deploy.
564
+ - Creates the Heroku app
565
+ - Sets up any specified labs (from initialization)
566
+ - Sets up any specified buildpacks
567
+ - Sets up specified config
568
+ - Calls the contents of the `before_deploy` block
569
+ - `app.before_deploy`: Allows you to update the `before_deploy` block
570
+
571
+ ```ruby
572
+ Hatchet::Runner.new("default_ruby").tap do |app|
573
+ app.before_deploy do
574
+ FileUtils.touch("foo.txt")
575
+ end
576
+ app.deploy do
577
+ end
578
+ end
579
+ ```
580
+
581
+ Has the same result as:
582
+
583
+ ```ruby
584
+ before_deploy_proc = Proc.new do
585
+ FileUtils.touch("foo.txt")
586
+ end
587
+
588
+ Hatchet::Runner.new("default_ruby", before_deploy: before_deploy_proc).deploy do |app|
589
+ end
248
590
  ```
249
- app.run("echo 'foo'", heroku: { "no-notify" => nil })
250
- # This is the same as `heroku run echo 'foo' --no-notify`
591
+
592
+ - `app.commit!`: Will updates the contents of your local git dir if you've modified files on disk
593
+
594
+ ```ruby
595
+ Hatchet::Runner.new("python_default").deploy do |app|
596
+ expect(app.output).to match(/Installing pip/)
597
+
598
+ # Redeploy with changed requirements file
599
+ run!(%Q{echo "" >> requirements.txt})
600
+ run!(%Q{echo "pygments" >> requirements.txt})
601
+
602
+ app.commit! # <=== Here
603
+
604
+ app.push!
605
+ end
251
606
  ```
252
607
 
253
- ## Modify Application Files on Disk
608
+ > Note: Any changes to disk from a `before_deploy` block will be committed automatically after the block executes
254
609
 
255
- While template apps provided from your `hatchet.json` can provide different test cases, you may want to test minor varriations of an app. You can do this by using the `before_deploy` hook to modify files on disk inside of an app in a threadsafe way that will only affect the app's local instance:
610
+ - `app.in_directory`: Runs the given block in a temp directory (but in the same process). One advanced debugging technique is to indefinitely pause test execution after outputting the directory so you can `cd` there and manually debug:
256
611
 
257
612
  ```ruby
258
- Hatchet::App.new("default_ruby", before_deploy: { FileUtils.touch("foo.txt")}).deploy do
259
- # Assert stuff
613
+ Hatchet::Runner.new("python_default").in_directory do |app|
614
+ puts "Temp dir is: #{Dir.pwd}"
615
+ STDIN.gets("foo") # <==== Pauses tests until stdin receives "foo"
260
616
  end
261
617
  ```
262
618
 
263
- After the `before_deploy` block fires, the results will be committed to git automatically before the app deploys.
619
+ > Note: If you want to execute tests in this temp directory, you likely want to use `in_directory_fork` otherwise, you might accidentally contaminate the current environment's variables if you modify them.
264
620
 
265
- You can also manually call the `before_deploy` method:
621
+ - `app.in_directory_fork`: Runs the given block in a temp directory and inside of a forked process, an example given above.
622
+ - `app.directory`: Returns the directory of the example application on disk, this is NOT the temp directory that you're currently executing against. It's probably not what you want.
623
+ - `app.deploy`: Your main method takes a block to execute after the deploy is successful. If no block is provided, you must manually call `app.teardown!` (see below for an example).
624
+ - `app.output`: The output contents of the deploy
625
+ - `app.platform_api`: Returns an instance of the [platform-api Heroku client](https://github.com/heroku/platform-api). If Hatchet doesn't give you access to a part of Heroku that you need, you can likely do it with the platform-api client.
626
+ - `app.push!`: Push code to your Heroku app. It can be used inside of a `deploy` block to re-deploy.
627
+ - `app.run_ci`: Runs Heroku CI against the app returns a TestRun object in the block
628
+ - `app.teardown!`: This method is called automatically when using `app.deploy` in block mode after the deploy block finishes. When called it will clean up resources, mark the app as being finished (by setting `{"maintenance" => true}` on the app) so that the reaper knows it is safe to delete later. Here is an example of a test that creates and deploys an app manually, then later tears it down manually. If you deploy an application without calling `teardown!` then Hatchet will not know it is safe to delete and may keep it around for much longer than required for the test to finish.
266
629
 
267
630
  ```ruby
268
- app = Hatchet::App.new("default_ruby")
269
- app.before_deploy do
270
- FileUtils.touch("foo.txt")
631
+ before(:each) do
632
+ @app = Hatchet::Runner.new("default_ruby")
633
+ @app.deploy
271
634
  end
272
- app.deploy do
273
- # Assert stuff
635
+
636
+ after(:each) do
637
+ @app.teardown! if @app
638
+ end
639
+
640
+ it "uses ruby" do
641
+ expect(@app.run("ruby -v")).to match("ruby")
274
642
  end
275
643
  ```
276
644
 
277
- Note: If you're going to shell out in this `before_deploy` section, you should check the success of your command, for example:
645
+ - `test_run.run_again`: Runs the app again in Heroku CI
646
+ - `test_run.status`: Returns the status of the CI run (possible values are `:pending`, `:building`, `:creating`, `:succeeded`, `:failed`, `:errored`)
647
+ - `test_run.output`: The output of a given test run
648
+
649
+ ### ENV vars
650
+
651
+ ```sh
652
+ HATCHET_BUILDPACK_BASE=https://github.com/heroku/heroku-buildpack-nodejs.git
653
+ HATCHET_BUILDPACK_BRANCH=<branch name if you dont want Hatchet to set it for you>
654
+ HATCHET_RETRIES=2
655
+ HATCHET_APP_LIMIT=(set to something low like 20 locally, set higher like 80-100 on CI)
656
+ HEROKU_API_KEY=<redacted>
657
+ HEROKU_API_USER=<redacted@redacted.com>
658
+ HATCHET_ALIVE_TTL_MINUTES=7
659
+
660
+ # HATCHET_RUN_MULTI=1 # WARNING: Setting this env var will incur charges against your account. To use this env var you must also enable `HATCHET_EXPENSIVE_MODE`
661
+ # HATCHET_EXPENSIVE_MODE=1 # WARNING: Do not set this environment variable unless you're okay with possibly large bills
662
+ ```
663
+
664
+ > The syntax to set an env var in Ruby is `ENV["HATCHET_RETRIES"] = "2"` all env vars are strings.
665
+
666
+ - `HATCHET_BUILDPACK_BASE`: This is the URL where Hatchet can find your buildpack. It must be public for Heroku to be able to use your buildpack.
667
+ - `HATCHET_BUILDPACK_BRANCH`: By default, Hatchet will use your current git branch name. If, for some reason, git is not available or you want to manually specify it like `ENV["HATCHET_BUILDPACK_BRANCH'] = ENV[`MY_CI_BRANCH`]` then you can.
668
+ - `HATCHET_RETRIES` If the `ENV['HATCHET_RETRIES']` is set to a number, deploys are expected to work and automatically retry that number of times. Due to testing using a network and random 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. The downside of a larger number is that your suite will keep running for much longer when there are legitimate failures.
669
+ - `HATCHET_APP_LIMIT`: The maximum number of **hatchet** apps that Hatchet will allow in the given account before running the reaper. For local execution, keep this low as you don't want your account dominated by hatchet apps. For CI, you want it to be much larger, 80-100 since it's not competing with non-hatchet apps. Your test runner account needs to be a dedicated account.
670
+ - `HEROKU_API_KEY`: The API key of your test account user. If you run locally without this set, it will use your personal credentials.
671
+ - `HEROKU_API_USER`: The email address of your user account. If you run locally without this set, it will use your personal credentials.
672
+ - `HATCHET_RUN_MULTI`: If enabled, this will scale up deployed apps to "standard-1x" once deployed instead of running on the free tier. This enables the `run_multi` method capability, however scaling up is not free. WARNING: Setting this env var will incur charges to your Heroku account. We recommended never to enable this setting unless you work for Heroku. To use this you must also set `HATCHET_EXPENSIVE_MODE=1`
673
+ - `HATCHET_EXPENSIVE_MODE`: This is intended to be a "safety" environment variable. If it is not set, Hatchet will prevent you from using the `run_multi: true` setting or the `HATCHET_RUN_MULTI` environment variables. There are still ways to incur charges without this feature, but unless you're absolutely confident your test setup will not leave "orphan" apps that are billing you, do not enable this setting. Even then, only set this value if you work for Heroku. To recap WARNING: setting this is expensive.
674
+
675
+ ## Basic
676
+
677
+ ### Basic rspec
678
+
679
+ Hatchet needs to run inside of a test framework such as minitest or rspec. Here's an example of some existing test suites that use Hatchet: [This project](https://github.com/heroku/hatchet) uses rspec to run it's own tests you can use these as a reference as well as the [heroku-ruby-buildpack](https://github.com/heroku/heroku-buildpack-ruby). If you're new to Ruby, testing, or Hatchet, it is recommended to reference other project's tests heavily. If you can't pick between minitest and rspec, go with rspec since that's what most reference tests use.
680
+
681
+ Whatever testing framework you chose, we recommend using a parallel test runner when running the full suite. [parallel_split_test](https://github.com/grosser/parallel_split_test).
682
+
683
+ **rspec plugins** - Rspec has useful plugins, such as `gem 'rspec-retry'` which will re-run any failed tests a given number of times (I recommend setting this to at least 2) to decrease false negatives in your tests when running on CI.
684
+
685
+ Rspec is a testing framework for Ruby. It allows you to "describe" your tests using strings and blocks. This section is intended to be a brief introduction and includes a few pitfalls but is not comprehensive.
686
+
687
+ In your directory rspec assumes a `spec/` folder. It's common to have a `spec_helper.rb` in the root of that folder:
688
+
689
+ - **spec/spec_helper.rb**
690
+
691
+ Here's an example of a `spec_helper.rb`: https://github.com/sharpstone/force_absolute_paths_buildpack/blob/master/spec/spec_helper.rb
692
+
693
+ In this file, you'll require files you need to set up the project. You can also set environment variables like `ENV["HATCHET_BUILDPACK_BASE"]`. You can use it to configure your app. Any methods you define in this file will be available to your tests. For example:
278
694
 
279
695
  ```ruby
280
- before_deploy = Proc.new do
281
- cmd = "bundle update"
282
- output = `#{cmd}`
283
- raise "Command #{cmd.inspect} failed unexpectedly with output: #{output}"
696
+ def run!(cmd)
697
+ out = `#{cmd}`
698
+ raise "Error running #{cmd}, output: #{out}" unless $?.success?
699
+ out
284
700
  end
285
- Hatchet::App.new("default_ruby", before_deploy: before_deploy).deploy do
286
- # Assert stuff
701
+ ```
702
+
703
+ - **spec/hatchet/buildpack_spec.rb**
704
+
705
+ Rspec knows a file is a test file or not by the name. It looks for files that end in `spec.rb` you can have as many as you want. I recommend putting them in a "spec/hatchet" sub-folder.
706
+
707
+ - **File contents**
708
+
709
+ In rspec you can group several tests under a "description" using `Rspec.describe`. Here's an example: https://github.com/sharpstone/force_absolute_paths_buildpack/blob/master/spec/hatchet/buildpack_spec.rb
710
+
711
+ An empty example of `spec/hatchet/buildpack_spec.rb` would look like this:
712
+
713
+ ```ruby
714
+ require_relative "../spec_helper.rb"
715
+
716
+ RSpec.describe "This buildpack" do
717
+ it "accepts absolute paths at build and runtime" do
718
+ # expect(true).to eq(true)
719
+ end
287
720
  end
288
721
  ```
289
722
 
290
- It's helpful to make a helper function in your library if this pattern happens a lot in your app.
723
+ Each `it` block represents a test case. If you ever get an error about no method `expect` it might be that you've forgotten to put your test case inside of a "describe" block.
291
724
 
292
- ## Heroku CI
725
+ - **expect syntax**
293
726
 
294
- Hatchet supports testing Heroku CI.
727
+ Once inside of a test, you can assert an expected value against an actual value:
295
728
 
296
729
  ```ruby
297
- Hatchet::Runner.new("rails5_ruby_schema_format").run_ci do |test_run|
298
- assert_match "Ruby buildpack tests completed successfully", test_run.output
730
+ value = true
731
+ expect(value).to eq(true)
732
+ ```
299
733
 
300
- test_run.run_again # Runs tests again, for example to make sure the cache was used
734
+ This might look like a weird syntax, but it's valid ruby. It's shorthand for this:
301
735
 
302
- assert_match "Using rake", test_run.output
736
+ ```ruby
737
+ expect(value).to(eq(true))
738
+ ```
739
+
740
+ Where `eq` is a method.
741
+
742
+ If you want to assert the opposite, you can use `to_not`:
743
+
744
+ ```ruby
745
+ expect(value).to_not eq(false)
746
+ ```
747
+
748
+ - **matcher syntax**
749
+
750
+ In the above example, the `eq` is called a "matcher". You're matching it against an object. In this case, you're looking for equality `==`.
751
+
752
+ There are other matchers: https://relishapp.com/rspec/rspec-expectations/v/3-2/docs/built-in-matchers
753
+
754
+ ```ruby
755
+ expect(value).to be_truthy
756
+
757
+ value = "hello there"
758
+ expect(value).to include("there")
759
+ ```
760
+
761
+ Rspec uses some "magic" to convert anything you pass to
762
+
763
+ Since most values in Hatchet are strings, the ones I use the most are:
764
+
765
+ - Rspec matchers
766
+ - include https://relishapp.com/rspec/rspec-expectations/v/3-2/docs/built-in-matchers/include-matcher#string-usage
767
+ - match https://relishapp.com/rspec/rspec-expectations/v/3-2/docs/built-in-matchers/match-matcher
768
+
769
+ Generally, I use the include when I know the exact value I want to assert against, I use match when there are dynamic values, and I want to be able to use a regular expression.
770
+
771
+ For building regular expressions, I like to use the tool https://rubular.com/ for developing and testing regular expressions. Ruby's regular expression engine is mighty.
772
+
773
+
774
+ - **Keep it simple**
775
+
776
+ Rspec is a massive library with a host of features. It's possible to quickly make your tests unmaintainable and unreadable in the efforts to keep your code [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself). I recommend sticking to only the features mentioned here at first before trying to do anything fancy.
777
+
778
+ - **What to test**
779
+
780
+ Here's a PR with a description of several standard failure modes that lots of buildpacks should be aware of and reference implementations:
781
+
782
+ https://github.com/heroku/heroku-buildpack-python/pull/969
783
+
784
+ - **before(:all) gotcha**
785
+
786
+ In rspec you can use `before` blocks to execute before a test, and `after` blocks to execute after a test. This might sound like you can deploy a hatchet app once and then write multiple tests against that app. However if `before(:all)` can be executed N times if you're running via parallel processes. Example:
787
+
788
+ ```ruby
789
+ # Warning running `before(:all)` in a multi-process test runner context likely executes your
790
+ # block N times where N is the number of tests in that context: https://github.com/grosser/parallel_split_test/pull/22/files
791
+ before(:all) do
792
+ @app = Hatchet::Runner.new("default_ruby") # Warning: This is a gotcha
793
+ @app.deploy
794
+ end
795
+
796
+ after(:all) do
797
+ @app.teardown! if @app # Warning: This is a gotcha
798
+ end
799
+
800
+ it "tests app somehow" do
801
+ expect(@app.run("ruby -v")).to match("ruby") # Warning: This is a gotcha
802
+ end
803
+
804
+
805
+ it "tests app somehow 2" do
806
+ expect(@app.run("ls")).to match("Gemfile") # Warning: This is a gotcha
303
807
  end
304
808
  ```
305
809
 
306
- Call the `run_ci` method on the hatchet `Runner`. The object passed to the block is a `Hatchet::TestRun` object. You can call:
810
+ Running this via the parallel_split_test gem will cause the `before(:all)` block to be invoked multiple times:
307
811
 
308
- - `test_run.output` will have the setup and test output of your tests.
309
- - `test_run.app` has a reference to the "app" you're testing against, however currently no `heroku create` is run (as it's not needed to run tests, only a pipeline and a blob of code).
812
+ ```
813
+ $ PARALLEL_SPLIT_TEST_PROCESSES=3 bundle exec parallel_split_test spec/
814
+ Hatchet setup: "hatchet-t-af7dffc006"
815
+ Hatchet setup: "hatchet-t-bf7dffc006"
816
+ ```
310
817
 
311
- An exception will be raised if either the test times out or a status of `:errored` or `:failed` is returned. If you expect your test to fail, you can pass in `allow_failure: true` when creating your hatchet runner. If you do that, you'll also get access to different statuses:
818
+ It would result in 2 apps being deployed. You can find more information [on the documentation](https://github.com/grosser/parallel_split_test#beforeall-rspec-hooks). For clarity of what will happen behind the scenes when running with multiple processes, it's recommended to use `before(:each)` instead of `before(:all)`.
312
819
 
313
- - `test_run.status` will return a symbol of the status of your test. Statuses include, but are not limited to `:pending`, `:building`, `:errored`, `:creating`, `:succeeded`, and `:failed`
820
+ ### Basic Ruby
314
821
 
315
- You can pass in a different timeout to the `run_ci` method `run_ci(timeout: 300)`.
822
+ If you're not a Ruby specialist, not to worry. Here are a few things you might want to do:
316
823
 
317
- You probably need an `app.json` in the root directory of the app you're deploying. For example:
824
+ - **Write a file and manipulate disk**
318
825
 
319
- ```json
320
- {
321
- "environments": {
322
- "test": {
323
- "addons":[
324
- "heroku-postgresql"
325
- ]
326
- }
327
- }
328
- }
826
+ ```ruby
827
+ File.open("facts.txt", "w+") do |f|
828
+ f.write("equal does not mean equitable")
829
+ end
329
830
  ```
330
831
 
331
- This is on [a Rails5 test app](https://github.com/sharpstone/rails5_ruby_schema_format/blob/master/app.json) that needs the database to run.
832
+ The first argument is the file name, and the second is the object "mode", here `"w+"` means open for writing and create the file if it doesn't exist. If you want to append to a file instead you can use the mode `"a"`.
332
833
 
333
- Do **NOT** specify a `buildpacks` key in the `app.json` because Hatchet will automatically do this for you. If you need to set buildpacks you can pass them into the `buildpacks:` keword argument:
834
+ The file name can be a relative or absolute path. My personal favorite though is using the Pathname class to represent files on disk [ruby Pathname api docs](https://ruby-doc.org/stdlib-2.7.1/libdoc/pathname/rdoc/Pathname.html). You can also use a pathname object to write and manipulate the disk directly:
334
835
 
836
+ ```ruby
837
+ require 'pathname'
838
+ Pathname.new("facts.txt").write("equal does not mean equitable")
335
839
  ```
336
- buildpacks = []
337
- buildpacks << "https://github.com/heroku/heroku-buildpack-pgbouncer.git"
338
- buildpacks << [HATCHET_BUILDPACK_BASE, HATCHET_BUILDPACK_BRANCH.call].join("#")
339
840
 
340
- Hatchet::Runner.new("rails5_ruby_schema_format", buildpacks: buildpacks).run_ci do |test_run|
341
- # ...
841
+ - API docs:
842
+ - [File](https://ruby-doc.org/core-2.7.0/File.html)
843
+ - [FileUtils](https://ruby-doc.org/stdlib-2.7.1/libdoc/fileutils/rdoc/FileUtils.html)
844
+ - [Pathname](https://ruby-doc.org/stdlib-2.7.1/libdoc/pathname/rdoc/Pathname.html)
845
+ - [Dir](https://ruby-doc.org/core-2.7.1/Dir.html)
846
+
847
+ - **HEREDOC**
848
+
849
+ You can define a multi-line string in Ruby using `<<~EOM` with a closing `EOM`. Technically, `EOM` can be any string, but you're not here for technicalities.
850
+
851
+ ```ruby
852
+ File.open("bin/yarn", "w") do |f|
853
+ f.write <<~EOM
854
+ #! /usr/bin/env bash
855
+
856
+ echo "Called bin/yarn binstub"
857
+ `yarn install`
858
+ EOM
342
859
  end
343
860
  ```
344
861
 
345
- ## Testing on CI
862
+ This version of heredoc will strip out indentation:
863
+
864
+ ```ruby
865
+ puts <<~EOM
866
+ # Notice that the spaces are stripped out of the front of this string
867
+ EOM
868
+ # => "# Notice that the spaces are stripped out of the front of this string"
869
+ ```
870
+
871
+ The `~` Is usually the operator for a heredoc that you want, it's supported in Ruby 2.5+.
346
872
 
347
- Once you've got your tests working locally, you'll likely want to get them running on CI. For reference see the [Circle CI config from this repo](https://github.com/heroku/hatchet/blob/master/.circleci/config.yml) and the [Heroku CI config from the ruby buildpack](https://github.com/heroku/heroku-buildpack-ruby/blob/master/app.json).
873
+ - **Hashes**
348
874
 
349
- To make running on CI easier, there is a setup script in Hatchet that can be run before your tests are executed:
875
+ A hash is like a dict in python. Docs: https://ruby-doc.org/core-2.7.1/Hash.html
350
876
 
351
- ```yml
352
- bundle exec hatchet ci:setup
877
+ ```ruby
878
+ person_hash = { "name" => "schneems", "level" => 6 }
879
+ puts person_hash["name"]
880
+ # => "schneems"
353
881
  ```
354
882
 
355
- If you're a Heroku employee see [private instructions for setting up test users](https://github.com/heroku/languages-team/blob/master/guides/create_test_users_for_ci.md) to generate a user a grab the API token. Once you have an API token you'll want to set up these env vars with your CI provider:
883
+ You can also mutate a hash:
356
884
 
885
+ ```ruby
886
+ person_hash = { "name" => "schneems", "level" => 6 }
887
+ person_hash["name"] = "Richard"
888
+ puts person_hash["name"]
889
+ # => "Richard"
357
890
  ```
358
- HATCHET_APP_LIMIT=100
359
- HATCHET_RETRIES=3
360
- HEROKU_API_KEY=<redacted>
361
- HEROKU_API_USER=<redacted@example.com>
891
+
892
+ You can inspect full objects by calling `inspect` on them:
893
+
894
+ ```ruby
895
+ puts person_hash.inspect
896
+ # => {"name"=>"schneems", "level"=>6}
362
897
  ```
363
898
 
364
- ## Extra App Commands
899
+ As an implementation detail note that hashes are ordered
900
+
901
+ - **ENV**
365
902
 
903
+ You can access the current processes' environment variables as a hash using the ENV object:
904
+
905
+ ```ruby
906
+ ENV["MY_CUSTOM_ENV_VAR"] = "blm"
907
+ puts `echo $MY_CUSTOM_ENV_VAR`.upcase
908
+ # => BLM
366
909
  ```
367
- app.add_database # adds a database to specified app
368
- app.heroku # returns a Herou Api client https://github.com/heroku/heroku.rb
910
+
911
+ All values in an env var must be a string. See the Hash docs for more information on manipulating hashes https://ruby-doc.org/core-2.7.1/Hash.html. Also see the current ENV docs https://ruby-doc.org/core-2.7.1/ENV.html.
912
+
913
+ - **Strings versus symbols**
914
+
915
+ In Ruby you can have a define a symbol `:thing` as well as a `"string"`. They look and behave very closely but are different. A symbol is a singleton object, while the string is unique object. One really confusing thing is you can have a hash with both string and symbol keys:
916
+
917
+ ```ruby
918
+ my_hash = {}
919
+ my_hash["dog"] = "cinco"
920
+ my_hash[:dog] = "river"
921
+ puts my_hash.inspect
922
+ # => {"dog"=>"cinco", :dog=>"river"}
369
923
  ```
370
924
 
925
+ - **Blocks, procs, and lambdas**
926
+
927
+ Blocks are a concept in Ruby for closure. Depending on how it's used it can be an anonymous method. It's always a method for passing around code. When you see `do |app|` that's the beginning of an implicit block. In addition to an implicit block you can create an explicit block using lambdas and procs. In Hatchet, these are most likely to be used to update the app `before_deploy`. Here's an example of some syntax for creating various blocks.
928
+
929
+ ```ruby
930
+ before_deploy = -> { FileUtils.touch("foo.txt") } # This syntax is called a "stabby lambda"
931
+ before_deploy = lambda { FileUtils.touch("foo.txt") } # This is a more verbose lambda
932
+ before_deploy = lambda do
933
+ FileUtils.touch("foo.txt") # Multi-line lambda
934
+ end
935
+ before_deploy = Proc.new { FileUtils.touch("foo.txt") } # A proc and lambda are subtly different, it mostly won't matter to you though
936
+ before_deploy = Proc.new do
937
+ FileUtils.touch("foo.txt") # Multi-line proc
938
+ end
939
+ ```
940
+
941
+ All of these things do the same thing more-or-less. You can execute a block/proc/lambda by running:
942
+
943
+ ```ruby
944
+ before_deploy.call
945
+ ```
946
+
947
+ - **Parens**
948
+
949
+ You might have noticed that some ruby methods use parens and some don't. I.e. `puts "yo"` versus `puts("yo")`. If the parser can determine your intent then you don't have to use parens.
950
+
951
+ - **Debugging**
952
+
953
+ If you're not used to debugging Ruby you can reference the [Ruby debugging magic cheat sheet](https://www.schneems.com/2016/01/25/ruby-debugging-magic-cheat-sheet.html). The Ruby language is very powerful in it's ability to [reflect on itself](https://en.wikipedia.org/wiki/Reflection_%28computer_programming%29). Essentially the Ruby code is able to introspect itself to tell you what it's doing. If you're ever lost, ask your ruby code. It might confuse you, but it won't lie to you.
954
+
955
+ Another good debugging tool is the [Pry debugger and repl](https://github.com/pry/pry).
956
+
957
+ - **Common Ruby errors**
958
+
959
+ ```
960
+ SyntaxError ((irb):14: syntax error, unexpected `end')
961
+ ```
962
+
963
+ If you see this, it likely means you forgot a `do` on a block, for example `.deploy |app|` instead of `.deploy do |app|`.
964
+
965
+ ```
966
+ NoMethodError (undefined method `upcase' for nil:NilClass)
967
+ ```
968
+
969
+ If you see this it means a variable you're using is `nil` unexpectedly. You'll need to use the [above debugging techniques](https://www.schneems.com/2016/01/25/ruby-debugging-magic-cheat-sheet.html) to figure out why.
970
+
971
+ - **More**
972
+
973
+ Ruby is full of multitudes, this isn't even close to being exhaustive, just enough to make you dangerous and write a few tests. It's infinitely useful for testing, writing CLIs and web apps.
974
+
371
975
  ## Hatchet CLI
372
976
 
373
977
  Hatchet has a CLI for installing and maintaining external repos you're
374
978
  using to test against. If you have Hatchet installed as a gem run
375
979
 
376
- $ hatchet --help
980
+ $ Hatchet --help
377
981
 
378
982
  For more info on commands. If you're using the source code you can run
379
983
  the command by going to the source code directory and running:
@@ -381,8 +985,30 @@ the command by going to the source code directory and running:
381
985
  $ ./bin/hatchet --help
382
986
 
383
987
 
384
- ## License
988
+ ## Developing Hatchet
385
989
 
386
- MIT
990
+ If you want to add a feature to Hatchet (this library) you'll need to install it locally and be able to run the tests:
991
+
992
+
993
+ ## Install locally
387
994
 
995
+ ```
996
+ $ git clone https://github.com/heroku/hatchet
997
+ $ cd hatchet
998
+ $ bundle install
999
+ ```
388
1000
 
1001
+ ### Run the Tests
1002
+
1003
+ ```
1004
+ $ PARALLEL_SPLIT_TEST_PROCESSES=10 bundle exec parallel_split_test spec/
1005
+ ```
1006
+ This will execute all tests, you can also run a single test by specifying a file and line number:
1007
+
1008
+ ```
1009
+ $ bundle exec rspec spec/hatchet/app_spec.rb:4
1010
+ ```
1011
+
1012
+ ## License
1013
+
1014
+ MIT