puppet-debugger 0.6.1 → 0.7.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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.gitlab-ci.yml +30 -13
  4. data/.rubocop.yml +3 -1
  5. data/.rubocop_todo.yml +11 -2
  6. data/.ruby-version +1 -1
  7. data/CHANGELOG.md +9 -0
  8. data/DEVELOPMENT.md +6 -1
  9. data/Gemfile +14 -12
  10. data/Plugin_development.md +304 -0
  11. data/README.md +4 -7
  12. data/Rakefile +6 -5
  13. data/bin/pdb +1 -0
  14. data/lib/awesome_print/ext/awesome_puppet.rb +1 -0
  15. data/lib/plugins/puppet-debugger/input_responders/benchmark.rb +40 -0
  16. data/lib/plugins/puppet-debugger/input_responders/classes.rb +15 -0
  17. data/lib/plugins/puppet-debugger/input_responders/classification.rb +14 -0
  18. data/lib/plugins/puppet-debugger/input_responders/commands.rb +95 -0
  19. data/lib/plugins/puppet-debugger/input_responders/datatypes.rb +17 -0
  20. data/lib/plugins/puppet-debugger/input_responders/environment.rb +14 -0
  21. data/lib/plugins/puppet-debugger/input_responders/exit.rb +14 -0
  22. data/lib/plugins/puppet-debugger/input_responders/facterdb_filter.rb +16 -0
  23. data/lib/plugins/puppet-debugger/input_responders/facts.rb +15 -0
  24. data/lib/plugins/puppet-debugger/input_responders/functions.rb +15 -0
  25. data/lib/plugins/puppet-debugger/input_responders/help.rb +14 -0
  26. data/lib/plugins/puppet-debugger/input_responders/krt.rb +14 -0
  27. data/lib/{puppet-debugger/support → plugins/puppet-debugger/input_responders}/play.rb +37 -26
  28. data/lib/plugins/puppet-debugger/input_responders/reset.rb +21 -0
  29. data/lib/plugins/puppet-debugger/input_responders/resources.rb +22 -0
  30. data/lib/plugins/puppet-debugger/input_responders/set.rb +55 -0
  31. data/lib/plugins/puppet-debugger/input_responders/types.rb +35 -0
  32. data/lib/plugins/puppet-debugger/input_responders/vars.rb +18 -0
  33. data/lib/plugins/puppet-debugger/input_responders/whereami.rb +29 -0
  34. data/lib/puppet-debugger.rb +12 -1
  35. data/lib/puppet-debugger/cli.rb +38 -22
  36. data/lib/puppet-debugger/code/code_file.rb +16 -15
  37. data/lib/puppet-debugger/code/code_range.rb +1 -0
  38. data/lib/puppet-debugger/code/loc.rb +1 -0
  39. data/lib/puppet-debugger/debugger_code.rb +1 -0
  40. data/lib/puppet-debugger/hooks.rb +174 -0
  41. data/lib/puppet-debugger/input_responder_plugin.rb +45 -0
  42. data/lib/puppet-debugger/plugin_test_helper.rb +44 -0
  43. data/lib/puppet-debugger/support.rb +13 -9
  44. data/lib/puppet-debugger/support/compiler.rb +1 -0
  45. data/lib/puppet-debugger/support/environment.rb +2 -0
  46. data/lib/puppet-debugger/support/errors.rb +9 -0
  47. data/lib/puppet-debugger/support/facts.rb +2 -1
  48. data/lib/puppet-debugger/support/functions.rb +3 -1
  49. data/lib/puppet-debugger/support/loader.rb +2 -0
  50. data/lib/puppet-debugger/support/node.rb +1 -0
  51. data/lib/puppet-debugger/support/scope.rb +1 -0
  52. data/lib/puppet/application/debugger.rb +1 -0
  53. data/lib/version.rb +2 -1
  54. data/puppet-debugger.gemspec +20 -15
  55. data/run_container_test.sh +1 -1
  56. data/spec/environment_spec.rb +2 -1
  57. data/spec/facts_spec.rb +1 -0
  58. data/spec/hooks_spec.rb +341 -0
  59. data/spec/input_responder_plugin_spec.rb +45 -0
  60. data/spec/input_responders/help_spec.rb +17 -0
  61. data/spec/input_responders/krt_spec.rb +12 -0
  62. data/spec/input_responders/play_spec.rb +160 -0
  63. data/spec/pdb_spec.rb +1 -0
  64. data/spec/puppet/application/debugger_spec.rb +1 -2
  65. data/spec/puppet_debugger_spec.rb +49 -88
  66. data/spec/remote_node_spec.rb +3 -2
  67. data/spec/spec_helper.rb +7 -0
  68. data/spec/support_spec.rb +5 -116
  69. data/test_matrix.rb +2 -0
  70. metadata +65 -12
  71. data/Gemfile.lock +0 -95
  72. data/lib/puppet-debugger/support/input_responders.rb +0 -191
