#!/usr/bin/env rspec

require 'spec_helper'

require 'puppet/util/instrumentation'

describe Puppet::Util::Instrumentation do

  Instrumentation = Puppet::Util::Instrumentation

  after(:each) do
    Instrumentation.clear
  end

  it "should instance-load instrumentation listeners" do
    Instrumentation.instance_loader(:listener).should be_instance_of(Puppet::Util::Autoload)
  end

  it "should have a method for registering instrumentation listeners" do
    Instrumentation.should respond_to(:new_listener)
  end

  it "should have a method for retrieving instrumentation listener by name" do
    Instrumentation.should respond_to(:listener)
  end

  describe "when registering listeners" do
    it "should evaluate the supplied block as code for a class" do
      Instrumentation.expects(:genclass).returns(Class.new { def notify(label, event, data) ; end })
      Instrumentation.new_listener(:testing, :label_pattern => :for_this_label, :event => :all) { }
    end

    it "should subscribe a new listener instance" do
      Instrumentation.expects(:genclass).returns(Class.new { def notify(label, event, data) ; end })
      Instrumentation.new_listener(:testing, :label_pattern => :for_this_label, :event => :all) { }
      Instrumentation.listeners.size.should == 1
      Instrumentation.listeners[0].pattern.should == "for_this_label"
    end

    it "should be possible to access listeners by name" do
      Instrumentation.expects(:genclass).returns(Class.new { def notify(label, event, data) ; end })
      Instrumentation.new_listener(:testing, :label_pattern => :for_this_label, :event => :all) { }
      Instrumentation["testing"].should_not be_nil
    end

    it "should be possible to store a new listener by name" do
      listener = stub 'listener'
      Instrumentation["testing"] = listener
      Instrumentation["testing"].should == listener
    end

    it "should fail if listener is already subscribed" do
      listener = stub 'listener', :notify => nil, :name => "mylistener"
      Instrumentation.subscribe(listener, :for_this_label, :all)
      expect { Instrumentation.subscribe(listener, :for_this_label, :all) }.to raise_error
    end

    it 'should call #unsubscribed' do
      listener = stub 'listener', :notify => nil, :name => "mylistener"

      listener.expects(:subscribed)

      Instrumentation.subscribe(listener, :for_this_label, :all)
    end
  end

  describe "when unsubscribing listener" do
    it "should remove it from the listeners" do
      listener = stub 'listener', :notify => nil, :name => "mylistener"
      Instrumentation.subscribe(listener, :for_this_label, :all)
      Instrumentation.unsubscribe(listener)
      Instrumentation.listeners.size.should == 0
    end

    it "should warn if the listener wasn't subscribed" do
      listener = stub 'listener', :notify => nil, :name => "mylistener"
      Puppet.expects(:warning)
      Instrumentation.unsubscribe(listener)
    end

    it 'should call #unsubscribed' do
      listener = stub 'listener', :notify => nil, :name => "mylistener"
      Instrumentation.subscribe(listener, :for_this_label, :all)

      listener.expects(:unsubscribed)

      Instrumentation.unsubscribe(listener)
    end
  end

  describe "when firing events" do
    it "should be able to find all listeners matching a label" do
      listener = stub 'listener', :notify => nil, :name => "mylistener"
      Instrumentation.subscribe(listener, :for_this_label, :all)
      Instrumentation.listeners[0].enabled = true

      count = 0
      Instrumentation.each_listener(:for_this_label) { |l| count += 1 }
      count.should == 1
    end

    it "should fire events to matching listeners" do
      listener = stub 'listener', :notify => nil, :name => "mylistener"
      Instrumentation.subscribe(listener, :for_this_label, :all)
      Instrumentation.listeners[0].enabled = true

      listener.expects(:notify).with(:for_this_label, :start, {})

      Instrumentation.publish(:for_this_label, :start, {})
    end

    it "should not fire events to non-matching listeners" do
      listener1 = stub 'listener1', :notify => nil, :name => "mylistener1"
      listener2 = stub 'listener2', :notify => nil, :name => "mylistener2"
      Instrumentation.subscribe(listener1, :for_this_label, :all)
      Instrumentation.listeners[0].enabled = true
      Instrumentation.subscribe(listener2, :for_this_other_label, :all)
      Instrumentation.listeners[1].enabled = true

      listener1.expects(:notify).never
      listener2.expects(:notify).with(:for_this_other_label, :start, {})

      Instrumentation.publish(:for_this_other_label, :start, {})
    end
  end

  describe "when instrumenting code" do
    before(:each) do
      Instrumentation.stubs(:publish)
    end
    describe "with a block" do
      it "should execute it" do
        executed = false
        Instrumentation.instrument(:event) do
          executed = true
        end
        executed.should be_true
      end

      it "should publish an event before execution" do
        Instrumentation.expects(:publish).with { |label,event,data| label == :event && event == :start }
        Instrumentation.instrument(:event) {}
      end

      it "should publish an event after execution" do
        Instrumentation.expects(:publish).with { |label,event,data| label == :event && event == :stop }
        Instrumentation.instrument(:event) {}
      end

      it "should publish the event even when block raised an exception" do
        Instrumentation.expects(:publish).with { |label,event,data| label == :event }
        lambda { Instrumentation.instrument(:event) { raise "not working" } }.should raise_error
      end

      it "should retain start end finish time of the event" do
        Instrumentation.expects(:publish).with { |label,event,data| data.include?(:started) and data.include?(:finished) }
        Instrumentation.instrument(:event) {}
      end
    end

    describe "without a block" do
      it "should raise an error if stop is called with no matching start" do
        lambda{ Instrumentation.stop(:event) }.should raise_error
      end

      it "should publish an event on stop" do
        Instrumentation.expects(:publish).with { |label,event,data| event == :start }
        Instrumentation.expects(:publish).with { |label,event,data| event == :stop and data.include?(:started) and data.include?(:finished) }
        data = {}
        Instrumentation.start(:event, data)
        Instrumentation.stop(:event, 1, data)
      end

      it "should return a different id per event" do
        data = {}
        Instrumentation.start(:event, data).should == 1
        Instrumentation.start(:event, data).should == 2
      end
    end
  end
end
