taste_tester 0.0.12 → 0.0.13

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b43e9bbdc0fa2bccc9c0bca3f4c749d6c0b4f356
4
- data.tar.gz: 941177bd83e37b1830ca0f24add48768261541e9
3
+ metadata.gz: 13e9b1fb00c7044fe801ccb1158bbaacbbe30dd9
4
+ data.tar.gz: 3fcd362ea2bbdfed358847a485a695c9669d44df
5
5
  SHA512:
6
- metadata.gz: 7446d6bef3d9f68860d2747a1a67141dcb3eb9fbbb9751713811afed02da87e192db4b4e473d11433fb908b4eafd14960ef610de052fbd3e8e3c82c5ccb037f2
7
- data.tar.gz: db111fd123f9faff1a37b40f128e2e8a2041b27ad5be234dd62e2cf99bede17218cbbb5450543c1b999f5eed001fce18f1f8150309155e8bf250b208081d2f73
6
+ metadata.gz: f4da6aa14ee5b8c3f732eae1480f9793318b377f1574bf1bb12f2ec8b39c91edd8e8635d9ebd6e9042bf62a767ae55655ee447b0b47bfb83fbba4d1b13832e66
7
+ data.tar.gz: 67db565bdee6e84b0ad23c73183f88af0873ac12a84ae1451c8587038222634e0bf6888ef1e9a30280631586ad7a350087ad7a2f74f249f3a324f208a3a2395a
data/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # Taste Tester
2
2
 
