puppet-debugger 0.4.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 (46) hide show
  1. checksums.yaml +7 -0
  2. data/.document +5 -0
  3. data/.gitignore +54 -0
  4. data/.gitlab-ci.yml +129 -0
  5. data/.rspec +3 -0
  6. data/CHANGELOG.md +61 -0
  7. data/Gemfile +18 -0
  8. data/Gemfile.lock +67 -0
  9. data/LICENSE.txt +20 -0
  10. data/README.md +276 -0
  11. data/Rakefile +32 -0
  12. data/bin/pdb +4 -0
  13. data/lib/awesome_print/ext/awesome_puppet.rb +40 -0
  14. data/lib/puppet-debugger/cli.rb +247 -0
  15. data/lib/puppet-debugger/code/code_file.rb +98 -0
  16. data/lib/puppet-debugger/code/code_range.rb +69 -0
  17. data/lib/puppet-debugger/code/loc.rb +80 -0
  18. data/lib/puppet-debugger/debugger_code.rb +318 -0
  19. data/lib/puppet-debugger/support/compiler.rb +20 -0
  20. data/lib/puppet-debugger/support/environment.rb +38 -0
  21. data/lib/puppet-debugger/support/errors.rb +75 -0
  22. data/lib/puppet-debugger/support/facts.rb +78 -0
  23. data/lib/puppet-debugger/support/functions.rb +72 -0
  24. data/lib/puppet-debugger/support/input_responders.rb +136 -0
  25. data/lib/puppet-debugger/support/node.rb +90 -0
  26. data/lib/puppet-debugger/support/play.rb +91 -0
  27. data/lib/puppet-debugger/support/scope.rb +42 -0
  28. data/lib/puppet-debugger/support.rb +176 -0
  29. data/lib/puppet-debugger.rb +55 -0
  30. data/lib/trollop.rb +861 -0
  31. data/lib/version.rb +3 -0
  32. data/puppet-debugger.gemspec +36 -0
  33. data/run_container_test.sh +12 -0
  34. data/spec/facts_spec.rb +86 -0
  35. data/spec/fixtures/environments/production/manifests/site.pp +1 -0
  36. data/spec/fixtures/invalid_node_obj.yaml +8 -0
  37. data/spec/fixtures/node_obj.yaml +298 -0
  38. data/spec/fixtures/sample_manifest.pp +2 -0
  39. data/spec/fixtures/sample_start_debugger.pp +13 -0
  40. data/spec/pdb_spec.rb +50 -0
  41. data/spec/puppet-debugger_spec.rb +492 -0
  42. data/spec/remote_node_spec.rb +170 -0
  43. data/spec/spec_helper.rb +57 -0
  44. data/spec/support_spec.rb +190 -0
  45. data/test_matrix.rb +42 -0
  46. metadata +148 -0
