diff --git a/lib/puppet/indirector/resource/active_record.rb b/lib/puppet/indirector/resource/active_record.rb index 50a214197..c2fd188ee 100644 --- a/lib/puppet/indirector/resource/active_record.rb +++ b/lib/puppet/indirector/resource/active_record.rb @@ -1,55 +1,90 @@ require 'puppet/indirector/active_record' class Puppet::Resource::ActiveRecord < Puppet::Indirector::ActiveRecord def search(request) type = request_to_type_name(request) host = request.options[:host] filter = request.options[:filter] query = build_active_record_query(type, host, filter) Puppet::Rails::Resource.find(:all, query) end private def request_to_type_name(request) name = request.key.split('/', 2)[0] type = Puppet::Type.type(name) or raise Puppet::Error, "Could not find type #{name}" type.name end + def filter_to_active_record(filter) + # Don't call me if you don't have a filter, please. + filter.is_a?(Array) or raise ArgumentError, "active record filters must be arrays" + a, op, b = filter + + case op + when /^(and|or)$/i then + extra = [] + first, args = filter_to_active_record a + extra += args + + second, args = filter_to_active_record b + extra += args + + return "(#{first}) #{op.upcase} (#{second})", extra + + when "==", "!=" then + op = '=' if op == '==' # SQL, yayz! + case a + when "title" then + return "title #{op} ?", [b] + + when "tag" then + return "puppet_tags.name #{op} ?", [b] + + else + return "param_names.name = ? AND param_values.value #{op} ?", [a, b] + end + + else + raise ArgumentError, "unknown operator #{op.inspect} in #{filter.inspect}" + end + end + def build_active_record_query(type, host, filter) raise Puppet::DevError, "Cannot collect resources for a nil host" unless host search = "(exported=? AND restype=?)" arguments = [true, type] - # REVISIT: This cannot stand. We need to abstract the search language - # away here, so that we can unbind our ActiveRecord schema and our parser - # of search inputs from each other. --daniel 2011-08-23 - search += " AND (#{filter})" if filter + if filter then + sql, values = filter_to_active_record(filter) + search += " AND #{sql}" + arguments += values + end # note: we're not eagerly including any relations here because it can # create large numbers of objects that we will just throw out later. We # used to eagerly include param_names/values but the way the search filter # is built ruined those efforts and we were eagerly loading only the # searched parameter and not the other ones. query = {} case search when /puppet_tags/ query = { :joins => { :resource_tags => :puppet_tag } } when /param_name/ query = { :joins => { :param_values => :param_name } } end # We're going to collect objects from rails, but we don't want any # objects from this host. if host = Puppet::Rails::Host.find_by_name(host) search += " AND (host_id != ?)" arguments << host.id end query[:conditions] = [search, *arguments] query end end diff --git a/lib/puppet/parser/ast/collection.rb b/lib/puppet/parser/ast/collection.rb index 565b83195..12f73281e 100644 --- a/lib/puppet/parser/ast/collection.rb +++ b/lib/puppet/parser/ast/collection.rb @@ -1,49 +1,49 @@ require 'puppet' require 'puppet/parser/ast/branch' require 'puppet/parser/collector' # An object that collects stored objects from the central cache and returns # them to the current host, yo. class Puppet::Parser::AST class Collection < AST::Branch attr_accessor :type, :query, :form attr_reader :override associates_doc # We return an object that does a late-binding evaluation. def evaluate(scope) - str, code = query && query.safeevaluate(scope) + match, code = query && query.safeevaluate(scope) resource_type = scope.find_resource_type(@type) fail "Resource type #{@type} doesn't exist" unless resource_type - newcoll = Puppet::Parser::Collector.new(scope, resource_type.name, str, code, self.form) + newcoll = Puppet::Parser::Collector.new(scope, resource_type.name, match, code, self.form) scope.compiler.add_collection(newcoll) # overrides if any # Evaluate all of the specified params. if @override params = @override.collect { |param| param.safeevaluate(scope) } newcoll.add_override( :parameters => params, :file => @file, :line => @line, :source => scope.source, :scope => scope ) end newcoll end # Handle our parameter ourselves def override=(override) @override = if override.is_a?(AST::ASTArray) override else AST::ASTArray.new(:line => override.line,:file => override.file,:children => [override]) end end end end diff --git a/lib/puppet/parser/ast/collexpr.rb b/lib/puppet/parser/ast/collexpr.rb index f912b4b33..d5bd4e9c5 100644 --- a/lib/puppet/parser/ast/collexpr.rb +++ b/lib/puppet/parser/ast/collexpr.rb @@ -1,86 +1,67 @@ require 'puppet' require 'puppet/parser/ast/branch' require 'puppet/parser/collector' # An object that collects stored objects from the central cache and returns # them to the current host, yo. class Puppet::Parser::AST class CollExpr < AST::Branch attr_accessor :test1, :test2, :oper, :form, :type, :parens # We return an object that does a late-binding evaluation. def evaluate(scope) # Make sure our contained expressions have all the info they need. [@test1, @test2].each do |t| if t.is_a?(self.class) t.form ||= self.form t.type ||= self.type end end # The code is only used for virtual lookups - str1, code1 = @test1.safeevaluate scope - str2, code2 = @test2.safeevaluate scope + match1, code1 = @test1.safeevaluate scope + match2, code2 = @test2.safeevaluate scope # First build up the virtual code. # If we're a conjunction operator, then we're calling code. I did # some speed comparisons, and it's at least twice as fast doing these # case statements as doing an eval here. code = proc do |resource| case @oper when "and"; code1.call(resource) and code2.call(resource) when "or"; code1.call(resource) or code2.call(resource) when "==" - if str1 == "tag" - resource.tagged?(str2) + if match1 == "tag" + resource.tagged?(match2) else - if resource[str1].is_a?(Array) - resource[str1].include?(str2) + if resource[match1].is_a?(Array) + resource[match1].include?(match2) else - resource[str1] == str2 + resource[match1] == match2 end end - when "!="; resource[str1] != str2 + when "!="; resource[match1] != match2 end end # Now build up the rails conditions code if self.parens and self.form == :exported Puppet.warning "Parentheses are ignored in Rails searches" end - case @oper - when "and", "or" - if form == :exported - raise Puppet::ParseError, "Puppet does not currently support collecting exported resources with more than one condition" - end - oper = @oper.upcase - when "=="; oper = "=" - else - oper = @oper + if form == :exported and (@oper =~ /^(and|or)$/i) then + raise Puppet::ParseError, "Puppet does not currently support collecting exported resources with more than one condition" end - if oper == "=" or oper == "!=" - # Add the rails association info where necessary - case str1 - when "title" - str = "title #{oper} '#{str2}'" - when "tag" - str = "puppet_tags.name #{oper} '#{str2}'" - else - str = "param_values.value #{oper} '#{str2}' and param_names.name = '#{str1}'" - end - else - str = "(#{str1}) #{oper} (#{str2})" - end + match = [match1, @oper, match2] - return str, code + return match, code end def initialize(hash = {}) super raise ArgumentError, "Invalid operator #{@oper}" unless %w{== != and or}.include?(@oper) end end end diff --git a/lib/puppet/parser/collector.rb b/lib/puppet/parser/collector.rb index 6668ac301..c9ab34a49 100644 --- a/lib/puppet/parser/collector.rb +++ b/lib/puppet/parser/collector.rb @@ -1,172 +1,174 @@ # An object that collects stored objects from the central cache and returns # them to the current host, yo. class Puppet::Parser::Collector attr_accessor :type, :scope, :vquery, :equery, :form, :resources, :overrides, :collected # Call the collection method, mark all of the returned objects as # non-virtual, optionally applying parameter overrides. The collector can # also delete himself from the compiler if there is no more resources to # collect (valid only for resource fixed-set collector which get their # resources from +collect_resources+ and not from the catalog) def evaluate # Shortcut if we're not using storeconfigs and they're trying to collect # exported resources. if form == :exported and Puppet[:storeconfigs] != true Puppet.warning "Not collecting exported resources without storeconfigs" return false end if self.resources unless objects = collect_resources and ! objects.empty? return false end else method = "collect_#{@form.to_s}" objects = send(method).each do |obj| obj.virtual = false end return false if objects.empty? end # we have an override for the collected resources if @overrides and !objects.empty? # force the resource to be always child of any other resource overrides[:source].meta_def(:child_of?) do true end # tell the compiler we have some override for him unless we already # overrided those resources objects.each do |res| unless @collected.include?(res.ref) newres = Puppet::Parser::Resource. new(res.type, res.title, :parameters => overrides[:parameters], :file => overrides[:file], :line => overrides[:line], :source => overrides[:source], :scope => overrides[:scope]) scope.compiler.add_override(newres) end end end # filter out object that this collector has previously found. objects.reject! { |o| @collected.include?(o.ref) } return false if objects.empty? # keep an eye on the resources we have collected objects.inject(@collected) { |c,o| c[o.ref]=o; c } # return our newly collected resources objects end def initialize(scope, type, equery, vquery, form) - @scope = scope + @scope = scope + @vquery = vquery + @equery = equery # initialisation @collected = {} # Canonize the type @type = Puppet::Resource.new(type, "whatever").type - @equery = equery - @vquery = vquery - raise(ArgumentError, "Invalid query form #{form}") unless [:exported, :virtual].include?(form) + unless [:exported, :virtual].include?(form) + raise ArgumentError, "Invalid query form #{form}" + end @form = form end # add a resource override to the soon to be exported/realized resources def add_override(hash) raise ArgumentError, "Exported resource try to override without parameters" unless hash[:parameters] # schedule an override for an upcoming collection @overrides = hash end private # Collect exported objects. def collect_exported resources = [] time = Puppet::Util.thinmark do # First get everything from the export table. Just reuse our # collect_virtual method but tell it to use 'exported? for the test. resources = collect_virtual(true).reject { |r| ! r.virtual? } # key is '#{type}/#{name}', and host and filter. found = Puppet::Resource.indirection. search(@type, :host => @scope.host, :filter => @equery) found.map {|x| x.to_resource(@scope) }.each do |item| if existing = @scope.findresource(item.type, item.title) unless existing.collector_id == item.collector_id # unless this is the one we've already collected raise Puppet::ParseError, "Exported resource #{item.ref} cannot override local resource" end else item.exported = false @scope.compiler.add_resource(@scope, item) resources << item end end end scope.debug("Collected %s %s resource%s in %.2f seconds" % [resources.length, @type, resources.length == 1 ? "" : "s", time]) resources end def collect_resources @resources = [@resources] unless @resources.is_a?(Array) method = "collect_#{form.to_s}_resources" send(method) end def collect_exported_resources raise Puppet::ParseError, "realize() is not yet implemented for exported resources" end # Collect resources directly; this is the result of using 'realize', # which specifies resources, rather than using a normal collection. def collect_virtual_resources return [] unless defined?(@resources) and ! @resources.empty? result = @resources.dup.collect do |ref| if res = @scope.findresource(ref.to_s) @resources.delete(ref) res end end.reject { |r| r.nil? }.each do |res| res.virtual = false end # If there are no more resources to find, delete this from the list # of collections. @scope.compiler.delete_collection(self) if @resources.empty? result end # Collect just virtual objects, from our local compiler. def collect_virtual(exported = false) scope.compiler.resources.find_all do |resource| resource.type == @type and (exported ? resource.exported? : true) and match?(resource) end end # Does the resource match our tests? We don't yet support tests, # so it's always true at the moment. def match?(resource) if self.vquery return self.vquery.call(resource) else return true end end end diff --git a/spec/integration/parser/parser_spec.rb b/spec/integration/parser/parser_spec.rb index f68aff670..f6abdb274 100755 --- a/spec/integration/parser/parser_spec.rb +++ b/spec/integration/parser/parser_spec.rb @@ -1,152 +1,150 @@ #!/usr/bin/env rspec require 'spec_helper' describe Puppet::Parser::Parser do module ParseMatcher class ParseAs def initialize(klass) @parser = Puppet::Parser::Parser.new "development" @class = klass end def result_instance @result.code[0] end def matches?(string) @string = string @result = @parser.parse(string) result_instance.instance_of?(@class) end def description "parse as a #{@class}" end def failure_message " expected #{@string} to parse as #{@class} but was #{result_instance.class}" end def negative_failure_message " expected #{@string} not to parse as #{@class}" end end def parse_as(klass) ParseAs.new(klass) end class ParseWith def initialize(block) @parser = Puppet::Parser::Parser.new "development" @block = block end def result_instance @result.code[0] end def matches?(string) @string = string @result = @parser.parse(string) @block.call(result_instance) end def description "parse with the block evaluating to true" end def failure_message " expected #{@string} to parse with a true result in the block" end def negative_failure_message " expected #{@string} not to parse with a true result in the block" end end def parse_with(&block) ParseWith.new(block) end end include ParseMatcher before :each do @resource_type_collection = Puppet::Resource::TypeCollection.new("env") @parser = Puppet::Parser::Parser.new "development" end describe "when parsing comments before statement" do it "should associate the documentation to the statement AST node" do ast = @parser.parse(""" # comment class test {} """) ast.code[0].should be_a(Puppet::Parser::AST::Hostclass) ast.code[0].name.should == 'test' ast.code[0].instantiate('')[0].doc.should == "comment\n" end end describe "when parsing" do it "should be able to parse normal left to right relationships" do "Notify[foo] -> Notify[bar]".should parse_as(Puppet::Parser::AST::Relationship) end it "should be able to parse right to left relationships" do "Notify[foo] <- Notify[bar]".should parse_as(Puppet::Parser::AST::Relationship) end it "should be able to parse normal left to right subscriptions" do "Notify[foo] ~> Notify[bar]".should parse_as(Puppet::Parser::AST::Relationship) end it "should be able to parse right to left subscriptions" do "Notify[foo] <~ Notify[bar]".should parse_as(Puppet::Parser::AST::Relationship) end it "should correctly set the arrow type of a relationship" do "Notify[foo] <~ Notify[bar]".should parse_with { |rel| rel.arrow == "<~" } end it "should be able to parse deep hash access" do %q{ $hash = { 'a' => { 'b' => { 'c' => 'it works' } } } $out = $hash['a']['b']['c'] }.should parse_with { |v| v.value.is_a?(Puppet::Parser::AST::ASTHash) } end it "should fail if asked to parse '$foo::::bar'" do expect { @parser.parse("$foo::::bar") }.should raise_error(Puppet::ParseError, /Syntax error at ':'/) end describe "function calls" do it "should be able to pass an array to a function" do "my_function([1,2,3])".should parse_with { |fun| fun.is_a?(Puppet::Parser::AST::Function) && fun.arguments[0].evaluate(stub 'scope') == ['1','2','3'] } end it "should be able to pass a hash to a function" do "my_function({foo => bar})".should parse_with { |fun| fun.is_a?(Puppet::Parser::AST::Function) && fun.arguments[0].evaluate(stub 'scope') == {'foo' => 'bar'} } end end describe "collections" do it "should find resources according to an expression" do - %q{ - File <| mode == 0700 + 0050 + 0050 |> - }.should parse_with { |coll| + %q{ File <| mode == 0700 + 0050 + 0050 |> }.should parse_with { |coll| coll.is_a?(Puppet::Parser::AST::Collection) && - coll.query.evaluate(stub 'scope').first == "param_values.value = '528' and param_names.name = 'mode'" + coll.query.evaluate(stub 'scope').first == ["mode", "==", 0700 + 0050 + 0050] } end end end end diff --git a/spec/unit/indirector/resource/active_record_spec.rb b/spec/unit/indirector/resource/active_record_spec.rb index 1d312cc16..5ec78281b 100755 --- a/spec/unit/indirector/resource/active_record_spec.rb +++ b/spec/unit/indirector/resource/active_record_spec.rb @@ -1,124 +1,178 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/rails' require 'puppet/node/facts' describe "Puppet::Resource::ActiveRecord", :if => Puppet.features.rails? do include PuppetSpec::Files before :each do dir = Pathname(tmpdir('puppet-var')) Puppet[:vardir] = dir.to_s Puppet[:dbadapter] = 'sqlite3' Puppet[:dblocation] = (dir + 'storeconfigs.sqlite').to_s Puppet[:storeconfigs] = true end after :each do ActiveRecord::Base.remove_connection end subject { require 'puppet/indirector/resource/active_record' Puppet::Resource.indirection.terminus(:active_record) } it "should automatically initialize Rails" do # Other tests in the suite may have established the connection, which will # linger; the assertion is just to enforce our assumption about the call, # not because I *really* want to test ActiveRecord works. Better to have # an early failure than wonder why the test overall doesn't DTRT. ActiveRecord::Base.remove_connection ActiveRecord::Base.should_not be_connected subject.should be ActiveRecord::Base.should be_connected end describe "#search" do before :each do Puppet::Rails.init end def search(type, host = 'default.local', filter = nil) args = { :host => host, :filter => filter } subject.search(Puppet::Resource.indirection.request(:search, type, args)) end it "should fail if the type is not known to Puppet" do expect { search("banana") }.to raise_error Puppet::Error, /Could not find type/ end it "should return an empty array if no resources match" do search("exec").should == [] end context "with a matching resource" do before :each do host = Puppet::Rails::Host.create!(:name => 'one.local') Puppet::Rails::Resource. create!(:host => host, :restype => 'exec', :title => 'whammo', :exported => true) end it "should return something responding to `to_resource` if a resource matches" do found = search("exec") found.length.should == 1 found.map do |item| item.should respond_to :to_resource item.restype.should == "exec" end end it "should not filter resources that have been found before" do search("exec").should == search("exec") end end end describe "#build_active_record_query" do before :each do Puppet::Rails.init end let :type do Puppet::Type.type('notify').name end def query(type, host, filter = nil) subject.send :build_active_record_query, type, host, filter end it "should exclude all database resources from the host" do host = Puppet::Rails::Host.create! :name => 'one.local' got = query(type, host.name) got.keys.should =~ [:conditions] got[:conditions][0] =~ /\(host_id != \?\)/ got[:conditions].last.should == host.id end it "should join appropriately when filtering on parameters" do - filter = "param_names.name = title" + filter = %w{propname == propval} got = query(type, 'whatever', filter) got.keys.should =~ [:conditions, :joins] got[:joins].should == { :param_values => :param_name } - got[:conditions].first.should =~ Regexp.new(Regexp.escape(filter)) + got[:conditions][0].should =~ /param_names\.name = \?/ + got[:conditions][0].should =~ /param_values\.value = \?/ + got[:conditions].should be_include filter.first + got[:conditions].should be_include filter.last end it "should join appropriately when filtering on tags" do - filter = "puppet_tags.name = test" + filter = %w{tag == test} got = query(type, 'whatever', filter) got.keys.should =~ [:conditions, :joins] got[:joins].should == {:resource_tags => :puppet_tag} - got[:conditions].first.should =~ Regexp.new(Regexp.escape(filter)) + got[:conditions].first.should =~ /puppet_tags/ + got[:conditions].should_not be_include filter.first + got[:conditions].should be_include filter.last end it "should only search for exported resources with the matching type" do got = query(type, 'whatever') got.keys.should =~ [:conditions] got[:conditions][0].should be_include "(exported=? AND restype=?)" got[:conditions][1].should == true got[:conditions][2].should == type end end + + describe "#filter_to_active_record" do + def filter_to_active_record(input) + subject.send :filter_to_active_record, input + end + + [nil, '', 'whatever', 12].each do |input| + it "should fail if filter is not an array (with #{input.inspect})" do + expect { filter_to_active_record(input) }. + to raise_error ArgumentError, /must be arrays/ + end + end + + # Not exhaustive, just indicative. + ['=', '<>', '=~', '+', '-', '!'].each do |input| + it "should fail with unexpected comparison operators (with #{input.inspect})" do + expect { filter_to_active_record(["one", input, "two"]) }. + to raise_error ArgumentError, /unknown operator/ + end + end + + { + ["title", "==", "whatever"] => ["title = ?", ["whatever"]], + ["title", "!=", "whatever"] => ["title != ?", ["whatever"]], + + # Technically, these are not supported by Puppet yet, but as we pay + # approximately zero cost other than a few minutes writing the tests, + # and it would be *harder* to fail on them, nested queries. + [["title", "==", "foo"], "or", ["title", "==", "bar"]] => + ["(title = ?) OR (title = ?)", ["foo", "bar"]], + + [["title", "==", "foo"], "or", ["tag", "==", "bar"]] => + ["(title = ?) OR (puppet_tags.name = ?)", ["foo", "bar"]], + + [["title", "==", "foo"], "or", ["param", "==", "bar"]] => + ["(title = ?) OR (param_names.name = ? AND param_values.value = ?)", + ["foo", "param", "bar"]], + + [[["title","==","foo"],"or",["tag", "==", "bar"]],"and",["param","!=","baz"]] => + ["((title = ?) OR (puppet_tags.name = ?)) AND "+ + "(param_names.name = ? AND param_values.value != ?)", + ["foo", "bar", "param", "baz"]] + + }.each do |input, expect| + it "should map #{input.inspect} to #{expect.inspect}" do + filter_to_active_record(input).should == expect + end + end + end end diff --git a/spec/unit/parser/ast/collexpr_spec.rb b/spec/unit/parser/ast/collexpr_spec.rb index 454e7481b..56297723a 100755 --- a/spec/unit/parser/ast/collexpr_spec.rb +++ b/spec/unit/parser/ast/collexpr_spec.rb @@ -1,114 +1,114 @@ #!/usr/bin/env rspec require 'spec_helper' describe Puppet::Parser::AST::CollExpr do ast = Puppet::Parser::AST before :each do @scope = Puppet::Parser::Scope.new end describe "when evaluating with two operands" do before :each do @test1 = mock 'test1' @test1.expects(:safeevaluate).with(@scope).returns("test1") @test2 = mock 'test2' @test2.expects(:safeevaluate).with(@scope).returns("test2") end it "should evaluate both" do - collexpr = ast::CollExpr.new(:test1 => @test1, :test2 => @test2, :oper=>"==") + collexpr = ast::CollExpr.new(:test1 => @test1, :test2 => @test2, :oper => "==") collexpr.evaluate(@scope) end - it "should produce a textual representation and code of the expression" do - collexpr = ast::CollExpr.new(:test1 => @test1, :test2 => @test2, :oper=>"==") + it "should produce a data and code representation of the expression" do + collexpr = ast::CollExpr.new(:test1 => @test1, :test2 => @test2, :oper => "==") result = collexpr.evaluate(@scope) - result[0].should == "param_values.value = 'test2' and param_names.name = 'test1'" + result[0].should == ["test1", "==", "test2"] result[1].should be_an_instance_of(Proc) end it "should propagate expression type and form to child if expression themselves" do [@test1, @test2].each do |t| t.expects(:is_a?).returns(true) t.expects(:form).returns(false) t.expects(:type).returns(false) t.expects(:type=) t.expects(:form=) end collexpr = ast::CollExpr.new(:test1 => @test1, :test2 => @test2, :oper=>"==", :form => true, :type => true) result = collexpr.evaluate(@scope) end describe "and when evaluating the produced code" do before :each do @resource = mock 'resource' @resource.expects(:[]).with("test1").at_least(1).returns("test2") end it "should evaluate like the original expression for ==" do collexpr = ast::CollExpr.new(:test1 => @test1, :test2 => @test2, :oper => "==") collexpr.evaluate(@scope)[1].call(@resource).should === (@resource["test1"] == "test2") end it "should evaluate like the original expression for !=" do collexpr = ast::CollExpr.new(:test1 => @test1, :test2 => @test2, :oper => "!=") collexpr.evaluate(@scope)[1].call(@resource).should === (@resource["test1"] != "test2") end end it "should warn if this is an exported collection containing parenthesis (unsupported)" do collexpr = ast::CollExpr.new(:test1 => @test1, :test2 => @test2, :oper=>"==", :parens => true, :form => :exported) Puppet.expects(:warning) collexpr.evaluate(@scope) end %w{and or}.each do |op| it "should raise an error if this is an exported collection with #{op} operator (unsupported)" do collexpr = ast::CollExpr.new(:test1 => @test1, :test2 => @test2, :oper=> op, :form => :exported) lambda { collexpr.evaluate(@scope) }.should raise_error(Puppet::ParseError) end end end describe "when evaluating with tags" do before :each do @tag = stub 'tag', :safeevaluate => 'tag' @value = stub 'value', :safeevaluate => 'value' @resource = stub 'resource' @resource.stubs(:tagged?).with("value").returns(true) end - it "should produce a textual representation of the expression" do + it "should produce a data representation of the expression" do collexpr = ast::CollExpr.new(:test1 => @tag, :test2 => @value, :oper=>"==") result = collexpr.evaluate(@scope) - result[0].should == "puppet_tags.name = 'value'" + result[0].should == ["tag", "==", "value"] end it "should inspect resource tags if the query term is on tags" do collexpr = ast::CollExpr.new(:test1 => @tag, :test2 => @value, :oper => "==") collexpr.evaluate(@scope)[1].call(@resource).should be_true end end [:exported,:virtual].each do |mode| it "should check for array member equality if resource parameter is an array for == in mode #{mode}" do array = mock 'array', :safeevaluate => "array" test1 = mock 'test1' test1.expects(:safeevaluate).with(@scope).returns("test1") resource = mock 'resource' resource.expects(:[]).with("array").at_least(1).returns(["test1","test2","test3"]) collexpr = ast::CollExpr.new(:test1 => array, :test2 => test1, :oper => "==", :form => mode) collexpr.evaluate(@scope)[1].call(resource).should be_true end end it "should raise an error for invalid operator" do lambda { collexpr = ast::CollExpr.new(:oper=>">") }.should raise_error end end diff --git a/spec/unit/parser/collector_spec.rb b/spec/unit/parser/collector_spec.rb index 822adae6d..d05d70e60 100755 --- a/spec/unit/parser/collector_spec.rb +++ b/spec/unit/parser/collector_spec.rb @@ -1,428 +1,428 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/rails' require 'puppet/parser/collector' describe Puppet::Parser::Collector, "when initializing" do before do @scope = mock 'scope' @resource_type = 'resource_type' @form = :exported @vquery = mock 'vquery' @equery = mock 'equery' @collector = Puppet::Parser::Collector.new(@scope, @resource_type, @equery, @vquery, @form) end it "should require a scope" do @collector.scope.should equal(@scope) end it "should require a resource type" do @collector.type.should == 'Resource_type' end it "should only accept :virtual or :exported as the collector form" do proc { @collector = Puppet::Parser::Collector.new(@scope, @resource_type, @vquery, @equery, :other) }.should raise_error(ArgumentError) end it "should accept an optional virtual query" do @collector.vquery.should equal(@vquery) end it "should accept an optional exported query" do @collector.equery.should equal(@equery) end it "should canonize the type name" do @collector = Puppet::Parser::Collector.new(@scope, "resource::type", @equery, @vquery, @form) @collector.type.should == "Resource::Type" end it "should accept an optional resource override" do @collector = Puppet::Parser::Collector.new(@scope, "resource::type", @equery, @vquery, @form) override = { :parameters => "whatever" } @collector.add_override(override) @collector.overrides.should equal(override) end end describe Puppet::Parser::Collector, "when collecting specific virtual resources" do before do @scope = mock 'scope' @vquery = mock 'vquery' @equery = mock 'equery' @collector = Puppet::Parser::Collector.new(@scope, "resource_type", @equery, @vquery, :virtual) end it "should not fail when it does not find any resources to collect" do @collector.resources = ["File[virtual1]", "File[virtual2]"] @scope.stubs(:findresource).returns(false) proc { @collector.evaluate }.should_not raise_error end it "should mark matched resources as non-virtual" do @collector.resources = ["File[virtual1]", "File[virtual2]"] one = stub_everything 'one' one.expects(:virtual=).with(false) @scope.stubs(:findresource).with("File[virtual1]").returns(one) @scope.stubs(:findresource).with("File[virtual2]").returns(nil) @collector.evaluate end it "should return matched resources" do @collector.resources = ["File[virtual1]", "File[virtual2]"] one = stub_everything 'one' @scope.stubs(:findresource).with("File[virtual1]").returns(one) @scope.stubs(:findresource).with("File[virtual2]").returns(nil) @collector.evaluate.should == [one] end it "should delete itself from the compile's collection list if it has found all of its resources" do @collector.resources = ["File[virtual1]"] one = stub_everything 'one' @compiler.expects(:delete_collection).with(@collector) @scope.expects(:compiler).returns(@compiler) @scope.stubs(:findresource).with("File[virtual1]").returns(one) @collector.evaluate end it "should not delete itself from the compile's collection list if it has unfound resources" do @collector.resources = ["File[virtual1]"] one = stub_everything 'one' @compiler.expects(:delete_collection).never @scope.stubs(:findresource).with("File[virtual1]").returns(nil) @collector.evaluate end end describe Puppet::Parser::Collector, "when collecting virtual and catalog resources" do before do @scope = mock 'scope' @compiler = mock 'compile' @scope.stubs(:compiler).returns(@compiler) @resource_type = "Mytype" @vquery = proc { |res| true } @collector = Puppet::Parser::Collector.new(@scope, @resource_type, nil, @vquery, :virtual) end it "should find all virtual resources matching the vquery" do one = stub_everything 'one', :type => "Mytype", :virtual? => true two = stub_everything 'two', :type => "Mytype", :virtual? => true @compiler.expects(:resources).returns([one, two]) @collector.evaluate.should == [one, two] end it "should find all non-virtual resources matching the vquery" do one = stub_everything 'one', :type => "Mytype", :virtual? => false two = stub_everything 'two', :type => "Mytype", :virtual? => false @compiler.expects(:resources).returns([one, two]) @collector.evaluate.should == [one, two] end it "should mark all matched resources as non-virtual" do one = stub_everything 'one', :type => "Mytype", :virtual? => true one.expects(:virtual=).with(false) @compiler.expects(:resources).returns([one]) @collector.evaluate end it "should return matched resources" do one = stub_everything 'one', :type => "Mytype", :virtual? => true two = stub_everything 'two', :type => "Mytype", :virtual? => true @compiler.expects(:resources).returns([one, two]) @collector.evaluate.should == [one, two] end it "should return all resources of the correct type if there is no virtual query" do one = stub_everything 'one', :type => "Mytype", :virtual? => true two = stub_everything 'two', :type => "Mytype", :virtual? => true one.expects(:virtual=).with(false) two.expects(:virtual=).with(false) @compiler.expects(:resources).returns([one, two]) @collector = Puppet::Parser::Collector.new(@scope, @resource_type, nil, nil, :virtual) @collector.evaluate.should == [one, two] end it "should not return or mark resources of a different type" do one = stub_everything 'one', :type => "Mytype", :virtual? => true two = stub_everything 'two', :type => :other, :virtual? => true one.expects(:virtual=).with(false) two.expects(:virtual=).never @compiler.expects(:resources).returns([one, two]) @collector.evaluate.should == [one] end it "should create a resource with overridden parameters" do one = stub_everything 'one', :type => "Mytype", :virtual? => true, :title => "test" param = stub 'param' @compiler.stubs(:add_override) @compiler.expects(:resources).returns([one]) @collector.add_override(:parameters => param ) Puppet::Parser::Resource.expects(:new).with { |type, title, h| h[:parameters] == param } @collector.evaluate end it "should define a new allow all child_of? on overriden resource" do one = stub_everything 'one', :type => "Mytype", :virtual? => true, :title => "test" param = stub 'param' source = stub 'source' @compiler.stubs(:add_override) @compiler.expects(:resources).returns([one]) @collector.add_override(:parameters => param, :source => source ) Puppet::Parser::Resource.stubs(:new) source.expects(:meta_def).with { |name,block| name == :child_of? } @collector.evaluate end it "should not override already overriden resources for this same collection in a previous run" do one = stub_everything 'one', :type => "Mytype", :virtual? => true, :title => "test" param = stub 'param' @compiler.stubs(:add_override) @compiler.expects(:resources).at_least(2).returns([one]) @collector.add_override(:parameters => param ) Puppet::Parser::Resource.expects(:new).once.with { |type, title, h| h[:parameters] == param } @collector.evaluate @collector.evaluate end it "should not return resources that were collected in a previous run of this collector" do one = stub_everything 'one', :type => "Mytype", :virtual? => true, :title => "test" @compiler.stubs(:resources).returns([one]) @collector.evaluate @collector.evaluate.should be_false end it "should tell the compiler about the overriden resources" do one = stub_everything 'one', :type => "Mytype", :virtual? => true, :title => "test" param = stub 'param' one.expects(:virtual=).with(false) @compiler.expects(:resources).returns([one]) @collector.add_override(:parameters => param ) Puppet::Parser::Resource.stubs(:new).returns("whatever") @compiler.expects(:add_override).with("whatever") @collector.evaluate end it "should not return or mark non-matching resources" do @collector.vquery = proc { |res| res.name == :one } one = stub_everything 'one', :name => :one, :type => "Mytype", :virtual? => true two = stub_everything 'two', :name => :two, :type => "Mytype", :virtual? => true one.expects(:virtual=).with(false) two.expects(:virtual=).never @compiler.expects(:resources).returns([one, two]) @collector.evaluate.should == [one] end end describe Puppet::Parser::Collector, "when collecting exported resources", :if => Puppet.features.rails? do include PuppetSpec::Files before do @compiler = Puppet::Parser::Compiler.new(Puppet::Node.new("mynode")) @scope = Puppet::Parser::Scope.new :compiler => @compiler @resource_type = "notify" - @equery = "1 = 1" + @equery = ["title", "!=", ""] @vquery = proc { |r| true } dir = Pathname(tmpdir('puppet-var')) Puppet[:vardir] = dir.to_s Puppet[:dbadapter] = 'sqlite3' Puppet[:dblocation] = (dir + 'storeconfigs.sqlite').to_s Puppet[:storeconfigs] = true Puppet[:environment] = "production" Puppet::Rails.init @collector = Puppet::Parser::Collector.new(@scope, @resource_type, @equery, @vquery, :exported) end after :each do ActiveRecord::Base.remove_connection end it "should just return false if :storeconfigs is not enabled" do Puppet[:storeconfigs] = false @collector.evaluate.should be_false end it "should return all matching resources from the current compile and mark them non-virtual and non-exported" do one = Puppet::Parser::Resource.new('notify', 'one', :virtual => true, :exported => true, :scope => @scope) two = Puppet::Parser::Resource.new('notify', 'two', :virtual => true, :exported => true, :scope => @scope) @compiler.resources << one @compiler.resources << two @collector.evaluate.should == [one, two] one.should_not be_virtual two.should_not be_virtual # REVISIT: Apparently we never actually marked local resources as # non-exported. So, this is what the previous test asserted, and checking # what it claims to do causes test failures. --daniel 2011-08-23 end it "should mark all returned resources as not virtual" do one = Puppet::Parser::Resource.new('notify', 'one', :virtual => true, :exported => true, :scope => @scope) @compiler.resources << one @collector.evaluate.should == [one] one.should_not be_virtual end it "should convert all found resources into parser resources" do host = Puppet::Rails::Host.create!(:name => 'one.local') Puppet::Rails::Resource. create!(:host => host, :restype => 'notify', :title => 'whammo', :exported => true) result = @collector.evaluate result.length.should == 1 result.first.should be_an_instance_of Puppet::Parser::Resource result.first.type.should == 'Notify' result.first.title.should == 'whammo' end it "should override all exported collected resources if collector has an override" do host = Puppet::Rails::Host.create!(:name => 'one.local') Puppet::Rails::Resource. create!(:host => host, :restype => 'notify', :title => 'whammo', :exported => true) param = Puppet::Parser::Resource::Param. new(:name => 'message', :value => 'howdy') @collector.add_override(:parameters => [param], :scope => @scope) got = @collector.evaluate got.first[:message].should == param.value end it "should store converted resources in the compile's resource list" do host = Puppet::Rails::Host.create!(:name => 'one.local') Puppet::Rails::Resource. create!(:host => host, :restype => 'notify', :title => 'whammo', :exported => true) @compiler.expects(:add_resource).with do |scope, resource| scope.should be_an_instance_of Puppet::Parser::Scope resource.type.should == 'Notify' resource.title.should == 'whammo' true end @collector.evaluate end # This way one host doesn't store another host's resources as exported. it "should mark resources collected from the database as not exported" do host = Puppet::Rails::Host.create!(:name => 'one.local') Puppet::Rails::Resource. create!(:host => host, :restype => 'notify', :title => 'whammo', :exported => true) got = @collector.evaluate got.length.should == 1 got.first.type.should == "Notify" got.first.title.should == "whammo" got.first.should_not be_exported end it "should fail if an equivalent resource already exists in the compile" do host = Puppet::Rails::Host.create!(:name => 'one.local') Puppet::Rails::Resource. create!(:host => host, :restype => 'notify', :title => 'whammo', :exported => true) local = Puppet::Parser::Resource.new('notify', 'whammo', :scope => @scope) @compiler.add_resource(@scope, local) expect { @collector.evaluate }. to raise_error Puppet::ParseError, /cannot override local resource/ end it "should ignore exported resources that match already-collected resources" do host = Puppet::Rails::Host.create!(:name => 'one.local') # One that we already collected... db = Puppet::Rails::Resource. create!(:host => host, :restype => 'notify', :title => 'whammo', :exported => true) # ...and one we didn't. Puppet::Rails::Resource. create!(:host => host, :restype => 'notify', :title => 'boingy-boingy', :exported => true) local = Puppet::Parser::Resource.new('notify', 'whammo', :scope => @scope, :collector_id => db.id) @compiler.add_resource(@scope, local) got = nil expect { got = @collector.evaluate }.not_to raise_error(Puppet::ParseError) got.length.should == 1 got.first.type.should == "Notify" got.first.title.should == "boingy-boingy" end end