#!/usr/bin/env ruby

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

require 'puppettest'
require 'mocha'
require 'puppettest/fileparsing'
require 'puppet/util/filetype'
require 'puppet/provider/parsedfile'
require 'facter'

class TestParsedFile < Test::Unit::TestCase
	include PuppetTest
	include PuppetTest::FileParsing

    Puppet::Type.newtype(:testparsedfiletype) do
        ensurable
        newproperty(:one) do
            newvalue(:a)
            newvalue(:b)
        end
        newproperty(:two) do
            newvalue(:c)
            newvalue(:d)
        end

        newparam(:name) do
        end

        # The target should always be a property, not a parameter.
        newproperty(:target) do
            defaultto { @parent.class.defaultprovider.default_target }
        end
    end

    # A simple block to skip the complexity of a full transaction.
    def apply(model)
        [:one, :two, :ensure].each do |st|
            Puppet.info "Setting %s: %s => %s" %
                [model[:name], st, model.should(st)]
            model.provider.send(st.to_s + "=", model.should(st))
        end
    end

    def mkmodel(name, options = {})
        options[:one] ||= "a"
        options[:two] ||= "c"
        options[:name] ||= name

        model = @type.create(options)
    end

    def mkprovider(name = :parsed)
        @provider = @type.provide(name, :parent => Puppet::Provider::ParsedFile,
            :filetype => :ram, :default_target => "yayness") do
            record_line name, :fields => %w{name one two}
        end
    end

    def setup
        super
        @type = Puppet::Type.type(:testparsedfiletype)
    end

    def teardown
        if defined? @provider
            @type.unprovide(@provider.name)
            @provider = nil
        end
        super
    end

    def test_create_provider
        assert_nothing_raised do
            mkprovider
        end
    end

    def test_model_attributes
        prov = nil
        assert_nothing_raised do
            prov = mkprovider
        end

        [:one, :two, :name].each do |attr|
            assert(prov.method_defined?(attr), "Did not define %s" % attr)
        end

        # Now make sure they stay around
        fakemodel = fakemodel(:testparsedfiletype, "yay")

        file = prov.new(fakemodel)

        assert_nothing_raised do
            file.name = :yayness
        end

        # The provider converts to strings
        assert_equal(:yayness, file.name)
    end

    def test_filetype
        prov = mkprovider

        flat = Puppet::Util::FileType.filetype(:flat)
        ram = Puppet::Util::FileType.filetype(:ram)
        assert_nothing_raised do
            prov.filetype = :flat
        end

        assert_equal(flat, prov.filetype)

        assert_nothing_raised do
            prov.filetype = ram
        end
        assert_equal(ram, prov.filetype)
    end

    # Make sure we correctly create a new filetype object, but only when
    # necessary.
    def test_fileobject
        prov = mkprovider

        path = tempfile()
        obj = nil
        assert_nothing_raised do
            obj = prov.target_object(path)
        end

        # The default filetype is 'ram'
        assert_instance_of(Puppet::Util::FileType.filetype(:ram), obj)

        newobj = nil
        assert_nothing_raised do
            newobj = prov.target_object(path)
        end

        assert_equal(obj, newobj, "did not reuse file object")

        # now make sure clear does the right thing
        assert_nothing_raised do
            prov.clear
        end
        assert_nothing_raised do
            newobj = prov.target_object(path)
        end

        assert(obj != newobj, "did not reuse file object")
    end

    def test_retrieve
        prov = mkprovider

        prov.filetype = :ram

        # Override the parse method with our own
        prov.meta_def(:parse) do |text|
            return [text]
        end

        path = :yayness
        file = prov.target_object(path)
        text = "a test"
        file.write(text)

        ret = nil
        assert_nothing_raised do
            ret = prov.retrieve(path)
        end

        assert_equal([text], ret)

        # Now set the text to nil and make sure we get an empty array
        file.write(nil)
        assert_nothing_raised do
            ret = prov.retrieve(path)
        end

        assert_equal([], ret)

        # And the empty string should return an empty array
        file.write("")
        assert_nothing_raised do
            ret = prov.retrieve(path)
        end

        assert_equal([], ret)
    end

    # Verify that prefetch will parse the file, create any necessary instances,
    # and set the 'is' values appropriately.
    def test_prefetch
        prov = mkprovider

        prov.filetype = :ram
        prov.default_target = :default

        # Create a couple of demo files
        prov.target_object(:file1).write "bill b c"

        prov.target_object(:file2).write "jill b d"

        prov.target_object(:default).write "will b d"

        # Create some models for some of those demo files
        model = mkmodel "bill", :target => :file1
        default = mkmodel "will", :target => :default

        assert_nothing_raised do
            prov.prefetch
        end

        # Make sure we prefetched our models.
        assert_equal("b", model.provider.one)
        assert_equal("b", default.provider.one)
        assert_equal("d", default.provider.two)

        # Now list all of them and make sure we get everything back
        hashes = nil
        assert_nothing_raised do
            hashes = prov.list
        end

        names = nil
        assert_nothing_raised do
            names = prov.list_by_name
        end

        %w{bill jill will}.each do |name|
            assert(hashes.find { |r| r[:name] == name},
                "Did not return %s in list" % name)
            assert(names.include?(name),
                "Did not return %s in list_by_name" % name)
        end
    end

    # Make sure we can correctly prefetch on a target.
    def test_prefetch_target
        prov = mkprovider

        prov.filetype = :ram
        target = :yayness
        prov.target_object(target).write "yay b d"

        model = mkmodel "yay", :target => :yayness

        assert_nothing_raised do
            prov.prefetch_target(:yayness)
        end

        # Now make sure we correctly got the hash
        mprov = model.provider
        assert_equal("b", mprov.one)
        assert_equal("d", mprov.two)
    end

    def test_prefetch_match
        prov = mkprovider

        prov.meta_def(:match) do |record|
            # Look for matches on :one
            self.model.find do |m|
                m.should(:one).to_s == record[:one].to_s
            end
        end

        prov.filetype = :ram
        target = :yayness
        prov.target_object(target).write "foo b d"

        model = mkmodel "yay", :target => :yayness, :one => "b"

        assert_nothing_raised do
            prov.prefetch_target(:yayness)
        end

        # Now make sure we correctly got the hash
        mprov = model.provider
        assert_equal("yay", model[:name])
        assert_equal("b", mprov.one)
        assert_equal("d", mprov.two)
    end

    # We need to test that we're retrieving files from all three locations:
    # from any existing target_objects, from the default file location, and
    # from any existing model instances.
    def test_targets
        prov = mkprovider

        files = {}

        # Set the default target
        default = tempfile()
        files[:default] = default
        prov.default_target = default

        # Create a file object
        inmem = tempfile()
        files[:inmemory] = inmem
        prov.target_object(inmem).write("inmem yay ness")

        # Lastly, create a model with separate is and should values
        mtarget = tempfile()
        istarget = tempfile()
        files[:models] = mtarget
        files[:ismodels] = istarget
        model = mkmodel "yay", :target => mtarget
        model.is = [:target, istarget]

        assert(model.should(:target), "Did not get a value for target")
        assert(model.is(:target), "Did not get a value for target")

        list = nil
        assert_nothing_raised do
            list = prov.targets
        end

        files.each do |name, file|
            assert(list.include?(file), "Provider did not find %s file" % name)
        end
    end

    # Make sure that flushing behaves correctly.  This is what actually writes
    # the data out to disk.
    def test_flush
        prov = mkprovider

        prov.filetype = :ram
        prov.default_target = :yayness

        # Create some models.
        one = mkmodel "one", :one => "a", :two => "c", :target => :yayness
        two = mkmodel "two", :one => "b", :two => "d", :target => :yayness

        # Write out a file with different data.
        prov.target_object(:yayness).write "one b d\ntwo a c"

        prov.prefetch

        # Apply and flush the first model.
        assert_nothing_raised do
            apply(one)
        end
        assert_nothing_raised { one.flush }

        # Make sure it changed our file
        assert_equal(:a, one.provider.one)
        assert_equal(:c, one.provider.two)

        # And make sure it's right on disk
        assert(prov.target_object(:yayness).read.include?("one a c"),
            "Did not write out correct data")

        # Make sure the second model has not been modified
        assert_equal("a", two.provider.one, "Two was flushed early")
        assert_equal("c", two.provider.two, "Two was flushed early")

        # And on disk
        assert(prov.target_object(:yayness).read.include?("two a c"),
            "Wrote out other model")

        # Now fetch the data again and make sure we're still right
        assert_nothing_raised { prov.prefetch }
        assert_equal("a", one.provider.one)
        assert_equal("a", two.provider.one)

        # Now flush the second model and make sure it goes well
        assert_nothing_raised { apply(two) }
        assert_nothing_raised { two.flush }

        assert_equal(:b, two.provider.one)
    end

    def test_creating_file
        prov = mkprovider
        prov.clear

        prov.default_target = :basic

        model = mkmodel "yay", :target => :basic, :one => "a", :two => "c"

        assert_equal(:present, model.should(:ensure))

        apply(model)

        assert_nothing_raised do
            model.flush
        end

        assert(prov.target_object(:basic).read.include?("yay a c"),
            "Did not create file")

        # Make a change
        model.provider.one = "b"

        # Flush it
        assert_nothing_raised do
            model.flush
        end

        # And make sure our model doesn't appear twice in the file.
        assert_equal("yay b c\n",
            prov.target_object(:basic).read)
    end

    # Make sure a record can switch targets.
    def test_switching_targets
        prov = mkprovider

        prov.filetype = :ram
        prov.default_target = :first

        # Make three models, one for each target and one to switch
        first = mkmodel "first", :target => :first
        second = mkmodel "second", :target => :second
        mover = mkmodel "mover", :target => :first

        [first, second, mover].each do |m|
            assert_nothing_raised("Could not apply %s" % m[:name]) do
                apply(m)
            end
        end

        # Flush.
        [first, second, mover].each do |m|
            assert_nothing_raised do
                m.flush
            end
        end

        check = proc do |target, name|
            assert(prov.target_object(target).read.include?("%s a c" % name),
                "Did not sync %s" % name)
        end
        # Make sure the data is there
        check.call(:first, :first)
        check.call(:second, :second)
        check.call(:first, :mover)

        # Now change the target for the mover
        mover[:target] = :second

        # Apply it
        assert_nothing_raised do
            apply(mover)
        end

        # Flush
        assert_nothing_raised do
            mover.flush
        end

        # Make sure the data is there
        check.call(:first, :first)
        check.call(:second, :second)
        check.call(:second, :mover)

        # And make sure the mover is no longer in the first file
        assert(prov.target_object(:first) !~ /mover/,
            "Mover was not removed from first file")
    end

    # Make sure that 'ensure' correctly calls 'sync' on all properties.
    def test_ensure
        prov = mkprovider

        prov.filetype = :ram
        prov.default_target = :first

        # Make two models, one that starts on disk and one that doesn't
        ondisk = mkmodel "ondisk", :target => :first
        notdisk = mkmodel "notdisk", :target => :first

        prov.target_object(:first).write "ondisk a c\n"
        prov.prefetch

        assert_equal(:present, notdisk.should(:ensure),
            "Did not get default ensure value")

        # Try creating the object
        assert_nothing_raised { notdisk.provider.create() }

        # Now make sure all of the data is copied over correctly.
        notdisk.class.validproperties.each do |property|
            assert_equal(notdisk.should(property), notdisk.provider.property_hash[property],
                "%s was not copied over during creation" % property)
        end

        # Flush it to disk and make sure it got copied down
        assert_nothing_raised do
            notdisk.flush
        end

        assert(prov.target_object(:first).read =~ /^notdisk/,
            "Did not write out object to disk")
        assert(prov.target_object(:first).read =~ /^ondisk/,
            "Lost object on disk")

        # Make sure our on-disk model behaves appropriately.
        assert_equal(:present, ondisk.provider.ensure)

        # Now destroy the object
        assert_nothing_raised { notdisk.provider.destroy() }

        assert_nothing_raised { notdisk.flush }

        # And make sure it's no longer present
        assert(prov.target_object(:first).read !~ /^notdisk/,
            "Did not remove thing from disk")
        assert(prov.target_object(:first).read =~ /^ondisk/,
            "Lost object on disk")
        assert_equal(:present, ondisk.provider.ensure)
    end

    def test_absent_fields
        prov = @type.provide(:record, :parent => Puppet::Provider::ParsedFile) do
            record_line :record, :fields => %w{name one two},
                :separator => "\s"
        end
        cleanup { @type.unprovide(:record) }

        line = prov.parse_line("a  d")

        assert_equal("a", line[:name], "field name was not set")
        assert_equal(:absent, line[:one], "field one was not set to absent")

        # Now use a different provider with a non-blank "absent"
        prov = @type.provide(:cronstyle, :parent => Puppet::Provider::ParsedFile) do
            record_line :cronstyle, :fields => %w{name one two},
                :separator => "\s", :absent => "*"
        end
        cleanup { @type.unprovide(:cronstyle) }
        line = prov.parse_line("a * d")

        assert_equal("a", line[:name], "field name was not set")
        assert_equal(:absent, line[:one], "field one was not set to absent")
    end

    # This test is because in x2puppet I was having problems where multiple
    # retrievals somehow destroyed the 'is' values.
    def test_value_retrieval
        prov = mkprovider
        prov.default_target = :yayness

        prov.target_object(:yayness).write "bill a c\njill b d"

        list = @type.list

        bill = list.find { |r| r[:name] == "bill" }
        jill = list.find { |r| r[:name] == "jill" }
        assert(bill, "Could not find bill")
        assert(jill, "Could not find jill")

        prov = bill.provider

        4.times do |i|
            assert(prov.one, "Did not get a value for 'one' on try %s" % (i + 1))
        end

        # First make sure we can retrieve values multiple times from the
        # provider
        assert(bill.is(:one), "Bill does not have a value for 'one'")
        assert(bill.is(:one), "Bill does not have a value for 'one' on second try")
        assert_nothing_raised do
            bill.retrieve
        end
        assert(bill.is(:one), "bill's value for 'one' disappeared")
    end

    # Make sure that creating a new model finds existing records in memory
    def test_initialize_finds_records
        prov = mkprovider
        prov.default_target = :yayness

        prov.target_object(:yayness).write "bill a c\njill b d"

        prov.prefetch

        # Now make a model
        bill = nil
        assert_nothing_raised do
            bill = @type.create :name => "bill"
        end

        assert_equal("a", bill.provider.one,
            "Record was not found in memory")
    end

    # Make sure invalid fields always show up as insync
    def test_invalid_fields
        prov = @type.provide(:test, :parent => Puppet::Provider::ParsedFile,
            :filetype => :ram, :default_target => :yayness) do
            record_line :test, :fields => %w{name two}
        end
        cleanup do @type.unprovide(:test) end

        bill = nil
        assert_nothing_raised do
            bill = @type.create :name => "bill",
                :one => "a", :two => "c"
        end

        assert_apply(bill)

        prov.prefetch
        assert_nothing_raised do
            bill.retrieve
        end

        assert(bill.insync?,
            "An invalid field marked the record out of sync")
    end

    # Make sure we call the prefetch hook at the right place.
    def test_prefetch_hook
        prov = @type.provide(:test, :parent => Puppet::Provider::ParsedFile,
            :filetype => :ram, :default_target => :yayness) do

            def self.prefetch_hook(records)
                records
            end

            record_line :test, :fields => %w{name two}
        end
        cleanup do @type.unprovide(:test) end

        target = "target"

        records = [{:target => "nope"}]
        targeted = {:target => "target"}
        prov.send(:instance_variable_set, "@records", records)
        prov.expects(:retrieve).with(target).returns([targeted])
        prov.expects(:target_records).with(target).returns([targeted])

        prov.expects(:prefetch_hook).with([targeted]).returns([targeted])

        prov.prefetch_target(target)
    end

    # #529
    def test_keep_content_with_target
        mkprovider
        @provider.filetype = :flat
        dpath = tempfile
        opath = tempfile
        @provider.default_target = dpath

        dtarget = @provider.target_object(dpath)
        otarget = @provider.target_object(opath)

        dtarget.write("dname a c\n")
        otarget.write("oname b d\n")

        # Now make a resource that targets elsewhat.
        res = @type.create(:name => "test", :one => "a", :two => "c",
            :target => opath)

        assert(res.property(:target), "Target is a parameter, not a property")

        assert_apply(res)

        assert_equal("oname b d\ntest a c\n", otarget.read,
            "did not get correct results in specified target")
    end
end

# $Id: parsedfile.rb 2290 2007-03-18 18:00:26Z luke $



syntax highlighted by Code2HTML, v. 0.9.1