@@ -0,0 +1,21 @@
1
+ require 'puppet-debugger/input_responder_plugin'
2
+ module PuppetDebugger
3
+ module InputResponders
4
+ class Reset < InputResponderPlugin
5
+ COMMAND_WORDS = %w(reset)
6
+ SUMMARY = 'Reset the debugger to a clean state.'
7
+ COMMAND_GROUP = :context
8
+
9
+ def run(args = [])
10
+ debugger.set_scope(nil)
11
+ debugger.set_remote_node_name(nil)
12
+ debugger.set_node(nil)
13
+ debugger.set_facts(nil)
14
+ debugger.set_environment(nil)
15
+ debugger.set_compiler(nil)
16
+ #debugger.handle_input(":set loglevel #{debugger.log_level}")
17
+ nil
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,22 @@
1
+ require 'puppet-debugger/input_responder_plugin'
2
+ module PuppetDebugger
3
+ module InputResponders
4
+ class Resources < InputResponderPlugin
5
+ COMMAND_WORDS = %w(resources)
6
+ SUMMARY = 'List all the resources current in the catalog.'
7
+ COMMAND_GROUP = :scope
8
+
9
+ def run(args = [])
10
+ res = debugger.scope.compiler.catalog.resources.map do |res|
11
+ res.to_s.gsub(/\[/, "['").gsub(/\]/, "']") # ensure the title has quotes
12
+ end
13
+ if !args.first.nil?
14
+ res[args.first.to_i].ai
15
+ else
16
+ output = "Resources not shown in any specific order\n".warning
17
+ output += res.ai
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,55 @@
1
+ require 'puppet-debugger/input_responder_plugin'
2
+ module PuppetDebugger
3
+ module InputResponders
4
+ class Set < InputResponderPlugin
5
+ COMMAND_WORDS = %w(set :set)
6
+ SUMMARY = 'Set the a puppet debugger config'
7
+ COMMAND_GROUP = :scope
8
+
9
+ def run(args = [])
10
+ handle_set(args)
11
+ end
12
+
13
+ private
14
+
15
+ def handle_set(input)
16
+ output = ''
17
+ # args = input.split(' ')
18
+ # args.shift # throw away the set
19
+ case input.shift
20
+ when /node/
21
+ if name = input.shift
22
+ output = "Resetting to use node #{name}"
23
+ debugger.set_scope(nil)
24
+ debugger.set_node(nil)
25
+ debugger.set_facts(nil)
26
+ debugger.set_environment(nil)
27
+ debugger.set_compiler(nil)
28
+ set_log_level(debugger.log_level)
29
+ debugger.set_remote_node_name(name)
30
+ else
31
+ debugger.out_buffer.puts 'Must supply a valid node name'
32
+ end
33
+ when /loglevel/
34
+ if level = input.shift
35
+ set_log_level(level)
36
+ output = "loglevel #{Puppet::Util::Log.level} is set"
37
+ end
38
+ end
39
+ output
40
+ end
41
+
42
+ def set_log_level(level)
43
+ debugger.log_level = level
44
+ Puppet::Util::Log.level = level.to_sym
45
+ buffer_log = Puppet::Util::Log.newdestination(:buffer)
46
+ if buffer_log
47
+ # if this is already set the buffer_log is nil
48
+ buffer_log.out_buffer = debugger.out_buffer
49
+ buffer_log.err_buffer = debugger.out_buffer
50
+ end
51
+ nil
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,35 @@
1
+ require 'puppet-debugger/input_responder_plugin'
2
+ module PuppetDebugger
3
+ module InputResponders
4
+ class Types < InputResponderPlugin
5
+ COMMAND_WORDS = %w(types)
6
+ SUMMARY = 'List all the types available in the environment.'
7
+ COMMAND_GROUP = :environment
8
+
9
+ # @return - returns a list of types available to the environment
10
+ # if a error occurs we we run the types function again
11
+ def run(args = [])
12
+ types
13
+ end
14
+
15
+ def types
16
+ loaded_types = []
17
+ begin
18
+ # this loads all the types, if already loaded the file is skipped
19
+ Puppet::Type.loadall
20
+ Puppet::Type.eachtype do |t|
21
+ next if t.name == :component
22
+ loaded_types << t.name.to_s
23
+ end
24
+ loaded_types.ai
25
+ rescue Puppet::Error => e
26
+ puts e.message.red
27
+ Puppet.info(e.message)
28
+ # prevent more than two calls and recursive loop
29
+ return if caller_locations(1, 10).find_all { |f| f.label == 'types' }.count > 2
30
+ types
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,18 @@
1
+ require 'puppet-debugger/input_responder_plugin'
2
+ module PuppetDebugger
3
+ module InputResponders
4
+ class Vars < InputResponderPlugin
5
+ COMMAND_WORDS = %w(vars ls)
6
+ SUMMARY = 'List all the variables in the current scopes.'
7
+ COMMAND_GROUP = :scope
8
+
9
+ def run(args = [])
10
+ # remove duplicate variables that are also in the facts hash
11
+ variables = debugger.scope.to_hash.delete_if { |key, _value| debugger.node.facts.values.key?(key) }
12
+ variables['facts'] = 'removed by the puppet-debugger' if variables.key?('facts')
13
+ output = 'Facts were removed for easier viewing'.ai + "\n"
14
+ output += variables.ai(sort_keys: true, indent: -1)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,29 @@
1
+ require 'puppet-debugger/input_responder_plugin'
2
+ module PuppetDebugger
3
+ module InputResponders
4
+ class Whereami < InputResponderPlugin
5
+ COMMAND_WORDS = %w(whereami)
6
+ SUMMARY = 'Show code surrounding the current context.'
7
+ COMMAND_GROUP = :context
8
+
9
+ # source_file and source_line_num instance variables must be set for this
10
+ # method to show the surrounding code
11
+ # @return [String] - string output of the code surrounded by the breakpoint or nil if file
12
+ # or line_num do not exist
13
+ def run(args = [])
14
+ file = debugger.source_file
15
+ line_num = debugger.source_line_num
16
+ if file && line_num
17
+ if file == :code
18
+ source_code = Puppet[:code]
19
+ code = DebuggerCode.from_string(source_code, :puppet)
20
+ else
21
+ code = DebuggerCode.from_file(file, :puppet)
22
+ end
23
+ return code.with_marker(line_num).around(line_num, 5)
24
+ .with_line_numbers.with_indentation(5).with_file_reference.to_s
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require_relative 'puppet-debugger/cli'
3
4
  require_relative 'version'
