#!/usr/bin/env ruby

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

require 'puppettest'
require 'mocha'
require 'puppettest/fileparsing'
require 'puppet/type/cron'

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


    FIELDS = {
        :crontab => %w{command minute hour month monthday weekday}.collect { |o| o.intern },
        :freebsd_special => %w{special command}.collect { |o| o.intern },
        :environment => [:line],
        :blank => [:line],
        :comment => [:line],
    }

    # These are potentially multi-line records; there's no one-to-one map, but they model
    # a full cron job.  These tests assume individual record types will always be correctly
    # parsed, so all they 
    def sample_crons
        unless defined? @sample_crons
            @sample_crons = YAML.load(File.read(File.join(@crondir, "crontab_collections.yaml")))
        end
        @sample_crons
    end

    # These are simple lines that can appear in the files; there is a one to one
    # mapping between records and lines.  We have plenty of redundancy here because
    # we use these records to build up our complex, multi-line cron jobs below.
    def sample_records
        unless defined? @sample_records
            @sample_records = YAML.load(File.read(File.join(@crondir, "crontab_sample_records.yaml")))
        end
        @sample_records
    end

    def setup
        super
        @type = Puppet::Type.type(:cron)
        @provider = @type.provider(:crontab)
        @provider.initvars
        @crondir = datadir(File.join(%w{providers cron}))

        @oldfiletype = @provider.filetype
    end

    def teardown
        Puppet::Util::FileType.filetype(:ram).clear
        @provider.filetype = @oldfiletype
        @provider.clear
        super
    end

    # Make sure a cron job matches up.  Any non-passed fields are considered absent.
    def assert_cron_equal(msg, cron, options)
        assert_instance_of(@provider, cron, "not an instance of provider in %s" % msg)
        options.each do |param, value|
            assert_equal(value, cron.send(param), "%s was not equal in %s" % [param, msg])
        end
        %w{command environment minute hour month monthday weekday}.each do |var|
            unless options.include?(var.intern)
                assert_equal(:absent, cron.send(var), "%s was not parsed absent in %s" % [var, msg])
            end
        end
    end

    # Make sure a cron record matches.  This only works for crontab records.
    def assert_record_equal(msg, record, options)
        unless options.include?(:record_type)
            raise ArgumentError, "You must pass the required record type"
        end
        assert_instance_of(Hash, record, "not an instance of a hash in %s" % msg)
        options.each do |param, value|
            assert_equal(value, record[param], "%s was not equal in %s" % [param, msg])
        end
        FIELDS[record[:record_type]].each do |var|
            unless options.include?(var)
                assert_equal(:absent, record[var], "%s was not parsed absent in %s" % [var, msg])
            end
        end
    end

    def assert_header(file)
        header = []
        file.gsub! /^(# HEADER: .+$)\n/ do
            header << $1
            ''
        end
        assert_equal(4, header.length, "Did not get four header lines")
    end

    # This handles parsing every possible iteration of cron records.  Note that this is only
    # single-line stuff and doesn't include multi-line values (e.g., with names and/or envs).
    # Those have separate tests.
    def test_parse_line
        # First just do each sample record one by one
        sample_records.each do |name, options|
            result = nil
            assert_nothing_raised("Could not parse %s: '%s'" % [name, options[:text]]) do
                result = @provider.parse_line(options[:text])
            end
            assert_record_equal("record for %s" % name, result, options[:record])
        end

        # Then do them all at once.
        records = []
        text = ""
        sample_records.each do |name, options|
            records << options[:record]
            text += options[:text] + "\n"
        end

        result = nil
        assert_nothing_raised("Could not match all records in one file") do
            result = @provider.parse(text)
        end

        records.zip(result).each do |should, record|
            assert_record_equal("record for %s in full match" % should.inspect, record, should)
        end
    end

    # Here we test that each record generates to the correct text.
    def test_generate_line
        # First just do each sample record one by one
        sample_records.each do |name, options|
            result = nil
            assert_nothing_raised("Could not generate %s: '%s'" % [name, options[:record]]) do
                result = @provider.to_line(options[:record])
            end
            assert_equal(options[:text], result, "Did not generate correct text for %s" % name)
        end

        # Then do them all at once.
        records = []
        text = ""
        sample_records.each do |name, options|
            records << options[:record]
            text += options[:text] + "\n"
        end

        result = nil
        assert_nothing_raised("Could not match all records in one file") do
            result = @provider.to_file(records)
        end

        assert_header(result)

        assert_equal(text, result, "Did not generate correct full crontab")
    end

    # Test cronjobs that are made up from multiple records.
    def test_multi_line_cronjobs
        fulltext = ""
        all_records = []
        sample_crons.each do |name, record_names|
            records = record_names.collect do |record_name|
                unless record = sample_records[record_name]
                    raise "Could not find sample record %s" % record_name
                end
                record
            end

            text = records.collect { |r| r[:text] }.join("\n") + "\n"
            record_list = records.collect { |r| r[:record] }

            # Add it to our full collection
            all_records += record_list
            fulltext += text

            # First make sure we generate each one correctly
            result = nil
            assert_nothing_raised("Could not generate multi-line cronjob %s" % [name]) do
                result = @provider.to_file(record_list)
            end
            assert_header(result)
            assert_equal(text, result, "Did not generate correct text for multi-line cronjob %s" % name)

            # Now make sure we parse each one correctly
            assert_nothing_raised("Could not parse multi-line cronjob %s" % [name]) do
                result = @provider.parse(text)
            end
            record_list.zip(result).each do |should, record|
                assert_record_equal("multiline cronjob %s" % name, record, should)
            end
        end

        # Make sure we can generate it all correctly
        result = nil
        assert_nothing_raised("Could not generate all multi-line cronjobs") do
            result = @provider.to_file(all_records)
        end
        assert_header(result)
        assert_equal(fulltext, result, "Did not generate correct text for all multi-line cronjobs")

        # Now make sure we parse them all correctly
        assert_nothing_raised("Could not parse multi-line cronjobs") do
            result = @provider.parse(fulltext)
        end
        all_records.zip(result).each do |should, record|
            assert_record_equal("multiline cronjob %s", record, should)
        end
    end

    # Take our sample files, and make sure we can entirely parse them,
    # then that we can generate them again and we get the same data.
    def test_parse_and_generate_sample_files
        @provider.filetype = :ram
        crondir = datadir(File.join(%w{providers cron}))
        files = Dir.glob("%s/crontab.*" % crondir)

        setme
        @provider.default_target = @me
        target = @provider.target_object(@me)
        files.each do |file|
            str = args = nil
            assert_nothing_raised("could not load %s" % file) do
                str, args = YAML.load(File.read(file))
            end
            
            # Stupid old yaml
            args.each do |hash|
                hash.each do |param, value|
                    if param.is_a?(String) and param =~ /^:/
                        hash.delete(param)
                        param = param.sub(/^:/,'').intern
                        hash[param] = value
                    end

                    if value.is_a?(String) and value =~ /^:/
                        value = value.sub(/^:/,'').intern
                        hash[param] = value
                    end
                end
            end
            target.write(str)
            assert_nothing_raised("could not parse %s" % file) do
                @provider.prefetch
            end
            records = @provider.send(:instance_variable_get, "@records")

            args.zip(records) do |should, sis|
                # Make the values a bit more equal.
                should[:target] = @me
                should[:ensure] = :present
                #should[:environment] ||= []
                should[:on_disk] = true
                is = sis.dup
                sis.dup.each do |p,v|
                    is.delete(p) if v == :absent
                end
                assert_equal(should, is,
                    "Did not parse %s correctly" % file)
            end

            assert_nothing_raised("could not generate %s" % file) do
                @provider.flush_target(@me)
            end

            assert_equal(str, target.read, "%s changed" % file)
            @provider.clear
        end
    end

    # A simple test to see if we can load the cron from disk.
    def test_load
        setme()
        records = nil
        assert_nothing_raised {
            records = @provider.retrieve(@me)
        }
        assert_instance_of(Array, records, "did not get correct response")
    end

    # Test that a cron job turns out as expected, by creating one and generating
    # it directly
    def test_simple_to_cron
        # make the cron
        setme()

        name = "yaytest"
        args = {:name => name,
            :command => "date > /dev/null",
            :minute => "30",
            :user => @me,
            :record_type => :crontab
        }
        # generate the text
        str = nil
        assert_nothing_raised {
            str = @provider.to_line(args)
        }

        assert_equal("# Puppet Name: #{name}\n30 * * * * date > /dev/null", str,
            "Cron did not generate correctly")
    end
    
    # Test that comments are correctly retained
    def test_retain_comments
        str = "# this is a comment\n#and another comment\n"
        user = "fakeuser"
        records = nil
        target = @provider.filetype = :ram
        target = @provider.target_object(user)
        target.write(str)
        assert_nothing_raised {
            @provider.prefetch
        }

        assert_nothing_raised {
            newstr = @provider.flush_target(user)
            assert(target.read.include?(str), "Comments were lost")
        }
    end

    def test_simpleparsing
        @provider.filetype = :ram
        text = "5 1,2 * 1 0 /bin/echo funtest"

        records = nil
        assert_nothing_raised {
            records = @provider.parse(text)
        }

        should = {
            :minute => %w{5},
            :hour => %w{1 2},
            :monthday => :absent,
            :month => %w{1},
            :weekday => %w{0},
            :command => "/bin/echo funtest"
        }

        is = records.shift
        assert(is, "Did not get record")

        should.each do |p, v|
            assert_equal(v, is[p], "did not parse %s correctly" % p)
        end
    end

    # Make sure we can create a cron in an empty tab
    def test_mkcron_if_empty
        setme
        @provider.filetype = @oldfiletype

        records = @provider.retrieve(@me)

        target = @provider.target_object(@me)

        cleanup do
            if records.length == 0
                target.remove
            else
                target.write(@provider.to_file(records))
            end
        end

        # Now get rid of it
        assert_nothing_raised("Could not remove cron tab") do
            target.remove
        end

        @provider.flush :target => @me, :command => "/do/something",
            :record_type => :crontab
        created = @provider.retrieve(@me)
        assert(created.detect { |r| r[:command] == "/do/something" },
            "Did not create cron tab")
    end

    # Make sure we correctly bidirectionally parse things.
    def test_records_and_strings
        @provider.filetype = :ram
        setme

        target = @provider.target_object(@me)
        
        [
            "* * * * * /some/command",
            "0,30 * * * * /some/command",
            "0-30 * * * * /some/command",
            "# Puppet Name: name\n0-30 * * * * /some/command",
            "# Puppet Name: name\nVAR=VALUE\n0-30 * * * * /some/command",
            "# Puppet Name: name\nVAR=VALUE\nC=D\n0-30 * * * * /some/command",
            "0 * * * * /some/command"
        ].each do |str|
            @provider.initvars
            str += "\n"
            target.write(str)
            assert_equal(str, target.read,
                "Did not write correctly")
            assert_nothing_raised("Could not prefetch with %s" % str.inspect) do
                @provider.prefetch
            end
            assert_nothing_raised("Could not flush with %s" % str.inspect) do
                @provider.flush_target(@me)
            end

            assert_equal(str, target.read,
                "Changed in read/write")

            @provider.clear
        end
    end

    # Test that a specified cron job will be matched against an existing job
    # with no name, as long as all fields match
    def test_matchcron
        mecron = "0,30 * * * * date

        * * * * * funtest
        # a comment
        0,30 * * 1 * date
        "

        youcron = "0,30 * * * * date

        * * * * * yaytest
        # a comment
        0,30 * * 1 * fooness
        "
        setme
        @provider.filetype = :ram
        you = "you"

        # Write the same tab to multiple targets
        @provider.target_object(@me).write(mecron.gsub(/^\s+/, ''))
        @provider.target_object(you).write(youcron.gsub(/^\s+/, ''))

        # Now make some crons that should match
        matchers = [
            @type.create(
                :name => "yaycron",
                :minute => [0, 30],
                :command => "date",
                :user => @me
            ),
            @type.create(
                :name => "youtest",
                :command => "yaytest",
                :user => you
            )
        ]

        nonmatchers = [
            @type.create(
                :name => "footest",
                :minute => [0, 30],
                :hour => 1,
                :command => "fooness",
                :user => @me # wrong target
            ),
            @type.create(
                :name => "funtest2",
                :command => "funtest",
                :user => you # wrong target for this cron
            )
        ]

        # Create another cron so we prefetch two of them
        @type.create(:name => "testing", :minute => 30, :command => "whatever", :user => "you")

        assert_nothing_raised("Could not prefetch cron") do
            @provider.prefetch([matchers, nonmatchers].flatten.inject({}) { |crons, cron| crons[cron.name] = cron; crons })
        end

        matchers.each do |cron|
            assert_equal(:present, cron.provider.ensure, "Cron %s was not matched" % cron.name)
            if value = cron.value(:minute) and value == "*"
                value = :absent
            end
            assert_equal(value, cron.provider.minute, "Minutes were not retrieved, so cron was not matched")
            assert_equal(cron.value(:target), cron.provider.target, "Cron %s was matched from the wrong target" % cron.name)
        end

        nonmatchers.each do |cron|
            assert_equal(:absent, cron.provider.ensure, "Cron %s was incorrectly matched" % cron.name)
        end
    end

    def test_data
        setme
        @provider.filetype = :ram
        target = @provider.target_object(@me)
        fakedata("data/providers/cron/examples").each do |file|
            text = File.read(file)
            target.write(text)

            assert_nothing_raised("Could not parse %s" % file) do
                @provider.prefetch
            end
            # mark the provider modified
            @provider.modified(@me)

            # and zero the text
            target.write("")

            result = nil
            assert_nothing_raised("Could not generate %s" % file) do
                @provider.flush_target(@me)
            end

            # Ignore whitespace differences, since those don't affect function.
            modtext = text.gsub(/[ \t]+/, " ")
            modtarget = target.read.gsub(/[ \t]+/, " ")
            assert_equal(modtext, modtarget,
                "File was not rewritten the same")

            @provider.clear
        end
    end

    # Match freebsd's annoying @daily stuff.
    def test_match_freebsd_special
        @provider.filetype = :ram
        setme

        target = @provider.target_object(@me)
        
        [
            "@daily /some/command",
            "@daily /some/command more"
        ].each do |str|
            @provider.initvars
            str += "\n"
            target.write(str)
            assert_equal(str, target.read,
                "Did not write correctly")
            assert_nothing_raised("Could not prefetch with %s" % str.inspect) do
                @provider.prefetch
            end
            records = @provider.send(:instance_variable_get, "@records")
            records.each do |r|
                assert_equal(:freebsd_special, r[:record_type],
                    "Did not create lines as freebsd lines")
            end
            assert_nothing_raised("Could not flush with %s" % str.inspect) do
                @provider.flush_target(@me)
            end

            assert_equal(str, target.read,
                "Changed in read/write")

            @provider.clear
        end
    end

    def test_prefetch
        cron = @type.create :command => "/bin/echo yay", :name => "test", :hour => 4

        assert_nothing_raised("Could not prefetch cron") do
            cron.provider.class.prefetch("test" => cron)
        end
    end

    # Testing #669.
    def test_environment_settings
        @provider.filetype = :ram
        setme

        target = @provider.target_object(@me)

        # First with no env settings
        resource = @type.create :command => "/bin/echo yay", :name => "test", :hour => 4
        cron = resource.provider

        cron.ensure = :present
        cron.command = "/bin/echo yay"
        cron.hour = %w{4}
        cron.flush

        result = target.read
        assert_equal("# Puppet Name: test\n* 4 * * * /bin/echo yay\n", result, "Did not write cron out correctly")

        # Now set the env
        cron.environment = "TEST=foo"
        cron.flush

        result = target.read
        assert_equal("# Puppet Name: test\nTEST=foo\n* 4 * * * /bin/echo yay\n", result, "Did not write out environment setting")

        # Modify it
        cron.environment = ["TEST=foo", "BLAH=yay"]
        cron.flush

        result = target.read
        assert_equal("# Puppet Name: test\nTEST=foo\nBLAH=yay\n* 4 * * * /bin/echo yay\n", result, "Did not write out environment setting")

        # And remove it
        cron.environment = :absent
        cron.flush

        result = target.read
        assert_equal("# Puppet Name: test\n* 4 * * * /bin/echo yay\n", result, "Did not write out environment setting")
    end
end

# $Id: crontab.rb 2750 2007-08-06 17:59:37Z luke $


syntax highlighted by Code2HTML, v. 0.9.1