@@ -0,0 +1,40 @@
1
+ module AwesomePrint
2
+ module Puppet
3
+ def self.included(base)
4
+ base.send :alias_method, :cast_without_puppet_resource, :cast
5
+ base.send :alias_method, :cast, :cast_with_puppet_resource
6
+ end
7
+
8
+ # this tells ap how to cast our object so we can be specific
9
+ # about printing different puppet objects
10
+ def cast_with_puppet_resource(object, type)
11
+ cast = cast_without_puppet_resource(object, type)
12
+ # check the object to see if it has an acestor (< ) of the specified type
13
+ if (defined?(::Puppet::Type)) && (object.class < ::Puppet::Type)
14
+ cast = :puppet_type
15
+ elsif (defined?(::Puppet::Pops::Types)) && (object.class < ::Puppet::Pops::Types)
16
+ cast = :puppet_type
17
+ elsif (defined?(::Puppet::Parser::Resource)) && (object.class < ::Puppet::Parser::Resource)
18
+ cast = :puppet_resource
19
+ elsif /Puppet::Pops::Types/.match(object.class.to_s)
20
+ cast = :puppet_type
21
+ end
22
+ cast
23
+ end
24
+
25
+ def awesome_puppet_resource(object)
26
+ return '' if object.nil?
27
+ awesome_puppet_type(object.to_ral)
28
+ end
29
+
30
+ def awesome_puppet_type(object)
31
+ return '' if object.nil?
32
+ return object.to_s unless object.respond_to?(:name) && object.respond_to?(:title)
33
+ h = object.to_hash.merge(:name => object.name, :title => object.title)
34
+ res_str = awesome_hash(h)
35
+ "#{object.class} #{res_str.gsub(':', '')}"
36
+ end
37
+ end
38
+ end
39
+
40
+ AwesomePrint::Formatter.send(:include, AwesomePrint::Puppet)
@@ -0,0 +1,247 @@
1
+ require 'puppet'
2
+ require 'readline'
3
+ require 'json'
4
+ require_relative 'support'
5
+
6
+ module PuppetDebugger
7
+ class Cli
8
+ include PuppetDebugger::Support
9
+
10
+ attr_accessor :settings, :log_level, :in_buffer, :out_buffer, :html_mode
11
+
12
+ def initialize(options={})
13
+ @log_level = 'notice'
14
+ @out_buffer = options[:out_buffer] || $stdout
15
+ @html_mode = options[:html_mode] || false
16
+ @source_file = options[:source_file] || nil
17
+ @source_line_num = options[:source_line] || nil
18
+ @in_buffer = options[:in_buffer] || $stdin
19
+ comp = Proc.new do |s|
20
+ key_words.grep(/^#{Regexp.escape(s)}/)
21
+ end
22
+ Readline.completion_append_character = ""
23
+ Readline.basic_word_break_characters = " "
24
+ Readline.completion_proc = comp
25
+ AwesomePrint.defaults = {
26
+ :html => @html_mode,
27
+ :sort_keys => true,
28
+ :indent => 2
29
+ }
30
+ do_initialize
31
+ end
32
+
33
+ # returns a cached list of key words
34
+ def key_words
35
+ # because dollar signs don't work we can't display a $ sign in the keyword
36
+ # list so its not explicitly clear what the keyword
37
+ variables = scope.to_hash.keys
38
+ # prepend a :: to topscope variables
39
+ scoped_vars = variables.map { |k,v| scope.compiler.topscope.exist?(k) ? "$::#{k}" : "$#{k}" }
40
+ # append a () to functions so we know they are functions
41
+ funcs = function_map.keys.map { |k| "#{k.split('::').last}()"}
42
+ (scoped_vars + funcs + static_responder_list).uniq.sort
43
+ end
44
+
45
+ # looks up the type in the catalog by using the type and title
46
+ # and returns the resource in ral format
47
+ def to_resource_declaration(type)
48
+ if type.respond_to?(:type_name) and type.respond_to?(:title)
49
+ title = type.title
50
+ type_name = type.type_name
51
+ elsif type_result = /(\w+)\['?(\w+)'?\]/.match(type.to_s)
52
+ # not all types have a type_name and title so we
53
+ # output to a string and parse the results
54
+ title = type_result[2]
55
+ type_name = type_result[1]
56
+ else
57
+ return type
58
+ end
59
+ res = scope.catalog.resource(type_name, title)
60
+ if res
61
+ return res.to_ral
62
+ end
63
+ # don't return anything or returns nil if item is not in the catalog
64
+ end
65
+
66
+ # ruturns a formatted array
67
+ def expand_resource_type(types)
68
+ output = [types].flatten.map do |t|
69
+ if t.class.to_s =~ /Puppet::Pops::Types/
70
+ to_resource_declaration(t)
71
+ else
72
+ t
73
+ end
74
+ end
75
+ output
76
+ end
77
+
78
+ def normalize_output(result)
79
+ if result.instance_of?(Array)
80
+ output = expand_resource_type(result)
81
+ if output.count == 1
82
+ return output.first
83
+ end
84
+ return output
85
+ elsif result.class.to_s =~ /Puppet::Pops::Types/
86
+ return to_resource_declaration(result)
87
+ end
88
+ result
89
+ end
90
+
91
+ # this method handles all input and expects a string of text.
92
+ #
93
+ def handle_input(input)
94
+ raise ArgumentError unless input.instance_of?(String)
95
+ begin
96
+ output = ''
97
+ case input
98
+ when /^play|^classification|^whereami|^facterdb_filter|^facts|^vars|^functions|^classes|^resources|^krt|^environment|^reset|^help/
99
+ args = input.split(' ')
100
+ command = args.shift.to_sym
101
+ if self.respond_to?(command)
102
+ output = self.send(command, args)
103
+ end
104
+ return out_buffer.puts output
105
+ when /exit/
106
+ exit 0
107
+ when /^:set/
108
+ output = handle_set(input)
109
+ when '_'
110
+ output = " => #{@last_item}"
111
+ else
112
+ result = puppet_eval(input)
113
+ @last_item = result
114
+ output = normalize_output(result)
115
+ if output.nil?
116
+ output = ""
117
+ else
118
+ output = output.ai
119
+ end
120
+ end
121
+ rescue LoadError => e
122
+ output = e.message.fatal
123
+ rescue Errno::ETIMEDOUT => e
124
+ output = e.message.fatal
125
+ rescue ArgumentError => e
126
+ output = e.message.fatal
127
+ rescue Puppet::ResourceError => e
128
+ output = e.message.fatal
129
+ rescue Puppet::Error => e
130
+ output = e.message.fatal
131
+ rescue Puppet::ParseErrorWithIssue => e
132
+ output = e.message.fatal
133
+ rescue PuppetDebugger::Exception::FatalError => e
134
+ output = e.message.fatal
135
+ out_buffer.puts output
136
+ exit 1 # this can sometimes causes tests to fail
137
+ rescue PuppetDebugger::Exception::Error => e
138
+ output = e.message.fatal
139
+ end
140
+ unless output.empty?
141
+ out_buffer.print " => "
142
+ out_buffer.puts output unless output.empty?
143
+ end
144
+ end
145
+
146
+ def self.print_repl_desc
147
+ output = <<-EOT
148
+ Ruby Version: #{RUBY_VERSION}
149
+ Puppet Version: #{Puppet.version}
150
+ Puppet Debugger Version: #{PuppetDebugger::VERSION}
151
+ Created by: NWOps <corey@nwops.io>
152
+ Type "exit", "functions", "vars", "krt", "whereami", "facts", "resources", "classes",
153
+ "play", "classification", "reset", or "help" for more information.
154
+
155
+ EOT
156
+ output
157
+ end
158
+
159
+ # tries to determine if the input is going to be a multiline input
160
+ # by reading the parser error message
161
+ def multiline_input?(e)
162
+ case e.message
163
+ when /Syntax error at end of file/i
164
+ true
165
+ else
166
+ false
167
+ end
168
+ end
169
+
170
+ # reads input from stdin, since readline requires a tty
171
+ # we cannot read from other sources as readline requires a file object
172
+ # we parse the string after each input to determine if the input is a multiline_input
173
+ # entry. If it is multiline we run through the loop again and concatenate the
174
+ # input
175
+ def read_loop
176
+ line_number = 1
177
+ full_buffer = ''
178
+ while buf = Readline.readline("#{line_number}:>> ", true)
179
+ begin
180
+ full_buffer += buf
181
+ # unless this is puppet code, otherwise skip repl keywords
182
+ unless keyword_expression.match(buf)
183
+ line_number = line_number.next
184
+ parser.parse_string(full_buffer)
185
+ end
186
+ rescue Puppet::ParseErrorWithIssue => e
187
+ if multiline_input?(e)
188
+ out_buffer.print ' '
189
+ full_buffer += "\n"
190
+ next
191
+ end
192
+ end
193
+ handle_input(full_buffer)
194
+ full_buffer = ''
195
+ end
196
+ end
197
+
198
+ # used to start a repl without attempting to read from stdin
199
+ # or
200
+ # @param [Hash] must contain at least the puppet scope object
201
+ def self.start_without_stdin(options={:scope => nil})
202
+ puts print_repl_desc unless options[:quiet]
203
+ repl_obj = PuppetDebugger::Cli.new(options)
204
+ repl_obj.remote_node_name = options[:node_name] if options[:node_name]
205
+ repl_obj.initialize_from_scope(options[:scope])
206
+ puts repl_obj.whereami if options[:source_file] and options[:source_line]
207
+ if options[:play]
208
+ repl_obj.play_back(options)
209
+ elsif ! options[:run_once]
210
+ repl_obj.read_loop
211
+ end
212
+ end
213
+
214
+ # start reads from stdin or from a file
215
+ # if from stdin, the repl will process the input and exit
216
+ # if from a file, the repl will process the file and continue to prompt
217
+ # @param [Hash] puppet scope object
218
+ def self.start(options={:scope => nil})
219
+ opts = Trollop::options do
220
+ opt :play, "Url or file to load from", :required => false, :type => String
221
+ opt :run_once, "Evaluate and quit", :required => false, :default => false
222
+ opt :node_name, "Remote Node to grab facts from", :required => false, :type => String
223
+ opt :quiet, "Do not display banner", :required => false, :default => false
224
+ end
225
+ options = opts.merge(options)
226
+ puts print_repl_desc unless options[:quiet]
227
+ repl_obj = PuppetDebugger::Cli.new(options)
228
+ repl_obj.remote_node_name = opts[:node_name] if opts[:node_name]
229
+ repl_obj.initialize_from_scope(options[:scope])
230
+ if options[:play]
231
+ repl_obj.play_back(opts)
232
+ # when the user supplied a file name without using the args (stdin)
233
+ elsif ARGF.filename != "-"
234
+ path = File.expand_path(ARGF.filename)
235
+ repl_obj.play_back(:play => path)
236
+ # when the user supplied a file content using stdin, aka. cat,pipe,echo or redirection
237
+ elsif ARGF.filename == "-" and (not STDIN.tty? and not STDIN.closed?)
238
+ input = ARGF.read
239
+ repl_obj.handle_input(input)
240
+ end
241
+ # helper code to make tests exit the loop
242
+ unless options[:run_once]
243
+ repl_obj.read_loop
244
+ end
245
+ end
246
+ end
247
+ end
@@ -0,0 +1,98 @@
1
+
2
+ class CodeFile
3
+ class SourceNotFound < Exception
4
+ end
5
+
6
+ DEFAULT_EXT = '.pp'
7
+
8
+ # List of all supported languages.
9
+ # @return [Hash]
10
+ EXTENSIONS = {
11
+ %w(.py) => :python,
12
+ %w(.js) => :javascript,
13
+ %w(.pp) => :puppet,
14
+ %w(.css) => :css,
15
+ %w(.xml) => :xml,
16
+ %w(.php) => :php,
17
+ %w(.html) => :html,
18
+ %w(.diff) => :diff,
19
+ %w(.java) => :java,
20
+ %w(.json) => :json,
21
+ %w(.c .h) => :c,
22
+ %w(.rhtml) => :rhtml,
23
+ %w(.yaml .yml) => :yaml,
24
+ %w(.cpp .hpp .cc .h cxx) => :cpp,
25
+ %w(.rb .ru .irbrc .gemspec .pryrc) => :ruby,
26
+ }
27
+
28
+ # @return [Symbol] The type of code stored in this wrapper.
29
+ attr_reader :code_type
30
+
31
+ # @param [String] filename The name of a file with code to be detected
32
+ # @param [Symbol] code_type The type of code the `filename` contains
33
+ def initialize(filename, code_type = type_from_filename(filename))
34
+ @filename = filename
35
+ @code_type = code_type
36
+ end
37
+
38
+ # @return [String] The code contained in the current `@filename`.
39
+ def code
40
+ path = abs_path
41
+ @code_type = type_from_filename(path)
42
+ File.read(path)
43
+ end
44
+
45
+ private
46
+
47
+ # @raise [MethodSource::SourceNotFoundError] if the `filename` is not
48
+ # readable for some reason.
49
+ # @return [String] absolute path for the given `filename`.
50
+ def abs_path
51
+ code_path.detect { |path| readable?(path) } or
52
+ raise SourceNotFound,
53
+ "Cannot open #{ @filename.inspect } for reading."
54
+ end
55
+
56
+ # @param [String] path
57
+ # @return [Boolean] if the path, with or without the default ext,
58
+ # is a readable file then `true`, otherwise `false`.
59
+ def readable?(path)
60
+ File.readable?(path) && !File.directory?(path) or
61
+ File.readable?(path << DEFAULT_EXT)
62
+ end
63
+
64
+ # @return [Array] All the paths that contain code that Pry can use for its
65
+ # API's. Skips directories.
66
+ def code_path
67
+ [from_pwd, from_pry_init_pwd, *from_load_path]
68
+ end
69
+
70
+ # @param [String] filename
71
+ # @param [Symbol] default (:unknown) the file type to assume if none could be
72
+ # detected.
73
+ # @return [Symbol, nil] The CodeRay type of a file from its extension, or
74
+ # `nil` if `:unknown`.
75
+ def type_from_filename(filename, default = :unknown)
76
+ _, @code_type = EXTENSIONS.find do |k, _|
77
+ k.any? { |ext| ext == File.extname(filename) }
78
+ end
79
+
80
+ code_type || default
81
+ end
82
+
83
+ # @return [String]
84
+ def from_pwd
85
+ File.expand_path(@filename, Dir.pwd)
86
+ end
87
+
88
+ # @return [String]
89
+ def from_pry_init_pwd
90
+ File.expand_path(@filename, Dir.pwd)
91
+ end
92
+
93
+ # @return [String]
94
+ def from_load_path
95
+ $LOAD_PATH.map { |path| File.expand_path(@filename, path) }
96
+ end
97
+
98
+ end
@@ -0,0 +1,69 @@
1
+ class DebuggerCode
2
+
3
+ # Represents a range of lines in a code listing.
4
+ #
5
+ # @api private
6
+ class CodeRange
7
+
8
+ # @param [Integer] start_line
9
+ # @param [Integer?] end_line
10
+ def initialize(start_line, end_line = nil)
11
+ @start_line = start_line
12
+ @end_line = end_line
13
+ force_set_end_line
14
+ end
15
+
16
+ # @param [Array<LOC>] lines
17
+ # @return [Range]
18
+ def indices_range(lines)
19
+ Range.new(*indices(lines))
20
+ end
21
+
22
+ private
23
+
24
+ def start_line; @start_line; end
25
+ def end_line; @end_line; end
26
+
27
+ # If `end_line` is equal to `nil`, then calculate it from the first
28
+ # parameter, `start_line`. Otherwise, leave it as it is.
29
+ # @return [void]
30
+ def force_set_end_line
31
+ if start_line.is_a?(Range)
32
+ set_end_line_from_range
33
+ else
34
+ @end_line ||= start_line
35
+ end
36
+ end
37
+
38
+ # Finds indices of `start_line` and `end_line` in the given Array of
39
+ # +lines+.
40
+ #
41
+ # @param [Array<LOC>] lines
42
+ # @return [Array<Integer>]
43
+ def indices(lines)
44
+ [find_start_index(lines), find_end_index(lines)]
45
+ end
46
+
47
+ # @return [Integer]
48
+ def find_start_index(lines)
49
+ return start_line if start_line < 0
50
+ lines.index { |loc| loc.lineno >= start_line } || lines.length
51
+ end
52
+
53
+ # @return [Integer]
54
+ def find_end_index(lines)
55
+ return end_line if end_line < 0
56
+ (lines.index { |loc| loc.lineno > end_line } || 0) - 1
57
+ end
58
+
59
+ # For example, if the range is 4..10, then `start_line` would be equal to
60
+ # 4 and `end_line` to 10.
61
+ # @return [void]
62
+ def set_end_line_from_range
63
+ @end_line = start_line.last
64
+ @end_line -= 1 if start_line.exclude_end?
65
+ @start_line = start_line.first
66
+ end
67
+ end
68
+
69
+ end
@@ -0,0 +1,80 @@
1
+ class DebuggerCode
2
+
3
+ # Represents a line of code. A line of code is a tuple, which consists of a
4
+ # line and a line number. A `LOC` object's state (namely, the line
5
+ # parameter) can be changed via instance methods. `Pry::Code` heavily uses
6
+ # this class.
7
+ #
8
+ # @api private
9
+ # @example
10
+ # loc = LOC.new("def example\n :example\nend", 1)
11
+ # puts loc.line
12
+ # def example
13
+ # :example
14
+ # end
15
+ # #=> nil
16
+ #
17
+ # loc.indent(3)
18
+ # loc.line #=> " def example\n :example\nend"
19
+ class LOC
20
+
21
+ # @return [Array<String, Integer>]
22
+ attr_reader :tuple
23
+
24
+ # @param [String] line The line of code.
25
+ # @param [Integer] lineno The position of the +line+.
26
+ def initialize(line, lineno)
27
+ @tuple = [line.chomp, lineno.to_i]
28
+ end
29
+
30
+ # @return [Boolean]
31
+ def ==(other)
32
+ other.tuple == tuple
33
+ end
34
+
35
+ def dup
36
+ self.class.new(line, lineno)
37
+ end
38
+
39
+ # @return [String]
40
+ def line
41
+ tuple.first
42
+ end
43
+
44
+ # @return [Integer]
45
+ def lineno
46
+ tuple.last
47
+ end
48
+
49
+ #Prepends the line number `lineno` to the `line`.
50
+ # @param [Integer] max_width
51
+ # @return [void]
52
+ def add_line_number(max_width = 0)
53
+ padded = lineno.to_s.rjust(max_width)
54
+ tuple[0] = "#{ padded }: #{ line }"
55
+ end
56
+
57
+ # Prepends a marker "=>" or an empty marker to the +line+.
58
+ #
59
+ # @param [Integer] marker_lineno If it is equal to the `lineno`, then
60
+ # prepend a hashrocket. Otherwise, an empty marker.
61
+ # @return [void]
62
+ def add_marker(marker_lineno)
63
+ tuple[0] =
64
+ if lineno == marker_lineno
65
+ " => #{ line }".cyan
66
+ else
67
+ " #{ line }"
68
+ end
69
+ end
70
+
71
+ # Indents the `line` with +distance+ spaces.
72
+ #
73
+ # @param [Integer] distance
74
+ # @return [void]
75
+ def indent(distance)
76
+ tuple[0] = "#{ ' ' * distance }#{ line }"
77
+ end
78
+ end
79
+
80
+ end