# # Copyright (c) 2009-2012 RightScale Inc # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper')) describe RightSupport::Stats::Activity do include FlexMock::ArgumentTypes before(:all) do @original_recent_size = RightSupport::Stats::Activity::RECENT_SIZE RightSupport::Stats::Activity.instance_eval { remove_const(:RECENT_SIZE) } RightSupport::Stats::Activity.const_set(:RECENT_SIZE, 10) end after(:all) do RightSupport::Stats::Activity.instance_eval { remove_const(:RECENT_SIZE) } RightSupport::Stats::Activity.const_set(:RECENT_SIZE, @original_recent_size) end before(:each) do @now = 1000000 flexmock(Time).should_receive(:now).and_return(@now).by_default @stats = RightSupport::Stats::Activity.new end context :initialize do it "initializes stats data" do @stats.instance_variable_get(:@interval).should == 0.0 @stats.instance_variable_get(:@last_start_time).should == @now @stats.instance_variable_get(:@avg_duration).should be_nil @stats.instance_variable_get(:@total).should == 0 @stats.instance_variable_get(:@count_per_type).should == {} end end context :update do it "updates count and interval information" do flexmock(Time).should_receive(:now).and_return(1000010) @stats.update @stats.instance_variable_get(:@interval).should == 1.0 @stats.instance_variable_get(:@last_start_time).should == @now + 10 @stats.instance_variable_get(:@avg_duration).should be_nil @stats.instance_variable_get(:@total).should == 1 @stats.instance_variable_get(:@count_per_type).should == {} end it "updates counts per type when type provided" do flexmock(Time).should_receive(:now).and_return(1000010) @stats.update("test") @stats.instance_variable_get(:@interval).should == 1.0 @stats.instance_variable_get(:@last_start_time).should == @now + 10 @stats.instance_variable_get(:@avg_duration).should be_nil @stats.instance_variable_get(:@total).should == 1 @stats.instance_variable_get(:@count_per_type).should == {"test" => 1} end it "limits length of type string when submitting update" do flexmock(Time).should_receive(:now).and_return(1000010) @stats.update("test 12345678901234567890123456789012345678901234567890123456789") @stats.instance_variable_get(:@total).should == 1 @stats.instance_variable_get(:@count_per_type).should == {"test 1234567890123456789012345678901234567890123456789012..." => 1} end it "doesn't convert symbol or boolean to string when submitting update" do flexmock(Time).should_receive(:now).and_return(1000010) @stats.update(:test) @stats.update(true) @stats.update(false) @stats.instance_variable_get(:@total).should == 3 @stats.instance_variable_get(:@count_per_type).should == {:test => 1, true => 1, false => 1} end it "converts arbitrary type value to limited-length string when submitting update" do flexmock(Time).should_receive(:now).and_return(1000010) @stats.update({1 => 11, 2 => 22}) @stats.update({1 => 11, 2 => 22, 3 => 12345678901234567890123456789012345678901234567890123456789}) @stats.instance_variable_get(:@total).should == 2 @stats.instance_variable_get(:@count_per_type).should == {"{1=>11, 2=>22}" => 1, "{1=>11, 2=>22, 3=>123456789012345678901234567890123456789..." => 1} end it "doesn't measure rate if disabled" do @stats = RightSupport::Stats::Activity.new(false) flexmock(Time).should_receive(:now).and_return(1000010) @stats.update @stats.instance_variable_get(:@interval).should == 0.0 @stats.instance_variable_get(:@last_start_time).should == @now + 10 @stats.instance_variable_get(:@avg_duration).should be_nil @stats.instance_variable_get(:@total).should == 1 @stats.instance_variable_get(:@count_per_type).should == {} end it "tracks the id associated with the update" do @stats.update("test", "id") @stats.instance_variable_get(:@last_id).should == "id" end end context :finish do it "updates duration when finish using internal start time by default" do flexmock(Time).should_receive(:now).and_return(1000010) @stats.avg_duration.should be_nil @stats.finish @stats.instance_variable_get(:@interval).should == 0.0 @stats.instance_variable_get(:@last_start_time).should == @now @stats.instance_variable_get(:@avg_duration).should == 1.0 @stats.instance_variable_get(:@total).should == 0 @stats.instance_variable_get(:@count_per_type).should == {} end it "updates duration when finish using specified start time" do flexmock(Time).should_receive(:now).and_return(1000030) @stats.avg_duration.should be_nil @stats.finish(1000010) @stats.instance_variable_get(:@interval).should == 0.0 @stats.instance_variable_get(:@last_start_time).should == @now @stats.instance_variable_get(:@avg_duration).should == 2.0 @stats.instance_variable_get(:@total).should == 0 @stats.instance_variable_get(:@count_per_type).should == {} end end context :measure do it "updates and finishes the activity" do flexmock(Time).should_receive(:now).and_return(1000010, 1000020) @stats.measure("test", "id") { } @stats.instance_variable_get(:@interval).should == 1.0 @stats.instance_variable_get(:@last_start_time).should == @now + 10 @stats.instance_variable_get(:@total).should == 1 @stats.instance_variable_get(:@count_per_type).should == {"test" => 1} @stats.instance_variable_get(:@last_id).should == 0 @stats.instance_variable_get(:@avg_duration).should == 1.0 end it "yields to the block" do called = 0 flexmock(Time).should_receive(:now).and_return(1000010, 1000030) @stats.measure("test", "id") { called += 1} called.should == 1 end it "returns the result of the yield" do @stats.measure("test", "id") { 99 }.should == 99 end it "raises exception if block is missing" do lambda { @stats.measure("test", "id") }.should raise_error(ArgumentError, "block missing") end end context :avg_rate do it "converts interval to rate" do flexmock(Time).should_receive(:now).and_return(1000020, 1000042) @stats.avg_rate.should be_nil @stats.update @stats.instance_variable_get(:@interval).should == 2.0 @stats.avg_rate.should == 0.25 end it "adjusts rate based on interval since last event" do flexmock(Time).should_receive(:now).and_return(1000020, 1000042, 1000086) @stats.avg_rate.should be_nil @stats.update @stats.update @stats.instance_variable_get(:@interval).should == 4.0 @stats.avg_rate.should == 0.125 end it "does not compute average unless measuring rate is enabled" do @stats = RightSupport::Stats::Activity.new(false) flexmock(Time).should_receive(:now).and_return(1000010) @stats.update @stats.avg_rate.should be_nil end it "does not compute average if there have been no events" do @stats.avg_rate.should be_nil end end context :last do it "reports number of seconds since last update or nil if no updates" do flexmock(Time).should_receive(:now).and_return(1000010) @stats.last.should be_nil @stats.update @stats.last.should == {"elapsed" => 0} end it "reports number of seconds since last update and last type" do @stats.update("test") flexmock(Time).should_receive(:now).and_return(1000010) @stats.last.should == {"elapsed" => 10, "type" => "test"} end it "reports whether last activity is still active" do @stats.update("test", "token") flexmock(Time).should_receive(:now).and_return(1000010) @stats.last.should == {"elapsed" => 10, "type" => "test", "active" => true} @stats.finish(@now - 10, "token") @stats.last.should == {"elapsed" => 10, "type" => "test", "active" => false} @stats.instance_variable_get(:@avg_duration).should == 2.0 end end context :percentage do it "converts count per type to percentages" do flexmock(Time).should_receive(:now).and_return(1000010) @stats.update("foo") @stats.instance_variable_get(:@total).should == 1 @stats.instance_variable_get(:@count_per_type).should == {"foo" => 1} @stats.percentage.should == {"total" => 1, "percent" => {"foo" => 100.0}} @stats.update("bar") @stats.instance_variable_get(:@total).should == 2 @stats.instance_variable_get(:@count_per_type).should == {"foo" => 1, "bar" => 1} @stats.percentage.should == {"total" => 2, "percent" => {"foo" => 50.0, "bar" => 50.0}} @stats.update("foo") @stats.update("foo") @stats.instance_variable_get(:@total).should == 4 @stats.instance_variable_get(:@count_per_type).should == {"foo" => 3, "bar" => 1} @stats.percentage.should == {"total" => 4, "percent" => {"foo" => 75.0, "bar" => 25.0}} end end context :all do it "returns all activity aspects that were measured except duration" do flexmock(Time).should_receive(:now).and_return(1000020, 1000042) @stats.update @stats.all.should == {"last" => {"elapsed" => 22}, "total" => 1, "rate" => 0.25} end it "excludes rate if it is not being measured" do @stats = RightSupport::Stats::Activity.new(false) flexmock(Time).should_receive(:now).and_return(1000010) @stats.update @stats.all.should == {"last" => {"elapsed" => 0}, "total" => 1} end it "includes percentage breakdown when update recorded per type" do @stats = RightSupport::Stats::Activity.new(false) flexmock(Time).should_receive(:now).and_return(1000010, 1000010, 1000020, 1000020, 1000030, 1000030, 1000040) @stats.update("foo") @stats.all.should == {"last" => {"elapsed" => 0, "type" => "foo"}, "total" => 1, "percent" => {"foo" => 100.0}} @stats.update("bar") @stats.all.should == {"last" => {"elapsed" => 0, "type" => "bar"}, "total" => 2, "percent" => {"foo" => 50.0, "bar" => 50.0}} @stats.update("bar") @stats.update("foo") @stats.all.should == {"last" => {"elapsed" => 10, "type" => "foo"}, "total" => 4, "percent" => {"foo" => 50.0, "bar" => 50.0}} end it "returns nil if there was no activity" do @stats.all.should be_nil end end context :average do it "weights average toward past activity" do @stats.send(:average, 0.0, 10.0).should == 1.0 @stats.send(:average, 1.0, 10.0).should == 1.9 @stats.send(:average, 1.9, 10.0).should == 2.71 @stats.send(:average, 2.71, 10.0).should == 3.439 @stats.send(:average, 3.439, 10.0).should == 4.0951 @stats.send(:average, 4.0951, 10.0).should == 4.68559 @stats.send(:average, 4.68559, 10.0).should == 5.217031 end end describe "class methods" do context :all do it "aggregates stats from multiple all calls" do stats = [{"last" => {"elapsed" => 10, "type" => "foo"}, "total" => 1, "percent" => {"foo" => 100.0}}, {"last" => {"elapsed" => 20, "type" => "bar"}, "total" => 4, "percent" => {"bar" => 100.0}}] RightSupport::Stats::Activity.all(stats).should == {"last" => {"elapsed" => 10, "type" => "foo"}, "total" => 5, "percent" => {"foo" => 20.0, "bar" => 80.0}} end it "includes rate if provided" do stats = [{"last" => {"elapsed" => 10, "type" => "foo"}, "total" => 1, "percent" => {"foo" => 100.0}, "rate" => 0.5}, {"last" => {"elapsed" => 20, "type" => "bar"}, "total" => 4, "percent" => {"bar" => 100.0}, "rate" => 0.1}] RightSupport::Stats::Activity.all(stats).should == {"last" => {"elapsed" => 10, "type" => "foo"}, "total" => 5, "percent" => {"foo" => 20.0, "bar" => 80.0}, "rate" => 0.18} end it "handles nil stat" do stats = [{"last" => {"elapsed" => 10, "type" => "foo"}, "total" => 1, "percent" => {"foo" => 100.0}}, nil, {"last" => {"elapsed" => 20, "type" => "bar"}, "total" => 4, "percent" => {"bar" => 100.0}}] RightSupport::Stats::Activity.all(stats).should == {"last" => {"elapsed" => 10, "type" => "foo"}, "total" => 5, "percent" => {"foo" => 20.0, "bar" => 80.0}} end it "returns nil if there are no stats" do RightSupport::Stats::Activity.all([]).should be_nil end end context :percentage do it "aggregates multiple percentage stats" do stats = [{"total" => 1, "percent" => {"foo" => 100.0}}, {"total" => 5, "percent" => {"bar" => 100.0}}, {"total" => 4, "percent" => {"foo" => 75.0, "bar" => 25.0}}] RightSupport::Stats::Activity.percentage(stats, 10).should == {"total" => 10, "percent" => {"foo" => 40.0, "bar" => 60.0}} end it "handles nil stat" do stats = [{"total" => 1, "percent" => {"foo" => 100.0}}, nil, {"total" => 5, "percent" => {"bar" => 100.0}}, nil, {"total" => 4, "percent" => {"foo" => 75.0, "bar" => 25.0}}] RightSupport::Stats::Activity.percentage(stats, 10).should == {"total" => 10, "percent" => {"foo" => 40.0, "bar" => 60.0}} end it "returns only total if there is no data" do RightSupport::Stats::Activity.percentage([], 0).should == {"total" => 0} end end context :avg_rate do it "computes average rate from multiple average rates" do stats = [{"total" => 1, "rate" => 0.5}, {"total" => 5, "rate" => 0.1}, {"total" => 4, "rate" => 0.2}] RightSupport::Stats::Activity.avg_rate(stats, 10).should == 0.18 end it "handles nil stat" do stats = [{"total" => 1, "rate" => 0.5}, nil, {"total" => 5, "rate" => 0.1}, nil, {"total" => 4, "rate" => 0.2}] RightSupport::Stats::Activity.avg_rate(stats, 10).should == 0.18 end it "returns nil if there is no data" do RightSupport::Stats::Activity.avg_rate([], 0).should be_nil end end context :last do it "determines last activity from multiple last activity stats" do stats = [{"last" => {"elapsed" => 0, "type" => "foo"}, "total" => 1, "percent" => {"foo" => 100.0}}, {"last" => {"elapsed" => 0, "type" => "foo"}, "total" => 1, "percent" => {"foo" => 100.0}}] end it "handles nil stat" do stats = [{"last" => {"elapsed" => 0, "type" => "foo"}, "total" => 1, "percent" => {"foo" => 100.0}}, nil, {"last" => {"elapsed" => 0, "type" => "foo"}, "total" => 1, "percent" => {"foo" => 100.0}}] end it "returns nil if there is no data" do RightSupport::Stats::Activity.last([]).should be_nil end end end end # RightSupport::Stats::Activity