#!/usr/bin/env rspec

require 'spec_helper'

require 'puppet/util/instrumentation'
require 'puppet/util/instrumentation/instrumentable'

describe Puppet::Util::Instrumentation::Instrumentable::Probe do

  before(:each) do
    Puppet::Util::Instrumentation.stubs(:start)
    Puppet::Util::Instrumentation.stubs(:stop)

    class ProbeTest
      def mymethod(arg1, arg2, arg3)
        :it_worked
      end
    end
  end

  after(:each) do
    if ProbeTest.method_defined?(:instrumented_mymethod)
      ProbeTest.class_eval {
        remove_method(:mymethod)
        alias_method(:mymethod, :instrumented_mymethod)
      }
    end
    Puppet::Util::Instrumentation::Instrumentable.clear_probes
  end

  describe "when enabling a probe" do
    it "should raise an error if the probe is already enabled" do
      probe = Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest)
      probe.enable
      lambda { probe.enable }.should raise_error
    end

    it "should rename the original method name" do
      probe = Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest)
      probe.enable
      ProbeTest.new.should respond_to(:instrumented_mymethod)
    end

    it "should create a new method of the original name" do
      probe = Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest)
      probe.enable
      ProbeTest.new.should respond_to(:mymethod)
    end
  end

  describe "when disabling a probe" do
    it "should raise an error if the probe is already enabled" do
      probe = Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest)
      lambda { probe.disable }.should raise_error
    end

    it "should rename the original method name" do
      probe = Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest)
      probe.enable
      probe.disable

      Puppet::Util::Instrumentation.expects(:start).never
      Puppet::Util::Instrumentation.expects(:stop).never
      ProbeTest.new.mymethod(1,2,3).should == :it_worked
    end

    it "should remove the created method" do
      probe = Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest)
      probe.enable
      probe.disable
      ProbeTest.new.should_not respond_to(:instrumented_mymethod)
    end
  end

  describe "when a probe is called" do
    it "should call the original method" do
      probe = Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest)
      probe.enable
      test = ProbeTest.new
      test.expects(:instrumented_mymethod).with(1,2,3)
      test.mymethod(1,2,3)
    end

    it "should start the instrumentation" do
      Puppet::Util::Instrumentation.expects(:start)
      probe = Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest)
      probe.enable
      test = ProbeTest.new
      test.mymethod(1,2,3)
    end

    it "should stop the instrumentation" do
      Puppet::Util::Instrumentation.expects(:stop)
      probe = Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest)
      probe.enable
      test = ProbeTest.new
      test.mymethod(1,2,3)
    end

    describe "and the original method raises an exception" do
      it "should propagate the exception" do
        probe = Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest)
        probe.enable
        test = ProbeTest.new
        test.expects(:instrumented_mymethod).with(1,2,3).raises
        lambda { test.mymethod(1,2,3) }.should raise_error
      end

      it "should stop the instrumentation" do
        Puppet::Util::Instrumentation.expects(:stop)
        probe = Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest)
        probe.enable
        test = ProbeTest.new
        test.expects(:instrumented_mymethod).with(1,2,3).raises
        lambda { test.mymethod(1,2,3) }.should raise_error
      end
    end

    describe "with a static label" do
      it "should send the label to the instrumentation layer" do
        probe = Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest, :label => :mylabel)
        probe.enable
        test = ProbeTest.new
        Puppet::Util::Instrumentation.expects(:start).with { |label,data| label == :mylabel }.returns(42)
        Puppet::Util::Instrumentation.expects(:stop).with(:mylabel, 42, {})
        test.mymethod(1,2,3)
      end
    end

    describe "with a dynamic label" do
      it "should send the evaluated label to the instrumentation layer" do
        probe = Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest, :label => Proc.new { |parent,args| "dynamic#{args[0]}" } )
        probe.enable
        test = ProbeTest.new
        Puppet::Util::Instrumentation.expects(:start).with { |label,data| label == "dynamic1" }.returns(42)
        Puppet::Util::Instrumentation.expects(:stop).with("dynamic1",42,{})
        test.mymethod(1,2,3)
      end
    end

    describe "with static data" do
      it "should send the data to the instrumentation layer" do
        probe = Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest, :data => { :static_data => "nothing" })
        probe.enable
        test = ProbeTest.new
        Puppet::Util::Instrumentation.expects(:start).with { |label,data| data == { :static_data => "nothing" }}
        test.mymethod(1,2,3)
      end
    end

    describe "with dynamic data" do
      it "should send the evaluated label to the instrumentation layer" do
        probe = Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest, :data => Proc.new { |parent, args| { :key => args[0] }  } )
        probe.enable
        test = ProbeTest.new
        Puppet::Util::Instrumentation.expects(:start).with { |label,data| data == { :key => 1 } }
        Puppet::Util::Instrumentation.expects(:stop)
        test.mymethod(1,2,3)
      end
    end
  end
end

describe Puppet::Util::Instrumentation::Instrumentable do
  before(:each) do
    class ProbeTest2
      extend Puppet::Util::Instrumentation::Instrumentable
      probe :mymethod
      def mymethod(arg1,arg2,arg3)
      end
    end
  end

  after do
    Puppet::Util::Instrumentation::Instrumentable.clear_probes
  end

  it "should allow probe definition" do
    Puppet::Util::Instrumentation::Instrumentable.probe_names.should be_include("ProbeTest2.mymethod")
  end

  it "should be able to enable all probes" do
    Puppet::Util::Instrumentation::Instrumentable.enable_probes
    ProbeTest2.new.should respond_to(:instrumented_mymethod)
  end
end