3
- [![Build Status](https://travis-ci.org/facebook/taste-tester.svg)](http://travis-ci.org/facebook/taste-tester)
3
+ [![TravisCI](https://travis-ci.org/facebook/taste-tester.svg)](http://travis-ci.org/facebook/taste-tester)
4
+ [![CircleCI](https://circleci.com/gh/facebook/taste-tester.svg?style=svg)](https://circleci.com/gh/facebook/taste-tester)
4
5
 
5
6
  ## Intro
6
7
  Ohai!
data/bin/taste-tester CHANGED
@@ -15,9 +15,7 @@
15
15
  # See the License for the specific language governing permissions and
16
16
  # limitations under the License.
17
17
 
18
- $LOAD_PATH.unshift(File.dirname(File.realpath(__FILE__)) + '/../lib')
19
-
20
- # rubocop:disable UnusedBlockArgument, AlignParameters
18
+ $LOAD_PATH.unshift(__dir__ + '/../lib')
21
19
 
22
20
  require 'rubygems'
23
21
  require 'time'
@@ -219,20 +217,22 @@ MODES:
219
217
  options[:linkonly] = true
220
218
  end
221
219
 
222
- opts.on(
223
- '-L', '--locallink', 'Configure the local chef client without ssh.'
224
- ) do
225
- options[:locallink] = true
220
+ opts.on('--transport TRANSPORT', ['locallink', 'ssh', 'noop'],
221
+ "Set the transport\n" +
222
+ "\t\tssh - [default] Use SSH to talk to remote host\n" +
223
+ "\t\tlocallink - Assume the remote host is ourself\n" +
224
+ "\t\tnoop - Ignore all remote commands") do |transport|
225
+ options[:transport] = transport
226
226
  end
227
227
 
228
228
  opts.on(
229
229
  '-t', '--testing-timestamp TIME',
230
- 'Until when should the host remain in testing.' +
231
- ' Anything parsable is ok, such as "5/18 4:35" or "16/9/13".'
230
+ 'Until when should the host remain in testing.' +
231
+ ' Anything parsable is ok, such as "5/18 4:35" or "16/9/13".'
232
232
  ) do |time|
233
233
  begin
234
234
  options[:testing_until] = Time.parse(time)
235
- rescue
235
+ rescue StandardError
236
236
  logger.error("Invalid date: #{time}")
237
237
  exit 1
238
238
  end
@@ -240,8 +240,8 @@ MODES:
240
240
 
241
241
  opts.on(
242
242
  '-t', '--testing-time TIME',
243
- 'How long should the host remain in testing.' +
244
- ' Takes a simple relative time string, such as "45m", "4h" or "2d".'
243
+ 'How long should the host remain in testing.' +
244
+ ' Takes a simple relative time string, such as "45m", "4h" or "2d".'
245
245
  ) do |time|
246
246
  m = time.match(/^(\d+)([d|h|m]+)$/)
247
247
  if m
@@ -407,6 +407,6 @@ MODES:
407
407
  end
408
408
  end
409
409
 
410
- if __FILE__ == $PROGRAM_NAME
410
+ if $PROGRAM_NAME == __FILE__
411
411
  include TasteTester
412
412
  end
@@ -52,8 +52,9 @@ module TasteTester
52
52
  logger,
53
53
  )
54
54
  unless @repo.exists?
55
- raise "Could not open repo from #{TasteTester::Config.repo}"
55
+ fail "Could not open repo from #{TasteTester::Config.repo}"
56
56
  end
57
+ @track_symlinks = TasteTester::Config.track_symlinks
57
58
  end
58
59
 
59
60
  def checks
@@ -136,6 +137,7 @@ module TasteTester
136
137
  :databag_dir =>
137
138
  TasteTester::Config.relative_databag_dir,
138
139
  },
140
+ @track_symlinks,
139
141
  )
140
142
 
141
143
  cbs = changeset.cookbooks
@@ -14,7 +14,6 @@
14
14
  # See the License for the specific language governing permissions and
15
15
  # limitations under the License.
16
16
 
17
- # rubocop:disable UnusedBlockArgument, UnusedMethodArgument
18
17
  require 'taste_tester/server'
19
18
  require 'taste_tester/host'
20
19
  require 'taste_tester/config'
@@ -84,7 +83,7 @@ module TasteTester
84
83
  logger,
85
84
  )
86
85
  unless repo.exists?
87
- raise "Could not open repo from #{TasteTester::Config.repo}"
86
+ fail "Could not open repo from #{TasteTester::Config.repo}"
88
87
  end
89
88
  end
90
89
  unless TasteTester::Config.skip_pre_test_hook ||
@@ -162,7 +161,7 @@ module TasteTester
162
161
  client.skip_checks = true if TasteTester::Config.skip_repo_checks
163
162
  client.force = true if TasteTester::Config.force_upload
164
163
  client.upload
165
- rescue => exception
164
+ rescue StandardError => exception
166
165
  # We're trying to recover from common chef-zero errors
167
166
  # Most of them happen due to half finished uploads, which leave
168
167
  # chef-zero in undefined state
@@ -41,6 +41,7 @@ module TasteTester
41
41
  timestamp false
42
42
  user 'root'
43
43
  ref_file "#{ENV['HOME']}/.chef/taste-tester-ref.json"
44
+ knife_config "#{ENV['HOME']}/.chef/knife-#{ENV['USER']}-taste-tester.rb"
44
45
  checksum_dir "#{ENV['HOME']}/.chef/checksums"
45
46
  skip_repo_checks false
46
47
  chef_client_command 'chef-client'
@@ -55,6 +56,8 @@ module TasteTester
55
56
  chef_config_path '/etc/chef'
56
57
  chef_config 'client.rb'
57
58
  my_hostname nil
59
+ track_symlinks false
60
+ transport 'ssh'
58
61
 
59
62
  skip_pre_upload_hook false
60
63
  skip_post_upload_hook false
@@ -20,5 +20,7 @@ module TasteTester
20
20
  end
21
21
  class LocalLinkError < StandardError
22
22
  end
23
+ class NoOpError < StandardError
24
+ end
23
25
  end
24
26
  end
@@ -26,34 +26,27 @@ module TasteTester
26
26
  extend BetweenMeals::Util
27
27
 
28
28
  # Do stuff before we upload to chef-zero
29
- def self.pre_upload(_dryrun, _repo, _last_ref, _cur_ref)
30
- end
29
+ def self.pre_upload(_dryrun, _repo, _last_ref, _cur_ref); end
31
30
 
32
31
  # Do stuff after we upload to chef-zero
33
- def self.post_upload(_dryrun, _repo, _last_ref, _cur_ref)
34
- end
32
+ def self.post_upload(_dryrun, _repo, _last_ref, _cur_ref); end
35
33
 
36
34
  # Do stuff before we put hosts in test mode
37
- def self.pre_test(_dryrun, _repo, _hosts)
38
- end
35
+ def self.pre_test(_dryrun, _repo, _hosts); end
39
36
 
40
37
  # This should return an array of commands to execute on
41
38
  # remote systems.
42
- def self.test_remote_cmds(_dryrun, _hostname)
43
- end
39
+ def self.test_remote_cmds(_dryrun, _hostname); end
44
40
 
45
41
  # Should return a string with extra stuff to shove
46
42
  # in the remote client.rb
47
- def self.test_remote_client_rb_extra_code(_hostname)
48
- end
43
+ def self.test_remote_client_rb_extra_code(_hostname); end
49
44
 
50
45
  # Do stuff after we put hosts in test mode
51
- def self.post_test(_dryrun, _repo, _hosts)
52
- end
46
+ def self.post_test(_dryrun, _repo, _hosts); end
53
47
 
54
48
  # Additional checks you want to do on the repo
55
- def self.repo_checks(_dryrun, _repo)
56
- end
49
+ def self.repo_checks(_dryrun, _repo); end
57
50
 
58
51
  def self.get(file)
59
52
  path = File.expand_path(file)
@@ -20,6 +20,7 @@ require 'open3'
20
20
  require 'colorize'
21
21
 
22
22
  require 'taste_tester/ssh'
23
+ require 'taste_tester/noop'
23
24
  require 'taste_tester/locallink'
24
25
  require 'taste_tester/tunnel'
25
26
 
@@ -72,12 +73,14 @@ module TasteTester
72
73
  end
73
74
 
74
75
  def get_transport
75
- if TasteTester::Config.locallink
76
- transport = TasteTester::LocalLink.new
76
+ case TasteTester::Config.transport
77
+ when 'locallink'
78
+ TasteTester::LocalLink.new
79
+ when 'noop'
80
+ TasteTester::NoOp.new
77
81
  else
78
- transport = TasteTester::SSH.new(@name)
82
+ TasteTester::SSH.new(@name)
79
83
  end
80
- transport
81
84
  end
82
85
 
83
86
  def test
@@ -99,7 +102,7 @@ module TasteTester
99
102
  transport << 'logger -t taste-tester Moving server into taste-tester' +
100
103
  " for #{@user}"
101
104
  transport << touchcmd
102
- transport << "echo -n '#{@serialized_config}' | base64 --decode" +
105
+ transport << "/bin/echo -n '#{@serialized_config}' | base64 --decode" +
103
106
  " > #{TasteTester::Config.chef_config_path}/client-taste-tester.rb"
104
107
  transport << "rm -vf #{TasteTester::Config.chef_config_path}/" +
105
108
  TasteTester::Config.chef_config
@@ -209,7 +212,7 @@ module TasteTester
209
212
  "-d @#{TasteTester::Config.testing_end_time.to_i} +'%Y-%m-%d %T')\" " +
210
213
  "#{TasteTester::Config.timestamp_file}; fi",
211
214
  ).delete("\n")
212
- "echo -n '#{touch}' | base64 --decode | bash"
215
+ "/bin/echo -n '#{touch}' | base64 --decode | bash"
213
216
  end
214
217
 
215
218
  def config
@@ -221,7 +224,7 @@ module TasteTester
221
224
  url << ":#{TasteTester::State.port}" if TasteTester::State.port
222
225
  end
223
226
  # rubocop:disable Metrics/LineLength
224
- ttconfig = <<-eos
227
+ ttconfig = <<-EOS
225
228
  # TasteTester by #{@user}
226
229
  # Prevent people from screwing up their permissions
227
230
  if Process.euid != 0
@@ -235,22 +238,22 @@ chef_server_url '#{url}'
235
238
  ssl_verify_mode :verify_none
236
239
  ohai.plugin_path << '#{TasteTester::Config.chef_config_path}/ohai_plugins'
237
240
 
238
- eos
241
+ EOS
239
242
  # rubocop:enable Metrics/LineLength
240
243
 
241
244
  extra = TasteTester::Hooks.test_remote_client_rb_extra_code(@name)
242
245
  if extra
243
- ttconfig += <<-eos
246
+ ttconfig += <<-EOS
244
247
  # Begin user-hook specified code
245
248
  #{extra}
246
249
  # End user-hook secified code
247
250
 
248
- eos
251
+ EOS
249
252
  end
250
253
 
251
- ttconfig += <<-eos
254
+ ttconfig += <<-EOS
252
255
  puts 'INFO: Running on #{@name} in taste-tester by #{@user}'
253
- eos
256
+ EOS
254
257
  return ttconfig
255
258
  end
256
259
  end
@@ -41,7 +41,7 @@ module TasteTester
41
41
 
42
42
  def run!
43
43
  @status, @output = exec!(cmd, logger)
44
- rescue => e
44
+ rescue StandardError => e
45
45
  logger.error(e.message)
46
46
  raise TasteTester::Exceptions::LocalLinkError
47
47
  end
@@ -14,7 +14,7 @@
14
14
  # See the License for the specific language governing permissions and
15
15
  # limitations under the License.
16
16
 
17
- # rubocop:disable ClassVars, UnusedMethodArgument, UnusedBlockArgument
17
+ # rubocop:disable ClassVars
18
18
  require 'logger'
19
19
 
20
20
  module TasteTester
@@ -50,14 +50,14 @@ module TasteTester
50
50
  def formatter
51
51
  return @@formatter_proc if @@formatter_proc
52
52
  if @@use_log_formatter
53
- proc do |severity, datetime, progname, msg|
53
+ proc do |severity, datetime, _progname, msg|
54
54
  if severity == 'ERROR'
55
55
  msg = msg.red
56
56
  end
57
57
  "[#{datetime.strftime('%Y-%m-%dT%H:%M:%S%:z')}] #{severity}: #{msg}\n"
58
58
  end
59
59
  else
60
- proc do |severity, datetime, progname, msg|
60
+ proc do |severity, _datetime, _progname, msg|
61
61
  msg.to_s.prepend("#{severity}: ") unless severity == 'WARN'
62
62
  if severity == 'ERROR'
63
63
  msg = msg.to_s.red
@@ -68,3 +68,4 @@ module TasteTester
68
68
  end
69
69
  end
70
70
  end
71
+ # rubocop:enable ClassVars
@@ -0,0 +1,67 @@
1
+ # vim: syntax=ruby:expandtab:shiftwidth=2:softtabstop=2:tabstop=2
2
+
3
+ # Copyright 2013-present Facebook
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'taste_tester/exceptions'
18
+
19
+ module TasteTester
20
+ # Wrapper for running commands on local system
21
+ class NoOp
22
+ include TasteTester::Logging
23
+ include BetweenMeals::Util
24
+
25
+ attr_reader :output
26
+
27
+ def initialize
28
+ print_noop_warning
29
+ @host = 'localhost'
30
+ @user = ENV['USER']
31
+ @cmds = []
32
+ end
33
+
34
+ def print_noop_warning
35
+ # This needs to be a Class var as this class is initialized more
36
+ # than once in a given tt run and we only want to warn once.
37
+ # rubocop:disable Style/ClassVars
38
+ @@printedwarning ||= logger.warn(
39
+ 'No-op plugin active, no remote commands will be run!',
40
+ )
41
+ # rubocop:enable Style/ClassVars
42
+ end
43
+
44
+ def add(string)
45
+ @cmds << string
46
+ end
47
+
48
+ alias << add
49
+
50
+ def run
51
+ run!
52
+ end
53
+
54
+ def run!
55
+ cmd
56
+ [0, "# TasteTester by #{@user}"]
57
+ end
58
+
59
+ private
60
+
61
+ def cmd
62
+ @cmds.each do |cmd|
63
+ logger.info("No-op, faking run of: '#{cmd}' on #{@host}")
64
+ end
65
+ end
66
+ end
67
+ end
@@ -41,7 +41,7 @@ module TasteTester
41
41
  unless File.directory?(ref_dir)
42
42
  begin
43
43
  FileUtils.mkpath(ref_dir)
44
- rescue => e
44
+ rescue StandardError => e
45
45
  logger.warn("Chef temp dir #{ref_dir} missing and can't be created")
46
46
  logger.warn(e)
47
47
  end
@@ -65,7 +65,7 @@ module TasteTester
65
65
  @addr = '::'
66
66
  begin
67
67
  @host = TasteTester::Config.my_hostname || Socket.gethostname
68
- rescue
68
+ rescue StandardError
69
69
  logger.error('Unable to find fqdn')
70
70
  exit 1
71
71
  end
@@ -140,6 +140,7 @@ module TasteTester
140
140
  :role_dir => TasteTester::Config.roles,
141
141
  :cookbook_dirs => TasteTester::Config.cookbooks,
142
142
  :checksum_dir => TasteTester::Config.checksum_dir,
143
+ :config => TasteTester::Config.knife_config,
143
144
  )