4
5
  require 'awesome_print'
@@ -6,14 +7,24 @@ require_relative 'awesome_print/ext/awesome_puppet'
6
7
  require_relative 'trollop'
7
8
  require 'puppet/util/log'
8
9
  require_relative 'puppet-debugger/debugger_code'
9
-
10
10
  require_relative 'puppet-debugger/support/errors'
11
+ require 'plugins/puppet-debugger/input_responders/commands'
12
+
13
+
11
14
  # monkey patch in some color effects string methods
12
15
  class String
13
16
  def red
14
17
  "\033[31m#{self}\033[0m"
15
18
  end
16
19
 
20
+ def bold
21
+ "\033[1m#{self}\033[22m"
22
+ end
23
+
24
+ def black
25
+ "\033[30m#{self}\033[0m"
26
+ end
27
+
17
28
  def green
18
29
  "\033[32m#{self}\033[0m"
19
30
  end
@@ -1,14 +1,20 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'puppet'
3
4
  require 'readline'
4
5
  require 'json'
5
6
  require_relative 'support'
7
+ require 'pluginator'
8
+ require 'puppet-debugger/hooks'
9
+ require 'forwardable'
6
10
 
7
11
  module PuppetDebugger
8
12
  class Cli
9
13
  include PuppetDebugger::Support
