beaker-puppet 4.1.1 → 4.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.
@@ -0,0 +1,635 @@
1
+ #encoding: UTF-8
2
+
3
+ # MIT License Copyright (c) 2006 - 2014
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the 'Software'), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ # This class represents the INI file and can be used to parse, modify,
24
+ # and write INI files.
25
+ class BeakerPuppet::IniFile
26
+ include Enumerable
27
+
28
+ class Error < StandardError; end
29
+ VERSION = '3.0.0'
30
+
31
+ # Public: Open an INI file and load the contents.
32
+ #
33
+ # filename - The name of the file as a String
34
+ # opts - The Hash of options (default: {})
35
+ # :comment - String containing the comment character(s)
36
+ # :parameter - String used to separate parameter and value
37
+ # :encoding - Encoding String for reading / writing
38
+ # :default - The String name of the default global section
39
+ #
40
+ # Examples
41
+ #
42
+ # IniFile.load('file.ini')
43
+ # #=> IniFile instance
44
+ #
45
+ # IniFile.load('does/not/exist.ini')
46
+ # #=> nil
47
+ #
48
+ # Returns an IniFile instance or nil if the file could not be opened.
49
+ def self.load( filename, opts = {} )
50
+ return unless File.file? filename
51
+ new(opts.merge(:filename => filename))
52
+ end
53
+
54
+ # Get and set the filename
55
+ attr_accessor :filename
56
+
57
+ # Get and set the encoding
58
+ attr_accessor :encoding
59
+
60
+ # Public: Create a new INI file from the given set of options. If :content
61
+ # is provided then it will be used to populate the INI file. If a :filename
62
+ # is provided then the contents of the file will be parsed and stored in the
63
+ # INI file. If neither the :content or :filename is provided then an empty
64
+ # INI file is created.
65
+ #
66
+ # opts - The Hash of options (default: {})
67
+ # :content - The String/Hash containing the INI contents
68
+ # :comment - String containing the comment character(s)
69
+ # :parameter - String used to separate parameter and value
70
+ # :encoding - Encoding String for reading / writing
71
+ # :default - The String name of the default global section
72
+ # :filename - The filename as a String
73
+ #
74
+ # Examples
75
+ #
76
+ # IniFile.new
77
+ # #=> an empty IniFile instance
78
+ #
79
+ # IniFile.new( :content => "[global]\nfoo=bar" )
80
+ # #=> an IniFile instance
81
+ #
82
+ # IniFile.new( :filename => 'file.ini', :encoding => 'UTF-8' )
83
+ # #=> an IniFile instance
84
+ #
85
+ # IniFile.new( :content => "[global]\nfoo=bar", :comment => '#' )
86
+ # #=> an IniFile instance
87
+ #
88
+ def initialize( opts = {} )
89
+ @comment = opts.fetch(:comment, ';#')
90
+ @param = opts.fetch(:parameter, '=')
91
+ @encoding = opts.fetch(:encoding, nil)
92
+ @default = opts.fetch(:default, 'global')
93
+ @filename = opts.fetch(:filename, nil)
94
+ content = opts.fetch(:content, nil)
95
+
96
+ @ini = Hash.new {|h,k| h[k] = Hash.new}
97
+
98
+ if content.is_a?(Hash) then merge!(content)
99
+ elsif content then parse(content)
100
+ elsif @filename then read
101
+ end
102
+ end
103
+
104
+ # Public: Write the contents of this IniFile to the file system. If left
105
+ # unspecified, the currently configured filename and encoding will be used.
106
+ # Otherwise the filename and encoding can be specified in the options hash.
107
+ #
108
+ # opts - The default options Hash
109
+ # :filename - The filename as a String
110
+ # :encoding - The encoding as a String
111
+ #
112
+ # Returns this IniFile instance.
113
+ def write( opts = {} )
114
+ filename = opts.fetch(:filename, @filename)
115
+ encoding = opts.fetch(:encoding, @encoding)
116
+ mode = encoding ? "w:#{encoding}" : "w"
117
+
118
+ File.open(filename, mode) do |f|
119
+ @ini.each do |section,hash|
120
+ f.puts "[#{section}]"
121
+ hash.each {|param,val| f.puts "#{param} #{@param} #{escape_value val}"}
122
+ f.puts
123
+ end
124
+ end
125
+
126
+ self
127
+ end
128
+ alias :save :write
129
+
130
+ # Public: Read the contents of the INI file from the file system and replace
131
+ # and set the state of this IniFile instance. If left unspecified the
132
+ # currently configured filename and encoding will be used when reading from
133
+ # the file system. Otherwise the filename and encoding can be specified in
134
+ # the options hash.
135
+ #
136
+ # opts - The default options Hash
137
+ # :filename - The filename as a String
138
+ # :encoding - The encoding as a String
139
+ #
140
+ # Returns this IniFile instance if the read was successful; nil is returned
141
+ # if the file could not be read.
142
+ def read( opts = {} )
143
+ filename = opts.fetch(:filename, @filename)
144
+ encoding = opts.fetch(:encoding, @encoding)
145
+ return unless File.file? filename
146
+
147
+ mode = encoding ? "r:#{encoding}" : "r"
148
+ File.open(filename, mode) { |fd| parse fd }
149
+ self
150
+ end
151
+ alias :restore :read
152
+
153
+ # Returns this IniFile converted to a String.
154
+ def to_s
155
+ s = []
156
+ @ini.each do |section,hash|
157
+ s << "[#{section}]"
158
+ hash.each {|param,val| s << "#{param} #{@param} #{escape_value val}"}
159
+ s << ""
160
+ end
161
+ s.join("\n")
162
+ end
163
+
164
+ # Returns this IniFile converted to a Hash.
165
+ def to_h
166
+ @ini.dup
167
+ end
168
+
169
+ # Public: Creates a copy of this inifile with the entries from the
170
+ # other_inifile merged into the copy.
171
+ #
172
+ # other - The other IniFile.
173
+ #
174
+ # Returns a new IniFile.
175
+ def merge( other )
176
+ self.dup.merge!(other)
177
+ end
178
+
179
+ # Public: Merges other_inifile into this inifile, overwriting existing
180
+ # entries. Useful for having a system inifile with user overridable settings
181
+ # elsewhere.
182
+ #
183
+ # other - The other IniFile.
184
+ #
185
+ # Returns this IniFile.
186
+ def merge!( other )
187
+ return self if other.nil?
188
+
189
+ my_keys = @ini.keys
190
+ other_keys = case other
191
+ when self.class
192
+ other.instance_variable_get(:@ini).keys
193
+ when Hash
194
+ other.keys
195
+ else
196
+ raise Error, "cannot merge contents from '#{other.class.name}'"
197
+ end
198
+
199
+ (my_keys & other_keys).each do |key|
200
+ case other[key]
201
+ when Hash
202
+ @ini[key].merge!(other[key])
203
+ when nil
204
+ nil
205
+ else
206
+ raise Error, "cannot merge section #{key.inspect} - unsupported type: #{other[key].class.name}"
207
+ end
208
+ end
209
+
210
+ (other_keys - my_keys).each do |key|
211
+ @ini[key] = case other[key]
212
+ when Hash
213
+ other[key].dup
214
+ when nil
215
+ {}
216
+ else
217
+ raise Error, "cannot merge section #{key.inspect} - unsupported type: #{other[key].class.name}"
218
+ end
219
+ end
220
+
221
+ self
222
+ end
223
+
224
+ # Public: Yield each INI file section, parameter, and value in turn to the
225
+ # given block.
226
+ #
227
+ # block - The block that will be iterated by the each method. The block will
228
+ # be passed the current section and the parameter/value pair.
229
+ #
230
+ # Examples
231
+ #
232
+ # inifile.each do |section, parameter, value|
233
+ # puts "#{parameter} = #{value} [in section - #{section}]"
234
+ # end
235
+ #
236
+ # Returns this IniFile.
237
+ def each
238
+ return unless block_given?
239
+ @ini.each do |section,hash|
240
+ hash.each do |param,val|
241
+ yield section, param, val
242
+ end
243
+ end
244
+ self
245
+ end
246
+
247
+ # Public: Yield each section in turn to the given block.
248
+ #
249
+ # block - The block that will be iterated by the each method. The block will
250
+ # be passed the current section as a Hash.
251
+ #
252
+ # Examples
253
+ #
254
+ # inifile.each_section do |section|
255
+ # puts section.inspect
256
+ # end
257
+ #
258
+ # Returns this IniFile.
259
+ def each_section
260
+ return unless block_given?
261
+ @ini.each_key {|section| yield section}
262
+ self
263
+ end
264
+
265
+ # Public: Remove a section identified by name from the IniFile.
266
+ #
267
+ # section - The section name as a String.
268
+ #
269
+ # Returns the deleted section Hash.
270
+ def delete_section( section )
271
+ @ini.delete section.to_s
272
+ end
273
+
274
+ # Public: Get the section Hash by name. If the section does not exist, then
275
+ # it will be created.
276
+ #
277
+ # section - The section name as a String.
278
+ #
279
+ # Examples
280
+ #
281
+ # inifile['global']
282
+ # #=> global section Hash
283
+ #
284
+ # Returns the Hash of parameter/value pairs for this section.
285
+ def []( section )
286
+ return nil if section.nil?
287
+ @ini[section.to_s]
288
+ end
289
+
290
+ # Public: Set the section to a hash of parameter/value pairs.
291
+ #
292
+ # section - The section name as a String.
293
+ # value - The Hash of parameter/value pairs.
294
+ #
295
+ # Examples
296
+ #
297
+ # inifile['tenderloin'] = { 'gritty' => 'yes' }
298
+ # #=> { 'gritty' => 'yes' }
299
+ #
300
+ # Returns the value Hash.
301
+ def []=( section, value )
302
+ @ini[section.to_s] = value
303
+ end
304
+
305
+ # Public: Create a Hash containing only those INI file sections whose names
306
+ # match the given regular expression.
307
+ #
308
+ # regex - The Regexp used to match section names.
309
+ #
310
+ # Examples
311
+ #
312
+ # inifile.match(/^tree_/)
313
+ # #=> Hash of matching sections
314
+ #
315
+ # Return a Hash containing only those sections that match the given regular
316
+ # expression.
317
+ def match( regex )
318
+ @ini.dup.delete_if { |section, _| section !~ regex }
319
+ end
320
+
321
+ # Public: Check to see if the IniFile contains the section.
322
+ #
323
+ # section - The section name as a String.
324
+ #
325
+ # Returns true if the section exists in the IniFile.
326
+ def has_section?( section )
327
+ @ini.has_key? section.to_s
328
+ end
329
+
330
+ # Returns an Array of section names contained in this IniFile.
331
+ def sections
332
+ @ini.keys
333
+ end
334
+
335
+ # Public: Freeze the state of this IniFile object. Any attempts to change
336
+ # the object will raise an error.
337
+ #
338
+ # Returns this IniFile.
339
+ def freeze
340
+ super
341
+ @ini.each_value {|h| h.freeze}
342
+ @ini.freeze
343
+ self
344
+ end
345
+
346
+ # Public: Produces a duplicate of this IniFile. The duplicate is independent
347
+ # of the original -- i.e. the duplicate can be modified without changing the
348
+ # original. The tainted state of the original is copied to the duplicate.
349
+ #
350
+ # Returns a new IniFile.
351
+ def dup
352
+ other = super
353
+ other.instance_variable_set(:@ini, Hash.new {|h,k| h[k] = Hash.new})
354
+ @ini.each_pair {|s,h| other[s].merge! h}
355
+ other
356
+ end
357
+
358
+ # Public: Produces a duplicate of this IniFile. The duplicate is independent
359
+ # of the original -- i.e. the duplicate can be modified without changing the
360
+ # original.
361
+ #
362
+ # Returns a new IniFile.
363
+ def clone
364
+ other = dup
365
+ other.freeze if self.frozen?
366
+ other
367
+ end
368
+
369
+ # Public: Compare this IniFile to some other IniFile. For two INI files to
370
+ # be equivalent, they must have the same sections with the same parameter /
371
+ # value pairs in each section.
372
+ #
373
+ # other - The other IniFile.
374
+ #
375
+ # Returns true if the INI files are equivalent and false if they differ.
376
+ def eql?( other )
377
+ return true if equal? other
378
+ return false unless other.instance_of? self.class
379
+ @ini == other.instance_variable_get(:@ini)
380
+ end
381
+ alias :== :eql?
382
+
383
+ # Escape special characters.
384
+ #
385
+ # value - The String value to escape.
386
+ #
387
+ # Returns the escaped value.
388
+ def escape_value( value )
389
+ value = value.to_s.dup
390
+ value.gsub!(%r/\\([0nrt])/, '\\\\\1')
391
+ value.gsub!(%r/\n/, '\n')
392
+ value.gsub!(%r/\r/, '\r')
393
+ value.gsub!(%r/\t/, '\t')
394
+ value.gsub!(%r/\0/, '\0')
395
+ value
396
+ end
397
+
398
+ # Parse the given content and store the information in this IniFile
399
+ # instance. All data will be cleared out and replaced with the information
400
+ # read from the content.
401
+ #
402
+ # content - A String or a file descriptor (must respond to `each_line`)
403
+ #
404
+ # Returns this IniFile.
405
+ def parse( content )
406
+ parser = Parser.new(@ini, @param, @comment, @default)
407
+ parser.parse(content)
408
+ self
409
+ end
410
+
411
+ # The IniFile::Parser has the responsibility of reading the contents of an
412
+ # .ini file and storing that information into a ruby Hash. The object being
413
+ # parsed must respond to `each_line` - this includes Strings and any IO
414
+ # object.
415
+ class Parser
416
+
417
+ attr_writer :section
418
+ attr_accessor :property
419
+ attr_accessor :value
420
+
421
+ # Create a new IniFile::Parser that can be used to parse the contents of
422
+ # an .ini file.
423
+ #
424
+ # hash - The Hash where parsed information will be stored
425
+ # param - String used to separate parameter and value
426
+ # comment - String containing the comment character(s)
427
+ # default - The String name of the default global section
428
+ #
429
+ def initialize( hash, param, comment, default )
430
+ @hash = hash
431
+ @default = default
432
+
433
+ comment = comment.to_s.empty? ? "\\z" : "\\s*(?:[#{comment}].*)?\\z"
434
+
435
+ @section_regexp = %r/\A\s*\[([^\]]+)\]#{comment}/
436
+ @ignore_regexp = %r/\A#{comment}/
437
+ @property_regexp = %r/\A(.*?)(?<!\\)#{param}(.*)\z/
438
+
439
+ @open_quote = %r/\A\s*(".*)\z/
440
+ @close_quote = %r/\A(.*(?<!\\)")#{comment}/
441
+ @full_quote = %r/\A\s*(".*(?<!\\)")#{comment}/
442
+ @trailing_slash = %r/\A(.*)(?<!\\)\\#{comment}/
443
+ @normal_value = %r/\A(.*?)#{comment}/
444
+ end
445
+
446
+ # Returns `true` if the current value starts with a leading double quote.
447
+ # Otherwise returns false.
448
+ def leading_quote?
449
+ value && value =~ %r/\A"/
450
+ end
451
+
452
+ # Given a string, attempt to parse out a value from that string. This
453
+ # value might be continued on the following line. So this method returns
454
+ # `true` if it is expecting more data.
455
+ #
456
+ # string - String to parse
457
+ #
458
+ # Returns `true` if the next line is also part of the current value.
459
+ # Returns `fase` if the string contained a complete value.
460
+ def parse_value( string )
461
+ continuation = false
462
+
463
+ # if our value starts with a double quote, then we are in a
464
+ # line continuation situation
465
+ if leading_quote?
466
+ # check for a closing quote at the end of the string
467
+ if string =~ @close_quote
468
+ value << $1
469
+
470
+ # otherwise just append the string to the value
471
+ else
472
+ value << string
473
+ continuation = true
474
+ end
475
+
476
+ # not currently processing a continuation line
477
+ else
478
+ case string
479
+ when @full_quote
480
+ self.value = $1
481
+
482
+ when @open_quote
483
+ self.value = $1
484
+ continuation = true
485
+
486
+ when @trailing_slash
487
+ self.value ? self.value << $1 : self.value = $1
488
+ continuation = true
489
+
490
+ when @normal_value
491
+ self.value ? self.value << $1 : self.value = $1
492
+
493
+ else
494
+ error
495
+ end
496
+ end
497
+
498
+ if continuation
499
+ self.value << $/ if leading_quote?
500
+ else
501
+ process_property
502
+ end
503
+
504
+ continuation
505
+ end
506
+
507
+ # Parse the ini file contents. This will clear any values currently stored
508
+ # in the ini hash.
509
+ #
510
+ # content - Any object that responds to `each_line`
511
+ #
512
+ # Returns nil.
513
+ def parse( content )
514
+ return unless content
515
+
516
+ continuation = false
517
+
518
+ @hash.clear
519
+ @line = nil
520
+ self.section = nil
521
+
522
+ content.each_line do |line|
523
+ @line = line.chomp
524
+
525
+ if continuation
526
+ continuation = parse_value @line
527
+ else
528
+ case @line
529
+ when @ignore_regexp
530
+ nil
531
+ when @section_regexp
532
+ self.section = @hash[$1]
533
+ when @property_regexp
534
+ self.property = $1.strip
535
+ error if property.empty?
536
+
537
+ continuation = parse_value $2
538
+ else
539
+ error
540
+ end
541
+ end
542
+ end
543
+
544
+ # check here if we have a dangling value ... usually means we have an
545
+ # unmatched open quote
546
+ if leading_quote?
547
+ error "Unmatched open quote"
548
+ elsif property && value
549
+ process_property
550
+ elsif value
551
+ error
552
+ end
553
+
554
+ nil
555
+ end
556
+
557
+ # Store the property/value pair in the currently active section. This
558
+ # method checks for continuation of the value to the next line.
559
+ #
560
+ # Returns nil.
561
+ def process_property
562
+ property.strip!
563
+ value.strip!
564
+
565
+ self.value = $1 if value =~ %r/\A"(.*)(?<!\\)"\z/m
566
+
567
+ section[property] = typecast(value)
568
+
569
+ self.property = nil
570
+ self.value = nil
571
+ end
572
+
573
+ # Returns the current section Hash.
574
+ def section
575
+ @section ||= @hash[@default]
576
+ end
577
+
578
+ # Raise a parse error using the given message and appending the current line
579
+ # being parsed.
580
+ #
581
+ # msg - The message String to use.
582
+ #
583
+ # Raises IniFile::Error
584
+ def error( msg = 'Could not parse line' )
585
+ raise Error, "#{msg}: #{@line.inspect}"
586
+ end
587
+
588
+ # Attempt to typecast the value string. We are looking for boolean values,
589
+ # integers, floats, and empty strings. Below is how each gets cast, but it
590
+ # is pretty logical and straightforward.
591
+ #
592
+ # "true" --> true
593
+ # "false" --> false
594
+ # "" --> nil
595
+ # "42" --> 42
596
+ # "3.14" --> 3.14
597
+ # "foo" --> "foo"
598
+ #
599
+ # Returns the typecast value.
600
+ def typecast( value )
601
+ case value
602
+ when %r/\Atrue\z/i; true
603
+ when %r/\Afalse\z/i; false
604
+ when %r/\A\s*\z/i; nil
605
+ else
606
+ Integer(value) rescue \
607
+ Float(value) rescue \
608
+ unescape_value(value)
609
+ end
610
+ end
611
+
612
+ # Unescape special characters found in the value string. This will convert
613
+ # escaped null, tab, carriage return, newline, and backslash into their
614
+ # literal equivalents.
615
+ #
616
+ # value - The String value to unescape.
617
+ #
618
+ # Returns the unescaped value.
619
+ def unescape_value( value )
620
+ value = value.to_s
621
+ value.gsub!(%r/\\[0nrt\\]/) { |char|
622
+ case char
623
+ when '\0'; "\0"
624
+ when '\n'; "\n"
625
+ when '\r'; "\r"
626
+ when '\t'; "\t"
627
+ when '\\\\'; "\\"
628
+ end
629
+ }
630
+ value
631
+ end
632
+ end
633
+
634
+ end # IniFile
635
+
@@ -1,3 +1,3 @@
1
1
  module BeakerPuppet
2
- VERSION = '4.1.1'
2
+ VERSION = '4.2.0'
3
3
  end
data/lib/beaker-puppet.rb CHANGED
@@ -3,6 +3,7 @@ require 'beaker'
3
3
  require 'in_parallel'
4
4
  require 'beaker-puppet/version'
5
5
  require 'beaker-puppet/wrappers'
6
+ require 'beaker-puppet/inifile'
6
7
 
7
8
  require 'beaker-puppet/helpers/rake_helpers'
8
9
 
@@ -62,7 +62,7 @@ describe ClassMixedWithDSLHelpers do
62
62
  allow(subject).to receive(:on).and_return(result)
63
63
  structured_fact = subject.fact_on('host', 'identity')
64
64
 
65
- expect(structured_fact['uid'].class).to be Fixnum
65
+ expect(structured_fact['uid'].class).to be Integer
66
66
  expect(structured_fact['user'].class).to be String
67
67
  expect(structured_fact['privileged'].class).to be (TrueClass or FalseClass)
68
68
  end