pdk 0.1.0 → 0.2.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +50 -0
  3. data/README.md +3 -9
  4. data/exe/pdk +1 -1
  5. data/lib/pdk.rb +5 -4
  6. data/lib/pdk/cli.rb +62 -59
  7. data/lib/pdk/cli/errors.rb +1 -1
  8. data/lib/pdk/cli/exec.rb +154 -29
  9. data/lib/pdk/cli/input.rb +2 -2
  10. data/lib/pdk/cli/new.rb +12 -27
  11. data/lib/pdk/cli/new/class.rb +28 -41
  12. data/lib/pdk/cli/new/module.rb +30 -41
  13. data/lib/pdk/cli/test.rb +9 -20
  14. data/lib/pdk/cli/test/unit.rb +38 -0
  15. data/lib/pdk/cli/util/option_normalizer.rb +45 -19
  16. data/lib/pdk/cli/util/option_validator.rb +24 -20
  17. data/lib/pdk/cli/validate.rb +65 -65
  18. data/lib/pdk/generate.rb +5 -0
  19. data/lib/pdk/generators/module.rb +37 -33
  20. data/lib/pdk/generators/puppet_class.rb +1 -1
  21. data/lib/pdk/generators/puppet_object.rb +19 -20
  22. data/lib/pdk/logger.rb +1 -1
  23. data/lib/pdk/module/metadata.rb +35 -18
  24. data/lib/pdk/module/templatedir.rb +40 -33
  25. data/lib/pdk/report.rb +76 -19
  26. data/lib/pdk/report/event.rb +276 -0
  27. data/lib/pdk/template_file.rb +8 -6
  28. data/lib/pdk/tests/unit.rb +8 -3
  29. data/lib/pdk/util.rb +65 -0
  30. data/lib/pdk/util/bundler.rb +167 -0
  31. data/lib/pdk/util/version.rb +34 -0
  32. data/lib/pdk/validate.rb +3 -4
  33. data/lib/pdk/validators/base_validator.rb +60 -4
  34. data/lib/pdk/validators/metadata.rb +29 -0
  35. data/lib/pdk/validators/puppet/puppet_lint.rb +47 -0
  36. data/lib/pdk/validators/puppet/puppet_parser.rb +34 -0
  37. data/lib/pdk/validators/puppet_validator.rb +30 -0
  38. data/lib/pdk/validators/ruby/rubocop.rb +59 -0
  39. data/lib/pdk/validators/ruby_validator.rb +29 -0
  40. data/lib/pdk/version.rb +1 -1
  41. data/lib/puppet/util/windows.rb +14 -0
  42. data/lib/puppet/util/windows/api_types.rb +278 -0
  43. data/lib/puppet/util/windows/file.rb +488 -0
  44. data/lib/puppet/util/windows/string.rb +16 -0
  45. data/locales/de/pdk.po +263 -78
  46. data/locales/pdk.pot +224 -65
  47. metadata +60 -8
  48. data/lib/pdk/cli/tests/unit.rb +0 -52
  49. data/lib/pdk/validators/puppet_lint.rb +0 -17
  50. data/lib/pdk/validators/puppet_parser.rb +0 -17
  51. data/lib/pdk/validators/ruby_lint.rb +0 -17
@@ -34,7 +34,7 @@ module PDK
34
34
  # @raise [ArgumentError] (see #validate_module_template!)
35
35
  #
36
36
  # @api public
37
- def initialize(path_or_url, &block)
37
+ def initialize(path_or_url)
38
38
  if File.directory?(path_or_url)
39
39
  @path = path_or_url
40
40
  else
@@ -47,12 +47,13 @@ module PDK
47
47
  temp_dir = PDK::Util.make_tmpdir_name('pdk-module-template')
48
48
 
49
49
  clone_result = PDK::CLI::Exec.git('clone', path_or_url, temp_dir)
50
- unless clone_result[:exit_code] == 0
50
+ unless clone_result[:exit_code].zero?
51
51
  PDK.logger.error clone_result[:stdout]
52
52
  PDK.logger.error clone_result[:stderr]
