heroku_hatchet 5.0.3 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +19 -1
  3. data/.gitignore +2 -0
  4. data/CHANGELOG.md +4 -0
  5. data/etc/ci_setup.rb +16 -12
  6. data/etc/setup_heroku.sh +0 -2
  7. data/hatchet.gemspec +4 -5
  8. data/hatchet.json +1 -1
  9. data/hatchet.lock +9 -9
  10. data/lib/hatchet/api_rate_limit.rb +7 -7
  11. data/lib/hatchet/test_run.rb +15 -9
  12. data/lib/hatchet/version.rb +1 -1
  13. data/{test → repo_fixtures}/different-folder-for-checked-in-repos/default_ruby/Gemfile +0 -0
  14. data/spec/hatchet/allow_failure_git_spec.rb +15 -0
  15. data/spec/hatchet/app_spec.rb +87 -0
  16. data/spec/hatchet/ci_spec.rb +58 -0
  17. data/spec/hatchet/config_spec.rb +34 -0
  18. data/spec/hatchet/edit_repo_spec.rb +17 -0
  19. data/spec/hatchet/git_spec.rb +9 -0
  20. data/spec/hatchet/heroku_api_spec.rb +30 -0
  21. data/spec/hatchet/local_repo_spec.rb +26 -0
  22. data/spec/hatchet/lock_spec.rb +19 -0
  23. data/spec/spec_helper.rb +25 -0
  24. metadata +35 -71
  25. data/test/fixtures/buildpacks/null-buildpack/bin/compile +0 -4
  26. data/test/fixtures/buildpacks/null-buildpack/bin/detect +0 -5
  27. data/test/fixtures/buildpacks/null-buildpack/bin/release +0 -3
  28. data/test/fixtures/buildpacks/null-buildpack/hatchet.json +0 -4
  29. data/test/fixtures/buildpacks/null-buildpack/readme.md +0 -41
  30. data/test/hatchet/allow_failure_git_test.rb +0 -16
  31. data/test/hatchet/app_test.rb +0 -96
  32. data/test/hatchet/ci_four_test.rb +0 -19
  33. data/test/hatchet/ci_test.rb +0 -11
  34. data/test/hatchet/ci_three_test.rb +0 -20
  35. data/test/hatchet/ci_too_test.rb +0 -19
  36. data/test/hatchet/config_test.rb +0 -51
  37. data/test/hatchet/edit_repo_test.rb +0 -20
  38. data/test/hatchet/git_test.rb +0 -16
  39. data/test/hatchet/heroku_api_test.rb +0 -30
  40. data/test/hatchet/labs_test.rb +0 -20
  41. data/test/hatchet/local_repo_test.rb +0 -26
  42. data/test/hatchet/lock_test.rb +0 -18
  43. data/test/hatchet/multi_cmd_runner_test.rb +0 -30
  44. data/test/test_helper.rb +0 -28
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aaba14f0335f61010579c2a28cebb4ddc3745673cd2bf9e60a79b9b77772fba1
4
- data.tar.gz: 46d867946f075054f67d2a5a1262ef203cfe20e19f6f025b2761f76a65b5b886
3
+ metadata.gz: ce2fc5243218285fa0505cf41a9fc51d1f7cdfd3cb69ff57e83c11be6137f57e
4
+ data.tar.gz: f61248e6cfd590ce04ca68b0a5233336a56bb3b1f135580eb10ff5016b59b36b
5
5
  SHA512:
6
- metadata.gz: c241f9a5422e4fba52afa9bb86c2e593452ab021bf111707deea9f6a132d7b4ea3afcba69b95575b82628838e5433e502ba9f64cf1cda8beaa8c764eb56dbd29
7
- data.tar.gz: b65f318865923df9da5aeb17e86bb39a1633b787b0497a5d991361650d816925accb986a2c0b015d6b67c949fc0cf67408a83373810a46328c9c353e0203af8e
6
+ metadata.gz: 269805f1a98ccf64e772e66a503ad9e0558635eca7a7c04b12567b1335f059b7dfa17e4e704b8fca1c088d38672eafce1bd8f290c564a62d44911059f5eb7700
7
+ data.tar.gz: f0d1d0a640b5de56214acd122aaa918e3038ecc65ceef526c86c44585cd5ad253061f05252c40154c7edac57aa84d038a8216843198373c7a9b1d67b1662fb12
@@ -3,7 +3,16 @@ 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
+ restore: &restore
8
+ restore_cache:
9
+ keys:
10
+ - v1_bundler_deps-{{ .Environment.CIRCLE_JOB }}
11
+ save: &save
12
+ save_cache:
13
+ paths:
14
+ - ./vendor/bundle
15
+ key: v1_bundler_deps-{{ .Environment.CIRCLE_JOB }} # CIRCLE_JOB e.g. "ruby-2.5"
7
16
  hatchet_setup: &hatchet_setup
8
17
  run:
9
18
  name: Hatchet setup
@@ -14,31 +23,40 @@ references:
14
23
  name: install dependencies
15
24
  command: |
16
25
  bundle install --jobs=4 --retry=3 --path vendor/bundle
26
+ bundle update
27
+ bundle clean
28
+
17
29
  jobs:
18
30
  "ruby-2.5":
19
31
  docker:
20
32
  - image: circleci/ruby:2.5
21
33
  steps:
22
34
  - checkout
35
+ - <<: *restore
23
36
  - <<: *bundle
24
37
  - <<: *hatchet_setup
25
38
  - <<: *unit
39
+ - <<: *save
26
40
  "ruby-2.6":
27
41
  docker:
28
42
  - image: circleci/ruby:2.6
29
43
  steps:
30
44
  - checkout
45
+ - <<: *restore
31
46
  - <<: *bundle
32
47
  - <<: *hatchet_setup
33
48
  - <<: *unit
49
+ - <<: *save
34
50
  "ruby-2.7":
35
51
  docker:
36
52
  - image: circleci/ruby:2.7
37
53
  steps:
38
54
  - checkout
55
+ - <<: *restore
39
56
  - <<: *bundle
40
57
  - <<: *hatchet_setup
41
58
  - <<: *unit
59
+ - <<: *save
42
60
 
43
61
  workflows:
44
62
  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,9 @@
1
1
  ## HEAD
2
2
 