10
-
11
- attr_accessor :settings, :log_level, :in_buffer, :out_buffer, :html_mode
14
+ extend Forwardable
15
+ attr_accessor :settings, :log_level, :in_buffer, :out_buffer, :html_mode, :extra_prompt, :bench
16
+ attr_reader :source_file, :source_line_num, :hooks
17
+ def_delegators :hooks, :exec_hook, :add_hook, :delete_hook
12
18
 
13
19
  def initialize(options = {})
14
20
  do_initialize if Puppet[:codedir].nil?
@@ -36,6 +42,10 @@ module PuppetDebugger
36
42
  }
37
43
  end
38
44
 
45
+ def hooks
46
+ @hooks ||= PuppetDebugger::Hooks.new
47
+ end
48
+
39
49
  # returns a cached list of key words
40
50
  def key_words
41
51
  # because dollar signs don't work we can't display a $ sign in the keyword
@@ -90,23 +100,23 @@ module PuppetDebugger
90
100
  result
91
101
  end
92
102
 
103
+ def responder_list
104
+ plugins = Pluginator.find(PuppetDebugger)
105
+ end
106
+
93
107
  # this method handles all input and expects a string of text.
94
108
  #
95
109
  def handle_input(input)
96
110
  raise ArgumentError unless input.instance_of?(String)
97
111
  begin
98
112
  output = ''
99
- case input
100
- when /^play|^classification|^whereami|^facterdb_filter|^facts|^datatypes|^types|^vars|^functions|^classes|^resources|^krt|^environment|^benchmark|^reset|^help/
113
+ case input.strip
114
+ when PuppetDebugger::InputResponders::Commands.command_list_regex
101
115
  args = input.split(' ')
102
- command = args.shift.to_sym
103
- output = send(command, args) if respond_to?(command)
104
- @bencmark = false
116
+ command = args.shift
117
+ plugin = PuppetDebugger::InputResponders::Commands.plugin_from_command(command)
118
+ output = plugin.execute(args, self)
105
119
  return out_buffer.puts output
106
- when /exit/
107
- exit 0
108
- when /^:set/
109
- output = handle_set(input)
110
120
  when '_'
111
121
  output = " => #{@last_item}"
112
122
  else
@@ -115,6 +125,8 @@ module PuppetDebugger
115
125
  output = normalize_output(result)
116
126
  output = output.nil? ? '' : output.ai
117
127
  end
128
+ rescue PuppetDebugger::Exception::InvalidCommand => e
129
+ output = e.message.fatal
118
130
  rescue LoadError => e
119
131
  output = e.message.fatal
120
132
  rescue Errno::ETIMEDOUT => e
@@ -137,6 +149,7 @@ module PuppetDebugger
137
149
  unless output.empty?
138
150
  out_buffer.print ' => '
139
151
  out_buffer.puts output unless output.empty?
152
+ exec_hook :after_output, out_buffer, self, self
140
153
  end
141
154
  end
142
155
 
@@ -146,9 +159,9 @@ Ruby Version: #{RUBY_VERSION}
146
159
  Puppet Version: #{Puppet.version}
147
160
  Puppet Debugger Version: #{PuppetDebugger::VERSION}
148
161
  Created by: NWOps <corey@nwops.io>
149
- Type "exit", "functions", "vars", "krt", "whereami", "facts", "resources", "classes",
150
- "play", "classification", "types", "datatypes", "benchmark",
151
- "reset", or "help" for more information.
162
+ Type "commands" for a list of debugger commands
163
+ or "help" to show the help screen.
164
+
152
165
 
153
166
  EOT
154
167
  output
