pdk 0.1.0 → 0.2.0

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