foodcritic 2.1.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (104) hide show
  1. data/CHANGELOG.md +691 -0
  2. data/LICENSE +21 -0
  3. data/README.md +28 -0
  4. data/chef_dsl_metadata/chef_0.10.10.json +22 -0
  5. data/chef_dsl_metadata/chef_10.12.0.json +22 -0
  6. data/chef_dsl_metadata/chef_10.14.0.json +22 -0
  7. data/chef_dsl_metadata/chef_10.14.2.json +22 -0
  8. data/chef_dsl_metadata/chef_10.14.4.json +22 -0
  9. data/chef_dsl_metadata/chef_10.16.0.json +22 -0
  10. data/chef_dsl_metadata/chef_10.16.2.json +22 -0
  11. data/chef_dsl_metadata/chef_10.16.4.json +22 -0
  12. data/chef_dsl_metadata/chef_10.16.6.json +22 -0
  13. data/chef_dsl_metadata/chef_10.18.0.json +22 -0
  14. data/chef_dsl_metadata/chef_10.18.2.json +22 -0
  15. data/chef_dsl_metadata/chef_10.20.0.json +22 -0
  16. data/chef_dsl_metadata/chef_10.22.0.json +22 -0
  17. data/chef_dsl_metadata/chef_10.24.0.json +22 -0
  18. data/chef_dsl_metadata/chef_10.24.4.json +8460 -0
  19. data/chef_dsl_metadata/chef_10.26.0.json +8460 -0
  20. data/chef_dsl_metadata/chef_11.0.0.json +22 -0
  21. data/chef_dsl_metadata/chef_11.2.0.json +22 -0
  22. data/chef_dsl_metadata/chef_11.4.0.json +22 -0
  23. data/chef_dsl_metadata/chef_11.4.2.json +8794 -0
  24. data/chef_dsl_metadata/chef_11.4.4.json +8794 -0
  25. data/features/002_check_string_interpolation.feature +45 -0
  26. data/features/003_check_for_chef_server.feature +56 -0
  27. data/features/004_check_service_resource_used.feature +53 -0
  28. data/features/005_check_for_resource_repetition.feature +64 -0
  29. data/features/006_check_file_mode.feature +35 -0
  30. data/features/007_check_for_undeclared_recipe_dependencies.feature +71 -0
  31. data/features/008_check_for_boilerplate_metadata.feature +25 -0
  32. data/features/009_check_for_unrecognised_resource_attributes.feature +77 -0
  33. data/features/010_check_search_syntax.feature +20 -0
  34. data/features/011_check_for_markdown_readme.feature +20 -0
  35. data/features/012_check_for_deprecated_readme_format.feature +20 -0
  36. data/features/013_check_for_hardcoded_tmpdir.feature +25 -0
  37. data/features/014_check_for_long_ruby_blocks.feature +30 -0
  38. data/features/015_check_for_definitions.feature +21 -0
  39. data/features/016_check_for_no_lwrp_default_action.feature +20 -0
  40. data/features/017_check_for_no_lwrp_notifications.feature +25 -0
  41. data/features/018_check_for_old_lwrp_notification_syntax.feature +25 -0
  42. data/features/019_check_for_consistent_node_access.feature +107 -0
  43. data/features/021_check_for_dodgy_lwrp_conditions.feature +28 -0
  44. data/features/022_check_for_dodgy_conditions_within_loop.feature +28 -0
  45. data/features/023_check_for_condition_around_resource.feature +52 -0
  46. data/features/024_check_for_missing_platforms.feature +43 -0
  47. data/features/025_check_for_deprecated_gem_install.feature +30 -0
  48. data/features/026_check_for_conditional_block_string.feature +20 -0
  49. data/features/027_check_for_internal_attribute_use.feature +22 -0
  50. data/features/028_check_for_incorrect_platform_method.feature +20 -0
  51. data/features/029_check_for_no_leading_cookbook_name.feature +18 -0
  52. data/features/030_check_for_debugger_breakpoints.feature +25 -0
  53. data/features/031_check_for_metadata_existence.feature +15 -0
  54. data/features/032_check_for_invalid_notification_timing.feature +22 -0
  55. data/features/033_check_for_missing_template.feature +75 -0
  56. data/features/034_check_for_unused_template_variables.feature +37 -0
  57. data/features/037_check_for_invalid_notification_action.feature +34 -0
  58. data/features/038_check_for_invalid_action.feature +51 -0
  59. data/features/039_check_for_key_access_to_node_methods.feature +33 -0
  60. data/features/040_check_raw_git_usage.feature +37 -0
  61. data/features/041_check_raw_download.feature +26 -0
  62. data/features/042_check_for_deprecated_require_recipe.feature +15 -0
  63. data/features/043_check_for_old_notification_style.feature +35 -0
  64. data/features/044_check_for_bare_attribute_keys.feature +43 -0
  65. data/features/045_check_for_cookbook_name_in_metadata.feature +20 -0
  66. data/features/046_check_for_assign_unless_nil_attributes.feature +21 -0
  67. data/features/build_framework_support.feature +99 -0
  68. data/features/checking_all_types_of_file.feature +40 -0
  69. data/features/choose_rules_to_apply.feature +49 -0
  70. data/features/command_line_help.feature +43 -0
  71. data/features/continuous_integration_support.feature +35 -0
  72. data/features/ignore_via_line_comments.feature +51 -0
  73. data/features/include_custom_rules.feature +29 -0
  74. data/features/individual_file.feature +12 -0
  75. data/features/limit_rules_to_specific_versions.feature +65 -0
  76. data/features/multiple_paths.feature +11 -0
  77. data/features/show_lines_matched.feature +20 -0
  78. data/features/sort_warnings.feature +10 -0
  79. data/features/specify_search_grammar.feature +25 -0
  80. data/features/step_definitions/cookbook_steps.rb +1791 -0
  81. data/features/support/command_helpers.rb +312 -0
  82. data/features/support/cookbook_helpers.rb +495 -0
  83. data/features/support/env.rb +11 -0
  84. data/lib/foodcritic.rb +0 -1
  85. data/lib/foodcritic/api.rb +3 -2
  86. data/lib/foodcritic/command_line.rb +4 -0
  87. data/lib/foodcritic/linter.rb +29 -6
  88. data/lib/foodcritic/output.rb +74 -26
  89. data/lib/foodcritic/rules.rb +6 -5
  90. data/lib/foodcritic/version.rb +1 -1
  91. data/man/foodcritic.1 +58 -0
  92. data/man/foodcritic.1.ronn +57 -0
  93. data/spec/foodcritic/api_spec.rb +1615 -0
  94. data/spec/foodcritic/chef_spec.rb +66 -0
  95. data/spec/foodcritic/command_line_spec.rb +51 -0
  96. data/spec/foodcritic/domain_spec.rb +24 -0
  97. data/spec/foodcritic/linter_spec.rb +91 -0
  98. data/spec/foodcritic/template_spec.rb +49 -0
  99. data/spec/regression/cookbooks.txt +135 -0
  100. data/spec/regression/expected-output.txt +443 -0
  101. data/spec/regression/regression_spec.rb +17 -0
  102. data/spec/regression_helpers.rb +37 -0
  103. data/spec/spec_helper.rb +10 -0
  104. metadata +87 -24
@@ -0,0 +1,11 @@
1
+ require 'simplecov'
2
+ SimpleCov.start do
3
+ add_filter '/features/'
4
+ end
5
+
6
+ require 'aruba/cucumber'
7
+ require 'foodcritic'
8
+
9
+ Before do
10
+ @aruba_timeout_seconds = 300
11
+ end
data/lib/foodcritic.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  require 'pathname'
2
2
  require 'gherkin'
3
3
  require 'treetop'
4
- require 'rak'
5
4
  require 'ripper'
6
5
  require 'yajl'
7
6
  require 'erubis'
@@ -190,7 +190,7 @@ module FoodCritic
190
190
 
191
191
  # Read the AST for the given Ruby source file
192
192
  def read_ast(file)
193
- source = if file.to_s.end_with? '.erb'
193
+ source = if file.to_s.split(File::SEPARATOR).include?('templates')
194
194
  Template::ExpressionExtractor.new.extract(
195
195
  File.read(file)).map{|e| e[:code]}.join(';')
196
196
  else
@@ -314,7 +314,8 @@ module FoodCritic
314
314
 
315
315
  # Templates in the current cookbook
316
316
  def template_paths(recipe_path)
317
- Dir[Pathname.new(recipe_path).dirname.dirname + 'templates' + '**/*.erb']
317
+ Dir[Pathname.new(recipe_path).dirname.dirname + 'templates' +
318
+ '**/*'].select{|path| File.file?(path)}
318
319
  end
319
320
 
320
321
  private
@@ -32,6 +32,10 @@ module FoodCritic
32
32
  "Additional rule file path(s) to load.") do |i|
33
33
  options[:include_rules] << i
34
34
  end
35
+ opts.on("-G", "--search-gems",
36
+ "Search rubygems for rule files with the path foodcritic/rules/**/*.rb") do |g|
37
+ options[:search_gems] = true
38
+ end
35
39
  opts.on("-S", "--search-grammar PATH",
36
40
  "Specify grammar to use when validating search syntax.") do |s|
37
41
  options[:search_grammar] = s
@@ -39,6 +39,7 @@ module FoodCritic
39
39
  # The `options` are a hash where the valid keys are:
40
40
  #
41
41
  # * `:include_rules` - Paths to additional rules to apply
42
+ # * `:search_gems - If true then search for custom rules in installed gems.
42
43
  # * `:tags` - The tags to filter rules based on
43
44
  # * `:fail_tags` - The tags to fail the build on
44
45
  # * `:exclude_paths` - Paths to exclude from linting
@@ -57,7 +58,8 @@ module FoodCritic
57
58
  # Loop through each file to be processed and apply the rules
58
59
  files_to_process(cookbook_paths, options[:exclude_paths]).each do |file|
59
60
  ast = read_ast(file)
60
- active_rules(options).each do |rule|
61
+ relevant_tags = options[:tags].any? ? options[:tags] : cookbook_tags(file)
62
+ active_rules(relevant_tags).each do |rule|
61
63
  rule_matches = matches(rule.recipe, ast, file)
62
64
 
63
65
  if dsl_method_for_file(file)
@@ -93,12 +95,20 @@ module FoodCritic
93
95
  end
94
96
 
95
97
  def load_rules!(options)
96
- @rules = RuleDsl.load([File.join(File.dirname(__FILE__), 'rules.rb')] +
97
- options[:include_rules], chef_version)
98
+ rule_files = [File.join(File.dirname(__FILE__), 'rules.rb')]
99
+ rule_files << options[:include_rules]
100
+ rule_files << rule_files_in_gems if options[:search_gems]
101
+ @rules = RuleDsl.load(rule_files.flatten.compact, chef_version)
98
102
  end