53
- raise PDK::CLI::FatalError, _("Unable to clone git repository '%{repo}' to '%{dest}'") % {:repo => path_or_url, :dest => temp_dir}
53
+ raise PDK::CLI::FatalError, _("Unable to clone git repository '%{repo}' to '%{dest}'") % { repo: path_or_url, dest: temp_dir }
54
54
  end
55
- @path = temp_dir
55
+
56
+ @path = PDK::Util.canonical_path(temp_dir)
56
57
  @repo = path_or_url
57
58
  end
58
59
 
@@ -78,13 +79,13 @@ module PDK
78
79
  #
79
80
  # @api public
80
81
  def metadata
81
- if @repo
82
- ref_result = PDK::CLI::Exec.git('--git-dir', File.join(@path, '.git'), 'describe', '--all', '--long')
83
- if ref_result[:exit_code] == 0
84
- {'template-url' => @repo, 'template-ref' => ref_result[:stdout].strip}
85
- else
86
- {}
87
- end
82
+ return {} unless @repo
83
+
84
+ ref_result = PDK::CLI::Exec.git('--git-dir', File.join(@path, '.git'), 'describe', '--all', '--long')
85
+ if ref_result[:exit_code].zero?
86
+ { 'template-url' => @repo, 'template-ref' => ref_result[:stdout].strip }
87
+ else
88
+ {}
88
89
  end
89
90
  end
90
91
 
@@ -101,18 +102,18 @@ module PDK
101
102
  # @return [void]
102
103
  #
103
104
  # @api public
104
- def render(&block)
105
+ def render
105
106
  files_in_template.each do |template_file|
106
- PDK.logger.debug(_("Rendering '%{template}'...") % {:template => template_file})
107
- dest_path = template_file.sub(/\.erb\Z/, '')
107
+ PDK.logger.debug(_("Rendering '%{template}'...") % { template: template_file })
108
+ dest_path = template_file.sub(%r{\.erb\Z}, '')
108
109
 
109
110
  begin
110
- dest_content = PDK::TemplateFile.new(File.join(@moduleroot_dir, template_file), {:configs => config_for(dest_path)}).render
111
+ dest_content = PDK::TemplateFile.new(File.join(@moduleroot_dir, template_file), configs: config_for(dest_path)).render
111
112
  rescue => e
112
113
  error_msg = _(
113
- "Failed to render template '%{template}'\n" +
114
- "%{exception}: %{message}"
115
- ) % {:template => template_file, :exception => e.class, :message => e.message}
114
+ "Failed to render template '%{template}'\n" \
115
+ '%{exception}: %{message}',
116
+ ) % { template: template_file, exception: e.class, message: e.message }
116
117
  raise PDK::CLI::FatalError, error_msg
117
118
  end
118
119
 
@@ -134,11 +135,11 @@ module PDK
134
135
  #
135
136
  # @api public
136
137
  def object_template_for(object_type)
137
- object_path = File.join(@object_dir, "#{object_type.to_s}.erb")
138
- spec_path = File.join(@object_dir, "#{object_type.to_s}_spec.erb")
138
+ object_path = File.join(@object_dir, "#{object_type}.erb")
139
+ spec_path = File.join(@object_dir, "#{object_type}_spec.erb")
139
140
 
140
141
  if File.file?(object_path) && File.readable?(object_path)
141
- result = {object: object_path}
142
+ result = { object: object_path }
142
143
  result[:spec] = spec_path if File.file?(spec_path) && File.readable?(spec_path)
143
144
  result
144
145
  else
@@ -159,7 +160,9 @@ module PDK
159
160
  def object_config
160
161
  config_for(nil)
161
162
  end
162
- private
163
+
164
+ private
165
+
163
166
  # Validate the content of the template directory.
164
167
  #
165
168
  # @raise [ArgumentError] If the specified path is not a directory.
@@ -171,11 +174,11 @@ module PDK
171
174
  # @api private
172
175
  def validate_module_template!
173
176
  unless File.directory?(@path)
174
- raise ArgumentError, _("The specified template '%{path}' is not a directory") % {:path => @path}
177
+ raise ArgumentError, _("The specified template '%{path}' is not a directory") % { path: @path }
175
178
  end