@@ -173,11 +186,11 @@ Type "exit", "functions", "vars", "krt", "whereami", "facts", "resources", "clas
173
186
  def read_loop
174
187
  line_number = 1
175
188
  full_buffer = ''
176
- while buf = Readline.readline("#{line_number}:#{$extra_prompt}>> ", true)
189
+ while buf = Readline.readline("#{line_number}:#{extra_prompt}>> ", true)
177
190
  begin
178
191
  full_buffer += buf
179
192
  # unless this is puppet code, otherwise skip repl keywords
180
- unless keyword_expression.match(buf)
193
+ unless PuppetDebugger::InputResponders::Commands.command_list_regex.match(buf)
181
194
  line_number = line_number.next
182
195
  parser.parse_string(full_buffer)
183
196
  end
@@ -197,12 +210,14 @@ Type "exit", "functions", "vars", "krt", "whereami", "facts", "resources", "clas
197
210
  # or
198
211
  # this is primarily used by the debug::break() module function and the puppet debugger face
199
212
  # @param [Hash] must contain at least the puppet scope object
213
+ # @option play - must be a path string
200
214
  def self.start_without_stdin(options = { scope: nil })
201
215
  puts print_repl_desc unless options[:quiet]
202
216
  repl_obj = PuppetDebugger::Cli.new(options)
217
+ options[:play] = options[:play].path if options[:play].respond_to?(:path)
203
218
  # TODO: make the output optional so we can have different output destinations
204
- puts repl_obj.whereami if options[:source_file] && options[:source_line]
205
- repl_obj.play_back(options) if options[:play]
219
+ repl_obj.handle_input('whereami') if options[:source_file] && options[:source_line]
220
+ repl_obj.handle_input("play #{options[:play]}") if options[:play]
206
221
  repl_obj.read_loop unless options[:run_once]
207
222
  end
208
223
 
@@ -219,15 +234,16 @@ Type "exit", "functions", "vars", "krt", "whereami", "facts", "resources", "clas
219
234
  end
220
235
  options = opts.merge(options)
221
236
  puts print_repl_desc unless options[:quiet]
237
+ options[:play] = options[:play].path if options[:play].respond_to?(:path)
222
238
  repl_obj = PuppetDebugger::Cli.new(options)
223
239
  if options[:play]
224
- repl_obj.play_back(opts)
225
- # when the user supplied a file name without using the args (stdin)
240
+ repl_obj.handle_input("play #{options[:play]}")
226
241
  elsif ARGF.filename != '-'
242
+ # when the user supplied a file name without using the args (stdin)
227
243
  path = File.expand_path(ARGF.filename)
228
- repl_obj.play_back(play: path)
229
- # when the user supplied a file content using stdin, aka. cat,pipe,echo or redirection
244
+ repl_obj.handle_input("play #{path}")
230
245
  elsif (ARGF.filename == '-') && (!STDIN.tty? && !STDIN.closed?)
246
+ # when the user supplied a file content using stdin, aka. cat,pipe,echo or redirection
231
247
  input = ARGF.read
232
248
  repl_obj.handle_input(input)
233
249
  end
@@ -1,5 +1,6 @@
1
1
 
2
2
  # frozen_string_literal: true
3
+
3
4
  class CodeFile
4
5
  class SourceNotFound < RuntimeError
5
6
  end
@@ -9,21 +10,21 @@ class CodeFile
9
10
  # List of all supported languages.
10
11
  # @return [Hash]