144
145
  knife.write_user_config
145
146
  end
@@ -43,7 +43,7 @@ module TasteTester
43
43
 
44
44
  def run!
45
45
  @status, @output = exec!(cmd, logger)
46
- rescue => e
46
+ rescue StandardError => e
47
47
  # rubocop:disable LineLength
48
48
  error = <<-MSG
49
49
  SSH returned error while connecting to #{TasteTester::Config.user}@#{@host}
@@ -29,13 +29,13 @@ module TasteTester
29
29
  include ::BetweenMeals::Util
30
30
 
31
31
  def initialize
32
- ref_dir = File.dirname(File.expand_path(
33
- TasteTester::Config.ref_file,
34
- ))
32
+ ref_dir = File.dirname(
33
+ File.expand_path(TasteTester::Config.ref_file),
34
+ )
35
35
  unless File.directory?(ref_dir)
36
36
  begin
37
37
  FileUtils.mkpath(ref_dir)
38
- rescue => e
38
+ rescue StandardError => e
39
39
  logger.error("Chef temp dir #{ref_dir} missing and can't be created")
40
40
  logger.error(e)
41
41
  exit(1)
@@ -108,7 +108,7 @@ module TasteTester
108
108
 
109
109
  def self.read(key)
110
110
  JSON.parse(File.read(TasteTester::Config.ref_file))[key.to_s]
