#!/usr/bin/env ruby

$:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/

require 'puppet'
require 'puppet/parser/interpreter'
require 'puppet/parser/parser'
require 'puppet/client'
require 'puppettest'
require 'puppettest/parsertesting'
require 'puppettest/resourcetesting'

# so, what kind of things do we want to test?

# we don't need to test function, since we're confident in the
# library tests.  We do, however, need to test how things are actually
# working in the language.

# so really, we want to do things like test that our ast is correct
# and test whether we've got things in the right scopes

class TestScope < Test::Unit::TestCase
    include PuppetTest::ParserTesting
    include PuppetTest::ResourceTesting

    def to_ary(hash)
        hash.collect { |key,value|
            [key,value]
        }
    end

    def test_variables
        scope = nil
        over = "over"

        scopes = []
        vars = []
        values = {}
        ovalues = []

        10.times { |index|
            # slap some recursion in there
            scope = mkscope(:parent => scope)
            scopes.push scope

            var = "var%s" % index
            value = rand(1000)
            ovalue = rand(1000)
            
            ovalues.push ovalue

            vars.push var
            values[var] = value

            # set the variable in the current scope
            assert_nothing_raised {
                scope.setvar(var,value)
            }

            # this should override previous values
            assert_nothing_raised {
                scope.setvar(over,ovalue)
            }

            assert_equal(value,scope.lookupvar(var))

            #puts "%s vars, %s scopes" % [vars.length,scopes.length]
            i = 0
            vars.zip(scopes) { |v,s|
                # this recurses all the way up the tree as necessary
                val = nil
                oval = nil

                # look up the values using the bottom scope
                assert_nothing_raised {
                    val = scope.lookupvar(v)
                    oval = scope.lookupvar(over)
                }

                # verify they're correct
                assert_equal(values[v],val)
                assert_equal(ovalue,oval)

                # verify that we get the most recent value
                assert_equal(ovalue,scope.lookupvar(over))

                # verify that they aren't available in upper scopes
                if parent = s.parent
                    val = nil
                    assert_nothing_raised {
                        val = parent.lookupvar(v)
                    }
                    assert_equal("", val, "Did not get empty string on missing var")

                    # and verify that the parent sees its correct value
                    assert_equal(ovalues[i - 1],parent.lookupvar(over))
                end
                i += 1
            }
        }
    end

    def test_declarative
        # set to declarative
        top = mkscope(:declarative => true)
        sub = mkscope(:parent => top)

        assert_nothing_raised {
            top.setvar("test","value")
        }
        assert_raise(Puppet::ParseError) {
            top.setvar("test","other")
        }
        assert_nothing_raised {
            sub.setvar("test","later")
        }
        assert_raise(Puppet::ParseError) {
            top.setvar("test","yeehaw")
        }
    end

    def test_notdeclarative
        # set to not declarative
        top = mkscope(:declarative => false)
        sub = mkscope(:parent => top)

        assert_nothing_raised {
            top.setvar("test","value")
        }
        assert_nothing_raised {
            top.setvar("test","other")
        }
        assert_nothing_raised {
            sub.setvar("test","later")
        }
        assert_nothing_raised {
            sub.setvar("test","yayness")
        }
    end

    def test_setdefaults
        interp, scope, source = mkclassframing

        # The setdefaults method doesn't really check what we're doing,
        # so we're just going to use fake defaults here.

        # First do a simple local lookup
        params = paramify(source, :one => "fun", :two => "shoe")
        origshould = {}
        params.each do |p| origshould[p.name] = p end
        assert_nothing_raised do
            scope.setdefaults(:file, params)
        end

        ret = nil
        assert_nothing_raised do
            ret = scope.lookupdefaults(:file)
        end

        assert_equal(origshould, ret)

        # Now create a subscope and add some more params.
        newscope = scope.newscope

        newparams = paramify(source, :one => "shun", :three => "free")
        assert_nothing_raised {
            newscope.setdefaults(:file, newparams)
        }

        # And make sure we get the appropriate ones back
        should = {}
        params.each do |p| should[p.name] = p end
        newparams.each do |p| should[p.name] = p end

        assert_nothing_raised do
            ret = newscope.lookupdefaults(:file)
        end

        assert_equal(should, ret)

        # Make sure we still only get the originals from the top scope
        assert_nothing_raised do
            ret = scope.lookupdefaults(:file)
        end

        assert_equal(origshould, ret)

        # Now create another scope and make sure we only get the top defaults
        otherscope = scope.newscope
        assert_equal(origshould, otherscope.lookupdefaults(:file))

        # And make sure none of the scopes has defaults for other types
        [scope, newscope, otherscope].each do |sc|
            assert_equal({}, sc.lookupdefaults(:exec))
        end
    end
    
    def test_strinterp
        scope = mkscope()

        assert_nothing_raised {
            scope.setvar("test","value")
        }
        val = nil
        assert_nothing_raised {
            val = scope.strinterp("string ${test}")
        }
        assert_equal("string value", val)

        assert_nothing_raised {
            val = scope.strinterp("string ${test} ${test} ${test}")
        }
        assert_equal("string value value value", val)

        assert_nothing_raised {
            val = scope.strinterp("string $test ${test} $test")
        }
        assert_equal("string value value value", val)

        assert_nothing_raised {
            val = scope.strinterp("string \\$test")
        }
        assert_equal("string $test", val)

        assert_nothing_raised {
            val = scope.strinterp("\\$test string")
        }
        assert_equal("$test string", val)
    end

    def test_setclass
        interp, scope, source = mkclassframing

        base = scope.findclass("base")
        assert(base, "Could not find base class")
        assert(! scope.setclass?(base), "Class incorrectly set")
        assert(! scope.classlist.include?("base"), "Class incorrectly in classlist")
        assert_nothing_raised do
            scope.setclass base
        end

        assert(scope.setclass?(base), "Class incorrectly unset")
        assert(scope.classlist.include?("base"), "Class not in classlist")

        # Now try it with a normal string
        assert_raise(Puppet::DevError) do
            scope.setclass "string"
        end

        assert(! scope.setclass?("string"), "string incorrectly set")

        # Set "" in the class list, and make sure it doesn't show up in the return
        top = scope.findclass("")
        assert(top, "Could not find top class")
        scope.setclass top

        assert(! scope.classlist.include?(""), "Class list included empty")
    end

    def test_validtags
        scope = mkscope()

        ["a class", "a.class"].each do |bad|
            assert_raise(Puppet::ParseError, "Incorrectly allowed %s" % bad.inspect) do
                scope.tag(bad)
            end
        end

        ["a-class", "a_class", "Class", "class", "yayNess"].each do |good|
            assert_nothing_raised("Incorrectly banned %s" % good.inspect) do
                scope.tag(good)
            end
        end

    end

    def test_tagfunction
        scope = mkscope()
        
        assert_nothing_raised {
            scope.function_tag(["yayness", "booness"])
        }

        assert(scope.tags.include?("yayness"), "tag 'yayness' did not get set")
        assert(scope.tags.include?("booness"), "tag 'booness' did not get set")

        # Now verify that the 'tagged' function works correctly
        assert(scope.function_tagged("yayness"),
            "tagged function incorrectly returned false")
        assert(scope.function_tagged("booness"),
            "tagged function incorrectly returned false")

        assert(! scope.function_tagged("funtest"),
            "tagged function incorrectly returned true")
    end

    def test_includefunction
        interp = mkinterp
        scope = mkscope :interp => interp

        myclass = interp.newclass "myclass"
        otherclass = interp.newclass "otherclass"

        function = Puppet::Parser::AST::Function.new(
            :name => "include",
            :ftype => :statement,
            :arguments => AST::ASTArray.new(
                :children => [nameobj("myclass"), nameobj("otherclass")]
            )
        )

        assert_nothing_raised do
            function.evaluate :scope => scope
        end

        [myclass, otherclass].each do |klass|
            assert(scope.setclass?(klass),
                "%s was not set" % klass.fqname)
        end
    end

    def test_definedfunction
        interp = mkinterp
        %w{one two}.each do |name|
            interp.newdefine name
        end

        scope = mkscope :interp => interp

        assert_nothing_raised {
            %w{one two file user}.each do |type|
                assert(scope.function_defined([type]),
                    "Class #{type} was not considered defined")
            end

            assert(!scope.function_defined(["nopeness"]),
                "Class 'nopeness' was incorrectly considered defined")
        }
    end

    # Make sure we know what we consider to be truth.
    def test_truth
        assert_equal(true, Puppet::Parser::Scope.true?("a string"),
            "Strings not considered true")
        assert_equal(true, Puppet::Parser::Scope.true?(true),
            "True considered true")
        assert_equal(false, Puppet::Parser::Scope.true?(""),
            "Empty strings considered true")
        assert_equal(false, Puppet::Parser::Scope.true?(false),
            "false considered true")
    end

    # Verify scope context is handled correctly.
    def test_scopeinside
        scope = mkscope()

        one = :one
        two = :two

        # First just test the basic functionality.
        assert_nothing_raised {
            scope.inside :one do
                assert_equal(:one, scope.inside, "Context did not get set")
            end
            assert_nil(scope.inside, "Context did not revert")
        }

        # Now make sure error settings work.
        assert_raise(RuntimeError) {
            scope.inside :one do
                raise RuntimeError, "This is a failure, yo"
            end
        }
        assert_nil(scope.inside, "Context did not revert")

        # Now test it a bit deeper in.
        assert_nothing_raised {
            scope.inside :one do
                scope.inside :two do
                    assert_equal(:two, scope.inside, "Context did not get set")
                end
                assert_equal(:one, scope.inside, "Context did not get set")
            end
            assert_nil(scope.inside, "Context did not revert")
        }

        # And lastly, check errors deeper in
        assert_nothing_raised {
            scope.inside :one do
                begin
                    scope.inside :two do
                        raise "a failure"
                    end
                rescue
                end
                assert_equal(:one, scope.inside, "Context did not get set")
            end
            assert_nil(scope.inside, "Context did not revert")
        }

    end

    if defined? ActiveRecord
    # Verify that we recursively mark as exported the results of collectable
    # components.
    def test_exportedcomponents
        interp, scope, source = mkclassframing
        children = []

        args = AST::ASTArray.new(
            :file => tempfile(),
            :line => rand(100),
            :children => [nameobj("arg")]
        )

        # Create a top-level component
        interp.newdefine "one", :arguments => [%w{arg}],
            :code => AST::ASTArray.new(
                :children => [
                    resourcedef("file", "/tmp", {"owner" => varref("arg")})
                ]
            )

        # And a component that calls it
        interp.newdefine "two", :arguments => [%w{arg}],
            :code => AST::ASTArray.new(
                :children => [
                    resourcedef("one", "ptest", {"arg" => varref("arg")})
                ]
            )

        # And then a third component that calls the second
        interp.newdefine "three", :arguments => [%w{arg}],
            :code => AST::ASTArray.new(
                :children => [
                    resourcedef("two", "yay", {"arg" => varref("arg")})
                ]
            )

        # lastly, create an object that calls our third component
        obj = resourcedef("three", "boo", {"arg" => "parentfoo"})

        # And mark it as exported
        obj.exported = true

        obj.evaluate :scope => scope

        # And then evaluate it
        interp.evaliterate(scope)

        %w{file}.each do |type|
            objects = scope.lookupexported(type)

            assert(!objects.empty?, "Did not get an exported %s" % type)
        end
    end

    # Verify that we can both store and collect an object in the same
    # run, whether it's in the same scope as a collection or a different
    # scope.
    def test_storeandcollect
        Puppet[:storeconfigs] = true
        Puppet::Rails.clear
        Puppet::Rails.init
        sleep 1
        children = []
        file = tempfile()
        File.open(file, "w") { |f|
            f.puts "
class yay {
    @@host { myhost: ip => \"192.168.0.2\" }
}
include yay
@@host { puppet: ip => \"192.168.0.3\" }
Host <<||>>"
        }

        interp = nil
        assert_nothing_raised {
            interp = Puppet::Parser::Interpreter.new(
                :Manifest => file,
                :UseNodes => false,
                :ForkSave => false
            )
        }

        objects = nil
        # We run it twice because we want to make sure there's no conflict
        # if we pull it up from the database.
        2.times { |i|
            assert_nothing_raised {
                objects = interp.run("localhost", {"hostname" => "localhost"})
            }

            flat = objects.flatten

            %w{puppet myhost}.each do |name|
                assert(flat.find{|o| o.name == name }, "Did not find #{name}")
            end
        }
    end
    else
        $stderr.puts "No ActiveRecord -- skipping collection tests"
    end

    # Make sure tags behave appropriately.
    def test_tags
        interp, scope, source = mkclassframing

        # First make sure we can only set legal tags
        ["an invalid tag", "-anotherinvalid", "bad*tag"].each do |tag|
            assert_raise(Puppet::ParseError, "Tag #{tag} was considered valid") do
                scope.tag tag
            end
        end

        # Now make sure good tags make it through.
        tags = %w{good-tag yaytag GoodTag another_tag}
        tags.each do |tag|
            assert_nothing_raised("Tag #{tag} was considered invalid") do
                scope.tag tag
            end
        end

        # And make sure we get each of them.
        ptags = scope.tags
        tags.each do |tag|
            assert(ptags.include?(tag), "missing #{tag}")
        end


        # Now create a subscope and set some tags there
        newscope = scope.newscope(:type => 'subscope')

        # set some tags
        newscope.tag "onemore", "yaytag"

        # And make sure we get them plus our parent tags
        assert_equal((ptags + %w{onemore subscope}).sort, newscope.tags.sort)
    end

    # Make sure we successfully translate objects
    def test_translate
        interp, scope, source = mkclassframing

        # Create a define that we'll be using
        interp.newdefine("wrapper", :code => AST::ASTArray.new(:children => [
            resourcedef("file", varref("name"), "owner" => "root")
        ]))

        # Now create a resource that uses that define
        define = mkresource(:type => "wrapper", :title => "/tmp/testing",
            :scope => scope, :source => source, :params => :none)

        scope.setresource define

        # And a normal resource
        scope.setresource mkresource(:type => "file", :title => "/tmp/rahness",
            :scope => scope, :source => source,
            :params => {:owner => "root"})

        # Evaluate the the define thing.
        define.evaluate

        # Now the scope should have a resource and a subscope.  Translate the
        # whole thing.
        ret = nil
        assert_nothing_raised do
            ret = scope.translate
        end

        assert_instance_of(Puppet::TransBucket, ret)

        ret.each do |obj|
            assert(obj.is_a?(Puppet::TransBucket) || obj.is_a?(Puppet::TransObject),
                "Got a non-transportable object %s" % obj.class)
        end

        rahness = ret.find { |c| c.type == "file" and c.name == "/tmp/rahness" }
        assert(rahness, "Could not find top-level file")
        assert_equal("root", rahness["owner"])

        bucket = ret.find { |c| c.class == Puppet::TransBucket and c.name == "/tmp/testing" }
        assert(bucket, "Could not find define bucket")

        testing = bucket.find { |c| c.type == "file" and c.name == "/tmp/testing" }
        assert(testing, "Could not find define file")
        assert_equal("root", testing["owner"])

    end
end

# $Id: scope.rb 1793 2006-10-16 22:01:40Z luke $
