puppet-debugger 0.6.1 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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