rspec-puppet-yaml 0.1.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.
@@ -0,0 +1,512 @@
1
+ # Converts the supplied YAML data into rspec tests. When this is called from
2
+ # a *_spec.rb file from RSpec, testing begins immediately upon parsing.
3
+ #
4
+ # @param yaml_file [String] Path to a YAML file containing rspec-puppet tests
5
+ def parse_rspec_puppet_yaml(yaml_file)
6
+ test_data = __load_rspec_puppet_yaml_data(yaml_file)
7
+
8
+ # The top-most entity must be a 'describe', which must have both name (which
9
+ # must be identical to the entity-under-test) and type-of-entity (in case the
10
+ # user failed to follow the prescribed directory structure for unit testing
11
+ # Puppet modules). RSpec docs often show more than one top-level describe,
12
+ # so this function supports the same.
13
+ rspec_file = caller_locations.select {|e| e.path =~ /.+_spec.rb$/}
14
+ .first
15
+ .path
16
+ default_describe = {
17
+ 'name' => __get_eut_name(yaml_file, rspec_file),
18
+ 'type' => guess_type_from_path(rspec_file)
19
+ }
20
+ describes = []
21
+ RSpec::Puppet::Yaml::DataHelpers.get_array_of_named_hashes(
22
+ 'describe',
23
+ test_data,
24
+ ).each { |desc| describes << default_describe.merge(desc)}
25
+ __apply_rspec_puppet_describes(describes, test_data)
26
+ end
27
+
28
+ # Identify the name of the entity under test.
29
+ #
30
+ # @note The __ prefix denotes this as a "private" function. Do not call this
31
+ # directly.
32
+ #
33
+ # @param [String] rspec_yaml_file_name YAML file name that describes tests.
34
+ # @param [String] rspec_file_name Name of the *_spec.rb file that is requesting
35
+ # parsed results from `rspec_yaml_file_name`.
36
+ # @return [String] Name of the entity under test.
37
+ def __get_eut_name(rspec_yaml_file_name, rspec_file_name)
38
+ base_yaml = File.basename(rspec_yaml_file_name)
39
+ base_caller = File.basename(rspec_file_name)
40
+
41
+ if base_yaml =~ /^(.+)(_spec)?\.ya?ml$/
42
+ $1.to_s
43
+ elsif base_caller =~ /^(.+)_spec\.rb$/
44
+ $1.to_s
45
+ else
46
+ 'unknown'
47
+ end
48
+ end
49
+
50
+ # Generates an RSpec `describe {}` and its contents.
51
+ #
52
+ # @note The __ prefix denotes this as a "private" function. Do not call this
53
+ # directly.
54
+ #
55
+ # @param apply_attrs [Hash] Definition of the entity and its contents.
56
+ # @param parent_data [Hash] Used for recursion, this is the parent for this
57
+ # entity.
58
+ def __apply_rspec_puppet_describe(apply_attrs = {}, parent_data = {})
59
+ desc_name = RSpec::Puppet::Yaml::DataHelpers.get_named_value(
60
+ 'name',
61
+ apply_attrs
62
+ )
63
+ desc_type = RSpec::Puppet::Yaml::DataHelpers.get_named_value(
64
+ 'type',
65
+ apply_attrs
66
+ )
67
+ if desc_type.nil?
68
+ describe(desc_name) do
69
+ __apply_rspec_puppet_content(apply_attrs, parent_data)
70
+ end
71
+ else
72
+ describe(desc_name, :type => desc_type) do
73
+ __apply_rspec_puppet_content(apply_attrs, parent_data)
74
+ end
75
+ end
76
+ end
77
+
78
+ # Generates an RSpec `context {}` and its contents.
79
+ #
80
+ # @note The __ prefix denotes this as a "private" function. Do not call this
81
+ # directly.
82
+ #
83
+ # @param apply_attrs [Hash] Definition of the entity and its contents.
84
+ # @param parent_data [Hash] Used for recursion, this is the parent for this
85
+ # entity.
86
+ def __apply_rspec_puppet_context(apply_attrs = {}, parent_data = {})
87
+ context_name = RSpec::Puppet::Yaml::DataHelpers.get_named_value(
88
+ 'name',
89
+ apply_attrs
90
+ )
91
+ context(context_name) do
92
+ __apply_rspec_puppet_content(apply_attrs, parent_data)
93
+ end
94
+ end
95
+
96
+ # An extension for RSpec, variants are contexts that repeat all parent tests
97
+ # with specified tweaks to their inputs and expectations.
98
+ #
99
+ # @note The __ prefix denotes this as a "private" function. Do not call this
100
+ # directly.
101
+ #
102
+ # @param apply_attrs [Hash] Definition of the entity and its contents.
103
+ # @param parent_data [Hash] Used for recursion, this is the parent for this
104
+ # entity.
105
+ def __apply_rspec_puppet_variant(apply_attrs = {}, parent_data = {})
106
+ variant_name = RSpec::Puppet::Yaml::DataHelpers.get_named_value(
107
+ 'name',
108
+ apply_attrs
109
+ )
110
+
111
+ # The deep_merge gem's funtionality unfortunately changes the destination
112
+ # Hash, even when you attempt to store the result to another variable and use
113
+ # the non-bang method call. This seems like a pretty serious bug, to me,
114
+ # despite the gem's documentation implying that this will happen. IMHO, the
115
+ # gem's author made a very poor decision in how the bang and non-bang behavior
116
+ # would differ (merely a difference in the default values of its options
117
+ # rather than the Ruby-norm of affecting or not-affecting the calling Object).
118
+ # To workaround this issue and protect the original destination against
119
+ # unwanted change, a deep copy of the destination Hash must be taken and used.
120
+ parent_dup = Marshal.load(Marshal.dump(parent_data))
121
+ context_data = parent_dup.select do |k,v|
122
+ !['variants', 'before', 'after', 'subject'].include?(k.to_s)
123
+ end
124
+ context_data.deep_merge!(
125
+ apply_attrs,
126
+ { :extend_existing_arrays => false,
127
+ :merge_hash_arrays => true,
128
+ :merge_nil_values => false,
129
+ :overwrite_arrays => false,
130
+ :preserve_unmergeables => false,
131
+ :sort_merged_arrays => false
132
+ }
133
+ )
134
+
135
+ context(variant_name) do
136
+ __apply_rspec_puppet_content(context_data, parent_data)
137
+ end
138
+ end
139
+
140
+ # Generates a set of RSpec `describe` entities.
141
+ #
142
+ # @note The __ prefix denotes this as a "private" function. Do not call this
143
+ # directly.
144
+ #
145
+ # @param describes [Array[Hash]] Set of entities to generate. Each element must
146
+ # be a Hash that has a :name or 'name' attribute.
147
+ # @param parent_data [Hash] Used for recursion, this is the parent for this
148
+ # entity.
149
+ def __apply_rspec_puppet_describes(describes = [], parent_data = {})
150
+ bad_input = false
151
+
152
+ # Input must be an Array
153
+ if !describes.kind_of?(Array)
154
+ bad_input = true
155
+ end
156
+
157
+ # Every element of the input must be a Hash, each with a :name attribute
158
+ describes.each do |container|
159
+ if !container.is_a?(Hash)
160
+ bad_input = true
161
+ elsif !container.has_key?('name') && !container.has_key?(:name)
162
+ bad_input = true
163
+ end
164
+ end
165
+
166
+ if bad_input
167
+ raise ArgumentError, "__apply_rspec_puppet_describes requires an Array of Hashes, each with a :name attribute."
168
+ end
169
+
170
+ describes.each do |container|
171
+ __apply_rspec_puppet_describe(container, parent_data)
172
+ end
173
+ end
174
+
175
+ # Generates a set of RSpec `context` entities.
176
+ #
177
+ # @note The __ prefix denotes this as a "private" function. Do not call this
178
+ # directly.
179
+ #
180
+ # @param contexts [Array[Hash]] Set of entities to generate. Each element must
181
+ # be a Hash that has a :name or 'name' attribute.
182
+ # @param parent_data [Hash] Used for recursion, this is the parent for this
183
+ # entity.
184
+ def __apply_rspec_puppet_contexts(contexts = [], parent_data = {})
185
+ bad_input = false
186
+
187
+ # Input must be an Array
188
+ if !contexts.kind_of?(Array)
189
+ bad_input = true
190
+ end
191
+
192
+ # Every element of the input must be a Hash, each with a :name attribute
193
+ contexts.each do |container|
194
+ if !container.is_a?(Hash)
195
+ bad_input = true
196
+ elsif !container.has_key?('name') && !container.has_key?(:name)
197
+ bad_input = true
198
+ end
199
+ end
200
+
201
+ if bad_input
202
+ raise ArgumentError, "__apply_rspec_puppet_contexts requires an Array of Hashes, each with a :name attribute."
203
+ end
204
+
205
+ contexts.each do |container|
206
+ __apply_rspec_puppet_context(container, parent_data)
207
+ end
208
+ end
209
+
210
+ # Generates a set of variants of RSpec entities.
211
+ #
212
+ # @note The __ prefix denotes this as a "private" function. Do not call this
213
+ # directly.
214
+ #
215
+ # @param variants [Array[Hash]] Set of entities to generate. Each element must
216
+ # be a Hash that has a :name or 'name' attribute.
217
+ # @param parent_data [Hash] Used for recursion, this is the parent for this
218
+ # entity.
219
+ def __apply_rspec_puppet_variants(variants = [], parent_data = {})
220
+ bad_input = false
221
+
222
+ # Input must be an Array
223
+ if !variants.kind_of?(Array)
224
+ bad_input = true
225
+ end
226
+
227
+ # Every element of the input must be a Hash, each with a :name attribute
228
+ variants.each do |variant|
229
+ if !variant.is_a?(Hash)
230
+ bad_input = true
231
+ elsif !variant.has_key?('name') && !variant.has_key?(:name)
232
+ bad_input = true
233
+ end
234
+ end
235
+
236
+ if bad_input
237
+ raise ArgumentError, "__apply_rspec_puppet_variants requires an Array of Hashes, each with a :name attribute."
238
+ end
239
+
240
+ variants.each { |variant| __apply_rspec_puppet_variant(variant, parent_data) }
241
+ end
242
+
243
+ # Generates a set of RSpec `it {}` "examples".
244
+ #
245
+ # @note The __ prefix denotes this as a "private" function. Do not call this
246
+ # directly.
247
+ #
248
+ # @param tests [Hash] Set of examples to build.
249
+ def __apply_rspec_puppet_tests(tests = {})
250
+ tests.each do |method, props|
251
+ # props must be split into args and tests based on method
252
+ case method.to_s
253
+ when /^(!)?((contain|create)_.+)$/
254
+ # There can be only one beyond this point, so recurse as necessary
255
+ if 1 < props.keys.count
256
+ props.each { |k,v| __apply_rspec_puppet_tests({method => {k => v}})}
257
+ return # Avoid processing the first entry twice
258
+ end
259
+
260
+ positive_test = $1.nil?
261
+ apply_method = $2
262
+ args = [ props.keys.first ]
263
+ calls = props.values.first
264
+ when /^(!)?(have_.+_count)$/
265
+ positive_test = $1.nil?
266
+ apply_method = $2
267
+ args = props
268
+ calls = {}
269
+ when /^(!)?(compile)$/
270
+ positive_test = $1.nil?
271
+ apply_method = $2
272
+ args = []
273
+ calls = props
274
+ when /^(!)?(run)$/
275
+ positive_test = $1.nil?
276
+ apply_method = $2
277
+ args = []
278
+ calls = props
279
+ when /^(!)?(be_valid_type)$/
280
+ positive_test = $1.nil?
281
+ apply_method = $2
282
+ args = []
283
+ calls = props
284
+ end
285
+
286
+ matcher = RSpec::Puppet::MatcherHelpers.get_matcher_for(
287
+ apply_method,
288
+ args,
289
+ calls
290
+ )
291
+
292
+ if positive_test
293
+ it { is_expected.to matcher }
294
+ else
295
+ it { is_expected.not_to matcher }
296
+ end
297
+ end
298
+ end
299
+
300
+ # Generates an RSpec `before {}` entity with one or more global-scope method
301
+ # calls as its contents.
302
+ #
303
+ # @note The __ prefix denotes this as a "private" function. Do not call this
304
+ # directly.
305
+ #
306
+ # @param commands [Variant[String,Array[String]]] Command or commands to call.
307
+ def __apply_rspec_puppet_before(commands)
308
+ if !commands.nil?
309
+ if commands.kind_of?(Array)
310
+ before do
311
+ commands.each { |command| Object.send(command.to_s.to_sym) }
312
+ end
313
+ elsif !commands.is_a?(Hash)
314
+ before { Object.send(commands.to_s.to_sym) }
315
+ else
316
+ raise ArgumentError, "__apply_rspec_puppet_before requires a command String or an Array of commands."
317
+ end
318
+ end
319
+ end
320
+
321
+ # Generates an RSpec `after {}` entity with one or more global-scope method
322
+ # calls as its contents.
323
+ #
324
+ # @note The __ prefix denotes this as a "private" function. Do not call this
325
+ # directly.
326
+ #
327
+ # @param commands [Variant[String,Array[String]]] Command or commands to call.
328
+ def __apply_rspec_puppet_after(commands)
329
+ if !commands.nil?
330
+ if commands.kind_of?(Array)
331
+ after do
332
+ commands.each { |command| Object.send(command.to_s.to_sym) }
333
+ end
334
+ elsif !commands.is_a?(Hash)
335
+ after { Object.send(commands.to_s.to_sym) }
336
+ else
337
+ raise ArgumentError, "__apply_rspec_puppet_after requires a command String or an Array of commands."
338
+ end
339
+ end
340
+ end
341
+
342
+ # Generates an RSpec `subject {}` entity.
343
+ #
344
+ # @note The __ prefix denotes this as a "private" function. Do not call this
345
+ # directly.
346
+ #
347
+ # @param subject [Any] The subject descriptor.
348
+ def __apply_rspec_puppet_subject(subject)
349
+ if !subject.nil?
350
+ subject { __expand_data_commands(subject) }
351
+ end
352
+ end
353
+
354
+ # Sets all let variables.
355
+ #
356
+ # @note The __ prefix denotes this as a "private" function. Do not call this
357
+ # directly.
358
+ #
359
+ # @param lets [Hash] The data to scan for let variables
360
+ #
361
+ # @example As YAML
362
+ # ---
363
+ # let:
364
+ # facts:
365
+ # kernel: Linux
366
+ # os:
367
+ # family: RedHat
368
+ # name: CentOS
369
+ # release:
370
+ # major: 7
371
+ # minor: 1
372
+ # params:
373
+ # require: '%{eval:ref("Package", "my-package")}'
374
+ # nodes:
375
+ # '%{eval:ref("Node", "dbnode")}': '%{eval:ref("Myapp::Mycomponent", "myapp")}'
376
+ def __apply_rspec_puppet_lets(lets = {})
377
+ __expand_data_commands(lets).each { |k,v| let(k.to_sym) { v } }
378
+ end
379
+
380
+ # Recursively expands specially-formatted commands with or without arguments to
381
+ # them found within serialized data.
382
+ #
383
+ # @param serialized_data [Any] The data to check for expansion markers and
384
+ # expand them, when present.
385
+ # @return [Any] Expanded or original (when there are no expansion markers) data.
386
+ def __expand_data_commands(serialized_data)
387
+ return nil if serialized_data.nil?
388
+ if serialized_data.kind_of?(Array)
389
+ expanded_data = []
390
+ serialized_data.each { |elem| expanded_data << __expand_data_commands(elem) }
391
+ elsif serialized_data.is_a?(Hash)
392
+ expanded_data = {}
393
+ serialized_data.each do |k,v|
394
+ expanded_data[__expand_data_commands(k)] = __expand_data_commands(v)
395
+ end
396
+ else
397
+ test_data = serialized_data.to_s
398
+ if test_data =~ /^%{eval:(.+)}$/
399
+ test_eval = $1
400
+ begin
401
+ test_value = eval test_eval
402
+ rescue Exception => ex
403
+ test_value = "#{ex.class}: #{test_eval}. #{ex.message}"
404
+ end
405
+ else
406
+ test_value = serialized_data
407
+ end
408
+ expanded_data = test_value
409
+ end
410
+ expanded_data
411
+ end
412
+
413
+ # Generates all specified RSpec entities. This is assumed to be run within a
414
+ # valid RSpec container, like `describe` or `context`.
415
+ #
416
+ # @note The __ prefix denotes this as a "private" function. Do not call this
417
+ # directly.
418
+ #
419
+ # @param apply_data [Hash] The entities to generate.
420
+ # @param parent_data [Hash] Used for recursion, this is the parent of the
421
+ # receiving entity.
422
+ def __apply_rspec_puppet_content(apply_data = {}, parent_data = {})
423
+ __apply_rspec_puppet_subject(
424
+ RSpec::Puppet::Yaml::DataHelpers.get_named_value(
425
+ 'subject',
426
+ apply_data
427
+ )
428
+ )
429
+ __apply_rspec_puppet_lets(
430
+ RSpec::Puppet::Yaml::DataHelpers.get_named_hash(
431
+ 'let',
432
+ apply_data
433
+ )
434
+ )
435
+ __apply_rspec_puppet_before(
436
+ RSpec::Puppet::Yaml::DataHelpers.get_named_value(
437
+ 'before',
438
+ apply_data
439
+ )
440
+ )
441
+ __apply_rspec_puppet_after(
442
+ RSpec::Puppet::Yaml::DataHelpers.get_named_value(
443
+ 'after',
444
+ apply_data
445
+ )
446
+ )
447
+ __apply_rspec_puppet_tests(
448
+ RSpec::Puppet::Yaml::DataHelpers.get_named_hash(
449
+ 'tests',
450
+ apply_data
451
+ )
452
+ )
453
+ __apply_rspec_puppet_describes(
454
+ RSpec::Puppet::Yaml::DataHelpers.get_array_of_named_hashes(
455
+ 'describe',
456
+ apply_data
457
+ ),
458
+ apply_data
459
+ )
460
+ __apply_rspec_puppet_contexts(
461
+ RSpec::Puppet::Yaml::DataHelpers.get_array_of_named_hashes(
462
+ 'context',
463
+ apply_data
464
+ ),
465
+ apply_data
466
+ )
467
+ __apply_rspec_puppet_variants(
468
+ RSpec::Puppet::Yaml::DataHelpers.get_array_of_named_hashes(
469
+ 'variants',
470
+ apply_data
471
+ ),
472
+ apply_data
473
+ )
474
+ end
475
+
476
+ # Attempts to load the YAML test data and return its data.
477
+ #
478
+ # @note The __ prefix denotes this as a "private" function. Do not call this
479
+ # directly.
480
+ #
481
+ # @param yaml_file [String] Path to the YAML file to load.
482
+ # @return [Hash] The data from the YAML file.
483
+ #
484
+ # @raise IOError when the source file is not valid YAML or does not
485
+ # contain a Hash.
486
+ def __load_rspec_puppet_yaml_data(yaml_file)
487
+ # The test data file must exist
488
+ if !File.exists?(yaml_file)
489
+ raise IOError, "#{yaml_file} does not exit."
490
+ end
491
+
492
+ begin
493
+ yaml_data = YAML.load_file(yaml_file)
494
+ rescue Psych::SyntaxError => ex
495
+ raise IOError, "#{yaml_file} contains a YAML syntax error."
496
+ rescue ArgumentError => ex
497
+ raise IOError, "#{yaml_file} contains missing or undefined entities."
498
+ rescue
499
+ raise IOError, "#{yaml_file} could not be read or is not YAML."
500
+ end
501
+
502
+ # Must be a populated Hash
503
+ if yaml_data.nil? || !yaml_data.is_a?(Hash)
504
+ yaml_data = nil
505
+ raise IOError, "#{yaml_file} is not a valid YAML Hash data structure."
506
+ elsif yaml_data.empty?
507
+ yaml_data = nil
508
+ raise IOError, "#{yaml_file} contains no legible tests."
509
+ end
510
+
511
+ yaml_data
512
+ end
@@ -0,0 +1,11 @@
1
+ # The RSpec namespace.
2
+ module RSpec
3
+ module Puppet
4
+ # Enables Puppet module authors to define RSpec tests in YAML rather than
5
+ # Ruby.
6
+ module Yaml
7
+ # Version number for the rspec-puppet-yaml Ruby gem
8
+ VERSION = "0.1.0"
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,8 @@
1
+ require 'rspec-puppet/matcher_helpers'
2
+ require 'rspec-puppet/support_copy'
3
+ require "rspec-puppet-yaml/version"
4
+ require 'rspec-puppet-yaml/data_helpers'
5
+ require 'rspec-puppet-yaml/extenders'
6
+ require 'rspec-puppet-yaml/parser'
7
+ require 'deep_merge'
8
+ require 'yaml'
@@ -0,0 +1,45 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "rspec-puppet-yaml/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "rspec-puppet-yaml"
8
+ spec.version = RSpec::Puppet::Yaml::VERSION
9
+ spec.authors = ["William W. Kimball, Jr., MBA, MSIS"]
10
+ spec.email = ["github-rspec-puppet-yaml@kimballstuff.com"]
11
+
12
+ spec.summary = %q{Enables the use of YAML to specify rspec tests for Puppet projects}
13
+ spec.description = %q{rspec is effective but quite hard to learn for Puppet authors who don't wish to take up Ruby. YAML is comparatively easy to pick up and most Puppet authors are necessarily exposed to it. This extension enables Puppet code authors to define their rspec-puppet tests in YAML instead of Ruby.}
14
+ spec.homepage = "https://github.com/wwkimball/rspec-puppet-yaml"
15
+ spec.license = "MIT"
16
+
17
+ # It's not clear why I would want to disable pushing this public gem to RubyGems.org...
18
+ ## Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
19
+ ## to allow pushing to a single host or delete this section to allow pushing to any host.
20
+ #if spec.respond_to?(:metadata)
21
+ # spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
22
+ #else
23
+ # raise "RubyGems 2.0 or newer is required to protect against " \
24
+ # "public gem pushes."
25
+ #end
26
+
27
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
28
+ f.match(%r{^(test|spec|features)/})
29
+ end
30
+ spec.bindir = "exe"
31
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
32
+ spec.require_paths = ["lib"]
33
+
34
+ spec.add_development_dependency "bundler", "~> 1.15"
35
+ spec.add_development_dependency "rake", "~> 12.0"
36
+ spec.add_development_dependency "yard", "~> 0.9"
37
+ spec.add_development_dependency "json", "~> 2.1"
38
+ spec.add_development_dependency "puppet", "~> 5"
39
+ spec.add_development_dependency "puppetlabs_spec_helper", "~> 2.3"
40
+ spec.add_development_dependency "puppet-strings", "~> 1"
41
+ spec.add_development_dependency "rspec-puppet-facts", "~> 1.8"
42
+
43
+ spec.add_dependency "rspec-puppet", "~> 2.6"
44
+ spec.add_dependency "deep_merge", "~> 1.1"
45
+ end