176
179
 
177
- unless File.directory?(@moduleroot_dir)
178
- raise ArgumentError, _("The template at '%{path}' does not contain a moduleroot directory") % {:path => @path}
180
+ unless File.directory?(@moduleroot_dir) # rubocop:disable Style/GuardClause
181
+ raise ArgumentError, _("The template at '%{path}' does not contain a moduleroot directory") % { path: @path }
179
182
  end
180
183
  end
181
184
 
@@ -186,11 +189,15 @@ module PDK
186
189
  #
187
190
  # @api private
188
191
  def files_in_template
189
- @files ||= Dir.glob(File.join(@moduleroot_dir, "**", "*"), File::FNM_DOTMATCH).select { |template_path|
190
- File.file?(template_path) && !File.symlink?(template_path)
191
- }.map { |template_path|
192
- template_path.sub(/\A#{Regexp.escape(@moduleroot_dir)}#{Regexp.escape(File::SEPARATOR)}/, '')
193
- }
192
+ @files ||= begin
193
+ template_paths = Dir.glob(File.join(@moduleroot_dir, '**', '*'), File::FNM_DOTMATCH).select do |template_path|
194
+ File.file?(template_path) && !File.symlink?(template_path)
195
+ end
196
+
197
+ template_paths.map do |template_path|
198
+ template_path.sub(%r{\A#{Regexp.escape(@moduleroot_dir)}#{Regexp.escape(File::SEPARATOR)}}, '')
199
+ end
200
+ end
194
201
  end
195
202
 
196
203
  # Generate a hash of data to be used when rendering the specified
@@ -213,9 +220,9 @@ module PDK
213
220
 
214
221
  if File.file?(config_path) && File.readable?(config_path)
215
222
  begin
216
- @config = YAML.load(File.read(config_path))
217
- rescue
218
- PDK.logger.warn(_("'%{file}' is not a valid YAML file") % {:file => config_path})
223
+ @config = YAML.safe_load(File.read(config_path), [], [], true)
224
+ rescue StandardError => e
225
+ PDK.logger.warn(_("'%{file}' is not a valid YAML file: %{message}") % { file: config_path, message: e.message })
219
226
  @config = {}
220
227
  end
221
228
  else
data/lib/pdk/report.rb CHANGED
@@ -1,38 +1,95 @@
1
+ require 'rexml/document'
2
+ require 'time'
3
+ require 'pdk/report/event'
4
+ require 'socket'
5
+
1
6
  module PDK
2
7
  class Report
3
- def initialize(path, format = nil)
4
- @path = path
5
- @format = format || self.class.default_format
6
- end
7
-
8
+ # @return [Array<String>] the list of supported report formats.
8
9
  def self.formats
9
- @report_formats ||= ['junit', 'text'].freeze
10
+ @report_formats ||= %w[junit text].freeze
10
11
  end
11
12
 
13
+ # @return [Symbol] the method name of the default report format.
12
14
  def self.default_format
13
- 'junit'
15
+ :to_text
14
16
  end
15
17
 
18
+ # @return [#write] the default target to write the report to.
16
19
  def self.default_target
17
- 'stdout' # TODO: actually write to stdout
20
+ $stdout
18
21
  end
19
22
 
20
- def write(text)
21
- if @format == 'junit'
22
- report = prepare_junit(text)
23
- elsif @format == 'text'
24
- report = prepare_text(text)
25
- end
23
+ # Memoised access to the report event storage hash.
24
+ #
25
+ # The keys of the Hash are the source names of the Events (see
26
+ # PDK::Report::Event#source).
27
+ #
28
+ # @example accessing events from the puppet-lint validator
29
+ # report = PDK::Report.new
30
+ # report.events['puppet-lint']
31
+ #
32
+ # @return [Hash{String=>Array<PDK::Report::Event>}] the events stored in
33
+ # the repuort.
34
+ def events
35
+ @events ||= {}
36
+ end
26
37
 
27
- File.open(@path, 'a') { |f| f.write(report) }
38
+ # Create a new PDK::Report::Event from a hash of values and add it to the
39
+ # report.
40
+ #
41
+ # @param data [Hash] (see PDK::Report::Event#initialize)
42
+ def add_event(data)
43
+ (events[data[:source]] ||= []) << PDK::Report::Event.new(data)
28
44
  end
29
45
 
30
- def prepare_junit(text)
31
- "junit: #{text}"
46
+ # Renders the report as a JUnit XML document.
47
+ #
48
+ # @param target [#write] an IO object that the report will be written to.
49
+ # Defaults to PDK::Report.default_target.
50
+ def to_junit(target = self.class.default_target)
51
+ document = REXML::Document.new
52
+ document << REXML::XMLDecl.new
53
+ testsuites = REXML::Element.new('testsuites')
54
+
55
+ id = 0
56
+ events.each do |testsuite_name, testcases|
57
+ testsuite = REXML::Element.new('testsuite')
58
+ testsuite.attributes['name'] = testsuite_name
59
+ testsuite.attributes['tests'] = testcases.length
60
+ testsuite.attributes['errors'] = testcases.select(&:error?).length
61
+ testsuite.attributes['failures'] = testcases.select(&:failure?).length
62
+ testsuite.attributes['time'] = 0
63
+ testsuite.attributes['timestamp'] = Time.now.strftime('%Y-%m-%dT%H:%M:%S')
64
+ testsuite.attributes['hostname'] = Socket.gethostname
65
+ testsuite.attributes['id'] = id
66
+ testsuite.attributes['package'] = testsuite_name
67
+ testsuite.add_element('properties')
68
+ testcases.each { |r| testsuite.elements << r.to_junit }
69
+ testsuite.add_element('system-out')
70
+ testsuite.add_element('system-err')
71
+
72
+ testsuites.elements << testsuite
73
+ id += 1
74
+ end
75
+
76
+ document.elements << testsuites
77
+ document.write(target, 2)
32
78
  end
33
79
 
34
- def prepare_text(text)
35
- "text: #{text}"
80
+ # Renders the report as plain text.
81
+ #
82
+ # This report is designed for interactive use by a human and so excludes
83
+ # all passing events in order to be consise.
84
+ #
85
+ # @param target [#write] an IO object that the report will be written to.
86
+ # Defaults to PDK::Report.default_target.
87
+ def to_text(target = self.class.default_target)
88
+ events.each do |_tool, tool_events|
89
+ tool_events.each do |event|
90
+ target.puts(event.to_text) unless event.pass?
91
+ end
92
+ end
36
93
  end
37
94
  end
38
95
  end
@@ -0,0 +1,276 @@
1
+ require 'rexml/document'
2
+ require 'pathname'
3
+
4
+ module PDK
5
+ class Report
6
+ class Event
7
+ # @return [String] The path to the file that the event is in reference
8
+ # to.
9
+ attr_reader :file
10
+
11
+ # @return [Integer] The line number in the file that the event is in
12
+ # reference to.
13
+ attr_reader :line
14
+
15
+ # @return [Integer] The column number in the line of the file that the
16
+ # event is in reference to.
17
+ attr_reader :column
18
+
19
+ # @return [String] The name of the source of the event (usually the name
20
+ # of the validation or testing tool that generated the event).
21
+ attr_reader :source
22
+
23
+ # @return [String] A freeform String containing a human readable message
24
+ # describing the event.
25
+ attr_reader :message
26
+
27
+ # @return [String] The severity of the event as reported by the
28
+ # underlying tool.
29
+ attr_reader :severity
30
+
31
+ # @return [String] The name of the test that generated the event.
32
+ attr_reader :test
33
+
34
+ # @return [Symbol] The state of the event. :passed, :failure, :error, or
35
+ # :skipped.
36
+ attr_reader :state
37
+
38
+ # Initailises a new PDK::Report::Event object.
39
+ #
40
+ # @param data [Hash{Symbol=>Object}
41
+ # @option data [String] :file (see #file)
42
+ # @option data [Integer] :line (see #line)
43
+ # @option data [Integer] :column (see #column)
44
+ # @option data [String] :source (see #source)
45
+ # @option data [String] :message (see #message)
46
+ # @option data [String] :severity (see #severity)
47
+ # @option data [String] :test (see #test)
48
+ # @option data [Symbol] :state (see #state)
49
+ #
50
+ # @raise [ArgumentError] (see #sanitise_data)
51
+ def initialize(data)
52
+ sanitise_data(data).each do |key, value|
53
+ instance_variable_set("@#{key}", value)
54
+ end
55
+ end
56
+
57
+ # Checks if the event is the result of a passing test.
58
+ #
59
+ # @return [Boolean] true if the test passed, otherwise false.
60
+ def pass?
61
+ state == :passed
62
+ end
63
+
64
+ # Checks if the event is the result of a test that could not complete due
65
+ # to an error.
66
+ #
67
+ # @return [Boolean] true if the test did not complete, otherwise false.
68
+ def error?
69
+ state == :error
70
+ end
71
+
72
+ # Checks if the event is the result of a failing test.
73
+ #
74
+ # @return [Boolean] true if the test failed, otherwise false.
75
+ def failure?
76
+ state == :failure
77
+ end
78
+
79
+ # Checks if the event is the result of test that was not run.
80
+ #
81
+ # @return [Boolean] true if the test was skipped, otherwise false.
82
+ def skipped?
83
+ state == :skipped
84
+ end
85
+
86
+ # Renders the event in a clang style text format.
87
+ #
88
+ # @return [String] The rendered event.
89
+ def to_text
90
+ location = [file, line, column].compact.join(':')
91
+
92
+ [location, severity, message].compact.join(': ')
93
+ end
94
+
95
+ # Renders the event as a JUnit XML testcase.
96
+ #
97
+ # @return [REXML::Element] The rendered event.
98
+ def to_junit
99
+ testcase = REXML::Element.new('testcase')
100
+ testcase.attributes['classname'] = [source, test].compact.join('.')
101
+ testcase.attributes['name'] = [file, line, column].compact.join(':')
102
+ testcase.attributes['time'] = 0
103
+
104
+ if failure?
105
+ failure = REXML::Element.new('failure')
106
+ failure.attributes['type'] = severity
107
+ failure.attributes['message'] = message
108
+ failure.text = to_text
109
+ testcase.elements << failure
110
+ elsif skipped?
111
+ testcase.add_element('skipped')
112
+ end
113
+
114
+ testcase
115
+ end
116
+
117
+ private
118
+
119
+ # Processes the data hash used to initialise the event, validating and
120
+ # munging the values as necessary.
121
+ #
122
+ # @param data [Hash{Symbol => Object}] (see #initialize)
123
+ #
124
+ # @return [Hash{Symbol => String}] A copy of the data hash passed to the
125
+ # method with sanitised values.
126
+ #
127
+ # @raise [ArgumentError] (see #sanitise_file)
128
+ # @raise [ArgumentError] (see #sanitise_state)
129
+ # @raise [ArgumentError] (see #sanitise_source)
130
+ def sanitise_data(data)
131
+ result = data.dup
132
+ data.each do |key, value|
133
+ key = key.to_sym unless key.is_a?(Symbol)
134
+ method = "sanitise_#{key}"
135
+ result[key] = send(method, value) if respond_to?(method, true)
136
+ end
137
+
138
+ result
139
+ end
140
+
141
+ # Munges and validates the file path used to instantiate the event.
142
+ #
143
+ # If the path is an absolute path, it will be rewritten so that it is
144
+ # relative to the module root instead.
145
+ #
146
+ # @param value [String] The path to the file that the event is
147
+ # describing.
148
+ #
149
+ # @return [String] The path to the file, relative to the module root.
150
+ #
151
+ # @raise [ArgumentError] if the value is nil, an empty String, or not
152
+ # a String.
153
+ def sanitise_file(value)
154
+ if value.nil? || (value.is_a?(String) && value.empty?)
155
+ raise ArgumentError, _('file not specified')
156
+ end
157
+
158
+ unless value.is_a?(String)
159
+ raise ArgumentError, _('file must be a String')
160
+ end
161
+
162
+ path = Pathname.new(value)
163
+
164
+ if path.absolute?
165
+ module_root = Pathname.new(PDK::Util.module_root)
166
+ path.relative_path_from(module_root).to_path
167
+ else
168
+ path.to_path
169
+ end
170
+ end
171
+
172
+ # Munges and validates the state of the event.
173
+ #
174
+ # The valid event states are:
175
+ # :passed - The event represents a passing test.
176
+ # :error - The event represents a test that could not be completed due
177
+ # to an unexpected error.
178
+ # :failure - The event represents a failing test.
179
+ # :skipped - The event represents a test that was skipped.
180
+ #
181
+ # @param value [Symbol, String] The state of the event. If passed as
182
+ # a String, it will be turned into a Symbol before validation.
183
+ #
184
+ # @return [Symbol] The sanitised state type.
185
+ #
186
+ # @raise [ArgumentError] if the value is nil, an empty String, or not
187
+ # a String or Symbol representation of a valid state.
188
+ def sanitise_state(value)
189
+ if value.nil? || (value.is_a?(String) && value.empty?)
190
+ raise ArgumentError, _('state not specified')
191
+ end
192
+
193
+ value = value.to_sym if value.is_a?(String)
194
+ unless value.is_a?(Symbol)
195
+ raise ArgumentError, _('state must be a Symbol, not %{type}') % { type: value.class }
196
+ end
197
+
198
+ valid_states = [:passed, :error, :failure, :skipped]
199
+ unless valid_states.include?(value)
200
+ raise ArgumentError, _('Invalid state %{state}, valid states are: %{valid}') % {
201
+ state: value.inspect,
202
+ valid: valid_states.map(&:inspect).join(', '),
203
+ }
204
+ end
205
+
206
+ value
207
+ end
208
+
209
+ # Validates the source of the event.
210
+ #
211
+ # @param value [String, Symbol] The name of the source of the event.
212
+ #
213
+ # @return [String] the value passed to the event, converted to a String
214
+ # if necessary.
215
+ #
216
+ # @raise [ArgumentError] if the value is nil or an empty String.
217
+ def sanitise_source(value)
218
+ if value.nil? || (value.is_a?(String) && value.empty?)
219
+ raise ArgumentError, _('source not specified')
220
+ end
221
+
222
+ value.to_s
223
+ end
224
+
225
+ # Munges the line number of the event into an Integer.
226
+ #
227
+ # @param value [Integer, String, Fixnum] The line number.
228
+ #
229
+ # @return [Integer] the provided value, converted into an Integer if
230
+ # necessary.
231
+ def sanitise_line(value)
232
+ return nil if value.nil?
233
+
234
+ valid_types = [String, Integer]
235
+ if RUBY_VERSION.split('.')[0..1].join('.').to_f < 2.4
236
+ valid_types << Fixnum # rubocop:disable Lint/UnifiedInteger
237
+ end
238
+
239
+ unless valid_types.include?(value.class)
240
+ raise ArgumentError, _('line must be an Integer or a String representation of an Integer')
241
+ end
242
+
243
+ if value.is_a?(String) && value !~ %r{\A[0-9]+\Z}
244
+ raise ArgumentError, _('the line number can only contain the digits 0-9')
245
+ end
246
+
247
+ value.to_i
248
+ end
249
+
250
+ # Munges the column number of the event into an Integer.
251
+ #
252
+ # @param value [Integer, String, Fixnum] The column number.
253
+ #
254
+ # @return [Integer] the provided value, converted into an Integer if
255
+ # necessary.
256
+ def sanitise_column(value)
257
+ return nil if value.nil?
258
+
259
+ valid_types = [String, Integer]
260
+ if RUBY_VERSION.split('.')[0..1].join('.').to_f < 2.4
261
+ valid_types << Fixnum # rubocop:disable Lint/UnifiedInteger
262
+ end
263
+
264
+ unless valid_types.include?(value.class)
265
+ raise ArgumentError, _('column must be an Integer or a String representation of an Integer')
266
+ end
267
+
268
+ if value.is_a?(String) && value !~ %r{\A[0-9]+\Z}
269
+ raise ArgumentError, _('the column number can only contain the digits 0-9')
270
+ end
271
+
272
+ value.to_i
273
+ end
274
+ end
275
+ end
276
+ end