diff --git a/spec/integration/parser/compiler4_spec.rb b/spec/integration/parser/compiler4_spec.rb deleted file mode 100644 index 7b4da353d..000000000 --- a/spec/integration/parser/compiler4_spec.rb +++ /dev/null @@ -1,770 +0,0 @@ -require 'spec_helper' -require 'puppet/pops' -require 'puppet/parser/parser_factory' -require 'puppet_spec/compiler' -require 'puppet_spec/pops' -require 'puppet_spec/scope' -require 'matchers/resource' -require 'rgen/metamodel_builder' - -describe "Puppet::Parser::Compiler" do - include PuppetSpec::Compiler - include Matchers::Resource - - describe "the compiler" do - it "should be able to determine the configuration version from a local version control repository" do - pending("Bug #14071 about semantics of Puppet::Util::Execute on Windows", :if => Puppet.features.microsoft_windows?) do - # This should always work, because we should always be - # in the puppet repo when we run this. - version = %x{git rev-parse HEAD}.chomp - - Puppet.settings[:config_version] = 'git rev-parse HEAD' - - compiler = Puppet::Parser::Compiler.new(Puppet::Node.new("testnode")) - compiler.catalog.version.should == version - end - end - - it "should not create duplicate resources when a class is referenced both directly and indirectly by the node classifier (4792)" do - node = Puppet::Node.new("testnodex") - node.classes = ['foo', 'bar'] - catalog = compile_to_catalog(<<-PP, node) - class foo - { - notify { foo_notify: } - include bar - } - class bar - { - notify { bar_notify: } - } - PP - - catalog = Puppet::Parser::Compiler.compile(node) - - expect(catalog).to have_resource("Notify[foo_notify]") - expect(catalog).to have_resource("Notify[bar_notify]") - end - - it 'applies defaults for defines with qualified names (PUP-2302)' do - catalog = compile_to_catalog(<<-CODE) - define my::thing($msg = 'foo') { notify {'check_me': message => $msg } } - My::Thing { msg => 'evoe' } - my::thing { 'name': } - CODE - - expect(catalog).to have_resource("Notify[check_me]").with_parameter(:message, "evoe") - end - - it 'Applies defaults from dynamic scopes (3x and 4x with reverted PUP-867)' do - catalog = compile_to_catalog(<<-CODE) - class a { - Notify { message => "defaulted" } - include b - notify { bye: } - } - class b { notify { hi: } } - - include a - CODE - expect(catalog).to have_resource("Notify[hi]").with_parameter(:message, "defaulted") - expect(catalog).to have_resource("Notify[bye]").with_parameter(:message, "defaulted") - end - - it 'gets default from inherited class (PUP-867)' do - catalog = compile_to_catalog(<<-CODE) - class a { - Notify { message => "defaulted" } - include c - notify { bye: } - } - class b { Notify { message => "inherited" } } - class c inherits b { notify { hi: } } - - include a - CODE - - expect(catalog).to have_resource("Notify[hi]").with_parameter(:message, "inherited") - expect(catalog).to have_resource("Notify[bye]").with_parameter(:message, "defaulted") - end - - it 'looks up default parameter values from inherited class (PUP-2532)' do - catalog = compile_to_catalog(<<-CODE) - class a { - Notify { message => "defaulted" } - include c - notify { bye: } - } - class b { Notify { message => "inherited" } } - class c inherits b { notify { hi: } } - - include a - notify {hi_test: message => Notify[hi][message] } - notify {bye_test: message => Notify[bye][message] } - CODE - - expect(catalog).to have_resource("Notify[hi_test]").with_parameter(:message, "inherited") - expect(catalog).to have_resource("Notify[bye_test]").with_parameter(:message, "defaulted") - end - - it 'does not allow override of class parameters using a resource override expression' do - expect do - compile_to_catalog(<<-CODE) - Class[a] { x => 2} - CODE - end.to raise_error(/Resource Override can only.*got: Class\[a\].*/) - end - - describe "when resolving class references" do - it "should not favor local scope (with class included in topscope)" do - catalog = compile_to_catalog(<<-PP) - class experiment { - class baz { - } - notify {"x" : require => Class[Baz] } - notify {"y" : require => Class[Experiment::Baz] } - } - class baz { - } - include baz - include experiment - include experiment::baz - PP - - expect(catalog).to have_resource("Notify[x]").with_parameter(:require, be_resource("Class[Baz]")) - expect(catalog).to have_resource("Notify[y]").with_parameter(:require, be_resource("Class[Experiment::Baz]")) - end - - it "should not favor local scope, (with class not included in topscope)" do - catalog = compile_to_catalog(<<-PP) - class experiment { - class baz { - } - notify {"x" : require => Class[Baz] } - notify {"y" : require => Class[Experiment::Baz] } - } - class baz { - } - include experiment - include experiment::baz - PP - - expect(catalog).to have_resource("Notify[x]").with_parameter(:require, be_resource("Class[Baz]")) - expect(catalog).to have_resource("Notify[y]").with_parameter(:require, be_resource("Class[Experiment::Baz]")) - end - end - - describe "(ticket #13349) when explicitly specifying top scope" do - ["class {'::bar::baz':}", "include ::bar::baz"].each do |include| - describe "with #{include}" do - it "should find the top level class" do - catalog = compile_to_catalog(<<-MANIFEST) - class { 'foo::test': } - class foo::test { - #{include} - } - class bar::baz { - notify { 'good!': } - } - class foo::bar::baz { - notify { 'bad!': } - } - MANIFEST - - expect(catalog).to have_resource("Class[Bar::Baz]") - expect(catalog).to have_resource("Notify[good!]") - expect(catalog).to_not have_resource("Class[Foo::Bar::Baz]") - expect(catalog).to_not have_resource("Notify[bad!]") - end - end - end - end - - it "should recompute the version after input files are re-parsed" do - Puppet[:code] = 'class foo { }' - Time.stubs(:now).returns(1) - node = Puppet::Node.new('mynode') - Puppet::Parser::Compiler.compile(node).version.should == 1 - Time.stubs(:now).returns(2) - Puppet::Parser::Compiler.compile(node).version.should == 1 # no change because files didn't change - Puppet::Resource::TypeCollection.any_instance.stubs(:stale?).returns(true).then.returns(false) # pretend change - Puppet::Parser::Compiler.compile(node).version.should == 2 - end - - ['define', 'class', 'node'].each do |thing| - it "'#{thing}' is not allowed inside evaluated conditional constructs" do - expect do - compile_to_catalog(<<-PP) - if true { - #{thing} foo { - } - notify { decoy: } - } - PP - end.to raise_error(Puppet::Error, /Classes, definitions, and nodes may only appear at toplevel/) - end - - it "'#{thing}' is not allowed inside un-evaluated conditional constructs" do - expect do - compile_to_catalog(<<-PP) - if false { - #{thing} foo { - } - notify { decoy: } - } - PP - end.to raise_error(Puppet::Error, /Classes, definitions, and nodes may only appear at toplevel/) - end - end - - describe "relationships can be formed" do - def extract_name(ref) - ref.sub(/File\[(\w+)\]/, '\1') - end - - def assert_creates_relationships(relationship_code, expectations) - base_manifest = <<-MANIFEST - file { [a,b,c]: - mode => '0644', - } - file { [d,e]: - mode => '0755', - } - MANIFEST - catalog = compile_to_catalog(base_manifest + relationship_code) - - resources = catalog.resources.select { |res| res.type == 'File' } - - actual_relationships, actual_subscriptions = [:before, :notify].map do |relation| - resources.map do |res| - dependents = Array(res[relation]) - dependents.map { |ref| [res.title, extract_name(ref)] } - end.inject(&:concat) - end - - actual_relationships.should =~ (expectations[:relationships] || []) - actual_subscriptions.should =~ (expectations[:subscriptions] || []) - end - - it "of regular type" do - assert_creates_relationships("File[a] -> File[b]", - :relationships => [['a','b']]) - end - - it "of subscription type" do - assert_creates_relationships("File[a] ~> File[b]", - :subscriptions => [['a', 'b']]) - end - - it "between multiple resources expressed as resource with multiple titles" do - assert_creates_relationships("File[a,b] -> File[c,d]", - :relationships => [['a', 'c'], - ['b', 'c'], - ['a', 'd'], - ['b', 'd']]) - end - - it "between collection expressions" do - assert_creates_relationships("File <| mode == '0644' |> -> File <| mode == '0755' |>", - :relationships => [['a', 'd'], - ['b', 'd'], - ['c', 'd'], - ['a', 'e'], - ['b', 'e'], - ['c', 'e']]) - end - - it "between resources expressed as Strings" do - assert_creates_relationships("'File[a]' -> 'File[b]'", - :relationships => [['a', 'b']]) - end - - it "between resources expressed as variables" do - assert_creates_relationships(<<-MANIFEST, :relationships => [['a', 'b']]) - $var = File[a] - $var -> File[b] - MANIFEST - - end - - it "between resources expressed as case statements" do - assert_creates_relationships(<<-MANIFEST, :relationships => [['s1', 't2']]) - $var = 10 - case $var { - 10: { - file { s1: } - } - 12: { - file { s2: } - } - } - -> - case $var + 2 { - 10: { - file { t1: } - } - 12: { - file { t2: } - } - } - MANIFEST - end - - it "using deep access in array" do - assert_creates_relationships(<<-MANIFEST, :relationships => [['a', 'b']]) - $var = [ [ [ File[a], File[b] ] ] ] - $var[0][0][0] -> $var[0][0][1] - MANIFEST - - end - - it "using deep access in hash" do - assert_creates_relationships(<<-MANIFEST, :relationships => [['a', 'b']]) - $var = {'foo' => {'bar' => {'source' => File[a], 'target' => File[b]}}} - $var[foo][bar][source] -> $var[foo][bar][target] - MANIFEST - - end - - it "using resource declarations" do - assert_creates_relationships("file { l: } -> file { r: }", :relationships => [['l', 'r']]) - end - - it "between entries in a chain of relationships" do - assert_creates_relationships("File[a] -> File[b] ~> File[c] <- File[d] <~ File[e]", - :relationships => [['a', 'b'], ['d', 'c']], - :subscriptions => [['b', 'c'], ['e', 'd']]) - end - end - - context "when dealing with variable references" do - it 'an initial underscore in a variable name is ok' do - catalog = compile_to_catalog(<<-MANIFEST) - class a { $_a = 10} - include a - notify { 'test': message => $a::_a } - MANIFEST - - expect(catalog).to have_resource("Notify[test]").with_parameter(:message, 10) - end - - it 'an initial underscore in not ok if elsewhere than last segment' do - expect do - catalog = compile_to_catalog(<<-MANIFEST) - class a { $_a = 10} - include a - notify { 'test': message => $_a::_a } - MANIFEST - end.to raise_error(/Illegal variable name/) - end - - it 'a missing variable as default value becomes undef' do - # strict variables not on - catalog = compile_to_catalog(<<-MANIFEST) - class a ($b=$x) { notify {test: message=>"yes ${undef == $b}" } } - include a - MANIFEST - - expect(catalog).to have_resource("Notify[test]").with_parameter(:message, "yes true") - end - end - - context 'when working with the trusted data hash' do - - it 'should make $trusted available' do - node = Puppet::Node.new("testing") - node.trusted_data = { "data" => "value" } - - catalog = compile_to_catalog(<<-MANIFEST, node) - notify { 'test': message => $trusted[data] } - MANIFEST - - expect(catalog).to have_resource("Notify[test]").with_parameter(:message, "value") - end - - it 'should not allow assignment to $trusted' do - node = Puppet::Node.new("testing") - node.trusted_data = { "data" => "value" } - - expect do - compile_to_catalog(<<-MANIFEST, node) - $trusted = 'changed' - notify { 'test': message => $trusted == 'changed' } - MANIFEST - end.to raise_error(Puppet::Error, /Attempt to assign to a reserved variable name: 'trusted'/) - end - end - - context 'when using typed parameters in definition' do - it 'accepts type compliant arguments' do - catalog = compile_to_catalog(<<-MANIFEST) - define foo(String $x) { } - foo { 'test': x =>'say friend' } - MANIFEST - expect(catalog).to have_resource("Foo[test]").with_parameter(:x, 'say friend') - end - - it 'accepts undef as the default for an Optional argument' do - catalog = compile_to_catalog(<<-MANIFEST) - define foo(Optional[String] $x = undef) { - notify { "expected": message => $x == undef } - } - foo { 'test': } - MANIFEST - expect(catalog).to have_resource("Notify[expected]").with_parameter(:message, true) - end - - it 'accepts anything when parameters are untyped' do - expect do - catalog = compile_to_catalog(<<-MANIFEST) - define foo($a, $b, $c) { } - foo { 'test': a => String, b=>10, c=>undef } - MANIFEST - end.to_not raise_error() - end - - it 'denies non type compliant arguments' do - expect do - catalog = compile_to_catalog(<<-MANIFEST) - define foo(Integer $x) { } - foo { 'test': x =>'say friend' } - MANIFEST - end.to raise_error(/type Integer, got String/) - end - - it 'denies undef for a non-optional type' do - expect do - catalog = compile_to_catalog(<<-MANIFEST) - define foo(Integer $x) { } - foo { 'test': x => undef } - MANIFEST - end.to raise_error(/type Integer, got Undef/) - end - - it 'denies non type compliant default argument' do - expect do - catalog = compile_to_catalog(<<-MANIFEST) - define foo(Integer $x = 'pow') { } - foo { 'test': } - MANIFEST - end.to raise_error(/type Integer, got String/) - end - - it 'denies undef as the default for a non-optional type' do - expect do - catalog = compile_to_catalog(<<-MANIFEST) - define foo(Integer $x = undef) { } - foo { 'test': } - MANIFEST - end.to raise_error(/type Integer, got Undef/) - end - - it 'accepts a Resource as a Type' do - catalog = compile_to_catalog(<<-MANIFEST) - define foo(Type[Bar] $x) { - notify { 'test': message => $x[text] } - } - define bar($text) { } - bar { 'joke': text => 'knock knock' } - foo { 'test': x => Bar[joke] } - MANIFEST - expect(catalog).to have_resource("Notify[test]").with_parameter(:message, 'knock knock') - end - end - - context 'when using typed parameters in class' do - it 'accepts type compliant arguments' do - catalog = compile_to_catalog(<<-MANIFEST) - class foo(String $x) { } - class { 'foo': x =>'say friend' } - MANIFEST - expect(catalog).to have_resource("Class[Foo]").with_parameter(:x, 'say friend') - end - - it 'accepts undef as the default for an Optional argument' do - catalog = compile_to_catalog(<<-MANIFEST) - class foo(Optional[String] $x = undef) { - notify { "expected": message => $x == undef } - } - class { 'foo': } - MANIFEST - expect(catalog).to have_resource("Notify[expected]").with_parameter(:message, true) - end - - it 'accepts anything when parameters are untyped' do - expect do - catalog = compile_to_catalog(<<-MANIFEST) - class foo($a, $b, $c) { } - class { 'foo': a => String, b=>10, c=>undef } - MANIFEST - end.to_not raise_error() - end - - it 'denies non type compliant arguments' do - expect do - catalog = compile_to_catalog(<<-MANIFEST) - class foo(Integer $x) { } - class { 'foo': x =>'say friend' } - MANIFEST - end.to raise_error(/type Integer, got String/) - end - - it 'denies undef for a non-optional type' do - expect do - catalog = compile_to_catalog(<<-MANIFEST) - class foo(Integer $x) { } - class { 'foo': x => undef } - MANIFEST - end.to raise_error(/type Integer, got Undef/) - end - - it 'denies non type compliant default argument' do - expect do - catalog = compile_to_catalog(<<-MANIFEST) - class foo(Integer $x = 'pow') { } - class { 'foo': } - MANIFEST - end.to raise_error(/type Integer, got String/) - end - - it 'denies undef as the default for a non-optional type' do - expect do - catalog = compile_to_catalog(<<-MANIFEST) - class foo(Integer $x = undef) { } - class { 'foo': } - MANIFEST - end.to raise_error(/type Integer, got Undef/) - end - - it 'accepts a Resource as a Type' do - catalog = compile_to_catalog(<<-MANIFEST) - class foo(Type[Bar] $x) { - notify { 'test': message => $x[text] } - } - define bar($text) { } - bar { 'joke': text => 'knock knock' } - class { 'foo': x => Bar[joke] } - MANIFEST - expect(catalog).to have_resource("Notify[test]").with_parameter(:message, 'knock knock') - end - end - - context 'when using typed parameters in lambdas' do - it 'accepts type compliant arguments' do - catalog = compile_to_catalog(<<-MANIFEST) - with('value') |String $x| { notify { "$x": } } - MANIFEST - expect(catalog).to have_resource("Notify[value]") - end - - it 'handles an array as a single argument' do - catalog = compile_to_catalog(<<-MANIFEST) - with(['value', 'second']) |$x| { notify { "${x[0]} ${x[1]}": } } - MANIFEST - expect(catalog).to have_resource("Notify[value second]") - end - - it 'denies when missing required arguments' do - expect do - compile_to_catalog(<<-MANIFEST) - with(1) |$x, $y| { } - MANIFEST - end.to raise_error(/Parameter \$y is required but no value was given/m) - end - - it 'accepts anything when parameters are untyped' do - catalog = compile_to_catalog(<<-MANIFEST) - ['value', 1, true, undef].each |$x| { notify { "value: $x": } } - MANIFEST - - expect(catalog).to have_resource("Notify[value: value]") - expect(catalog).to have_resource("Notify[value: 1]") - expect(catalog).to have_resource("Notify[value: true]") - expect(catalog).to have_resource("Notify[value: ]") - end - - it 'accepts type-compliant, slurped arguments' do - catalog = compile_to_catalog(<<-MANIFEST) - with(1, 2) |Integer *$x| { notify { "${$x[0] + $x[1]}": } } - MANIFEST - expect(catalog).to have_resource("Notify[3]") - end - - it 'denies non-type-compliant arguments' do - expect do - compile_to_catalog(<<-MANIFEST) - with(1) |String $x| { } - MANIFEST - end.to raise_error(/expected.*String.*actual.*Integer/m) - end - - it 'denies non-type-compliant, slurped arguments' do - expect do - compile_to_catalog(<<-MANIFEST) - with(1, "hello") |Integer *$x| { } - MANIFEST - end.to raise_error(/called with mis-matched arguments.*expected.*Integer.*actual.*Integer, String/m) - end - - it 'denies non-type-compliant default argument' do - expect do - compile_to_catalog(<<-MANIFEST) - with(1) |$x, String $defaulted = 1| { notify { "${$x + $defaulted}": }} - MANIFEST - end.to raise_error(/expected.*Any.*String.*actual.*Integer.*Integer/m) - end - - it 'raises an error when a default argument value is an incorrect type and there are no arguments passed' do - expect do - compile_to_catalog(<<-MANIFEST) - with() |String $defaulted = 1| {} - MANIFEST - end.to raise_error(/expected.*String.*actual.*Integer/m) - end - - it 'raises an error when the default argument for a slurped parameter is an incorrect type' do - expect do - compile_to_catalog(<<-MANIFEST) - with() |String *$defaulted = 1| {} - MANIFEST - end.to raise_error(/expected.*String.*actual.*Integer/m) - end - - it 'allows using an array as the default slurped value' do - catalog = compile_to_catalog(<<-MANIFEST) - with() |String *$defaulted = [hi]| { notify { $defaulted[0]: } } - MANIFEST - - expect(catalog).to have_resource('Notify[hi]') - end - - it 'allows using a value of the type as the default slurped value' do - catalog = compile_to_catalog(<<-MANIFEST) - with() |String *$defaulted = hi| { notify { $defaulted[0]: } } - MANIFEST - - expect(catalog).to have_resource('Notify[hi]') - end - - it 'allows specifying the type of a slurped parameter as an array' do - catalog = compile_to_catalog(<<-MANIFEST) - with() |Array[String] *$defaulted = hi| { notify { $defaulted[0]: } } - MANIFEST - - expect(catalog).to have_resource('Notify[hi]') - end - - it 'raises an error when the number of default values does not match the parameter\'s size specification' do - expect do - compile_to_catalog(<<-MANIFEST) - with() |Array[String, 2] *$defaulted = hi| { } - MANIFEST - end.to raise_error(/expected.*arg count \{2,\}.*actual.*arg count \{1\}/m) - end - - it 'raises an error when the number of passed values does not match the parameter\'s size specification' do - expect do - compile_to_catalog(<<-MANIFEST) - with(hi) |Array[String, 2] *$passed| { } - MANIFEST - end.to raise_error(/expected.*arg count \{2,\}.*actual.*arg count \{1\}/m) - end - - it 'matches when the number of arguments passed for a slurp parameter match the size specification' do - catalog = compile_to_catalog(<<-MANIFEST) - with(hi, bye) |Array[String, 2] *$passed| { - $passed.each |$n| { notify { $n: } } - } - MANIFEST - - expect(catalog).to have_resource('Notify[hi]') - expect(catalog).to have_resource('Notify[bye]') - end - - it 'raises an error when the number of allowed slurp parameters exceeds the size constraint' do - expect do - compile_to_catalog(<<-MANIFEST) - with(hi, bye) |Array[String, 1, 1] *$passed| { } - MANIFEST - end.to raise_error(/expected.*arg count \{1\}.*actual.*arg count \{2\}/m) - end - - it 'allows passing slurped arrays by specifying an array of arrays' do - catalog = compile_to_catalog(<<-MANIFEST) - with([hi], [bye]) |Array[Array[String, 1, 1]] *$passed| { - notify { $passed[0][0]: } - notify { $passed[1][0]: } - } - MANIFEST - - expect(catalog).to have_resource('Notify[hi]') - expect(catalog).to have_resource('Notify[bye]') - end - - it 'raises an error when a required argument follows an optional one' do - expect do - compile_to_catalog(<<-MANIFEST) - with() |$y = first, $x, Array[String, 1] *$passed = bye| {} - MANIFEST - end.to raise_error(/Parameter \$x is required/) - end - - it 'raises an error when the minimum size of a slurped argument makes it required and it follows an optional argument' do - expect do - compile_to_catalog(<<-MANIFEST) - with() |$x = first, Array[String, 1] *$passed| {} - MANIFEST - end.to raise_error(/Parameter \$passed is required/) - end - - it 'allows slurped arguments with a minimum size of 0 after an optional argument' do - catalog = compile_to_catalog(<<-MANIFEST) - with() |$x = first, Array[String, 0] *$passed| { - notify { $x: } - } - MANIFEST - - expect(catalog).to have_resource('Notify[first]') - end - - it 'accepts a Resource as a Type' do - catalog = compile_to_catalog(<<-MANIFEST) - define bar($text) { } - bar { 'joke': text => 'knock knock' } - - with(Bar[joke]) |Type[Bar] $joke| { notify { "${joke[text]}": } } - MANIFEST - expect(catalog).to have_resource("Notify[knock knock]") - end - end - end - - context 'when evaluating collection' do - it 'matches on container inherited tags' do - Puppet[:code] = <<-MANIFEST - class xport_test { - tag('foo_bar') - @notify { 'nbr1': - message => 'explicitly tagged', - tag => 'foo_bar' - } - - @notify { 'nbr2': - message => 'implicitly tagged' - } - - Notify <| tag == 'foo_bar' |> { - message => 'overridden' - } - } - include xport_test - MANIFEST - - catalog = Puppet::Parser::Compiler.compile(Puppet::Node.new("mynode")) - - expect(catalog).to have_resource("Notify[nbr1]").with_parameter(:message, 'overridden') - expect(catalog).to have_resource("Notify[nbr2]").with_parameter(:message, 'overridden') - end - end - -end diff --git a/spec/integration/parser/compiler_spec.rb b/spec/integration/parser/compiler_spec.rb index 5b59c5669..dc7b4e537 100755 --- a/spec/integration/parser/compiler_spec.rb +++ b/spec/integration/parser/compiler_spec.rb @@ -1,466 +1,928 @@ -#! /usr/bin/env ruby require 'spec_helper' -require 'puppet/parser/parser_factory' require 'puppet_spec/compiler' require 'matchers/resource' -describe "Puppet::Parser::Compiler" do - include PuppetSpec::Compiler +class CompilerTestResource + attr_accessor :builtin, :virtual, :evaluated, :type, :title + + def initialize(type, title) + @type = type + @title = title + end + + def [](attr) + return nil if attr == :stage + :main + end + + def ref + "#{type.to_s.capitalize}[#{title}]" + end + + def evaluated? + @evaluated + end + + def builtin_type? + @builtin + end + + def virtual? + @virtual + end + + def class? + false + end + + def stage? + false + end + + def evaluate + end + + def file + "/fake/file/goes/here" + end + + def line + "42" + end +end + +describe Puppet::Parser::Compiler do + include PuppetSpec::Files include Matchers::Resource + def resource(type, title) + Puppet::Parser::Resource.new(type, title, :scope => @scope) + end + before :each do - @node = Puppet::Node.new "testnode" + # Push me faster, I wanna go back in time! (Specifically, freeze time + # across the test since we have a bunch of version == timestamp code + # hidden away in the implementation and we keep losing the race.) + # --daniel 2011-04-21 + now = Time.now + Time.stubs(:now).returns(now) + + environment = Puppet::Node::Environment.create(:testing, []) + @node = Puppet::Node.new("testnode", + :facts => Puppet::Node::Facts.new("facts", {}), + :environment => environment) + @known_resource_types = environment.known_resource_types + @compiler = Puppet::Parser::Compiler.new(@node) + @scope = Puppet::Parser::Scope.new(@compiler, :source => stub('source')) + @scope_resource = Puppet::Parser::Resource.new(:file, "/my/file", :scope => @scope) + @scope.resource = @scope_resource + end + + it "should have a class method that compiles, converts, and returns a catalog" do + compiler = stub 'compiler' + Puppet::Parser::Compiler.expects(:new).with(@node).returns compiler + catalog = stub 'catalog' + compiler.expects(:compile).returns catalog + converted_catalog = stub 'converted_catalog' + catalog.expects(:to_resource).returns converted_catalog - @scope_resource = stub 'scope_resource', :builtin? => true, :finish => nil, :ref => 'Class[main]' - @scope = stub 'scope', :resource => @scope_resource, :source => mock("source") + Puppet::Parser::Compiler.compile(@node).should equal(converted_catalog) end - it "should be able to determine the configuration version from a local version control repository" do - pending("Bug #14071 about semantics of Puppet::Util::Execute on Windows", :if => Puppet.features.microsoft_windows?) do - # This should always work, because we should always be - # in the puppet repo when we run this. - version = %x{git rev-parse HEAD}.chomp + it "should fail intelligently when a class-level compile fails" do + Puppet::Parser::Compiler.expects(:new).raises ArgumentError + lambda { Puppet::Parser::Compiler.compile(@node) }.should raise_error(Puppet::Error) + end - Puppet.settings[:config_version] = 'git rev-parse HEAD' + it "should use the node's environment as its environment" do + @compiler.environment.should equal(@node.environment) + end - @parser = Puppet::Parser::ParserFactory.parser "development" - @compiler = Puppet::Parser::Compiler.new(@node) + it "fails if the node's environment has validation errors" do + conflicted_environment = Puppet::Node::Environment.create(:testing, [], '/some/environment.conf/manifest.pp') + conflicted_environment.stubs(:validation_errors).returns(['bad environment']) + @node.environment = conflicted_environment + expect { Puppet::Parser::Compiler.compile(@node) }.to raise_error(Puppet::Error, /Compilation has been halted because.*bad environment/) + end - @compiler.catalog.version.should == version - end + it "should include the resource type collection helper" do + Puppet::Parser::Compiler.ancestors.should be_include(Puppet::Resource::TypeCollectionHelper) end - it "should not create duplicate resources when a class is referenced both directly and indirectly by the node classifier (4792)" do - Puppet[:code] = <<-PP - class foo - { - notify { foo_notify: } - include bar - } - class bar - { - notify { bar_notify: } - } - PP + it "should be able to return a class list containing all added classes" do + @compiler.add_class "" + @compiler.add_class "one" + @compiler.add_class "two" + + @compiler.classlist.sort.should == %w{one two}.sort + end - @node.stubs(:classes).returns(['foo', 'bar']) + it "should clear the global caches before compile" do + compiler = stub 'compiler' + Puppet::Parser::Compiler.expects(:new).with(@node).returns compiler + catalog = stub 'catalog' + compiler.expects(:compile).returns catalog + catalog.expects(:to_resource) - catalog = Puppet::Parser::Compiler.compile(@node) + $known_resource_types = "rspec" + $env_module_directories = "rspec" - catalog.resource("Notify[foo_notify]").should_not be_nil - catalog.resource("Notify[bar_notify]").should_not be_nil + Puppet::Parser::Compiler.compile(@node) + + $known_resource_types = nil + $env_module_directories = nil end - describe "when resolving class references" do - it "should favor local scope, even if there's an included class in topscope" do - Puppet[:code] = <<-PP - class experiment { - class baz { - } - notify {"x" : require => Class[Baz] } - } - class baz { - } - include baz - include experiment - include experiment::baz - PP + describe "when initializing" do - catalog = Puppet::Parser::Compiler.compile(Puppet::Node.new("mynode")) + it "should set its node attribute" do + @compiler.node.should equal(@node) + end + it "should detect when ast nodes are absent" do + @compiler.ast_nodes?.should be_false + end - notify_resource = catalog.resource( "Notify[x]" ) + it "should detect when ast nodes are present" do + @known_resource_types.expects(:nodes?).returns true + @compiler.ast_nodes?.should be_true + end - notify_resource[:require].title.should == "Experiment::Baz" + it "should copy the known_resource_types version to the catalog" do + @compiler.catalog.version.should == @known_resource_types.version end - it "should favor local scope, even if there's an unincluded class in topscope" do - Puppet[:code] = <<-PP - class experiment { - class baz { - } - notify {"x" : require => Class[Baz] } - } - class baz { - } - include experiment - include experiment::baz - PP + it "should copy any node classes into the class list" do + node = Puppet::Node.new("mynode") + node.classes = %w{foo bar} + compiler = Puppet::Parser::Compiler.new(node) + + compiler.classlist.should =~ ['foo', 'bar'] + end - catalog = Puppet::Parser::Compiler.compile(Puppet::Node.new("mynode")) + it "should transform node class hashes into a class list" do + node = Puppet::Node.new("mynode") + node.classes = {'foo'=>{'one'=>'p1'}, 'bar'=>{'two'=>'p2'}} + compiler = Puppet::Parser::Compiler.new(node) - notify_resource = catalog.resource( "Notify[x]" ) + compiler.classlist.should =~ ['foo', 'bar'] + end - notify_resource[:require].title.should == "Experiment::Baz" + it "should add a 'main' stage to the catalog" do + @compiler.catalog.resource(:stage, :main).should be_instance_of(Puppet::Parser::Resource) end end - describe "(ticket #13349) when explicitly specifying top scope" do - ["class {'::bar::baz':}", "include ::bar::baz"].each do |include| - describe "with #{include}" do - it "should find the top level class" do - Puppet[:code] = <<-MANIFEST - class { 'foo::test': } - class foo::test { - #{include} - } - class bar::baz { - notify { 'good!': } - } - class foo::bar::baz { - notify { 'bad!': } - } - MANIFEST - catalog = Puppet::Parser::Compiler.compile(Puppet::Node.new("mynode")) + describe "when managing scopes" do - catalog.resource("Class[Bar::Baz]").should_not be_nil - catalog.resource("Notify[good!]").should_not be_nil - catalog.resource("Class[Foo::Bar::Baz]").should be_nil - catalog.resource("Notify[bad!]").should be_nil - end - end + it "should create a top scope" do + @compiler.topscope.should be_instance_of(Puppet::Parser::Scope) end - end - it "should recompute the version after input files are re-parsed" do - Puppet[:code] = 'class foo { }' - Time.stubs(:now).returns(1) - node = Puppet::Node.new('mynode') - Puppet::Parser::Compiler.compile(node).version.should == 1 - Time.stubs(:now).returns(2) - Puppet::Parser::Compiler.compile(node).version.should == 1 # no change because files didn't change - Puppet::Resource::TypeCollection.any_instance.stubs(:stale?).returns(true).then.returns(false) # pretend change - Puppet::Parser::Compiler.compile(node).version.should == 2 - end + it "should be able to create new scopes" do + @compiler.newscope(@compiler.topscope).should be_instance_of(Puppet::Parser::Scope) + end - ['class', 'define', 'node'].each do |thing| - it "should not allow '#{thing}' inside evaluated conditional constructs" do - Puppet[:code] = <<-PP - if true { - #{thing} foo { - } - notify { decoy: } - } - PP - - begin - Puppet::Parser::Compiler.compile(Puppet::Node.new("mynode")) - raise "compilation should have raised Puppet::Error" - rescue Puppet::Error => e - e.message.should =~ /at line 2/ - end + it "should set the parent scope of the new scope to be the passed-in parent" do + scope = mock 'scope' + newscope = @compiler.newscope(scope) + + newscope.parent.should equal(scope) end - end - it "should not allow classes inside unevaluated conditional constructs" do - Puppet[:code] = <<-PP - if false { - class foo { - } - } - PP + it "should set the parent scope of the new scope to its topscope if the parent passed in is nil" do + scope = mock 'scope' + newscope = @compiler.newscope(nil) - lambda { Puppet::Parser::Compiler.compile(Puppet::Node.new("mynode")) }.should raise_error(Puppet::Error) + newscope.parent.should equal(@compiler.topscope) + end end - describe "when defining relationships" do - def extract_name(ref) - ref.sub(/File\[(\w+)\]/, '\1') + describe "when compiling" do + + def compile_methods + [:set_node_parameters, :evaluate_main, :evaluate_ast_node, :evaluate_node_classes, :evaluate_generators, :fail_on_unevaluated, + :finish, :store, :extract, :evaluate_relationships] end - let(:node) { Puppet::Node.new('mynode') } - let(:code) do - <<-MANIFEST - file { [a,b,c]: - mode => '0644', - } - file { [d,e]: - mode => '0755', - } - MANIFEST + # Stub all of the main compile methods except the ones we're specifically interested in. + def compile_stub(*except) + (compile_methods - except).each { |m| @compiler.stubs(m) } end - let(:expected_relationships) { [] } - let(:expected_subscriptions) { [] } - before :each do - Puppet[:code] = code + it "should set node parameters as variables in the top scope" do + params = {"a" => "b", "c" => "d"} + @node.stubs(:parameters).returns(params) + compile_stub(:set_node_parameters) + @compiler.compile + @compiler.topscope['a'].should == "b" + @compiler.topscope['c'].should == "d" end - after :each do - catalog = Puppet::Parser::Compiler.compile(node) + it "should set the client and server versions on the catalog" do + params = {"clientversion" => "2", "serverversion" => "3"} + @node.stubs(:parameters).returns(params) + compile_stub(:set_node_parameters) + @compiler.compile + @compiler.catalog.client_version.should == "2" + @compiler.catalog.server_version.should == "3" + end - resources = catalog.resources.select { |res| res.type == 'File' } + it "should evaluate the main class if it exists" do + compile_stub(:evaluate_main) + main_class = @known_resource_types.add Puppet::Resource::Type.new(:hostclass, "") + main_class.expects(:evaluate_code).with { |r| r.is_a?(Puppet::Parser::Resource) } + @compiler.topscope.expects(:source=).with(main_class) - actual_relationships, actual_subscriptions = [:before, :notify].map do |relation| - resources.map do |res| - dependents = Array(res[relation]) - dependents.map { |ref| [res.title, extract_name(ref)] } - end.inject(&:concat) - end + @compiler.compile + end - actual_relationships.should =~ expected_relationships - actual_subscriptions.should =~ expected_subscriptions + it "should create a new, empty 'main' if no main class exists" do + compile_stub(:evaluate_main) + @compiler.compile + @known_resource_types.find_hostclass([""], "").should be_instance_of(Puppet::Resource::Type) end - it "should create a relationship" do - code << "File[a] -> File[b]" + it "should add an edge between the main stage and main class" do + @compiler.compile + (stage = @compiler.catalog.resource(:stage, "main")).should be_instance_of(Puppet::Parser::Resource) + (klass = @compiler.catalog.resource(:class, "")).should be_instance_of(Puppet::Parser::Resource) - expected_relationships << ['a','b'] + @compiler.catalog.edge?(stage, klass).should be_true end - it "should create a subscription" do - code << "File[a] ~> File[b]" + it "should evaluate all added collections" do + colls = [] + # And when the collections fail to evaluate. + colls << mock("coll1-false") + colls << mock("coll2-false") + colls.each { |c| c.expects(:evaluate).returns(false) } + + @compiler.add_collection(colls[0]) + @compiler.add_collection(colls[1]) - expected_subscriptions << ['a', 'b'] + compile_stub(:evaluate_generators) + @compiler.compile end - it "should create relationships using title arrays" do - code << "File[a,b] -> File[c,d]" + it "should ignore builtin resources" do + resource = resource(:file, "testing") - expected_relationships.concat [ - ['a', 'c'], - ['b', 'c'], - ['a', 'd'], - ['b', 'd'], - ] + @compiler.add_resource(@scope, resource) + resource.expects(:evaluate).never + + @compiler.compile end - it "should create relationships using collection expressions" do - code << "File <| mode == 0644 |> -> File <| mode == 0755 |>" + it "should evaluate unevaluated resources" do + resource = CompilerTestResource.new(:file, "testing") + + @compiler.add_resource(@scope, resource) - expected_relationships.concat [ - ['a', 'd'], - ['b', 'd'], - ['c', 'd'], - ['a', 'e'], - ['b', 'e'], - ['c', 'e'], - ] + # We have to now mark the resource as evaluated + resource.expects(:evaluate).with { |*whatever| resource.evaluated = true } + + @compiler.compile end - it "should create relationships using resource names" do - code << "'File[a]' -> 'File[b]'" + it "should not evaluate already-evaluated resources" do + resource = resource(:file, "testing") + resource.stubs(:evaluated?).returns true + + @compiler.add_resource(@scope, resource) + resource.expects(:evaluate).never - expected_relationships << ['a', 'b'] + @compiler.compile end - it "should create relationships using variables" do - code << <<-MANIFEST - $var = File[a] - $var -> File[b] - MANIFEST + it "should evaluate unevaluated resources created by evaluating other resources" do + resource = CompilerTestResource.new(:file, "testing") + @compiler.add_resource(@scope, resource) + + resource2 = CompilerTestResource.new(:file, "other") + + # We have to now mark the resource as evaluated + resource.expects(:evaluate).with { |*whatever| resource.evaluated = true; @compiler.add_resource(@scope, resource2) } + resource2.expects(:evaluate).with { |*whatever| resource2.evaluated = true } - expected_relationships << ['a', 'b'] + + @compiler.compile end - it "should create relationships using case statements" do - code << <<-MANIFEST - $var = 10 - case $var { - 10: { - file { s1: } - } - 12: { - file { s2: } - } - } - -> - case $var + 2 { - 10: { - file { t1: } - } - 12: { - file { t2: } - } - } - MANIFEST + describe "when finishing" do + before do + @compiler.send(:evaluate_main) + @catalog = @compiler.catalog + end + + def add_resource(name, parent = nil) + resource = Puppet::Parser::Resource.new "file", name, :scope => @scope + @compiler.add_resource(@scope, resource) + @catalog.add_edge(parent, resource) if parent + resource + end + + it "should call finish() on all resources" do + # Add a resource that does respond to :finish + resource = Puppet::Parser::Resource.new "file", "finish", :scope => @scope + resource.expects(:finish) + + @compiler.add_resource(@scope, resource) + + # And one that does not + dnf_resource = stub_everything "dnf", :ref => "File[dnf]", :type => "file" + + @compiler.add_resource(@scope, dnf_resource) + + @compiler.send(:finish) + end + + it "should call finish() in add_resource order" do + resources = sequence('resources') + + resource1 = add_resource("finish1") + resource1.expects(:finish).in_sequence(resources) + + resource2 = add_resource("finish2") + resource2.expects(:finish).in_sequence(resources) + + @compiler.send(:finish) + end + + it "should add each container's metaparams to its contained resources" do + main = @catalog.resource(:class, :main) + main[:noop] = true + + resource1 = add_resource("meh", main) + + @compiler.send(:finish) + resource1[:noop].should be_true + end + + it "should add metaparams recursively" do + main = @catalog.resource(:class, :main) + main[:noop] = true + + resource1 = add_resource("meh", main) + resource2 = add_resource("foo", resource1) + + @compiler.send(:finish) + resource2[:noop].should be_true + end + + it "should prefer metaparams from immediate parents" do + main = @catalog.resource(:class, :main) + main[:noop] = true - expected_relationships << ['s1', 't2'] + resource1 = add_resource("meh", main) + resource2 = add_resource("foo", resource1) + + resource1[:noop] = false + + @compiler.send(:finish) + resource2[:noop].should be_false + end + + it "should merge tags downward" do + main = @catalog.resource(:class, :main) + main.tag("one") + + resource1 = add_resource("meh", main) + resource1.tag "two" + resource2 = add_resource("foo", resource1) + + @compiler.send(:finish) + resource2.tags.should be_include("one") + resource2.tags.should be_include("two") + end + + it "should work if only middle resources have metaparams set" do + main = @catalog.resource(:class, :main) + + resource1 = add_resource("meh", main) + resource1[:noop] = true + resource2 = add_resource("foo", resource1) + + @compiler.send(:finish) + resource2[:noop].should be_true + end + end + + it "should return added resources in add order" do + resource1 = resource(:file, "yay") + @compiler.add_resource(@scope, resource1) + resource2 = resource(:file, "youpi") + @compiler.add_resource(@scope, resource2) + + @compiler.resources.should == [resource1, resource2] end - it "should create relationships using array members" do - code << <<-MANIFEST - $var = [ [ [ File[a], File[b] ] ] ] - $var[0][0][0] -> $var[0][0][1] - MANIFEST + it "should add resources that do not conflict with existing resources" do + resource = resource(:file, "yay") + @compiler.add_resource(@scope, resource) - expected_relationships << ['a', 'b'] + @compiler.catalog.should be_vertex(resource) end - it "should create relationships using hash members" do - code << <<-MANIFEST - $var = {'foo' => {'bar' => {'source' => File[a], 'target' => File[b]}}} - $var[foo][bar][source] -> $var[foo][bar][target] - MANIFEST + it "should fail to add resources that conflict with existing resources" do + path = make_absolute("/foo") + file1 = resource(:file, path) + file2 = resource(:file, path) - expected_relationships << ['a', 'b'] + @compiler.add_resource(@scope, file1) + lambda { @compiler.add_resource(@scope, file2) }.should raise_error(Puppet::Resource::Catalog::DuplicateResourceError) end - it "should create relationships using resource declarations" do - code << "file { l: } -> file { r: }" + it "should add an edge from the scope resource to the added resource" do + resource = resource(:file, "yay") + @compiler.add_resource(@scope, resource) - expected_relationships << ['l', 'r'] + @compiler.catalog.should be_edge(@scope.resource, resource) end - it "should chain relationships" do - code << "File[a] -> File[b] ~> File[c] <- File[d] <~ File[e]" + it "should not add non-class resources that don't specify a stage to the 'main' stage" do + main = @compiler.catalog.resource(:stage, :main) + resource = resource(:file, "foo") + @compiler.add_resource(@scope, resource) - expected_relationships << ['a', 'b'] << ['d', 'c'] - expected_subscriptions << ['b', 'c'] << ['e', 'd'] + @compiler.catalog.should_not be_edge(main, resource) + end + + it "should not add any parent-edges to stages" do + stage = resource(:stage, "other") + @compiler.add_resource(@scope, stage) + + @scope.resource = resource(:class, "foo") + + @compiler.catalog.edge?(@scope.resource, stage).should be_false + end + + it "should not attempt to add stages to other stages" do + other_stage = resource(:stage, "other") + second_stage = resource(:stage, "second") + @compiler.add_resource(@scope, other_stage) + @compiler.add_resource(@scope, second_stage) + + second_stage[:stage] = "other" + + @compiler.catalog.edge?(other_stage, second_stage).should be_false + end + + it "should have a method for looking up resources" do + resource = resource(:yay, "foo") + @compiler.add_resource(@scope, resource) + @compiler.findresource("Yay[foo]").should equal(resource) + end + + it "should be able to look resources up by type and title" do + resource = resource(:yay, "foo") + @compiler.add_resource(@scope, resource) + @compiler.findresource("Yay", "foo").should equal(resource) + end + + it "should not evaluate virtual defined resources" do + resource = resource(:file, "testing") + resource.virtual = true + @compiler.add_resource(@scope, resource) + + resource.expects(:evaluate).never + + @compiler.compile end end - context 'when working with immutable node data' do - def node_with_facts(facts) - Puppet[:facts_terminus] = :memory - Puppet::Node::Facts.indirection.save(Puppet::Node::Facts.new("testing", facts)) - node = Puppet::Node.new("testing") - node.fact_merge - node - end - - matcher :fail_compile_with do |node, message_regex| - match do |manifest| - @error = nil - begin - PuppetSpec::Compiler.compile_to_catalog(manifest, node) - false - rescue Puppet::Error => e - @error = e - message_regex.match(e.message) + describe "when evaluating collections" do + + it "should evaluate each collection" do + 2.times { |i| + coll = mock 'coll%s' % i + @compiler.add_collection(coll) + + # This is the hard part -- we have to emulate the fact that + # collections delete themselves if they are done evaluating. + coll.expects(:evaluate).with do + @compiler.delete_collection(coll) end + } + + @compiler.compile + end + + it "should not fail when there are unevaluated resource collections that do not refer to specific resources" do + coll = stub 'coll', :evaluate => false + coll.expects(:unresolved_resources).returns(nil) + + @compiler.add_collection(coll) + + lambda { @compiler.compile }.should_not raise_error + end + + it "should fail when there are unevaluated resource collections that refer to a specific resource" do + coll = stub 'coll', :evaluate => false + coll.expects(:unresolved_resources).returns(:something) + + @compiler.add_collection(coll) + + lambda { @compiler.compile }.should raise_error(Puppet::ParseError, 'Failed to realize virtual resources something') + end + + it "should fail when there are unevaluated resource collections that refer to multiple specific resources" do + coll = stub 'coll', :evaluate => false + coll.expects(:unresolved_resources).returns([:one, :two]) + + @compiler.add_collection(coll) + + lambda { @compiler.compile }.should raise_error(Puppet::ParseError, 'Failed to realize virtual resources one, two') + end + end + + describe "when evaluating relationships" do + it "should evaluate each relationship with its catalog" do + dep = stub 'dep' + dep.expects(:evaluate).with(@compiler.catalog) + @compiler.add_relationship dep + @compiler.evaluate_relationships + end + end + + describe "when told to evaluate missing classes" do + + it "should fail if there's no source listed for the scope" do + scope = stub 'scope', :source => nil + proc { @compiler.evaluate_classes(%w{one two}, scope) }.should raise_error(Puppet::DevError) + end + + it "should raise an error if a class is not found" do + @scope.expects(:find_hostclass).with("notfound", {:assume_fqname => false}).returns(nil) + lambda{ @compiler.evaluate_classes(%w{notfound}, @scope) }.should raise_error(Puppet::Error, /Could not find class/) + end + + it "should raise an error when it can't find class" do + klasses = {'foo'=>nil} + @node.classes = klasses + @compiler.topscope.expects(:find_hostclass).with('foo', {:assume_fqname => false}).returns(nil) + lambda{ @compiler.compile }.should raise_error(Puppet::Error, /Could not find class foo for testnode/) + end + end + + describe "when evaluating found classes" do + + before do + Puppet.settings[:data_binding_terminus] = "none" + @class = stub 'class', :name => "my::class" + @scope.stubs(:find_hostclass).with("myclass", {:assume_fqname => false}).returns(@class) + + @resource = stub 'resource', :ref => "Class[myclass]", :type => "file" + end + + it "should evaluate each class" do + @compiler.catalog.stubs(:tag) + + @class.expects(:ensure_in_catalog).with(@scope) + @scope.stubs(:class_scope).with(@class) + + @compiler.evaluate_classes(%w{myclass}, @scope) + end + + describe "and the classes are specified as a hash with parameters" do + before do + @node.classes = {} + @ast_obj = Puppet::Parser::AST::Leaf.new(:value => 'foo') + end + + # Define the given class with default parameters + def define_class(name, parameters) + @node.classes[name] = parameters + klass = Puppet::Resource::Type.new(:hostclass, name, :arguments => {'p1' => @ast_obj, 'p2' => @ast_obj}) + @compiler.topscope.known_resource_types.add klass + end + + def compile + @catalog = @compiler.compile + end + + it "should record which classes are evaluated" do + classes = {'foo'=>{}, 'bar::foo'=>{}, 'bar'=>{}} + classes.each { |c, params| define_class(c, params) } + compile() + classes.each { |name, p| @catalog.classes.should include(name) } + end + + it "should provide default values for parameters that have no values specified" do + define_class('foo', {}) + compile() + @catalog.resource(:class, 'foo')['p1'].should == "foo" + end + + it "should use any provided values" do + define_class('foo', {'p1' => 'real_value'}) + compile() + @catalog.resource(:class, 'foo')['p1'].should == "real_value" + end + + it "should support providing some but not all values" do + define_class('foo', {'p1' => 'real_value'}) + compile() + @catalog.resource(:class, 'Foo')['p1'].should == "real_value" + @catalog.resource(:class, 'Foo')['p2'].should == "foo" end - failure_message_for_should do - if @error - "failed with #{@error}\n#{@error.backtrace}" - else - "did not fail" + it "should ensure each node class is in catalog and has appropriate tags" do + klasses = ['bar::foo'] + @node.classes = klasses + ast_obj = Puppet::Parser::AST::Leaf.new(:value => 'foo') + klasses.each do |name| + klass = Puppet::Resource::Type.new(:hostclass, name, :arguments => {'p1' => ast_obj, 'p2' => ast_obj}) + @compiler.topscope.known_resource_types.add klass end + catalog = @compiler.compile + + r2 = catalog.resources.detect {|r| r.title == 'Bar::Foo' } + r2.tags.should == Puppet::Util::TagSet.new(['bar::foo', 'class', 'bar', 'foo']) end end - it 'should make $facts available' do - node = node_with_facts('the_facts' => 'straight') + it "should fail if required parameters are missing" do + klass = {'foo'=>{'a'=>'one'}} + @node.classes = klass + klass = Puppet::Resource::Type.new(:hostclass, 'foo', :arguments => {'a' => nil, 'b' => nil}) + @compiler.topscope.known_resource_types.add klass + lambda { @compiler.compile }.should raise_error(Puppet::ParseError, "Must pass b to Class[Foo]") + end + + it "should fail if invalid parameters are passed" do + klass = {'foo'=>{'3'=>'one'}} + @node.classes = klass + klass = Puppet::Resource::Type.new(:hostclass, 'foo', :arguments => {}) + @compiler.topscope.known_resource_types.add klass + lambda { @compiler.compile }.should raise_error(Puppet::ParseError, "Invalid parameter 3 on Class[Foo]") + end + + it "should ensure class is in catalog without params" do + @node.classes = klasses = {'foo'=>nil} + foo = Puppet::Resource::Type.new(:hostclass, 'foo') + @compiler.topscope.known_resource_types.add foo + catalog = @compiler.compile + catalog.classes.should include 'foo' + end + + it "should not evaluate the resources created for found classes unless asked" do + @compiler.catalog.stubs(:tag) - catalog = compile_to_catalog(<<-MANIFEST, node) - notify { 'test': message => $facts[the_facts] } - MANIFEST + @resource.expects(:evaluate).never - catalog.resource("Notify[test]")[:message].should == "straight" + @class.expects(:ensure_in_catalog).returns(@resource) + @scope.stubs(:class_scope).with(@class) + + @compiler.evaluate_classes(%w{myclass}, @scope) end - it 'should make $facts reserved' do - node = node_with_facts('the_facts' => 'straight') + it "should immediately evaluate the resources created for found classes when asked" do + @compiler.catalog.stubs(:tag) + + @resource.expects(:evaluate) + @class.expects(:ensure_in_catalog).returns(@resource) + @scope.stubs(:class_scope).with(@class) - expect('$facts = {}').to fail_compile_with(node, /assign to a reserved variable name: 'facts'/) - expect('class a { $facts = {} } include a').to fail_compile_with(node, /assign to a reserved variable name: 'facts'/) + @compiler.evaluate_classes(%w{myclass}, @scope, false) end - it 'should make $facts immutable' do - node = node_with_facts('string' => 'value', 'array' => ['string'], 'hash' => { 'a' => 'string' }, 'number' => 1, 'boolean' => true) + it "should skip classes that have already been evaluated" do + @compiler.catalog.stubs(:tag) + + @scope.stubs(:class_scope).with(@class).returns(@scope) + + @compiler.expects(:add_resource).never - expect('$i=inline_template("<% @facts[%q{new}] = 2 %>")').to fail_compile_with(node, /frozen Hash/i) - expect('$i=inline_template("<% @facts[%q{string}].chop! %>")').to fail_compile_with(node, /frozen String/i) + @resource.expects(:evaluate).never - expect('$i=inline_template("<% @facts[%q{array}][0].chop! %>")').to fail_compile_with(node, /frozen String/i) - expect('$i=inline_template("<% @facts[%q{array}][1] = 2 %>")').to fail_compile_with(node, /frozen Array/i) + Puppet::Parser::Resource.expects(:new).never + @compiler.evaluate_classes(%w{myclass}, @scope, false) + end - expect('$i=inline_template("<% @facts[%q{hash}][%q{a}].chop! %>")').to fail_compile_with(node, /frozen String/i) - expect('$i=inline_template("<% @facts[%q{hash}][%q{b}] = 2 %>")').to fail_compile_with(node, /frozen Hash/i) + it "should skip classes previously evaluated with different capitalization" do + @compiler.catalog.stubs(:tag) + @scope.stubs(:find_hostclass).with("MyClass",{:assume_fqname => false}).returns(@class) + @scope.stubs(:class_scope).with(@class).returns(@scope) + @compiler.expects(:add_resource).never + @resource.expects(:evaluate).never + Puppet::Parser::Resource.expects(:new).never + @compiler.evaluate_classes(%w{MyClass}, @scope, false) end + end - it 'should make $facts available even if there are no facts' do - Puppet[:facts_terminus] = :memory - node = Puppet::Node.new("testing2") - node.fact_merge + describe "when evaluating AST nodes with no AST nodes present" do - catalog = compile_to_catalog(<<-MANIFEST, node) - notify { 'test': message => $facts } - MANIFEST + it "should do nothing" do + @compiler.expects(:ast_nodes?).returns(false) + @compiler.known_resource_types.expects(:nodes).never + Puppet::Parser::Resource.expects(:new).never - expect(catalog).to have_resource("Notify[test]").with_parameter(:message, {}) + @compiler.send(:evaluate_ast_node) end end - context 'when working with the trusted data hash' do + describe "when evaluating AST nodes with AST nodes present" do - it 'should make $trusted available' do - node = Puppet::Node.new("testing") - node.trusted_data = { "data" => "value" } + before do + @compiler.known_resource_types.stubs(:nodes?).returns true - catalog = compile_to_catalog(<<-MANIFEST, node) - notify { 'test': message => $trusted[data] } - MANIFEST + # Set some names for our test + @node.stubs(:names).returns(%w{a b c}) + @compiler.known_resource_types.stubs(:node).with("a").returns(nil) + @compiler.known_resource_types.stubs(:node).with("b").returns(nil) + @compiler.known_resource_types.stubs(:node).with("c").returns(nil) + + # It should check this last, of course. + @compiler.known_resource_types.stubs(:node).with("default").returns(nil) + end - catalog.resource("Notify[test]")[:message].should == "value" + it "should fail if the named node cannot be found" do + proc { @compiler.send(:evaluate_ast_node) }.should raise_error(Puppet::ParseError) end - it 'should not allow assignment to $trusted' do - node = Puppet::Node.new("testing") - node.trusted_data = { "data" => "value" } + it "should evaluate the first node class matching the node name" do + node_class = stub 'node', :name => "c", :evaluate_code => nil + @compiler.known_resource_types.stubs(:node).with("c").returns(node_class) - expect do - catalog = compile_to_catalog(<<-MANIFEST, node) - $trusted = 'changed' - notify { 'test': message => $trusted == 'changed' } - MANIFEST - catalog.resource("Notify[test]")[:message].should == true - end.to raise_error(Puppet::Error, /Attempt to assign to a reserved variable name: 'trusted'/) + node_resource = stub 'node resource', :ref => "Node[c]", :evaluate => nil, :type => "node" + node_class.expects(:ensure_in_catalog).returns(node_resource) + + @compiler.compile end - it 'should not allow addition to $trusted hash' do - node = Puppet::Node.new("testing") - node.trusted_data = { "data" => "value" } + it "should match the default node if no matching node can be found" do + node_class = stub 'node', :name => "default", :evaluate_code => nil + @compiler.known_resource_types.stubs(:node).with("default").returns(node_class) + + node_resource = stub 'node resource', :ref => "Node[default]", :evaluate => nil, :type => "node" + node_class.expects(:ensure_in_catalog).returns(node_resource) - expect do - catalog = compile_to_catalog(<<-MANIFEST, node) - $trusted['extra'] = 'added' - notify { 'test': message => $trusted['extra'] == 'added' } - MANIFEST - catalog.resource("Notify[test]")[:message].should == true - # different errors depending on regular or future parser - end.to raise_error(Puppet::Error, /(can't modify frozen [hH]ash)|(Illegal attempt to assign)/) + @compiler.compile end - it 'should not allow addition to $trusted hash via Ruby inline template' do - node = Puppet::Node.new("testing") - node.trusted_data = { "data" => "value" } + it "should evaluate the node resource immediately rather than using lazy evaluation" do + node_class = stub 'node', :name => "c" + @compiler.known_resource_types.stubs(:node).with("c").returns(node_class) + + node_resource = stub 'node resource', :ref => "Node[c]", :type => "node" + node_class.expects(:ensure_in_catalog).returns(node_resource) + + node_resource.expects(:evaluate) - expect do - catalog = compile_to_catalog(<<-MANIFEST, node) - $dummy = inline_template("<% @trusted['extra'] = 'added' %> lol") - notify { 'test': message => $trusted['extra'] == 'added' } - MANIFEST - catalog.resource("Notify[test]")[:message].should == true - end.to raise_error(Puppet::Error, /can't modify frozen [hH]ash/) + @compiler.send(:evaluate_ast_node) end end - context 'when evaluating collection' do - it 'matches on container inherited tags' do - Puppet[:code] = <<-MANIFEST - class xport_test { - tag 'foo_bar' - @notify { 'nbr1': - message => 'explicitly tagged', - tag => 'foo_bar' - } - - @notify { 'nbr2': - message => 'implicitly tagged' - } - - Notify <| tag == 'foo_bar' |> { - message => 'overridden' - } - } - include xport_test - MANIFEST + describe "when evaluating node classes" do + include PuppetSpec::Compiler + + describe "when provided classes in array format" do + let(:node) { Puppet::Node.new('someone', :classes => ['something']) } + + describe "when the class exists" do + it "should succeed if the class is already included" do + manifest = <<-MANIFEST + class something {} + include something + MANIFEST + + catalog = compile_to_catalog(manifest, node) + + catalog.resource('Class', 'Something').should_not be_nil + end + + it "should evaluate the class without parameters if it's not already included" do + manifest = "class something {}" + + catalog = compile_to_catalog(manifest, node) + + catalog.resource('Class', 'Something').should_not be_nil + end + end + + it "should fail if the class doesn't exist" do + expect { compile_to_catalog('', node) }.to raise_error(Puppet::Error, /Could not find class something/) + end + end + + describe "when provided classes in hash format" do + describe "for classes without parameters" do + let(:node) { Puppet::Node.new('someone', :classes => {'something' => {}}) } + + describe "when the class exists" do + it "should succeed if the class is already included" do + manifest = <<-MANIFEST + class something {} + include something + MANIFEST + + catalog = compile_to_catalog(manifest, node) + + catalog.resource('Class', 'Something').should_not be_nil + end + + it "should evaluate the class if it's not already included" do + manifest = <<-MANIFEST + class something {} + MANIFEST + + catalog = compile_to_catalog(manifest, node) + + catalog.resource('Class', 'Something').should_not be_nil + end + end + + it "should fail if the class doesn't exist" do + expect { compile_to_catalog('', node) }.to raise_error(Puppet::Error, /Could not find class something/) + end + end + + describe "for classes with parameters" do + let(:node) { Puppet::Node.new('someone', :classes => {'something' => {'configuron' => 'defrabulated'}}) } + + describe "when the class exists" do + it "should fail if the class is already included" do + manifest = <<-MANIFEST + class something($configuron=frabulated) {} + include something + MANIFEST + + expect { compile_to_catalog(manifest, node) }.to raise_error(Puppet::Error, /Class\[Something\] is already declared/) + end + + it "should evaluate the class if it's not already included" do + manifest = <<-MANIFEST + class something($configuron=frabulated) {} + MANIFEST + + catalog = compile_to_catalog(manifest, node) + + resource = catalog.resource('Class', 'Something') + resource['configuron'].should == 'defrabulated' + end + end + + it "should fail if the class doesn't exist" do + expect { compile_to_catalog('', node) }.to raise_error(Puppet::Error, /Could not find class something/) + end + + it 'evaluates classes declared with parameters before unparameterized classes' do + node = Puppet::Node.new('someone', :classes => { 'app::web' => {}, 'app' => { 'port' => 8080 } }) + manifest = <<-MANIFEST + class app($port = 80) { } + + class app::web($port = $app::port) inherits app { + notify { expected: message => "$port" } + } + MANIFEST + + catalog = compile_to_catalog(manifest, node) + + expect(catalog).to have_resource("Class[App]").with_parameter(:port, 8080) + expect(catalog).to have_resource("Class[App::Web]") + expect(catalog).to have_resource("Notify[expected]").with_parameter(:message, "8080") + end + end + end + end + + describe "when managing resource overrides" do + + before do + @override = stub 'override', :ref => "File[/foo]", :type => "my" + @resource = resource(:file, "/foo") + end + + it "should be able to store overrides" do + lambda { @compiler.add_override(@override) }.should_not raise_error + end + + it "should apply overrides to the appropriate resources" do + @compiler.add_resource(@scope, @resource) + @resource.expects(:merge).with(@override) + + @compiler.add_override(@override) + + @compiler.compile + end + + it "should accept overrides before the related resource has been created" do + @resource.expects(:merge).with(@override) + + # First store the override + @compiler.add_override(@override) + + # Then the resource + @compiler.add_resource(@scope, @resource) + + # And compile, so they get resolved + @compiler.compile + end - catalog = Puppet::Parser::Compiler.compile(Puppet::Node.new("mynode")) + it "should fail if the compile is finished and resource overrides have not been applied" do + @compiler.add_override(@override) - expect(catalog).to have_resource("Notify[nbr1]").with_parameter(:message, 'overridden') - expect(catalog).to have_resource("Notify[nbr2]").with_parameter(:message, 'overridden') + lambda { @compiler.compile }.should raise_error Puppet::ParseError, 'Could not find resource(s) File[/foo] for overriding' end end end