3
+ ## 6.0.0
4
+
5
+ - Rate throttling is now provided directly by `platform-api` (https://github.com/heroku/hatchet/pull/82)
6
+
3
7
  ## 5.0.3
4
8
 
5
9
  - Allow repos to be "locked" to master instead of a specific commit (https://github.com/heroku/hatchet/pull/80)
@@ -1,6 +1,14 @@
1
1
  #!/usr/bin/env ruby
2
- require 'bundler'
3
2
  require 'shellwords'
3
+
4
+ STDOUT.sync = true
5
+
6
+ def run_cmd(command)
7
+ puts "== Running: #{command}"
8
+ result = `#{command}`
9
+ raise "Command failed: #{command.inspect}\nResult: #{result}" unless $?.success?
10
+ end
11
+
4
12
  puts "== Setting Up CI =="
5
13
 
6
14
  netrc_file = "#{ENV['HOME']}/.netrc"
@@ -14,18 +22,14 @@ machine api.heroku.com
14
22
  login #{ENV.fetch('HEROKU_API_USER')}
15
23
  password #{ENV.fetch('HEROKU_API_KEY')}
16
24
  EOF
17
- `chmod 0600 "$HOME/.netrc"`
25
+ run_cmd 'chmod 0600 "$HOME/.netrc"'
18
26
  end
19
27
  end
20
28
 
21
- [
22
- "bundle exec hatchet ci:install_heroku",
23
- "bundle exec hatchet install",
24
- "git config --get user.email > /dev/null || git config --global user.email #{ENV.fetch('HEROKU_API_USER').shellescape}",
25
- "git config --get user.name > /dev/null || git config --global user.name 'BuildpackTester'",
26
- ].each do |command|
27
- puts "== Running: #{command}"
28
- result = `#{command}`
29
- raise "Command failed: #{command.inspect}\nResult: #{result}" unless $?.success?
30
- end
29
+ run_cmd "bundle exec hatchet ci:install_heroku"
30
+ run_cmd "bundle exec hatchet install"
31
+ run_cmd "git config --get user.email > /dev/null || git config --global user.email #{ENV.fetch('HEROKU_API_USER').shellescape}"
32
+ run_cmd "git config --get user.name > /dev/null || git config --global user.name 'BuildpackTester'"
33
+
31
34
  puts "== Done =="
35
+
@@ -1,6 +1,4 @@
1
1
  #!/usr/bin/env bash
2
2
 
3
3
  set -euo pipefail
4
- sudo apt-get -qq update
5
- sudo apt-get install software-properties-common
6
4
  curl --fail --retry 3 --retry-delay 1 --connect-timeout 3 --max-time 30 https://cli-assets.heroku.com/install-ubuntu.sh | sh
@@ -18,18 +18,17 @@ Gem::Specification.new do |gem|
18
18
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
19
  gem.require_paths = ["lib"]
20
20
 
21
- gem.add_dependency "platform-api", "~> 2"
21
+ gem.add_dependency "platform-api", "3.0.0.pre.1 "
22
22
  gem.add_dependency "rrrretry", "~> 1"
23
23
  gem.add_dependency "excon", "~> 0"
24
24
  gem.add_dependency "thor", "~> 0"
25
25
  gem.add_dependency "repl_runner", "~> 0.0.3"
26
26
  gem.add_dependency "threaded", "~> 0"
27
- gem.add_dependency 'minitest-retry', '~> 0.1.9'
28
27
 
29
- gem.add_development_dependency "minitest", ">= 5.1"
28
+ gem.add_development_dependency "rspec"
30
29
  gem.add_development_dependency "rake", ">= 10"
31
30
  gem.add_development_dependency "mocha", ">= 1"
32
- gem.add_development_dependency "parallel_tests", ">= 2"
31
+ gem.add_development_dependency "parallel_split_test"
33
32
  gem.add_development_dependency "travis", ">= 1"
34
- gem.add_development_dependency "m"
33
+ gem.add_development_dependency "rspec-retry"
35
34
  end
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "hatchet": {
3
- "directory": "test/fixtures"
3
+ "directory": "repo_fixtures"
4
4
  },
5
5
  "rails3": ["sharpstone/rails3_mri_193"],
6
6
  "rails2": ["sharpstone/rails2blog"],
@@ -1,17 +1,17 @@
1
1
  ---
2
- - - test/fixtures/repos/bundler/no_lockfile
2
+ - - repo_fixtures/repos/bundler/no_lockfile
3
3
  - 1947ce9a9c276d5df1c323b2ad78d1d85c7ab4c0
4
- - - test/fixtures/repos/ci/rails5_ci_fails_no_database
4
+ - - repo_fixtures/repos/ci/rails5_ci_fails_no_database
5
5
  - 3044f05febdfbbe656f0f5113cf5968ca07e34fd
6
- - - test/fixtures/repos/ci/rails5_ruby_schema_format
7
- - 3e63c3e13f435cf4ab11265e9abd161cc28cc552
8
- - - test/fixtures/repos/default/default_ruby
6
+ - - repo_fixtures/repos/ci/rails5_ruby_schema_format
7
+ - d76be86c66ae8f45ec611fb2c4d8eb3adac0ad4b
8
+ - - repo_fixtures/repos/default/default_ruby
9
9
  - 6e642963acec0ff64af51bd6fba8db3c4176ed6e
10
- - - test/fixtures/repos/lock/lock_fail
10
+ - - repo_fixtures/repos/lock/lock_fail
11
11
  - da748a59340be8b950e7bbbfb32077eb67d70c3c
12
- - - test/fixtures/repos/lock/lock_fail_master
12
+ - - repo_fixtures/repos/lock/lock_fail_master
13
13
  - master
14
- - - test/fixtures/repos/rails2/rails2blog
14
+ - - repo_fixtures/repos/rails2/rails2blog
15
15
  - b37357a498ae5e8429f5601c5ab9524021dc2aaa
16
- - - test/fixtures/repos/rails3/rails3_mri_193
16
+ - - repo_fixtures/repos/rails3/rails3_mri_193
17
17
  - 88c5d0d067cfd11e4452633994a85b04627ae8c7
@@ -25,15 +25,15 @@ class ApiRateLimit
25
25
  # to partially mitigate this, only check capacity every 5
26
26
  # api calls, or if the current capacity is under 1000
27
27
  def call
28
- @called += 1
28
+ # @called += 1
29
29
 
30
- if @called > 5 || @capacity < 1000
31
- @called = 0
32
- @capacity = @platform_api.rate_limit.info["remaining"]
33
- end
30
+ # if @called > 5 || @capacity < 1000
31
+ # @called = 0
32
+ # @capacity = @platform_api.rate_limit.info["remaining"]
33
+ # end
34
34
 
35
- sleep_time = (60/@capacity) if @capacity > 0.1 # no divide by zero
36
- sleep(sleep_time || 60)
35
+ # sleep_time = (60/@capacity) if @capacity > 0.1 # no divide by zero
36
+ # sleep(sleep_time || 60)
37
37
 
38
38
  return @platform_api
39
39
  end
@@ -181,10 +181,11 @@ module Hatchet
181
181
 
182
182
  source_put_url = @app.create_source
183
183
  Hatchet::RETRIES.times.retry do
184
- @api_rate_limit.call
185
- Excon.put(source_put_url,
186
- expects: [200],
187
- body: File.read('slug.tgz'))
184
+ PlatformAPI.rate_throttle.call do
185
+ Excon.put(source_put_url,
186
+ expects: [200],
187
+ body: File.read('slug.tgz'))
188
+ end
188
189
  end
189
190
  end
190
191
  return @app.source_get_url
@@ -192,8 +193,11 @@ module Hatchet
192
193
 
193
194
  private
194
195
  def get_contents_or_whatever(url)
195
- @api_rate_limit.call
196
- Excon.get(url, read_timeout: @pause).body
196
+ response = PlatformAPI.rate_throttle.call do
197
+ Excon.get(url, read_timeout: @pause)
198
+ end
199
+
200
+ return response.body
197
201
  rescue Excon::Error::Timeout
198
202
  ""
199
203
  end
@@ -216,9 +220,11 @@ module Hatchet
216
220
  options[:body] = JSON.generate(options[:body]) if options[:body]
217
221
 
218
222
  Hatchet::RETRIES.times.retry do
219
- @api_rate_limit.call
220
- connection = Excon.new("https://api.heroku.com")
221
- return connection.request(options)
223
+ PlatformAPI.rate_throttle.call do
224
+ connection = Excon.new("https://api.heroku.com")
225
+
226
+ return connection.request(options)
227
+ end
222
228
  end
223
229
  end
224
230
  end
@@ -1,3 +1,3 @@
1
1
  module Hatchet
2
- VERSION = "5.0.3"
2
+ VERSION = "6.0.0"
3
3
  end
@@ -0,0 +1,15 @@
1
+ require("spec_helper")
2
+
3
+ describe "AllowFailureGitTest" do
4
+ it "allowed failure" do
5
+ Hatchet::GitApp.new("no_lockfile", allow_failure: true).deploy do |app|
6
+ puts app.output
7
+ expect(app.deployed?).to be_falsey
8
+ expect(app.output).to match("Gemfile.lock required")
9
+ end
10
+ end
11
+
12
+ it "failure with no flag" do
13
+ expect { Hatchet::GitApp.new("no_lockfile").deploy }.to(raise_error(Hatchet::App::FailedDeploy))
14
+ end
15
+ end
@@ -0,0 +1,87 @@
1
+ require("spec_helper")
2
+
3
+ describe "AppTest" do
4
+ it "app with default" do
5
+ app = Hatchet::App.new("default_ruby", buildpacks: [:default])
6
+ expect(app.buildpacks.first).to match("https://github.com/heroku/heroku-buildpack-ruby")
7
+ end
8
+
9
+ it "create app with stack" do
10
+ stack = "heroku-16"
11
+ app = Hatchet::App.new("default_ruby", stack: stack)
12
+ app.create_app
13
+ expect(app.platform_api.app.info(app.name)["build_stack"]["name"]).to eq(stack)
14
+ end
15
+
16
+ it "before deploy" do
17
+ @called = false
18
+ @dir = false
19
+ app = Hatchet::App.new("default_ruby")
20
+ def app.push_with_retry!
21
+ # do nothing
22
+ end
23
+ app.before_deploy do
24
+ @called = true
25
+ @dir = Dir.pwd
26
+ end
27
+ app.deploy do
28
+ expect(@called).to eq(true)
29
+ expect(@dir).to eq(Dir.pwd)
30
+ end
31
+ expect(@dir).to_not eq(Dir.pwd)
32
+ end
33
+
34
+ it "auto commits code" do
35
+ string = "foo#{SecureRandom.hex}"
36
+ app = Hatchet::App.new("default_ruby")
37
+ def app.push_with_retry!
38
+ # do nothing
39
+ end
40
+ app.before_deploy do |app|
41
+ expect(app.send(:needs_commit?)).to eq(false)
42
+ `echo "#{string}" > Gemfile`
43
+ expect(app.send(:needs_commit?)).to eq(true)
44
+ end
45
+ app.deploy do
46
+ expect(File.read("Gemfile").chomp).to eq(string)
47
+ expect(app.send(:needs_commit?)).to eq(false)
48
+ end
49
+ end
50
+
51
+ it "nested in directory" do
52
+ string = "foo#{SecureRandom.hex}"
53
+ app = Hatchet::App.new("default_ruby")
54
+ def app.push_with_retry!
55
+ # do nothing
56
+ end
57
+ app.in_directory do
58
+ `echo "#{string}" > Gemfile`
59
+ dir = Dir.pwd
60
+ app.deploy do
61
+ expect(File.read("Gemfile").chomp).to eq(string)
62
+ expect(dir).to eq(Dir.pwd)
63
+ end
64
+ end
65
+ end
66
+
67
+ it "run" do
68
+ app = Hatchet::GitApp.new("default_ruby")
69
+ app.deploy do
70
+ expect(app.run("ls -a Gemfile 'foo bar #baz'")).to match(/ls: cannot access 'foo bar #baz': No such file or directory\s+Gemfile/)
71
+ expect((0 != $?.exitstatus)).to be_truthy
72
+ sleep(4)
73
+ app.run("ls erpderp", heroku: ({ "exit-code" => (Hatchet::App::SkipDefaultOption) }))
74
+ expect((0 == $?.exitstatus)).to be_truthy
75
+ sleep(4)
76
+ app.run("ls erpderp", heroku: ({ "no-tty" => nil }))
77
+ expect((0 != $?.exitstatus)).to be_truthy
78
+ sleep(4)
79
+ expect(app.run("echo \\$HELLO \\$NAME", raw: true, heroku: ({ "env" => "HELLO=ohai;NAME=world" }))).to match(/ohai world/)
80
+ sleep(4)
81
+ expect(app.run("echo \\$HELLO \\$NAME", raw: true, heroku: ({ "env" => "" }))).to_not match(/ohai world/)
82
+ sleep(4)
83
+ random_name = SecureRandom.hex
84
+ expect(app.run("mkdir foo; touch foo/#{random_name}; ls foo/")).to match(/#{random_name}/)
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,58 @@
1
+ require "spec_helper"
2
+
3
+ describe "CIFourTest" do
4
+ it "error with bad app" do
5
+ string = SecureRandom.hex
6
+
7
+ Hatchet::GitApp.new("default_ruby").run_ci do |test_run|
8
+ expect(test_run.output).to_not match(string)
9
+ expect(test_run.output).to match("Installing rake")
10
+
11
+ run!("echo 'puts \"#{string}\"' >> Rakefile")
12
+ test_run.run_again
13
+
14
+ expect(test_run.output).to match(string)
15
+ expect(test_run.output).to match("Using rake")
16
+ expect(test_run.output).to_not match("Installing rake")
17
+ end
18
+ end
19
+
20
+ it "error with bad app" do
21
+ expect {
22
+ Hatchet::GitApp.new("rails5_ci_fails_no_database").run_ci { }
23
+ }.to raise_error(/PG::ConnectionBad: could not connect to server/)
24
+ end
25
+
26
+ it "error with bad app" do
27
+ @before_deploy_called = false
28
+ @before_deploy_dir_pwd = nil
29
+
30
+ before_deploy = -> do
31
+ @before_deploy_called = true
32
+ @before_deploy_dir_pwd = Dir.pwd
33
+ end
34
+
35
+ Hatchet::GitApp.new("rails5_ci_fails_no_database", allow_failure: true, before_deploy: before_deploy).run_ci do |test_run|
36
+ expect(test_run.status).to eq(:errored)
37
+ expect(@before_deploy_dir_pwd).to eq(Dir.pwd)
38
+ expect(@before_deploy_called).to be_truthy
39
+ end
40
+
41
+ expect(@before_deploy_dir_pwd).to_not eq(Dir.pwd)
42
+ end
43
+
44
+ it "ci create app with stack" do
45
+ app = Hatchet::GitApp.new("rails5_ruby_schema_format")
46
+ app.run_ci do |test_run|
47
+ expect(test_run.output).to match("Ruby buildpack tests completed successfully")
48
+ expect(test_run.status).to eq(:succeeded)
49
+ expect(app.pipeline_id).to_not be_nil
50
+
51
+ api_rate_limit = app.api_rate_limit.call
52
+ couplings = api_rate_limit.pipeline_coupling.list_by_pipeline(app.pipeline_id)
53
+ coupled_app = api_rate_limit.app.info(couplings.first["app"]["id"])
54
+ expect(coupled_app["name"]).to eq(app.name)
55
+ end
56
+ expect(app.pipeline_id).to be_nil
57
+ end
58
+ end
@@ -0,0 +1,34 @@
1
+ require("spec_helper")
2
+ describe "ConfigTest" do
3
+ before { @config = Hatchet::Config.new }
4
+
5
+ it("config path for name") do
6
+ expect(@config.path_for_name("rails3_mri_193")).to(eq("repo_fixtures/repos/rails3/rails3_mri_193"))
7
+ end
8
+
9
+ it("config dirs") do
10
+ { "repo_fixtures/repos/bundler/no_lockfile" => "https://github.com/sharpstone/no_lockfile.git", "repo_fixtures/repos/default/default_ruby" => "https://github.com/sharpstone/default_ruby.git", "repo_fixtures/repos/rails2/rails2blog" => "https://github.com/sharpstone/rails2blog.git", "repo_fixtures/repos/rails3/rails3_mri_193" => "https://github.com/sharpstone/rails3_mri_193.git" }.each do |key, value|
11
+ assert_include(key, value, @config.dirs)
12
+ end
13
+ end
14
+
15
+ it("config repos") do
16
+ { "default_ruby" => "repo_fixtures/repos/default/default_ruby", "no_lockfile" => "repo_fixtures/repos/bundler/no_lockfile", "rails2blog" => "repo_fixtures/repos/rails2/rails2blog", "rails3_mri_193" => "repo_fixtures/repos/rails3/rails3_mri_193" }.each do |key, value|
17
+ assert_include(key, value, @config.repos)
18
+ end
19
+ end
20
+
21
+ it("no internal config raises no errors") do
22
+ @config.send(:set_internal_config!, {})
23
+ expect(@config.repo_directory_path).to(eq("./repos"))
24
+ end
25
+
26
+ it("github shortcuts") do
27
+ @config.send(:init_config!, "foo" => (["schneems/sextant"]))
28
+ expect(@config.dirs["./repos/foo/sextant"]).to(eq("https://github.com/schneems/sextant.git"))
29
+ end
30
+
31
+ private def assert_include(key, value, actual)
32
+ expect(actual[key]).to eq(value), "Expected #{actual.inspect} to include #{{ key => value }} but it did not"
33
+ end
34
+ end