#!/usr/bin/env ruby

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

require 'mocha'
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_lookupvar
        interp = mkinterp
        scope = mkscope :interp => interp

        # first do the plain lookups
        assert_equal("", scope.lookupvar("var"), "scope did not default to string")
        assert_equal("", scope.lookupvar("var", true), "scope ignored usestring setting")
        assert_equal(:undefined, scope.lookupvar("var", false), "scope ignored usestring setting when false")

        # Now set the var
        scope.setvar("var", "yep")
        assert_equal("yep", scope.lookupvar("var"), "did not retrieve value correctly")

        # Now test the parent lookups 
        subscope = mkscope :interp => interp
        subscope.parent = scope
        assert_equal("", subscope.lookupvar("nope"), "scope did not default to string with parent")
        assert_equal("", subscope.lookupvar("nope", true), "scope ignored usestring setting with parent")
        assert_equal(:undefined, subscope.lookupvar("nope", false), "scope ignored usestring setting when false with parent")

        assert_equal("yep", subscope.lookupvar("var"), "did not retrieve value correctly from parent")

        # Now override the value in the subscope
        subscope.setvar("var", "sub")
        assert_equal("sub", subscope.lookupvar("var"), "did not retrieve overridden value correctly")

        # Make sure we punt when the var is qualified.  Specify the usestring value, so we know it propagates.
        scope.expects(:lookup_qualified_var).with("one::two", false).returns(:punted)
        assert_equal(:punted, scope.lookupvar("one::two", false), "did not return the value of lookup_qualified_var")
    end

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

        scopes = {}
        classes = ["", "one", "one::two", "one::two::three"].each do |name|
            klass = interp.newclass(name)
            klass.evaluate(:scope => scope)
            scopes[name] = scope.class_scope(klass)
        end

        classes.each do |name|
            var = [name, "var"].join("::")
            scopes[name].expects(:lookupvar).with("var", false).returns(name)

            assert_equal(name, scope.send(:lookup_qualified_var, var, false), "did not get correct value from lookupvar")
        end
    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
        # Make and evaluate our classes so the qualified lookups work
        interp = mkinterp
        klass = interp.newclass("")
        scope = mkscope(:interp => interp)
        klass.evaluate(:scope => scope)

        klass = interp.newclass("one")
        klass.evaluate(:scope => scope)

        klass = interp.newclass("one::two")
        klass.evaluate(:scope => scope)


        scope = scope.class_scope("")
        assert_nothing_raised {
            scope.setvar("test","value")
        }

        scopes = {"" => scope}

        %w{one one::two one::two::three}.each do |name|
            klass = interp.newclass(name)
            klass.evaluate(:scope => scope)
            scopes[name] = scope.class_scope(klass)
            scopes[name].setvar("test", "value-%s" % name.sub(/.+::/,''))
        end

        assert_equal("value", scope.lookupvar("::test"), "did not look up qualified value correctly")
        tests = {
            "string ${test}" => "string value",
            "string ${one::two::three::test}" => "string value-three",
            "string $one::two::three::test" => "string value-three",
            "string ${one::two::test}" => "string value-two",
            "string $one::two::test" => "string value-two",
            "string ${one::test}" => "string value-one",
            "string $one::test" => "string value-one",
            "string ${::test}" => "string value",
            "string $::test" => "string value",
            "string ${test} ${test} ${test}" => "string value value value",
            "string $test ${test} $test" => "string value value value",
            "string \\$test" => "string $test",
            '\\$test string' => "$test string",
            '$test string' => "value string",
            'a testing $' => "a testing $",
            'a testing \$' => "a testing $",
            "an escaped \\\n carriage return" => "an escaped  carriage return",
            '\$' => "$",
            '\s' => "\s",
            '\t' => "\t",
            '\n' => "\n"
        }

        tests.each do |input, output|
            assert_nothing_raised("Failed to scan %s" % input.inspect) do
                assert_equal(output, scope.strinterp(input),
                    'did not interpret %s correctly' % input.inspect)
            end
        end

        logs = []
        Puppet::Util::Log.close
        Puppet::Util::Log.newdestination(logs)

        # #523
        %w{d f h l w z}.each do |l|
            string = "\\" + l
            assert_nothing_raised do
                assert_equal(string, scope.strinterp(string),
                    'did not interpret %s correctly' % string)
            end

            assert(logs.detect { |m| m.message =~ /Unrecognised escape/ },
                "Did not get warning about escape sequence with %s" % string)
            logs.clear
        end
    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")

        # Make sure we can retrieve the scope.
        assert_equal(scope, scope.class_scope(base),
            "class scope was not set correctly")

        # Now try it with a normal string
        Puppet[:trace] = false
        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.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

    def test_namespaces
        interp, scope, source = mkclassframing

        assert_equal([""], scope.namespaces,
            "Started out with incorrect namespaces")
        assert_nothing_raised { scope.add_namespace("fun::test") }
        assert_equal(["fun::test"], scope.namespaces,
            "Did not add namespace correctly")
        assert_nothing_raised { scope.add_namespace("yay::test") }
        assert_equal(["fun::test", "yay::test"], scope.namespaces,
            "Did not add extra namespace correctly")
    end

    def test_findclass_and_finddefine
        interp = mkinterp

        # Make sure our scope calls the interp findclass method with
        # the right namespaces
        scope = mkscope :interp => interp

        interp.metaclass.send(:attr_accessor, :last)

        methods = [:findclass, :finddefine]
        methods.each do |m|
            interp.meta_def(m) do |namespace, name|
                @checked ||= []
                @checked << [namespace, name]

                # Only return a value on the last call.
                if @last == namespace
                    ret = @checked.dup
                    @checked.clear
                    return ret
                else
                    return nil
                end
            end
        end

        test = proc do |should|
            interp.last = scope.namespaces[-1]
            methods.each do |method|
                result = scope.send(method, "testing")
                assert_equal(should, result,
                    "did not get correct value from %s with namespaces %s" %
                    [method, scope.namespaces.inspect])
            end
        end

        # Start with the empty namespace
        assert_nothing_raised { test.call([["", "testing"]]) }

        # Now add a namespace
        scope.add_namespace("a")
        assert_nothing_raised { test.call([["a", "testing"]]) }

        # And another
        scope.add_namespace("b")
        assert_nothing_raised { test.call([["a", "testing"], ["b", "testing"]]) }
    end
end

# $Id: scope.rb 2418 2007-04-26 19:09:24Z luke $


syntax highlighted by Code2HTML, v. 0.9.1