99
103
 
100
104
  private
101
105
 
106
+ def rule_files_in_gems
107
+ Gem::Specification.latest_specs(true).map do |spec|
108
+ spec.matches_for_glob('foodcritic/rules/**/*.rb')
109
+ end.flatten
110
+ end
111
+
102
112
  def remove_ignored(matches, rule, file)
103
113
  matches.reject do |m|
104
114
  matched_file = m[:filename] || file
@@ -122,9 +132,22 @@ module FoodCritic
122
132
  rule.applies_to.yield(Gem::Version.create(version))
123
133
  end
124
134
 
125
- def active_rules(options)
135
+ def cookbook_tags(file)
136
+ tags = []
137
+ fc_file = "#{cookbook_dir(file)}/.foodcritic"
138
+ if File.exist? fc_file
139
+ begin
140
+ tag_text = File.read fc_file
141
+ tags = tag_text.split(/\s/)
142
+ rescue Errno::EACCES
143
+ end
144
+ end
145
+ tags
146
+ end
147
+
148
+ def active_rules(tags)
126
149
  @rules.select do |rule|
127
- matching_tags?(options[:tags], rule.tags) and
150
+ matching_tags?(tags, rule.tags) and
128
151
  applies_to_version?(rule, chef_version)
129
152
  end
130
153
  end
@@ -160,7 +183,7 @@ module FoodCritic
160
183
  dirs.each do |dir|
161
184
  exclusions = Dir.glob(exclude_paths.map{|p| File.join(dir, p)})
162
185
  if File.directory? dir
163
- cookbook_glob = '{metadata.rb,{attributes,libraries,providers,recipes,resources}/*.rb,templates/*/*.erb}'
186
+ cookbook_glob = '{metadata.rb,{attributes,definitions,libraries,providers,recipes,resources}/*.rb,templates/*/*.erb}'
164
187
  files += (Dir.glob(File.join(dir, cookbook_glob)) +
165
188
  Dir.glob(File.join(dir, "*/#{cookbook_glob}")) - exclusions)
166
189
  else
@@ -1,3 +1,5 @@
1
+ require 'set'
2
+
1
3
  module FoodCritic
2
4
 
3
5
  # Default output showing a summary view.
@@ -22,34 +24,54 @@ module FoodCritic
22
24
  puts review; return
23
25
  end
24
26
 
25
- # Cheating here and mis-using Rak (Ruby port of Ack) to generate pretty
26
- # colourised context.
27
- #
28
- # Rak supports evaluating a custom expression as an alternative to a
29
- # regex. Our expression consults a hash of the matches found and then we
30
- # let Rak take care of the presentation.
31
- line_lookup = key_by_file_and_line(review)
32
- Rak.class_eval do
33
- const_set(:RULE_COLOUR, "\033[1;36m")
34
- @warnings = line_lookup
35
- end
36
- ARGV.replace(['--context', '--eval', %q{
37
- # This code will be evaluated inline by Rak.
38
- fn = fn.split("\n").first
39
- if @warnings.key?(fn) and @warnings[fn].key?($.) # filename and line no
40
- rule_name = opt[:colour] ? RULE_COLOUR : ''
41
- rule_name += "#{@warnings[fn][$.].to_a.join("\n")}#{CLEAR_COLOURS}"
42
- if ! displayed_filename
43
- fn = "#{fn}\n#{rule_name}"
44
- else
45
- puts rule_name
46
- end
47
- else
27
+ context = 3
28
+
29
+ print_fn = lambda { |fn| ansi_print(fn, :red, nil, :bold) }
30
+ print_rule = lambda { |warn| ansi_print(warn, :cyan, nil, :bold) }
31
+ print_line = lambda { |line| ansi_print(line, nil, :red, :bold) }
32
+
33
+ key_by_file_and_line(review).each do |fn, warnings|
34
+ print_fn.call fn
35
+ unless File.exists?(fn)
36
+ print_rule.call warnings[1].to_a.join("\n")
48
37
  next
49
38
  end
50
- }] + review.cookbook_paths)
51
- Rak.send(:remove_const, :VERSION) # Prevent duplicate VERSION warning
52
- load Gem.bin_path('rak', 'rak') # Assumes Rubygems
39
+
40
+ # Set of line numbers with warnings
41
+ warn_lines = warnings.keys.to_set
42
+ # Moving set of line numbers within the context of our position
43
+ context_set = (0..context).to_set
44
+ # The last line number we printed a warning for
45
+ last_warn = -1
46
+
47
+ File.open(fn) do |file|
48
+ file.each do |line|
49
+ context_set.add(file.lineno + context)
50
+ context_set.delete(file.lineno - context - 1)
51
+
52
+ # Find the first warning within our context
53
+ context_warns = context_set & warn_lines
54
+ next_warn = context_warns.min
55
+ # We may need to interrupt the trailing context of a previous warning
56
+ next_warn = file.lineno if warn_lines.include? file.lineno
57
+
58
+ # Display a warning
59
+ if next_warn && next_warn > last_warn
60
+ print_rule.call warnings[next_warn].to_a.join("\n")
61
+ last_warn = next_warn
62
+ end
63
+
64
+ # Display any relevant lines
65
+ if warn_lines.include? file.lineno
66
+ print '%4i|' % file.lineno
67
+ print_line.call line.chomp
68
+ elsif not context_warns.empty?
69
+ print '%4i|' % file.lineno
70
+ puts line.chomp
71
+ end
72
+ end
73
+ end
74
+ end
53
75
  end
54
76
 
55
77
  private
@@ -73,6 +95,32 @@ module FoodCritic
73
95
  warn_hash
74
96
  end
75
97
 
98
+ # Print an ANSI escape-code formatted string (and a newline)
99
+ #
100
+ # @param text [String] the string to format
101
+ # @param fg [String] foreground color
102
+ # @param bg [String] background color
103
+ # @param attr [String] any formatting options
104
+ def ansi_print(text, fg, bg = nil, attr = nil)
105
+ unless STDOUT.tty?
106
+ puts text
107
+ return
108
+ end
109
+
110
+ colors = %w(black red green yellow blue magenta cyan white)
111
+ attrs = %w(reset bold dim underscore blink reverse hidden)
112
+ escape = "\033[%sm"
113
+ fmt = []
114
+ fmt << 30 + colors.index(fg.to_s) if fg
115
+ fmt << 40 + colors.index(bg.to_s) if bg
116
+ fmt << attrs.index(attr.to_s) if attr
117
+ if fmt
118
+ puts "#{escape % fmt.join(';')}#{text}#{escape % 0}"
119
+ else
120
+ puts text
121
+ end
122
+ end
123
+
76
124
  end
77
125
 
78
126
  end
@@ -173,8 +173,9 @@ rule "FC014", "Consider extracting long ruby_block to library" do
173
173
  tags %w{style libraries}
174
174
  recipe do |ast|
175
175
  find_resources(ast, :type => 'ruby_block').find_all do |rb|
