#!/usr/bin/env ruby

require File.dirname(__FILE__) + '/../../spec_helper'

require 'facter/util/resolution'

describe Facter::Util::Resolution do
    it "should require a name" do
        lambda { Facter::Util::Resolution.new }.should raise_error(ArgumentError)
    end

    it "should have a name" do
        Facter::Util::Resolution.new("yay").name.should == "yay"
    end

    it "should have a method for setting the code" do
        Facter::Util::Resolution.new("yay").should respond_to(:setcode)
    end

    it "should support a timeout value" do
        Facter::Util::Resolution.new("yay").should respond_to(:timeout=)
    end

    it "should default to a timeout of 0 seconds" do
        Facter::Util::Resolution.new("yay").limit.should == 0
    end

    it "should provide a 'limit' method that returns the timeout" do
        res = Facter::Util::Resolution.new("yay")
        res.timeout = "testing"
        res.limit.should == "testing"
    end

    describe "when setting the code" do
        before do
            @resolve = Facter::Util::Resolution.new("yay")
        end

        it "should default to /bin/sh as the interpreter if a string is provided" do
            @resolve.setcode "foo"
            @resolve.interpreter.should == "/bin/sh"
        end

        it "should set the code to any provided string" do
            @resolve.setcode "foo"
            @resolve.code.should == "foo"
        end

        it "should set the code to any provided block" do
            block = lambda { }
            @resolve.setcode(&block)
            @resolve.code.should equal(block)
        end

        it "should prefer the string over a block" do
            @resolve.setcode("foo") { }
            @resolve.code.should == "foo"
        end

        it "should fail if neither a string nor block has been provided" do
            lambda { @resolve.setcode }.should raise_error(ArgumentError)
        end
    end

    it "should be able to return a value" do
        Facter::Util::Resolution.new("yay").should respond_to(:value)
    end

    describe "when returning the value" do
        before do
            @resolve = Facter::Util::Resolution.new("yay")
        end

        describe "and the code is a string" do
            it "should return the result of executing the code with the interpreter" do
                @resolve.setcode "/bin/foo"
                Facter::Util::Resolution.expects(:exec).with("/bin/foo", "/bin/sh").returns "yup"

                @resolve.value.should == "yup"
            end

            it "should return nil if the value is an empty string" do
                @resolve.setcode "/bin/foo"
                Facter::Util::Resolution.stubs(:exec).returns ""
                @resolve.value.should be_nil
            end
        end

        describe "and the code is a block" do
            it "should return the value returned by the block" do
                @resolve.setcode { "yayness" }
                @resolve.value.should == "yayness"
            end

            it "should return nil if the value is an empty string" do
                @resolve.setcode { "" }
                @resolve.value.should be_nil
            end

            it "should use its limit method to determine the timeout, to avoid conflict when a 'timeout' method exists for some other reason" do
                @resolve.expects(:timeout).never
                @resolve.expects(:limit).returns "foo"
                Timeout.expects(:timeout).with("foo")

                @resolve.value
            end

            it "should timeout after the provided timeout" do
                @resolve.expects(:warn)
                @resolve.timeout = 0.1
                @resolve.setcode { sleep 2; raise "This is a test" }

                @resolve.value.should be_nil
            end

            it "should waitall to avoid zombies if the timeout is exceeded" do
                @resolve.stubs(:warn)
                @resolve.timeout = 0.1
                @resolve.setcode { sleep 2; raise "This is a test" }

                Thread.expects(:new).yields
                Process.expects(:waitall)

                @resolve.value
            end
        end
    end

    it "should return its value when converted to a string" do
        @resolve = Facter::Util::Resolution.new("yay")
        @resolve.expects(:value).returns "myval"
        @resolve.to_s.should == "myval"
    end

    it "should allow the adding of confines" do
        Facter::Util::Resolution.new("yay").should respond_to(:confine)
    end

    it "should provide a method for returning the number of confines" do
        @resolve = Facter::Util::Resolution.new("yay")
        @resolve.confine "one" => "foo", "two" => "fee"
        @resolve.length.should == 2
    end

    it "should return 0 confines when no confines have been added" do
        Facter::Util::Resolution.new("yay").length.should == 0
    end

    it "should have a method for determining if it is suitable" do
        Facter::Util::Resolution.new("yay").should respond_to(:suitable?)
    end

    describe "when adding confines" do
        before do
            @resolve = Facter::Util::Resolution.new("yay")
        end

        it "should accept a hash of fact names and values" do
            lambda { @resolve.confine :one => "two" }.should_not raise_error
        end

        it "should create a Util::Confine instance for every argument in the provided hash" do
            Facter::Util::Confine.expects(:new).with("one", "foo")
            Facter::Util::Confine.expects(:new).with("two", "fee")

            @resolve.confine "one" => "foo", "two" => "fee"
        end

    end

    describe "when determining suitability" do
        before do
            @resolve = Facter::Util::Resolution.new("yay")
        end

        it "should always be suitable if no confines have been added" do
            @resolve.should be_suitable
        end

        it "should be unsuitable if any provided confines return false" do
            confine1 = mock 'confine1', :true? => true
            confine2 = mock 'confine2', :true? => false
            Facter::Util::Confine.expects(:new).times(2).returns(confine1).then.returns(confine2)
            @resolve.confine :one => :two, :three => :four

            @resolve.should_not be_suitable
        end

        it "should be suitable if all provided confines return true" do
            confine1 = mock 'confine1', :true? => true
            confine2 = mock 'confine2', :true? => true
            Facter::Util::Confine.expects(:new).times(2).returns(confine1).then.returns(confine2)
            @resolve.confine :one => :two, :three => :four

            @resolve.should be_suitable
        end
    end

    it "should have a class method for executing code" do
        Facter::Util::Resolution.should respond_to(:exec)
    end

    # It's not possible, AFAICT, to mock %x{}, so I can't really test this bit.
    describe "when executing code" do
        it "should fail if any interpreter other than /bin/sh is requested" do
            lambda { Facter::Util::Resolution.exec("/something", "/bin/perl") }.should raise_error(ArgumentError)
        end
    end
end