111
- rescue => e
111
+ rescue StandardError => e
112
112
  logger.debug(e)
113
113
  nil
114
114
  end
@@ -122,7 +122,7 @@ module TasteTester
122
122
  def merge(vals)
123
123
  begin
124
124
  state = JSON.parse(File.read(TasteTester::Config.ref_file))
125
- rescue
125
+ rescue StandardError
126
126
  state = {}
127
127
  end
128
128
  state.merge!(vals)
@@ -132,7 +132,7 @@ module TasteTester
132
132
  )
133
133
  ff.write(state.to_json)
134
134
  ff.close
135
- rescue => e
135
+ rescue StandardError => e
136
136
  logger.error('Unable to write the reffile')
137
137
  logger.debug(e)
138
138
  exit 0
@@ -38,21 +38,19 @@ module TasteTester
38
38
  @port = TasteTester::Config.tunnel_port
39
39
  logger.info("Setting up tunnel on port #{@port}")
40
40
  @status, @output = exec!(cmd, logger)
41
- rescue
41
+ rescue StandardError
42
42
  logger.error 'Failed bringing up ssh tunnel'
43
43
  exit(1)
44
44
  end
45
45
 
46
46
  def cmd
47
- if TasteTester::Config.user != 'root'
48
- pid = '$$'
49
- else
50
- pid = '\\$\\$'
51
- end
52
- cmds = "ps -p #{pid} -o pgid | grep -v PGID" +
47
+ @max_ping = @delta_secs / 10
48
+ pid = '$$'
49
+ @ts = TasteTester::Config.testing_end_time.strftime('%y%m%d%H%M.%S')
50
+ cmds = "ps -o pgid= -p $(ps -o ppid= -p #{pid}) | sed \"s| ||g\" " +
53
51
  " > #{TasteTester::Config.timestamp_file} &&" +