11
12
  EXTENSIONS = {
12
- %w(.py) => :python,
13
- %w(.js) => :javascript,
14
- %w(.pp) => :puppet,
15
- %w(.css) => :css,
16
- %w(.xml) => :xml,
17
- %w(.php) => :php,
18
- %w(.html) => :html,
19
- %w(.diff) => :diff,
20
- %w(.java) => :java,
21
- %w(.json) => :json,
22
- %w(.c .h) => :c,
23
- %w(.rhtml) => :rhtml,
24
- %w(.yaml .yml) => :yaml,
25
- %w(.cpp .hpp .cc .h cxx) => :cpp,
26
- %w(.rb .ru .irbrc .gemspec .pryrc) => :ruby
13
+ %w[.py] => :python,
14
+ %w[.js] => :javascript,
15
+ %w[.pp] => :puppet,
16
+ %w[.css] => :css,
17
+ %w[.xml] => :xml,
18
+ %w[.php] => :php,
19
+ %w[.html] => :html,
20
+ %w[.diff] => :diff,
21
+ %w[.java] => :java,
22
+ %w[.json] => :json,
23
+ %w[.c .h] => :c,
24
+ %w[.rhtml] => :rhtml,
25
+ %w[.yaml .yml] => :yaml,
26
+ %w[.cpp .hpp .cc .h cxx] => :cpp,
27
+ %w[.rb .ru .irbrc .gemspec .pryrc] => :ruby
27
28
  }.freeze
28
29
 
29
30
  # @return [Symbol] The type of code stored in this wrapper.
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  class DebuggerCode
3
4
  # Represents a range of lines in a code listing.
4
5
  #
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  class DebuggerCode
3
4
  # Represents a line of code. A line of code is a tuple, which consists of a
4
5
  # line and a line number. A `LOC` object's state (namely, the line
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require_relative 'code/loc'
3
4
  require_relative 'code/code_range'
4
5
  require_relative 'code/code_file'