176
- ! rb.xpath("//fcall[ident/@value='block' and count(ancestor::*) = 8]/../
177
- ../do_block[count(descendant::*) > 100]").empty?
176
+ lines = rb.xpath("descendant::fcall[ident/@value='block']/../../
177
+ descendant::*[@line]/@line").map{|n| n.value.to_i}.sort
178
+ (! lines.empty?) && (lines.last - lines.first) > 15
178
179
  end
179
180
  end
180
181
  end
@@ -468,9 +469,9 @@ rule "FC034", "Unused template variables" do
468
469
  Array(resource_attributes_by_type(ast)['template']).select do
469
470
  |t| t['variables'] and t['variables'].respond_to?(:xpath)
470
471
  end.map do |resource|
471
- template_paths = Dir[Pathname.new(filename).dirname.dirname +
472
- 'templates' + '**/*.erb']
473
- template_path = template_paths.find{|p| File.basename(p) == resource['source']}
472
+ template_path = template_paths(filename).find do |p|
473
+ File.basename(p) == resource['source']
474
+ end
474
475
  next unless template_path
475
476
  passed_vars = resource['variables'].xpath('symbol/ident/@value').map{|tv| tv.to_s}
476
477
  template_vars = read_ast(template_path).xpath('//var_ref/ivar/' +
@@ -1,4 +1,4 @@
1
1
  module FoodCritic
2
2
  # The current version of foodcritic
3
- VERSION = '2.1.0'
3
+ VERSION = '2.2.0'
4
4
  end
data/man/foodcritic.1 ADDED
@@ -0,0 +1,58 @@
1
+ .\" generated with Ronn/v0.7.3
2
+ .\" http://github.com/rtomayko/ronn/tree/0.7.3
3
+ .
4
+ .TH "FOODCRITIC" "1" "July 2013" "" ""
5
+ .
6
+ .SH "NAME"
7
+ \fBfoodcritic\fR \- lint tool for chef cookbooks
8
+ .
9
+ .SH "SYNOPSIS"
10
+ \fBfoodcritic\fR [\fIoptions\fR\.\.\.] \fIcookbook\fR\.\.\.
11
+ .
12
+ .SH "DESCRIPTION"
13
+ \fBfoodcritic\fR makes it easier to flag problems in your Chef cookbooks that will cause Chef to blow up when you attempt to converge\. This is about faster feedback\.
14
+ .
15
+ .P
16
+ Each \fBcookbook\fR path specified will be examined for common problems and poor style\.
17
+ .
18
+ .SH "OPTIONS"
19
+ .
20
+ .TP
21
+ \fB\-t\fR, \fB\-\-tags\fR \fITAGS\fR
22
+ Only check against rules with the specified tags\.
23
+ .
24
+ .TP
25
+ \fB\-f\fR, \fB\-\-epic\-fail\fR \fITAGS\fR
26
+ Exit non\-zero if any of the specified tags are matched\. Use the pseudo\-tag \fBany\fR to fail if any tag is matched\.
27
+ .
28
+ .TP
29
+ \fB\-c\fR, \fB\-\-chef\-version\fR \fIVERSION\fR
30
+ Only check against rules valid for this version of Chef\.
31
+ .
32
+ .TP
33
+ \fB\-C\fR, \fB\-\-\fR[\fBno\-\fR]\fBcontext\fR
34
+ Show lines matched against rather than the default summary\.
35
+ .
36
+ .TP
37
+ \fB\-I\fR, \fB\-\-include\fR \fIPATH\fR
38
+ Additional rule file path(s) to load\.
39
+ .
40
+ .TP
41
+ \fB\-S\fR, \fB\-\-search\-grammar\fR \fIPATH\fR
42
+ Specify grammar to use when validating search syntax\. (Default: the grammar of any installed Chef)
43
+ .
44
+ .TP
45
+ \fB\-V\fR, \fB\-\-version\fR
46
+ Display the foodcritic version\.
47
+ .
48
+ .SH "RETURN VALUES"
49
+ By default, \fBfoodcritic\fR will always return \fB0\fR\.
50
+ .
51
+ .P
52
+ If \fB\-\-epic\-fail\fR is specified, then \fBfoodcritic\fR will return \fB3\fR if any tags are matched\.
53
+ .
54
+ .SH "COPYRIGHT"
55
+ \fBfoodcritic\fR is Copyright 2011 by Andrew Crump\.
56
+ .
57
+ .SH "SEE ALSO"
58
+ chef(1)
@@ -0,0 +1,57 @@
1
+ foodcritic(1) -- lint tool for chef cookbooks
2
+ =============================================
3
+
4
+ ## SYNOPSIS
5
+
6
+ `foodcritic` [<options>...] <cookbook>...
7
+
8
+
9
+ ## DESCRIPTION
10
+
11
+ `foodcritic` makes it easier to flag problems in your Chef cookbooks that will
12
+ cause Chef to blow up when you attempt to converge.
13
+ This is about faster feedback.
14
+
15
+ Each `cookbook` path specified will be examined for common problems and
16
+ poor style.
17
+
18
+
19
+ ## OPTIONS
20
+
21
+ * `-t`, `--tags` <TAGS>:
22
+ Only check against rules with the specified tags.
23
+
24
+ * `-f`, `--epic-fail` <TAGS>:
25
+ Exit non-zero if any of the specified tags are matched.
26
+ Use the pseudo-tag `any` to fail if any tag is matched.
27
+
28
+ * `-c`, `--chef-version` <VERSION>:
29
+ Only check against rules valid for this version of Chef.
30
+
31
+ * `-C`, `--`[`no-`]`context`:
32
+ Show lines matched against rather than the default summary.
33
+
34
+ * `-I`, `--include` <PATH>:
35
+ Additional rule file path(s) to load.
36
+
37
+ * `-S`, `--search-grammar` <PATH>:
38
+ Specify grammar to use when validating search syntax.
39
+ (Default: the grammar of any installed Chef)
40
+
41
+ * `-V`, `--version`:
42
+ Display the foodcritic version.
43
+
44
+
45
+ ## RETURN VALUES
46
+
47
+ By default, `foodcritic` will always return `0`.
48
+
49
+ If `--epic-fail` is specified, then `foodcritic` will return `3` if any tags are matched.
50
+
51
+ ## COPYRIGHT
52
+
53
+ `foodcritic` is Copyright 2011 by Andrew Crump.
54
+
55
+ ## SEE ALSO
56
+
57
+ chef(1)
@@ -0,0 +1,1615 @@
1
+ require_relative '../spec_helper'
2
+
3
+ describe FoodCritic::Api do
4
+
5
+ def parse_ast(str)
6
+ api.send(:build_xml, Ripper::SexpBuilder.new(str).parse)
7
+ end
8
+
9
+ let(:api) { Object.new.extend(FoodCritic::Api) }
10
+
11
+ describe :exposed_api do
12
+ let(:ignorable_methods) do
13
+ api.class.ancestors.map{|a| a.public_methods}.flatten.sort.uniq
14
+ end
15
+ it "exposes the expected api to rule authors" do
16
+ (api.public_methods.sort - ignorable_methods).must_equal([
17
+ :attribute_access,
18
+ :checks_for_chef_solo?,
19
+ :chef_dsl_methods,
20
+ :chef_node_methods,
21
+ :chef_solo_search_supported?,
22
+ :cookbook_name,
23
+ :declared_dependencies,
24
+ :file_match,
25
+ :find_resources,
26
+ :gem_version,
27
+ :included_recipes,
28
+ :literal_searches,
29
+ :match,
30
+ :notifications,
31
+ :read_ast,
32
+ :resource_action?,
33
+ :resource_attribute,
34
+ :resource_attribute?,
35
+ :resource_attributes,
36
+ :resource_attributes_by_type,
37
+ :resource_name,
38
+ :resource_type,
39
+ :resources_by_type,
40
+ :ruby_code?,
41
+ :searches,
42
+ :standard_cookbook_subdirs,
43
+ :supported_platforms,
44
+ :template_file,
45
+ :template_paths,
46
+ :valid_query?
47
+ ])
48
+ end
49
+ end
50
+
51
+ describe "#attribute_access" do
52
+ let(:ast) { MiniTest::Mock.new }
53
+ it "returns empty if the provided ast does not support XPath" do
54
+ api.attribute_access(nil, :type => :vivified).must_be_empty
55
+ end
56
+ it "returns empty if the provided ast has no matches" do
57
+ ast.expect :xpath, [], [String]
58
+ [:vivified, :string, :symbol].each do |access_type|
59
+ api.attribute_access([], :type => :vivified).must_be_empty
60
+ end
61
+ end
62
+ it "raises if the specified node type is not recognised" do
63
+ ast.expect :xpath, [], [String]
64
+ lambda do
65
+ api.attribute_access(ast, :type => :cymbals)
66
+ end.must_raise(ArgumentError)
67
+ end
68
+ it "does not raise if the specified node type is valid" do
69
+ ast.expect :xpath, [], [/field/, FoodCritic::Api::AttFilter]
70
+ ast.expect :xpath, [], [/symbol/, FoodCritic::Api::AttFilter]
71
+ ast.expect :xpath, [], [/tstring_content/, FoodCritic::Api::AttFilter]
72
+ [:vivified, :symbol, :string].each do |access_type|
73
+ api.attribute_access(ast, :type => access_type)
74
+ end
75
+ end
76
+ it "returns vivified attributes access" do
77
+ call = MiniTest::Mock.new
78
+ call.expect :xpath, [], [/args_add_block/]
79
+ call.expect :xpath, ["node", "bar"], [/ident/]
80
+ call.expect :xpath, ["foo"], [/@value/]
81
+ ast.expect :xpath, [call], [String, FoodCritic::Api::AttFilter]
82
+ api.attribute_access(ast, :type => :vivified).must_equal([call])
83
+ ast.verify
84
+ call.verify
85
+ end
86
+ it "doesn't flag searching for a node by name as symbol access" do
87
+ ast = parse_ast(%q{baz = search(:node, "name:#{node['foo']['bar']}")[0]})
88
+ api.attribute_access(ast, :type => :symbol).must_be_empty
89
+ end
90
+ end
91
+
92
+ describe "#checks_for_chef_solo?" do
93
+ let(:ast) { ast = MiniTest::Mock.new }
94
+ it "raises if the provided ast does not support XPath" do
95
+ lambda{api.checks_for_chef_solo?(nil)}.must_raise(ArgumentError)
96
+ end
97
+ it "returns false if there is no reference to chef solo" do
98
+ ast.expect :xpath, [], [String]
99
+ ast.expect :xpath, [], [String]
100
+ refute api.checks_for_chef_solo?(ast)
101
+ end
102
+ it "returns true if there is one reference to chef solo" do
103
+ ast.expect :xpath, ['aref'], [String]
104
+ assert api.checks_for_chef_solo?(ast)
105
+ end
106
+ it "returns true if there are multiple references to chef solo" do
107
+ ast.expect :xpath, ['aref', 'aref'], [String]
108
+ assert api.checks_for_chef_solo?(ast)
109
+ end
110
+ end
111
+
112
+ describe "#chef_solo_search_supported?" do
113
+ it "returns false if the recipe path is nil" do
114
+ refute api.chef_solo_search_supported?(nil)
115
+ end
116
+ it "returns false if the recipe path does not exist" do
117
+ refute api.chef_solo_search_supported?('/tmp/non-existent-path')
118
+ end
119
+ end
120
+
121
+ describe "#cookbook_name" do
122
+ it "raises if passed a nil" do
123
+ lambda{api.cookbook_name(nil)}.must_raise ArgumentError
124
+ end
125
+ it "raises if passed an empty string" do
126
+ lambda{api.cookbook_name('')}.must_raise ArgumentError
127
+ end
128
+ it "returns the cookbook name when passed a recipe" do
129
+ recipe_path = 'cookbooks/apache2/recipes/default.rb'
130
+ api.cookbook_name(recipe_path).must_equal 'apache2'
131
+ end
132
+ it "returns the cookbook name when passed the cookbook metadata" do
133
+ api.cookbook_name('cookbooks/apache2/metadata.rb').must_equal 'apache2'
134
+ end
135
+ it "returns the cookbook name when passed a template" do
136
+ erb_path = 'cookbooks/apache2/templates/default/a2ensite.erb'
137
+ api.cookbook_name(erb_path).must_equal 'apache2'
138
+ end
139
+ end
140
+
141
+ describe "#declared_dependencies" do
142
+ it "raises if the ast does not support XPath" do
143
+ lambda{api.declared_dependencies(nil)}.must_raise ArgumentError
144
+ end
145
+ it "returns an empty if there are no declared dependencies" do
146
+ ast = MiniTest::Mock.new
147
+ 3.times do
148
+ ast.expect :xpath, [], [String]
149
+ end
150
+ api.declared_dependencies(ast).must_be_empty
151
+ end
152
+ it "includes only cookbook names in the returned array" do
153
+ ast = Nokogiri::XML(%q{
154
+ <command>
155
+ <ident value="depends">
156
+ <pos line="14" column="0"/>
157
+ </ident>
158
+ <args_add_block value="false">
159
+ <args_add>
160
+ <args_add>
161
+ <args_new/>
162
+ <string_literal>
163
+ <string_add>
164
+ <string_content/>
165
+ <tstring_content value="mysql">
166
+ <pos line="14" column="9"/>
167
+ </tstring_content>
168
+ </string_add>
169
+ </string_literal>
170
+ </args_add>
171
+ <string_literal>
172
+ <string_add>
173
+ <string_content/>
174
+ <tstring_content value="&gt;= 1.2.0">
175
+ <pos line="14" column="18"/>
176
+ </tstring_content>
177
+ </string_add>
178
+ </string_literal>
179
+ </args_add>
180
+ </args_add_block>
181
+ </command>
182
+ })
183
+ api.declared_dependencies(ast).must_equal ['mysql']
184
+ end
185
+ end
186
+
187
+ describe "#file_match" do
188
+ it "includes the provided filename in the match" do
189
+ api.file_match("foo.rb")[:filename].must_equal "foo.rb"
190
+ end
191
+ it "retains the full provided filename path in the match" do
192
+ api.file_match("foo/bar/foo.rb")[:filename].must_equal "foo/bar/foo.rb"
193
+ end
194
+ it "raises an error if the provided filename is nil" do
195
+ lambda{api.file_match(nil)}.must_raise(ArgumentError)
196
+ end
197
+ it "sets the line and column to the beginning of the file" do
198
+ match = api.file_match("bar.rb")
199
+ match[:line].must_equal 1
200
+ match[:column].must_equal 1
201
+ end
202
+ end
203
+
204
+ describe "#find_resources" do
205
+ let(:ast) { MiniTest::Mock.new }
206
+ it "returns empty unless the ast supports XPath" do
207
+ api.find_resources(nil).must_be_empty
208
+ end
209
+ it "restricts by resource type when provided" do
210
+ ast.expect :xpath, ['method_add_block'],
211
+ ["//method_add_block[command/ident[@value='file']]" +
212
+ "[command/ident/@value != 'action']"]
213
+ api.find_resources(ast, :type => 'file')
214
+ ast.verify
215
+ end
216
+ it "does not restrict by resource type when not provided" do
217
+ ast.expect :xpath, ['method_add_block'],
218
+ ["//method_add_block[command/ident]" +
219
+ "[command/ident/@value != 'action']"]
220
+ api.find_resources(ast)
221
+ ast.verify
222
+ end
223
+ it "allows resource type to be specified as :any" do
224
+ ast.expect :xpath, ['method_add_block'],
225
+ ["//method_add_block[command/ident]" +
226
+ "[command/ident/@value != 'action']"]
227
+ api.find_resources(ast, :type => :any)
228
+ ast.verify
229
+ end
230
+ it "returns any matches" do
231
+ ast.expect :xpath, ['method_add_block'], [String]
232
+ api.find_resources(ast).must_equal ['method_add_block']
233
+ end
234
+ end
235
+
236
+ describe "#included_recipes" do
237
+ it "raises if the ast does not support XPath" do
238
+ lambda{api.included_recipes(nil)}.must_raise ArgumentError
239
+ end
240
+ it "returns an empty hash if there are no included recipes" do
241
+ ast = MiniTest::Mock.new.expect :xpath, [], [String]
242
+ api.included_recipes(ast).keys.must_be_empty
243
+ end
244
+ it "returns a hash keyed by recipe name" do
245
+ ast = MiniTest::Mock.new.expect :xpath, [{'value' => 'foo::bar'}],
246
+ [String]
247
+ api.included_recipes(ast).keys.must_equal ['foo::bar']
248
+ end
249
+ it "returns a hash where the values are the matching nodes" do
250
+ ast = MiniTest::Mock.new.expect :xpath, [{'value' => 'foo::bar'}],
251
+ [String]
252
+ api.included_recipes(ast).values.must_equal [[{'value' => 'foo::bar'}]]
253
+ end
254
+ it "correctly keys an included recipe specified as a string literal" do
255
+ api.included_recipes(parse_ast(%q{
256
+ include_recipe "foo::default"
257
+ })).keys.must_equal ["foo::default"]
258
+ end
259
+ describe "embedded expression - recipe name" do
260
+ let(:ast) do
261
+ parse_ast(%q{
262
+ include_recipe "foo::#{bar}"
263
+ })
264
+ end
265
+ it "returns the literal string component by default" do
266
+ api.included_recipes(ast).keys.must_equal ["foo::"]
267
+ end
268
+ it "returns the literal string part of the AST" do
269
+ api.included_recipes(ast)['foo::'].first.must_respond_to(:xpath)
270
+ end
271
+ it "returns empty if asked to exclude statements with embedded expressions" do
272
+ api.included_recipes(ast, :with_partial_names => false).must_be_empty
273
+ end
274
+ it "returns the literals if asked to include statements with embedded expressions" do
275
+ api.included_recipes(ast, :with_partial_names => true).keys.must_equal ["foo::"]
276
+ end
277
+ end
278
+ describe "embedded expression - cookbook name" do
279
+ let(:ast) do
280
+ parse_ast(%q{
281
+ include_recipe "#{foo}::bar"
282
+ })
283
+ end
284
+ it "returns the literal string component by default" do
285
+ api.included_recipes(ast).keys.must_equal ["::bar"]
286
+ end
287
+ it "returns the literal string part of the AST" do
288
+ api.included_recipes(ast)['::bar'].first.must_respond_to(:xpath)
289
+ end
290
+ it "returns empty if asked to exclude statements with embedded expressions" do
291
+ api.included_recipes(ast, :with_partial_names => false).must_be_empty
292
+ end
293
+ end
294
+ describe "embedded expression - partial cookbook name" do
295
+ let(:ast) do
296
+ parse_ast(%q{
297
+ include_recipe "#{foo}_foo::bar"
298
+ })
299
+ end
300
+ it "returns the literal string component by default" do
301
+ api.included_recipes(ast).keys.must_equal ["_foo::bar"]
302
+ end
303
+ it "returns the literal string part of the AST" do
304
+ api.included_recipes(ast)['_foo::bar'].first.must_respond_to(:xpath)
305
+ end
306
+ it "returns empty if asked to exclude statements with embedded expressions" do
307
+ api.included_recipes(ast, :with_partial_names => false).must_be_empty
308
+ end
309
+ end
310
+ end
311
+
312
+ describe :AttFilter do
313
+ describe "#is_att_type" do
314
+ let(:filter) { FoodCritic::Api::AttFilter.new }
315
+ it "returns empty if the argument is not enumerable" do
316
+ filter.is_att_type(nil).must_be_empty
317
+ end
318
+ it "filters out values that are not Chef node attribute types" do
319
+ nodes = %w{node node badger default override ostrich set normal}
320
+ filter.is_att_type(nodes).uniq.size.must_equal 5
321
+ end
322
+ it "returns all filtered nodes" do
323
+ nodes = %w{node node override default normal set set override}
324
+ filter.is_att_type(nodes).must_equal nodes
325
+ end
326
+ it "returns empty if there are no Chef node attribute types" do
327
+ nodes = %w{squirrel badger pooh tigger}
328
+ filter.is_att_type(nodes).must_be_empty
329
+ end
330
+ end
331
+ end
332
+
333
+ describe "#literal_searches" do
334
+ let(:ast) { ast = MiniTest::Mock.new }
335
+ it "returns empty if the AST does not support XPath expressions" do
336
+ api.literal_searches(nil).must_be_empty
337
+ end
338
+ it "returns empty if the AST has no elements" do
339
+ ast.expect :xpath, [], [String]
340
+ api.literal_searches(ast).must_be_empty
341
+ end
342
+ it "returns the AST elements for each literal search" do
343
+ ast.expect :xpath, ['tstring_content'], [String]
344
+ api.literal_searches(ast).must_equal ['tstring_content']
345
+ end
346
+ end
347
+
348
+ describe "#match" do
349
+ it "raises if the provided node is nil" do
350
+ lambda{api.match(nil)}.must_raise(ArgumentError)
351
+ end
352
+ it "raises if the provided node does not support XPath" do
353
+ lambda{api.match(Object.new)}.must_raise(ArgumentError)
354
+ end
355
+ it "returns nil if there is no nested position node" do
356
+ node = MiniTest::Mock.new
357
+ node.expect :xpath, [], ['descendant::pos']
358
+ api.match(node).must_be_nil
359
+ end
360
+ it "uses the position of the first position node if there are multiple" do
361
+ node = MiniTest::Mock.new
362
+ node.expect(:xpath,
363
+ [{'name' => 'pos', 'line' => '1', 'column' => '10'},
364
+ {'name' => 'pos', 'line' => '3', 'column' => '16'}],
365
+ ['descendant::pos'])
366
+ match = api.match(node)
367
+ match[:line].must_equal(1)
368
+ match[:column].must_equal(10)
369
+ end
370
+ describe :matched_node_name do
371
+ let(:node) do
372
+ node = MiniTest::Mock.new
373
+ node.expect :xpath, [{'name' => 'pos', 'line' => '1',
374
+ 'column' => '10'}], ['descendant::pos']
375
+ node
376
+ end
377
+ it "includes the name of the node in the match" do
378
+ node.expect :name, 'command'
379
+ api.match(node).must_equal({:matched => 'command', :line => 1,
380
+ :column => 10})
381
+ end
382
+ it "sets the matched name to empty if the element does not have a name" do
383
+ api.match(node).must_equal({:matched => '', :line => 1, :column => 10})
384
+ end
385
+ end
386
+ end
387
+
388
+ describe "#notifications" do
389
+ it "returns empty if the provided AST does not support XPath" do
390
+ api.notifications(nil).must_be_empty
391
+ end
392
+ it "returns empty if there are no notifications" do
393
+ api.notifications(parse_ast(%q{
394
+ template "/etc/nscd.conf" do
395
+ source "nscd.conf"
396
+ owner "root"
397
+ group "root"
398
+ end
399
+ })).must_be_empty
400
+ end
401
+ describe "malformed syntax" do
402
+ it "returns empty if no notifies value is provided" do
403
+ api.notifications(parse_ast(%q{
404
+ template "/etc/nscd.conf" do
405
+ source "nscd.conf"
406
+ owner "root"
407
+ group "root"
408
+ notifies
409
+ end
410
+ })).must_be_empty
411
+ end
412
+ it "returns empty if no subscribes value is provided" do
413
+ api.notifications(parse_ast(%q{
414
+ template "/etc/nscd.conf" do
415
+ source "nscd.conf"
416
+ owner "root"
417
+ group "root"
418
+ subscribes
419
+ end
420
+ })).must_be_empty
421
+ end
422
+ it "returns empty if only the notifies action is provided" do
423
+ api.notifications(parse_ast(%q{
424
+ template "/etc/nscd.conf" do
425
+ source "nscd.conf"
426
+ owner "root"
427
+ group "root"
428
+ notifies :restart
429
+ end
430
+ })).must_be_empty
431
+ end
432
+ it "returns empty if only the subscribes action is provided" do
433
+ api.notifications(parse_ast(%q{
434
+ template "/etc/nscd.conf" do
435
+ source "nscd.conf"
436
+ owner "root"
437
+ group "root"
438
+ subscribes :restart
439
+ end
440
+ })).must_be_empty
441
+ end
442
+ describe "returns empty if the service name is missing" do
443
+ it "old-style notifications" do
444
+ api.notifications(parse_ast(%q{
445
+ template "/etc/nscd.conf" do
446
+ source "nscd.conf"
447
+ owner "root"
448
+ group "root"
449
+ notifies :restart, resources(:service)
450
+ end
451
+ })).must_be_empty
452
+ end
453
+ it "old-style subscriptions" do
454
+ api.notifications(parse_ast(%q{
455
+ template "/etc/nscd.conf" do
456
+ source "nscd.conf"
457
+ owner "root"
458
+ group "root"
459
+ subscribes:restart, resources(:service)
460
+ end
461
+ })).must_be_empty
462
+ end
463
+ it "new-style notifications" do
464
+ api.notifications(parse_ast(%q{
465
+ template "/etc/nscd.conf" do
466
+ source "nscd.conf"
467
+ owner "root"
468
+ group "root"
469
+ notifies :restart, "service"
470
+ end
471
+ })).must_be_empty
472
+ end
473
+ it "new-style subscriptions" do
474
+ api.notifications(parse_ast(%q{
475
+ template "/etc/nscd.conf" do
476
+ source "nscd.conf"
477
+ owner "root"
478
+ group "root"
479
+ subscribes:restart, "service"
480
+ end
481
+ })).must_be_empty
482
+ end
483
+ end
484
+ describe "returns empty if the resource type is missing" do
485
+ it "old-style notifications" do
486
+ api.notifications(parse_ast(%q{
487
+ template "/etc/nscd.conf" do
488
+ source "nscd.conf"
489
+ owner "root"
490
+ group "root"
491
+ notifies :restart, resources("nscd")
492
+ end
493
+ })).must_be_empty
494
+ end
495
+ it "old-style subscriptions" do
496
+ api.notifications(parse_ast(%q{
497
+ template "/etc/nscd.conf" do
498
+ source "nscd.conf"
499
+ owner "root"
500
+ group "root"
501
+ subscribes :restart, resources("nscd")
502
+ end
503
+ })).must_be_empty
504
+ end
505
+ it "new-style notifications" do
506
+ api.notifications(parse_ast(%q{
507
+ template "/etc/nscd.conf" do
508
+ source "nscd.conf"
509
+ owner "root"
510
+ group "root"
511
+ notifies :restart, "nscd"
512
+ end
513
+ })).must_be_empty
514
+ end
515
+ it "new-style subscriptions" do
516
+ api.notifications(parse_ast(%q{
517
+ template "/etc/nscd.conf" do
518
+ source "nscd.conf"
519
+ owner "root"
520
+ group "root"
521
+ subscribes :restart, "nscd"
522
+ end
523
+ })).must_be_empty
524
+ end
525
+ end
526
+ describe "returns empty if the resource name is missing" do
527
+ it "old-style notifications" do
528
+ api.notifications(parse_ast(%q{
529
+ template "/etc/nscd.conf" do
530
+ source "nscd.conf"
531
+ owner "root"
532
+ group "root"
533
+ notifies :restart, resources(:service)
534
+ end
535
+ })).must_be_empty
536
+ end
537
+ it "old-style subscriptions" do
538
+ api.notifications(parse_ast(%q{
539
+ template "/etc/nscd.conf" do
540
+ source "nscd.conf"
541
+ owner "root"
542
+ group "root"
543
+ subscribes :restart, resources(:service)
544
+ end
545
+ })).must_be_empty
546
+ end
547
+ it "new-style notifications" do
548
+ api.notifications(parse_ast(%q{
549
+ template "/etc/nscd.conf" do
550
+ source "nscd.conf"
551
+ owner "root"
552
+ group "root"
553
+ notifies :restart, "service[]"
554
+ end
555
+ })).must_be_empty
556
+ end
557
+ it "new-style subscriptions" do
558
+ api.notifications(parse_ast(%q{
559
+ template "/etc/nscd.conf" do
560
+ source "nscd.conf"
561
+ owner "root"
562
+ group "root"
563
+ subscribes :restart, "service[]"
564
+ end
565
+ })).must_be_empty
566
+ end
567
+ end
568
+ it "returns empty if the left square bracket is missing" do
569
+ api.notifications(parse_ast(%q{
570
+ template "/etc/nscd.conf" do
571
+ source "nscd.conf"
572
+ owner "root"
573
+ group "root"
574
+ subscribes :restart, "servicefoo]"
575
+ end
576
+ })).must_be_empty
577
+ end
578
+ it "returns empty if the right square bracket is missing" do
579
+ api.notifications(parse_ast(%q{
580
+ template "/etc/nscd.conf" do
581
+ source "nscd.conf"
582
+ owner "root"
583
+ group "root"
584
+ subscribes :restart, "service[foo"
585
+ end
586
+ })).must_be_empty
587
+ end
588
+ end
589
+ it "understands the old-style notifications" do
590
+ api.notifications(parse_ast(%q{
591
+ template "/etc/nscd.conf" do
592
+ source "nscd.conf"
593
+ owner "root"
594
+ group "root"
595
+ notifies :restart, resources(:service => "nscd")
596
+ end
597
+ })).must_equal(
598
+ [{
599
+ :type => :notifies,
600
+ :action => :restart,
601
+ :resource_type => :service,
602
+ :resource_name => 'nscd',
603
+ :timing => :delayed,
604
+ :style => :old
605
+ }]
606
+ )
607
+ end
608
+ it "understands old-style notifications with :'symbol' literals as action" do
609
+ api.notifications(parse_ast(%q{
610
+ template "/etc/nscd.conf" do
611
+ source "nscd.conf"
612
+ owner "root"
613
+ group "root"
614
+ notifies :'soft-restart', resources(:service => "nscd")
615
+ end
616
+ })).must_equal(
617
+ [{
618
+ :type => :notifies,
619
+ :action => :'soft-restart',
620
+ :resource_type => :service,
621
+ :resource_name => 'nscd',
622
+ :timing => :delayed,
623
+ :style => :old
624
+ }]
625
+ )
626
+ end
627
+ it "understands old-style notifications with added parentheses" do
628
+ api.notifications(parse_ast(%q{
629
+ template "/etc/nscd.conf" do
630
+ source "nscd.conf"
631
+ owner "root"
632
+ group "root"
633
+ notifies(:restart, resources(:service => "nscd"))
634
+ end
635
+ })).must_equal(
636
+ [{
637
+ :type => :notifies,
638
+ :action => :restart,
639
+ :resource_type => :service,
640
+ :resource_name => 'nscd',
641
+ :timing => :delayed,
642
+ :style => :old
643
+ }]
644
+ )
645
+ end
646
+ it "understands the old-style subscriptions" do
647
+ api.notifications(parse_ast(%q{
648
+ template "/etc/nscd.conf" do
649
+ source "nscd.conf"
650
+ owner "root"
651
+ group "root"
652
+ subscribes :restart, resources(:service => "nscd")
653
+ end
654
+ })).must_equal(
655
+ [{
656
+ :type => :subscribes,
657
+ :action => :restart,
658
+ :resource_type => :service,
659
+ :resource_name => 'nscd',
660
+ :timing => :delayed,
661
+ :style => :old
662
+ }]
663
+ )
664
+ end
665
+ it "understands old-style subscriptions with added parentheses" do
666
+ api.notifications(parse_ast(%q{
667
+ template "/etc/nscd.conf" do
668
+ source "nscd.conf"
669
+ owner "root"
670
+ group "root"
671
+ subscribes(:restart, resources(:service => "nscd"))
672
+ end
673
+ })).must_equal(
674
+ [{
675
+ :type => :subscribes,
676
+ :action => :restart,
677
+ :resource_type => :service,
678
+ :resource_name => 'nscd',
679
+ :timing => :delayed,
680
+ :style => :old
681
+ }]
682
+ )
683
+ end
684
+ it "understands the new-style notifications" do
685
+ api.notifications(parse_ast(%q{
686
+ template "/etc/nscd.conf" do
687
+ source "nscd.conf"
688
+ owner "root"
689
+ group "root"
690
+ notifies :restart, "service[nscd]"
691
+ end
692
+ })).must_equal(
693
+ [{
694
+ :type => :notifies,
695
+ :action => :restart,
696
+ :resource_type => :service,
697
+ :resource_name => 'nscd',
698
+ :timing => :delayed,
699
+ :style => :new
700
+ }]
701
+ )
702
+ end
703
+ it "understands new-style notifications with :'symbol' literals as action" do
704
+ api.notifications(parse_ast(%q{
705
+ template "/etc/nscd.conf" do
706
+ source "nscd.conf"
707
+ owner "root"
708
+ group "root"
709
+ notifies :'soft-restart', "service[nscd]"
710
+ end
711
+ })).must_equal(
712
+ [{
713
+ :type => :notifies,
714
+ :action => :'soft-restart',
715
+ :resource_type => :service,
716
+ :resource_name => 'nscd',
717
+ :timing => :delayed,
718
+ :style => :new
719
+ }]
720
+ )
721
+ end
722
+ it "understands new-style notifications with added parentheses" do
723
+ api.notifications(parse_ast(%q{
724
+ template "/etc/nscd.conf" do
725
+ source "nscd.conf"
726
+ owner "root"
727
+ group "root"
728
+ notifies(:restart, "service[nscd]")
729
+ end
730
+ })).must_equal(
731
+ [{
732
+ :type => :notifies,
733
+ :action => :restart,
734
+ :resource_type => :service,
735
+ :resource_name => 'nscd',
736
+ :timing => :delayed,
737
+ :style => :new
738
+ }]
739
+ )
740
+ end
741
+ it "understands the new-style subscriptions" do
742
+ api.notifications(parse_ast(%q{
743
+ template "/etc/nscd.conf" do
744
+ source "nscd.conf"
745
+ owner "root"
746
+ group "root"
747
+ subscribes :restart, "service[nscd]"
748
+ end
749
+ })).must_equal(
750
+ [{
751
+ :type => :subscribes,
752
+ :action => :restart,
753
+ :resource_type => :service,
754
+ :resource_name => 'nscd',
755
+ :timing => :delayed,
756
+ :style => :new
757
+ }]
758
+ )
759
+ end
760
+ it "understands new-style subscriptions with added parentheses" do
761
+ api.notifications(parse_ast(%q{
762
+ template "/etc/nscd.conf" do
763
+ source "nscd.conf"
764
+ owner "root"
765
+ group "root"
766
+ subscribes(:restart, "service[nscd]")
767
+ end
768
+ })).must_equal(
769
+ [{
770
+ :type => :subscribes,
771
+ :action => :restart,
772
+ :resource_type => :service,
773
+ :resource_name => 'nscd',
774
+ :timing => :delayed,
775
+ :style => :new
776
+ }]
777
+ )
778
+ end
779
+ describe "supports a resource both notifying and subscribing" do
780
+ it "old-style notifications" do
781
+ api.notifications(parse_ast(%q{
782
+ template "/etc/nscd.conf" do
783
+ source "nscd.conf"
784
+ owner "root"
785
+ group "root"
786
+ notifies :restart, resources(:service => "nscd")
787
+ subscribes :create, resources(:template => "/etc/nscd.conf")
788
+ end
789
+ })).must_equal([
790
+ {
791
+ :type => :notifies,
792
+ :action => :restart,
793
+ :resource_type => :service,
794
+ :resource_name => 'nscd',
795
+ :timing => :delayed,
796
+ :style => :old
797
+ },
798
+ {
799
+ :type => :subscribes,
800
+ :action => :create,
801
+ :resource_type => :template,
802
+ :resource_name => '/etc/nscd.conf',
803
+ :timing => :delayed,
804
+ :style => :old
805
+ }
806
+ ])
807
+ end
808
+ it "new-style notifications" do
809
+ api.notifications(parse_ast(%q{
810
+ template "/etc/nscd.conf" do
811
+ source "nscd.conf"
812
+ owner "root"
813
+ group "root"
814
+ notifies :restart, "service[nscd]"
815
+ subscribes :create, "template[/etc/nscd.conf]"
816
+ end
817
+ })).must_equal([
818
+ {
819
+ :type => :notifies,
820
+ :action => :restart,
821
+ :resource_type => :service,
822
+ :resource_name => 'nscd',
823
+ :timing => :delayed,
824
+ :style => :new
825
+ },
826
+ {
827
+ :type => :subscribes,
828
+ :action => :create,
829
+ :resource_type => :template,
830
+ :resource_name => '/etc/nscd.conf',
831
+ :timing => :delayed,
832
+ :style => :new
833
+ }
834
+ ])
835
+ end
836
+ end
837
+ it "understands the old-style notifications with timing" do
838
+ api.notifications(parse_ast(%q{
839
+ template "/etc/nscd.conf" do
840
+ source "nscd.conf"
841
+ owner "root"
842
+ group "root"
843
+ notifies :restart, resources(:service => "nscd"), :immediately
844
+ end
845
+ })).must_equal(
846
+ [{
847
+ :type => :notifies,
848
+ :action => :restart,
849
+ :resource_type => :service,
850
+ :resource_name => 'nscd',
851
+ :timing => :immediate,
852
+ :style => :old
853
+ }]
854
+ )
855
+ end
856
+ it "understands the old-style subscriptions with timing" do
857
+ api.notifications(parse_ast(%q{
858
+ template "/etc/nscd.conf" do
859
+ source "nscd.conf"
860
+ owner "root"
861
+ group "root"
862
+ subscribes :restart, resources(:service => "nscd"), :immediately
863
+ end
864
+ })).must_equal(
865
+ [{
866
+ :type => :subscribes,
867
+ :action => :restart,
868
+ :resource_type => :service,
869
+ :resource_name => 'nscd',
870
+ :timing => :immediate,
871
+ :style => :old
872
+ }]
873
+ )
874
+ end
875
+ it "understands the new-style notifications with timing" do
876
+ api.notifications(parse_ast(%q{
877
+ template "/etc/nscd.conf" do
878
+ source "nscd.conf"
879
+ owner "root"
880
+ group "root"
881
+ notifies :restart, "service[nscd]", :immediately
882
+ end
883
+ })).must_equal(
884
+ [{
885
+ :type => :notifies,
886
+ :action => :restart,
887
+ :resource_type => :service,
888
+ :resource_name => 'nscd',
889
+ :timing => :immediate,
890
+ :style => :new
891
+ }]
892
+ )
893
+ end
894
+ it "understands the new-style subscriptions with timing" do
895
+ api.notifications(parse_ast(%q{
896
+ template "/etc/nscd.conf" do
897
+ source "nscd.conf"
898
+ owner "root"
899
+ group "root"
900
+ subscribes :restart, "service[nscd]", :immediately
901
+ end
902
+ })).must_equal(
903
+ [{
904
+ :type => :subscribes,
905
+ :action => :restart,
906
+ :resource_type => :service,
907
+ :resource_name => 'nscd',
908
+ :timing => :immediate,
909
+ :style => :new
910
+ }]
911
+ )
912
+ end
913
+ describe "can be passed an individual resource" do
914
+ it "old-style notifications" do
915
+ api.notifications(api.find_resources(parse_ast(%q{
916
+ service "nscd" do
917
+ action :start
918
+ end
919
+ template "/etc/nscd.conf" do
920
+ source "nscd.conf"
921
+ owner "root"
922
+ group "root"
923
+ notifies :restart, resources(:service => "nscd")
924
+ end
925
+ }), :type => :template).first).must_equal([
926
+ {:type => :notifies, :action => :restart, :resource_type => :service,
927
+ :resource_name => 'nscd', :timing => :delayed,
928
+ :style => :old}
929
+ ])
930
+ end
931
+ it "old-style subscriptions" do
932
+ api.notifications(api.find_resources(parse_ast(%q{
933
+ service "nscd" do
934
+ action :start
935
+ end
936
+ template "/etc/nscd.conf" do
937
+ source "nscd.conf"
938
+ owner "root"
939
+ group "root"
940
+ subscribes :restart, resources(:service => "nscd")
941
+ end
942
+ }), :type => :template).first).must_equal([
943
+ {:type => :subscribes, :action => :restart, :resource_type => :service,
944
+ :resource_name => 'nscd', :timing => :delayed,
945
+ :style => :old}
946
+ ])
947
+ end
948
+ it "new-style notifications" do
949
+ api.notifications(api.find_resources(parse_ast(%q{
950
+ service "nscd" do
951
+ action :start
952
+ end
953
+ template "/etc/nscd.conf" do
954
+ source "nscd.conf"
955
+ owner "root"
956
+ group "root"
957
+ notifies :restart, "service[nscd]"
958
+ end
959
+ }), :type => :template).first).must_equal([
960
+ {:type => :notifies, :action => :restart, :resource_type => :service,
961
+ :resource_name => 'nscd', :timing => :delayed,
962
+ :style => :new}
963
+ ])
964
+ end
965
+ it "new-style subscriptions" do
966
+ api.notifications(api.find_resources(parse_ast(%q{
967
+ service "nscd" do
968
+ action :start
969
+ end
970
+ template "/etc/nscd.conf" do
971
+ source "nscd.conf"
972
+ owner "root"
973
+ group "root"
974
+ subscribes :restart, "service[nscd]"
975
+ end
976
+ }), :type => :template).first).must_equal([
977
+ {:type => :subscribes, :action => :restart, :resource_type => :service,
978
+ :resource_name => 'nscd', :timing => :delayed,
979
+ :style => :new}
980
+ ])
981
+ end
982
+ end
983
+ describe "supports multiple notifications on a single resource" do
984
+ it "old-style notifications" do
985
+ api.notifications(parse_ast(%q{
986
+ template "/etc/nscd.conf" do
987
+ source "nscd.conf"
988
+ owner "root"
989
+ group "root"
990
+ notifies :stop, resources(:service => "nscd")
991
+ notifies :start, resources(:service => "nscd")
992
+ end
993
+ })).must_equal(
994
+ [
995
+ {:type => :notifies, :action => :stop, :resource_type => :service,
996
+ :resource_name => 'nscd', :timing => :delayed,
997
+ :style => :old},
998
+ {:type => :notifies, :action => :start, :resource_type => :service,
999
+ :resource_name => 'nscd', :timing => :delayed,
1000
+ :style => :old}
1001
+ ]
1002
+ )
1003
+ end
1004
+ it "old-style subscriptions" do
1005
+ api.notifications(parse_ast(%q{
1006
+ template "/etc/nscd.conf" do
1007
+ source "nscd.conf"
1008
+ owner "root"
1009
+ group "root"
1010
+ subscribes :stop, resources(:service => "nscd")
1011
+ subscribes :start, resources(:service => "nscd")
1012
+ end
1013
+ })).must_equal(
1014
+ [
1015
+ {:type => :subscribes, :action => :stop, :resource_type => :service,
1016
+ :resource_name => 'nscd', :timing => :delayed,
1017
+ :style => :old},
1018
+ {:type => :subscribes, :action => :start, :resource_type => :service,
1019
+ :resource_name => 'nscd', :timing => :delayed,
1020
+ :style => :old}
1021
+ ]
1022
+ )
1023
+ end
1024
+ it "new-style notifications" do
1025
+ api.notifications(parse_ast(%q{
1026
+ template "/etc/nscd.conf" do
1027
+ source "nscd.conf"
1028
+ owner "root"
1029
+ group "root"
1030
+ notifies :stop, "service[nscd]"
1031
+ notifies :start, "service[nscd]"
1032
+ end
1033
+ })).must_equal(
1034
+ [
1035
+ {:type => :notifies, :action => :stop, :resource_type => :service,
1036
+ :resource_name => 'nscd', :timing => :delayed,
1037
+ :style => :new},
1038
+ {:type => :notifies, :action => :start, :resource_type => :service,
1039
+ :resource_name => 'nscd', :timing => :delayed,
1040
+ :style => :new}
1041
+ ]
1042
+ )
1043
+ end
1044
+ it "new-style subscriptions" do
1045
+ api.notifications(parse_ast(%q{
1046
+ template "/etc/nscd.conf" do
1047
+ source "nscd.conf"
1048
+ owner "root"
1049
+ group "root"
1050
+ subscribes :stop, "service[nscd]"
1051
+ subscribes :start, "service[nscd]"
1052
+ end
1053
+ })).must_equal(
1054
+ [
1055
+ {:type => :subscribes, :action => :stop, :resource_type => :service,
1056
+ :resource_name => 'nscd', :timing => :delayed,
1057
+ :style => :new},
1058
+ {:type => :subscribes, :action => :start, :resource_type => :service,
1059
+ :resource_name => 'nscd', :timing => :delayed,
1060
+ :style => :new}
1061
+ ]
1062
+ )
1063
+ end
1064
+ end
1065
+ describe "understands notifications for an execute resource" do
1066
+ it "old-style notifications" do
1067
+ api.notifications(parse_ast(%q{
1068
+ template "/tmp/foo.bar" do
1069
+ source "foo.bar.erb"
1070
+ notifies :run, resources(:execute => "foo")
1071
+ end
1072
+ })).must_equal(
1073
+ [{:type => :notifies, :action => :run, :resource_type => :execute,
1074
+ :resource_name => 'foo', :timing => :delayed,
1075
+ :style => :old}]
1076
+ )
1077
+ end
1078
+ it "old-style subscriptions" do
1079
+ api.notifications(parse_ast(%q{
1080
+ template "/tmp/foo.bar" do
1081
+ source "foo.bar.erb"
1082
+ subscribes :run, resources(:execute => "foo")
1083
+ end
1084
+ })).must_equal(
1085
+ [{:type => :subscribes, :action => :run, :resource_type => :execute,
1086
+ :resource_name => 'foo', :timing => :delayed,
1087
+ :style => :old}]
1088
+ )
1089
+ end
1090
+ it "old-style notifications" do
1091
+ api.notifications(parse_ast(%q{
1092
+ template "/tmp/foo.bar" do
1093
+ source "foo.bar.erb"
1094
+ notifies :run, "execute[foo]"
1095
+ end
1096
+ })).must_equal(
1097
+ [{:type => :notifies, :action => :run, :resource_type => :execute,
1098
+ :resource_name => 'foo', :timing => :delayed,
1099
+ :style => :new}]
1100
+ )
1101
+ end
1102
+ it "old-style subscriptions" do
1103
+ api.notifications(parse_ast(%q{
1104
+ template "/tmp/foo.bar" do
1105
+ source "foo.bar.erb"
1106
+ subscribes :run, "execute[foo]"
1107
+ end
1108
+ })).must_equal(
1109
+ [{:type => :subscribes, :action => :run, :resource_type => :execute,
1110
+ :resource_name => 'foo', :timing => :delayed,
1111
+ :style => :new}]
1112
+ )
1113
+ end
1114
+ end
1115
+ describe "sets the notification timing to delayed if specified" do
1116
+ it "old-style notifications" do
1117
+ api.notifications(parse_ast(%q{
1118
+ template "/etc/foo.conf" do
1119
+ notifies :run, resources(execute => "robespierre"), :delayed
1120
+ end
1121
+ })).first[:timing].must_equal(:delayed)
1122
+ end
1123
+ it "old-style subscriptions" do
1124
+ api.notifications(parse_ast(%q{
1125
+ template "/etc/foo.conf" do
1126
+ subscribes :run, resources(execute => "robespierre"), :delayed
1127
+ end
1128
+ })).first[:timing].must_equal(:delayed)
1129
+ end
1130
+ it "new-style notifications" do
1131
+ api.notifications(parse_ast(%q{
1132
+ template "/etc/foo.conf" do
1133
+ notifies :run, "execute[robespierre]", :delayed
1134
+ end
1135
+ })).first[:timing].must_equal(:delayed)
1136
+ end
1137
+ it "new-style subscriptions" do
1138
+ api.notifications(parse_ast(%q{
1139
+ template "/etc/foo.conf" do
1140
+ subscribes :run, "execute[robespierre]", :delayed
1141
+ end
1142
+ })).first[:timing].must_equal(:delayed)
1143
+ end
1144
+ end
1145
+ describe "sets the notification timing to immediate if specified as immediate" do
1146
+ it "old-style notifications" do
1147
+ api.notifications(parse_ast(%q{
1148
+ template "/etc/foo.conf" do
1149
+ notifies :run, resources(execute => "robespierre"), :immediate
1150
+ end
1151
+ })).first[:timing].must_equal(:immediate)
1152
+ end
1153
+ it "old-style subscriptions" do
1154
+ api.notifications(parse_ast(%q{
1155
+ template "/etc/foo.conf" do
1156
+ subscribes :run, resources(execute => "robespierre"), :immediate
1157
+ end
1158
+ })).first[:timing].must_equal(:immediate)
1159
+ end
1160
+ it "new-style notifications" do
1161
+ api.notifications(parse_ast(%q{
1162
+ template "/etc/foo.conf" do
1163
+ notifies :run, "execute[robespierre]", :immediate
1164
+ end
1165
+ })).first[:timing].must_equal(:immediate)
1166
+ end
1167
+ it "new-style subscriptions" do
1168
+ api.notifications(parse_ast(%q{
1169
+ template "/etc/foo.conf" do
1170
+ subscribes :run, "execute[robespierre]", :immediate
1171
+ end
1172
+ })).first[:timing].must_equal(:immediate)
1173
+ end
1174
+
1175
+ end
1176
+ describe "sets the notification timing to immediate if specified as immediately" do
1177
+ it "old-style notifications" do
1178
+ api.notifications(parse_ast(%q{
1179
+ template "/etc/foo.conf" do
1180
+ notifies :run, resources(execute => "robespierre"), :immediately
1181
+ end
1182
+ })).first[:timing].must_equal(:immediate)
1183
+ end
1184
+ it "old-style subscriptions" do
1185
+ api.notifications(parse_ast(%q{
1186
+ template "/etc/foo.conf" do
1187
+ subscribes :run, resources(execute => "robespierre"), :immediately
1188
+ end
1189
+ })).first[:timing].must_equal(:immediate)
1190
+ end
1191
+ it "new-style notifications" do
1192
+ api.notifications(parse_ast(%q{
1193
+ template "/etc/foo.conf" do
1194
+ notifies :run, "execute[robespierre]", :immediately
1195
+ end
1196
+ })).first[:timing].must_equal(:immediate)
1197
+ end
1198
+ it "new-style subscriptions" do
1199
+ api.notifications(parse_ast(%q{
1200
+ template "/etc/foo.conf" do
1201
+ subscribes :run, "execute[robespierre]", :immediately
1202
+ end
1203
+ })).first[:timing].must_equal(:immediate)
1204
+ end
1205
+ end
1206
+ it "passes unrecognised notification timings through unchanged" do
1207
+ api.notifications(parse_ast(%q{
1208
+ template "/etc/foo.conf" do
1209
+ notifies :run, resources(execute => "robespierre"), :forthwith
1210
+ end
1211
+ })).first[:timing].must_equal(:forthwith)
1212
+ end
1213
+ describe "resource names as expressions" do
1214
+ describe "returns the AST for an embedded string" do
1215
+ it "old-style notifications" do
1216
+ assert api.notifications(parse_ast(%q{
1217
+ template "/etc/foo.conf" do
1218
+ notifies :create, resources(:template => "/etc/bar/#{resource}.bar")
1219
+ end
1220
+ })).first[:resource_name].respond_to?(:xpath),
1221
+ "Expected resource_name with string expression to respond to #xpath"
1222
+ end
1223
+ it "new-style notifications" do
1224
+ assert api.notifications(parse_ast(%q{
1225
+ template "/etc/foo.conf" do
1226
+ notifies :create, "template[/etc/bar/#{resource}.bar]"
1227
+ end
1228
+ })).first[:resource_name].respond_to?(:xpath),
1229
+ "Expected resource_name with string expression to respond to #xpath"
1230
+ end
1231
+ it "new-style notifications - complete resource_name" do
1232
+ assert api.notifications(parse_ast(%q{
1233
+ template "/etc/foo.conf" do
1234
+ notifies :create, "template[#{template_path}]"
1235
+ end
1236
+ })).first[:resource_name].respond_to?(:xpath),
1237
+ "Expected resource_name with string expression to respond to #xpath"
1238
+ end
1239
+ end
1240
+ describe "returns the AST for node attribute" do
1241
+ it "old-style notifications" do
1242
+ assert api.notifications(parse_ast(%q{
1243
+ template "/etc/foo.conf" do
1244
+ notifies :restart, resources(:service => node['foo']['service'])
1245
+ end
1246
+ })).first[:resource_name].respond_to?(:xpath),
1247
+ "Expected resource_name with node attribute to respond to #xpath"
1248
+ end
1249
+ it "new-style notifications" do
1250
+ assert api.notifications(parse_ast(%q{
1251
+ template "/etc/foo.conf" do
1252
+ notifies :restart, "service[#{node['foo']['service']}]"
1253
+ end
1254
+ })).first[:resource_name].respond_to?(:xpath),
1255
+ "Expected resource_name with node attribute to respond to #xpath"
1256
+ end
1257
+ end
1258
+ describe "returns the AST for variable reference" do
1259
+ it "old-style notifications" do
1260
+ assert api.notifications(parse_ast(%q{
1261
+ template "/etc/foo.conf" do
1262
+ notifies :restart, resources(:service => my_service)
1263
+ end
1264
+ })).first[:resource_name].respond_to?(:xpath),
1265
+ "Expected resource_name with var ref to respond to #xpath"
1266
+ end
1267
+ it "new-style notifications" do
1268
+ assert api.notifications(parse_ast(%q{
1269
+ template "/etc/foo.conf" do
1270
+ notifies :restart, "service[#{my_service}]"
1271
+ end
1272
+ })).first[:resource_name].respond_to?(:xpath),
1273
+ "Expected resource_name with var ref to respond to #xpath"
1274
+ end
1275
+ end
1276
+ end
1277
+ describe "mark style of notification" do
1278
+ it "specifies that the notification was in the old style" do
1279
+ assert api.notifications(parse_ast(%q{
1280
+ template "/etc/foo.conf" do
1281
+ notifies :restart, resources(:service => 'foo')
1282
+ end
1283
+ })).first[:style].must_equal :old
1284
+ end
1285
+ it "specifies that the notification was in the new style" do
1286
+ assert api.notifications(parse_ast(%q{
1287
+ template "/etc/foo.conf" do
1288
+ notifies :restart, "service[foo]"
1289
+ end
1290
+ })).first[:style].must_equal :new
1291
+ end
1292
+ end
1293
+ end
1294
+
1295
+ describe "#read_ast" do
1296
+ it "raises if the file cannot be read" do
1297
+ lambda {api.read_ast(nil)}.must_raise(TypeError)
1298
+ end
1299
+ end
1300
+
1301
+ describe "#resource_attribute" do
1302
+ let(:resource) do
1303
+ Class.new do
1304
+ def xpath(str)
1305
+ raise "Not expected"
1306
+ end
1307
+ end.new
1308
+ end
1309
+ it "raises if the resource does not support XPath" do
1310
+ lambda{api.resource_attribute(nil, "mode")}.must_raise ArgumentError
1311
+ end
1312
+ it "raises if the attribute name is empty" do
1313
+ lambda{api.resource_attribute(resource, "")}.must_raise ArgumentError
1314
+ end
1315
+ end
1316
+
1317
+ describe "#resource_attributes" do
1318
+ def str_to_atts(str)
1319
+ api.resource_attributes(api.find_resources(parse_ast(str)).first)
1320
+ end
1321
+ it "raises if the resource does not support XPath" do
1322
+ lambda{api.resource_attributes(nil)}.must_raise ArgumentError
1323
+ end
1324
+ it "returns a string value for a literal string" do
1325
+ atts = str_to_atts(%q{
1326
+ directory "/foo/bar" do
1327
+ owner "root"
1328
+ end
1329
+ })
1330
+ atts['owner'].wont_be_nil
1331
+ atts['owner'].must_equal 'root'
1332
+ end
1333
+ it "returns a truthy value for a literal true" do
1334
+ atts = str_to_atts(%q{
1335
+ directory "/foo/bar" do
1336
+ recursive true
1337
+ end
1338
+ })
1339
+ atts['recursive'].wont_be_nil
1340
+ atts['recursive'].must_equal true
1341
+ end
1342
+ it "returns a truthy value for a literal false" do
1343
+ atts = str_to_atts(%q{
1344
+ directory "/foo/bar" do
1345
+ recursive false
1346
+ end
1347
+ })
1348
+ atts['recursive'].wont_be_nil
1349
+ atts['recursive'].must_equal false
1350
+ end
1351
+ it "decodes numeric attributes correctly" do
1352
+ atts = str_to_atts(%q{
1353
+ directory "/foo/bar" do
1354
+ owner "root"
1355
+ mode 0755
1356
+ end
1357
+ })
1358
+ atts['mode'].wont_be_nil
1359
+ atts['mode'].must_equal "0755"
1360
+ end
1361
+ describe "block attributes" do
1362
+ it "includes attributes with brace block values in the result" do
1363
+ atts = str_to_atts(%q{
1364
+ file "/etc/foo" do
1365
+ mode "0600"
1366
+ action :create
1367
+ only_if { File.exists?("/etc/bar") }
1368
+ end
1369
+ })
1370
+ atts['only_if'].wont_be_nil
1371
+ atts['only_if'].must_respond_to :xpath
1372
+ atts['only_if'].name.must_equal 'brace_block'
1373
+ end
1374
+ it "includes attributes with do block values in the result" do
1375
+ atts = str_to_atts(%q{
1376
+ file "/etc/foo" do
1377
+ mode "0600"
1378
+ action :create
1379
+ only_if do
1380
+ !File.exists?(foo) || (File.exists?(bar) &&
1381
+ File.mtime(baz) < last_changedate)
1382
+ end
1383
+ end
1384
+ })
1385
+ atts['only_if'].wont_be_nil
1386
+ atts['only_if'].must_respond_to :xpath
1387
+ atts['only_if'].name.must_equal 'do_block'
1388
+ end
1389
+ it "supports multiple block attributes" do
1390
+ atts = str_to_atts(%q{
1391
+ file "/etc/foo" do
1392
+ mode "0600"
1393
+ action :create
1394
+ only_if { false }
1395
+ not_if { true }
1396
+ end
1397
+ })
1398
+ atts['only_if'].wont_be_nil
1399
+ atts['only_if'].must_respond_to :xpath
1400
+ atts['only_if'].name.must_equal 'brace_block'
1401
+ atts['not_if'].wont_be_nil
1402
+ atts['not_if'].must_respond_to :xpath
1403
+ atts['not_if'].name.must_equal 'brace_block'
1404
+ end
1405
+ it "doesn't include method calls in ruby blocks" do
1406
+ atts = str_to_atts(%q{
1407
+ ruby_block "example" do
1408
+ block do
1409
+ foo do |bar|
1410
+ Chef::Log.info(bar)
1411
+ end
1412
+ end
1413
+ only_if { true }
1414
+ not_if { false }
1415
+ end
1416
+ })
1417
+ atts.keys.wont_include 'foo'
1418
+ atts['block'].wont_be_nil
1419
+ atts['block'].must_respond_to :xpath
1420
+ atts['block'].name.must_equal 'do_block'
1421
+ atts['only_if'].wont_be_nil
1422
+ atts['only_if'].must_respond_to :xpath
1423
+ atts['only_if'].name.must_equal 'brace_block'
1424
+ atts['not_if'].wont_be_nil
1425
+ atts['not_if'].must_respond_to :xpath
1426
+ atts['not_if'].name.must_equal 'brace_block'
1427
+ end
1428
+ it "includes notifications in the result" do
1429
+ atts = str_to_atts(%q{
1430
+ template "/etc/httpd.conf" do
1431
+ notifies :restart, "service[apache]"
1432
+ end
1433
+ })
1434
+ atts['notifies'].wont_be_nil
1435
+ atts['notifies'].must_respond_to :xpath
1436
+ atts['notifies'].name.must_equal 'args_add_block'
1437
+ end
1438
+ it "includes old-style notifications in the result" do
1439
+ atts = str_to_atts(%q{
1440
+ template "/etc/httpd.conf" do
1441
+ notifies :restart, resources(:service => "apache")
1442
+ end
1443
+ })
1444
+ atts['notifies'].wont_be_nil
1445
+ atts['notifies'].must_respond_to :xpath
1446
+ atts['notifies'].name.must_equal 'args_add_block'
1447
+ end
1448
+ end
1449
+ end
1450
+
1451
+ describe "#resource_attributes_by_type" do
1452
+ it "raises if the ast does not support XPath" do
1453
+ lambda{api.resource_attributes_by_type(nil)}.must_raise ArgumentError
1454
+ end
1455
+ it "returns an empty hash if there are no resources" do
1456
+ ast = MiniTest::Mock.new.expect :xpath, [], [String]
1457
+ api.resource_attributes_by_type(ast).keys.must_be_empty
1458
+ end
1459
+ end
1460
+
1461
+ describe "#resource_name" do
1462
+ it "raises if the resource does not support XPath" do
1463
+ lambda {api.resource_name('foo')}.must_raise ArgumentError
1464
+ end
1465
+ it "returns the resource name for a resource" do
1466
+ ast = MiniTest::Mock.new
1467
+ ast.expect :xpath, 'bob', [String]
1468
+ api.resource_name(ast).must_equal 'bob'
1469
+ end
1470
+ end
1471
+
1472
+ describe "#resources_by_type" do
1473
+ it "raises if the ast does not support XPath" do
1474
+ lambda{api.resources_by_type(nil)}.must_raise ArgumentError
1475
+ end
1476
+ it "returns an empty hash if there are no resources" do
1477
+ ast = MiniTest::Mock.new.expect :xpath, [], [String]
1478
+ api.resources_by_type(ast).keys.must_be_empty
1479
+ end
1480
+ end
1481
+
1482
+ describe "#resource_type" do
1483
+ it "raises if the resource does not support XPath" do
1484
+ lambda {api.resource_type(nil)}.must_raise ArgumentError
1485
+ end
1486
+ it "raises if the resource type cannot be determined" do
1487
+ ast = MiniTest::Mock.new
1488
+ ast.expect :xpath, '', [String]
1489
+ lambda {api.resource_type(ast)}.must_raise ArgumentError
1490
+ end
1491
+ it "returns the resource type for a resource" do
1492
+ ast = MiniTest::Mock.new
1493
+ ast.expect :xpath, 'directory', [String]
1494
+ api.resource_type(ast).must_equal 'directory'
1495
+ end
1496
+ end
1497
+
1498
+ describe "#ruby_code?" do
1499
+ it "says a nil is not ruby code" do
1500
+ refute api.ruby_code?(nil)
1501
+ end
1502
+ it "says an empty string is not ruby code" do
1503
+ refute api.ruby_code?('')
1504
+ end
1505
+ it "coerces arguments to a string" do
1506
+ assert api.ruby_code?(%w{foo bar})
1507
+ end
1508
+ it "returns true for a snippet of ruby code" do
1509
+ assert api.ruby_code?("assert api.ruby_code?(nil)")
1510
+ end
1511
+ it "returns false for a unix command" do
1512
+ refute api.ruby_code?("find -type f -print")
1513
+ end
1514
+ end
1515
+
1516
+ describe "#searches" do
1517
+ let(:ast) { ast = MiniTest::Mock.new }
1518
+ it "returns empty if the AST does not support XPath expressions" do
1519
+ api.searches('not-an-ast').must_be_empty
1520
+ end
1521
+ it "returns empty if the AST has no elements" do
1522
+ ast.expect :xpath, [], [String]
1523
+ api.searches(ast).must_be_empty
1524
+ end
1525
+ it "returns the AST elements for each use of search" do
1526
+ ast.expect :xpath, ['ident'], [String]
1527
+ api.searches(ast).must_equal ['ident']
1528
+ end
1529
+ end
1530
+
1531
+ describe "#standard_cookbook_subdirs" do
1532
+ it "is enumerable" do
1533
+ api.standard_cookbook_subdirs.each{|s| s}
1534
+ end
1535
+ it "is sorted in alphabetical order" do
1536
+ api.standard_cookbook_subdirs.must_equal(
1537
+ api.standard_cookbook_subdirs.sort)
1538
+ end
1539
+ it "includes the directories generated by knife create cookbook" do
1540
+ %w{attributes definitions files libraries providers recipes resources
1541
+ templates}.each do |dir|
1542
+ api.standard_cookbook_subdirs.must_include dir
1543
+ end
1544
+ end
1545
+ it "does not include the spec directory" do
1546
+ api.standard_cookbook_subdirs.wont_include 'spec'
1547
+ end
1548
+ it "does not include a subdirectory of a subdirectory" do
1549
+ api.standard_cookbook_subdirs.wont_include 'default'
1550
+ end
1551
+ end
1552
+
1553
+ describe "#supported_platforms" do
1554
+ def supports(str)
1555
+ api.supported_platforms(parse_ast(str))
1556
+ end
1557
+ it "returns an empty if no platforms are specified as supported" do
1558
+ supports("name 'example'").must_be_empty
1559
+ end
1560
+ describe :ignored_support_declarations do
1561
+ it "should ignore supports without any arguments" do
1562
+ supports('supports').must_be_empty
1563
+ end
1564
+ it "should ignore supports where an embedded string expression is used" do
1565
+ supports('supports "red#{hat}"').must_be_empty
1566
+ end
1567
+ end
1568
+ it "returns the supported platform names if multiple are given" do
1569
+ supports(%q{
1570
+ supports "redhat"
1571
+ supports "scientific"
1572
+ }).must_equal([{:platform => 'redhat', :versions => []},
1573
+ {:platform => 'scientific', :versions => []}])
1574
+ end
1575
+ it "sorts the platform names in alphabetical order" do
1576
+ supports(%q{
1577
+ supports "scientific"
1578
+ supports "redhat"
1579
+ }).must_equal([{:platform => 'redhat', :versions => []},
1580
+ {:platform => 'scientific', :versions => []}])
1581
+ end
1582
+ it "handles support declarations that include version constraints" do
1583
+ supports(%q{
1584
+ supports "redhat", '>= 6'
1585
+ }).must_equal([{:platform => 'redhat', :versions => ['>= 6']}])
1586
+ end
1587
+ it "handles support declarations that include obsoleted version constraints" do
1588
+ supports(%q{
1589
+ supports 'redhat', '> 5.0', '< 7.0'
1590
+ supports 'scientific', '> 5.0', '< 6.0'
1591
+ }).must_equal([{:platform => 'redhat', :versions => ['> 5.0', '< 7.0']},
1592
+ {:platform => 'scientific', :versions => ['> 5.0', '< 6.0']}])
1593
+ end
1594
+ it "normalises platform symbol references to strings" do
1595
+ supports(%q{
1596
+ supports :ubuntu
1597
+ }).must_equal([{:platform => 'ubuntu', :versions => []}])
1598
+ end
1599
+ it "handles support declarations as symbols that include version constraints" do
1600
+ supports(%q{
1601
+ supports :redhat, '>= 6'
1602
+ }).must_equal([{:platform => 'redhat', :versions => ['>= 6']}])
1603
+ end
1604
+ it "understands support declarations that use word lists" do
1605
+ supports(%q{
1606
+ %w{redhat centos fedora}.each do |os|
1607
+ supports os
1608
+ end
1609
+ }).must_equal([{:platform => 'centos', :versions => []},
1610
+ {:platform => 'fedora', :versions => []},
1611
+ {:platform => 'redhat', :versions => []}])
1612
+ end
1613
+ end
1614
+
1615
+ end