54
- " touch -t #{TasteTester::Config.testing_end_time}" +
55
- " #{TasteTester::Config.timestamp_file} && sleep #{@delta_secs}"
52
+ " touch -t #{@ts} #{TasteTester::Config.timestamp_file} &&" +
53
+ " sleep #{@delta_secs}"
56
54
  # As great as it would be to have ExitOnForwardFailure=yes,
57
55
  # we had multiple cases of tunnels dying
58
56
  # if -f and ExitOnForwardFailure are used together.
@@ -62,14 +60,14 @@ module TasteTester
62
60
  cmd = "#{TasteTester::Config.ssh_command} " +
63
61
  "-T -o BatchMode=yes -o ConnectTimeout=#{@timeout} " +
64
62
  '-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no ' +
65
- '-o ServerAliveInterval=10 -o ServerAliveCountMax=6 ' +
63
+ "-o ServerAliveInterval=10 -o ServerAliveCountMax=#{@max_ping} " +
66
64
  "-f -R #{@port}:localhost:#{@server.port} "
67
65
  if TasteTester::Config.user != 'root'
68
66
  cc = Base64.encode64(cmds).delete("\n")
69
67
  cmd += "#{TasteTester::Config.user}@#{@host} \"echo '#{cc}' | base64" +
70
68
  ' --decode | sudo bash -x"'