@@ -0,0 +1,174 @@
1
+ require 'puppet-debugger/support/errors'
2
+
3
+ module PuppetDebugger
4
+ # This code was borrowed from Pry hooks file
5
+ # Implements a hooks system for Puppet Debugger. A hook is a callable that is associated
6
+ # with an event. A number of events are currently provided by Pry, these
7
+ # include: `:when_started`, `:before_session`, `:after_session`. A hook must
8
+ # have a name, and is connected with an event by the `PuppetDebugger::Hooks#add_hook`
9
+ # method.
10
+ #
11
+ # @example Adding a hook for the `:before_session` event.
12
+ # debugger.hooks.add_hook(:before_session, :say_hi) do
13
+ # puts "hello"
14
+ # end
15
+ class Hooks
16
+ def initialize
17
+ @hooks = Hash.new { |h, k| h[k] = [] }
18
+ end
19
+
20
+ # Ensure that duplicates have their @hooks object.
21
+ def initialize_copy(orig)
22
+ hooks_dup = @hooks.dup
23
+ @hooks.each do |k, v|
24
+ hooks_dup[k] = v.dup
25
+ end
26
+
27
+ @hooks = hooks_dup
28
+ end
29
+
30
+ def errors
31
+ @errors ||= []
32
+ end
33
+
34
+ # Destructively merge the contents of two `PuppetDebugger::Hooks` instances.
35
+ #
36
+ # @param [PuppetDebugger::Hooks] other The `PuppetDebugger::Hooks` instance to merge
37
+ # @return [PuppetDebugger::Hooks] The receiver.
38
+ # @see {#merge}
39
+ def merge!(other)
40
+ @hooks.merge!(other.dup.hooks) do |key, array, other_array|
41
+ temp_hash, output = {}, []
42
+
43
+ (array + other_array).reverse_each do |pair|
44
+ temp_hash[pair.first] ||= output.unshift(pair)
45
+ end
46
+
47
+ output
48
+ end
49
+
50
+ self
51
+ end
52
+
53
+ # @example
54
+ # hooks = PuppetDebugger::Hooks.new.add_hook(:before_session, :say_hi) { puts "hi!" }
55
+ # PuppetDebugger::Hooks.new.merge(hooks)
56
+ # @param [PuppetDebugger::Hooks] other The `PuppetDebugger::Hooks` instance to merge
57
+ # @return [PuppetDebugger::Hooks] a new `PuppetDebugger::Hooks` instance containing a merge of the
58
+ # contents of two `PuppetDebugger::Hooks` instances.
59
+ def merge(other)
60
+ self.dup.tap do |v|
61
+ v.merge!(other)
62
+ end
63
+ end
64
+
65
+ # Add a new hook to be executed for the `event_name` event.
66
+ # @param [Symbol] event_name The name of the event.
67
+ # @param [Symbol] hook_name The name of the hook.
68
+ # @param [#call] callable The callable.
69
+ # @yield The block to use as the callable (if no `callable` provided).
70
+ # @return [PuppetDebugger::Hooks] The receiver.
71
+ def add_hook(event_name, hook_name, callable=nil, &block)
72
+ event_name = event_name.to_s
73
+
74
+ # do not allow duplicates, but allow multiple `nil` hooks
75
+ # (anonymous hooks)
76
+ if hook_exists?(event_name, hook_name) && !hook_name.nil?
77
+ raise ArgumentError, "Hook with name '#{hook_name}' already defined!"
78
+ end
79
+
80
+ if !block && !callable
81
+ raise ArgumentError, "Must provide a block or callable."
82
+ end
83
+
84
+ # ensure we only have one anonymous hook
85
+ @hooks[event_name].delete_if { |h, k| h.nil? } if hook_name.nil?
86
+
87
+ if block
88
+ @hooks[event_name] << [hook_name, block]
89
+ elsif callable
90
+ @hooks[event_name] << [hook_name, callable]
91
+ end
92
+
93
+ self
94
+ end
95
+
96
+ # Execute the list of hooks for the `event_name` event.
97
+ # @param [Symbol] event_name The name of the event.
98
+ # @param [Array] args The arguments to pass to each hook function.
99
+ # @return [Object] The return value of the last executed hook.
100
+ def exec_hook(event_name, *args, &block)
101
+ @hooks[event_name.to_s].map do |hook_name, callable|
102
+ begin
103
+ callable.call(*args, &block)
104
+ rescue PuppetDebugger::Exception::Error, ::RuntimeError => e
105
+ errors << e
106
+ e
107
+ end
108
+ end.last
109
+ end
110
+
111
+ # @param [Symbol] event_name The name of the event.
112
+ # @return [Fixnum] The number of hook functions for `event_name`.
113
+ def hook_count(event_name)
114
+ @hooks[event_name.to_s].size
115
+ end
116
+
117
+ # @param [Symbol] event_name The name of the event.
118
+ # @param [Symbol] hook_name The name of the hook
119
+ # @return [#call] a specific hook for a given event.
120
+ def get_hook(event_name, hook_name)
121
+ hook = @hooks[event_name.to_s].find do |current_hook_name, callable|
122
+ current_hook_name == hook_name
123
+ end
124
+ hook.last if hook
125
+ end
126
+
127
+ # @param [Symbol] event_name The name of the event.
128
+ # @return [Hash] The hash of hook names / hook functions.
129
+ # @note Modifying the returned hash does not alter the hooks, use
130
+ # `add_hook`/`delete_hook` for that.
131
+ def get_hooks(event_name)
132
+ Hash[@hooks[event_name.to_s]]
133
+ end
134
+
135
+ # @param [Symbol] event_name The name of the event.
136
+ # @param [Symbol] hook_name The name of the hook.
137
+ # to delete.
138
+ # @return [#call] The deleted hook.
139
+ def delete_hook(event_name, hook_name)
140
+ deleted_callable = nil
141
+
142
+ @hooks[event_name.to_s].delete_if do |current_hook_name, callable|
143
+ if current_hook_name == hook_name
144
+ deleted_callable = callable
145
+ true
146
+ else
147
+ false
148
+ end
149
+ end
150
+ deleted_callable
151
+ end
152
+
153
+ # Clear all hooks functions for a given event.
154
+ #
155
+ # @param [String] event_name The name of the event.
156
+ # @return [void]
157
+ def clear_event_hooks(event_name)
158
+ @hooks[event_name.to_s] = []
159
+ end
160
+
161
+ # @param [Symbol] event_name Name of the event.
162
+ # @param [Symbol] hook_name Name of the hook.
163
+ # @return [Boolean] Whether the hook by the name `hook_name`.
164
+ def hook_exists?(event_name, hook_name)
165
+ @hooks[event_name.to_s].map(&:first).include?(hook_name)
166
+ end
167
+
168
+ protected
169
+
170
+ def hooks
171
+ @hooks
172
+ end
173
+ end
174
+ end