71
69
  else
72
- cmd += "root@#{@host} \"#{cmds}\""
70
+ cmd += "root@#{@host} '#{cmds}'"
73
71
  end
74
72
  cmd
75
73
  end
@@ -85,7 +83,8 @@ module TasteTester
85
83
  end
86
84
  cmd = "( [ -s #{TasteTester::Config.timestamp_file} ]" +
87
85
  " && #{sudo}kill -9 -- " +
88
- "-\$(cat #{TasteTester::Config.timestamp_file}); true )"
86
+ "-\$(cat #{TasteTester::Config.timestamp_file}) 2>/dev/null; " +
87
+ ' true )'
89
88
  ssh << cmd
90
89
  ssh.run!
91
90
  end
@@ -14,6 +14,14 @@
14
14
  # See the License for the specific language governing permissions and
15
15
  # limitations under the License.
16
16
 
17
+ CONFIG_PATH='/etc/chef'
18
+ CONFIG_NAME='client'
19
+
20
+ CONFIG_FILE="${CONFIG_FILE:-/etc/taste-untester-config}"
21
+ if [ -e "$CONFIG_FILE" ]; then
22
+ source "$COFNIG_FILE"
23
+ fi
24
+
17
25
  CONFLINK='/etc/chef/client.rb'
18
26
  PRODCONF='/etc/chef/client-prod.rb'
19
27
  CERTLINK='/etc/chef/client.pem'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: taste_tester
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.12
4
+ version: 0.0.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - Phil Dibowitz
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2016-12-21 00:00:00.000000000 Z
12
+ date: 2017-12-18 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: between_meals
@@ -115,6 +115,7 @@ files:
115
115
  - lib/taste_tester/host.rb
116
116
  - lib/taste_tester/locallink.rb
117
117
  - lib/taste_tester/logging.rb
118
+ - lib/taste_tester/noop.rb
118
119
  - lib/taste_tester/server.rb
119
120
  - lib/taste_tester/ssh.rb
120
121
  - lib/taste_tester/state.rb
@@ -141,7 +142,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
141
142
  version: '0'
142
143
  requirements: []
143
144
  rubyforge_project:
144
- rubygems_version: 2.5.1
145
+ rubygems_version: 2.6.11
145
146
  signing_key:
146
147
  specification_version: 4
147
148
  summary: Taste Tester