pulse-meter 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +19 -0
- data/.rbenv-version +1 -0
- data/.rspec +1 -0
- data/.rvmrc +1 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/Procfile +3 -0
- data/README.md +440 -0
- data/Rakefile +53 -0
- data/bin/pulse +6 -0
- data/examples/basic.ru +109 -0
- data/examples/basic_sensor_data.rb +38 -0
- data/examples/full/Procfile +2 -0
- data/examples/full/client.rb +82 -0
- data/examples/full/server.ru +114 -0
- data/examples/minimal/Procfile +2 -0
- data/examples/minimal/client.rb +16 -0
- data/examples/minimal/server.ru +20 -0
- data/examples/readme_client_example.rb +52 -0
- data/lib/cmd.rb +150 -0
- data/lib/pulse-meter.rb +17 -0
- data/lib/pulse-meter/mixins/dumper.rb +72 -0
- data/lib/pulse-meter/mixins/utils.rb +91 -0
- data/lib/pulse-meter/sensor.rb +44 -0
- data/lib/pulse-meter/sensor/base.rb +75 -0
- data/lib/pulse-meter/sensor/counter.rb +36 -0
- data/lib/pulse-meter/sensor/hashed_counter.rb +31 -0
- data/lib/pulse-meter/sensor/indicator.rb +33 -0
- data/lib/pulse-meter/sensor/timeline.rb +180 -0
- data/lib/pulse-meter/sensor/timelined/average.rb +26 -0
- data/lib/pulse-meter/sensor/timelined/counter.rb +16 -0
- data/lib/pulse-meter/sensor/timelined/hashed_counter.rb +22 -0
- data/lib/pulse-meter/sensor/timelined/max.rb +25 -0
- data/lib/pulse-meter/sensor/timelined/median.rb +14 -0
- data/lib/pulse-meter/sensor/timelined/min.rb +25 -0
- data/lib/pulse-meter/sensor/timelined/percentile.rb +31 -0
- data/lib/pulse-meter/version.rb +3 -0
- data/lib/pulse-meter/visualize/app.rb +43 -0
- data/lib/pulse-meter/visualize/dsl.rb +0 -0
- data/lib/pulse-meter/visualize/dsl/errors.rb +46 -0
- data/lib/pulse-meter/visualize/dsl/layout.rb +55 -0
- data/lib/pulse-meter/visualize/dsl/page.rb +50 -0
- data/lib/pulse-meter/visualize/dsl/sensor.rb +21 -0
- data/lib/pulse-meter/visualize/dsl/widget.rb +84 -0
- data/lib/pulse-meter/visualize/layout.rb +54 -0
- data/lib/pulse-meter/visualize/page.rb +30 -0
- data/lib/pulse-meter/visualize/public/css/application.css +19 -0
- data/lib/pulse-meter/visualize/public/css/bootstrap.css +4883 -0
- data/lib/pulse-meter/visualize/public/css/bootstrap.min.css +729 -0
- data/lib/pulse-meter/visualize/public/favicon.ico +0 -0
- data/lib/pulse-meter/visualize/public/img/glyphicons-halflings-white.png +0 -0
- data/lib/pulse-meter/visualize/public/img/glyphicons-halflings.png +0 -0
- data/lib/pulse-meter/visualize/public/js/application.coffee +262 -0
- data/lib/pulse-meter/visualize/public/js/application.js +279 -0
- data/lib/pulse-meter/visualize/public/js/backbone-min.js +38 -0
- data/lib/pulse-meter/visualize/public/js/bootstrap.js +1835 -0
- data/lib/pulse-meter/visualize/public/js/highcharts.js +203 -0
- data/lib/pulse-meter/visualize/public/js/jquery-1.7.2.min.js +4 -0
- data/lib/pulse-meter/visualize/public/js/json2.js +487 -0
- data/lib/pulse-meter/visualize/public/js/underscore-min.js +32 -0
- data/lib/pulse-meter/visualize/sensor.rb +60 -0
- data/lib/pulse-meter/visualize/views/main.haml +40 -0
- data/lib/pulse-meter/visualize/widget.rb +68 -0
- data/lib/pulse-meter/visualizer.rb +30 -0
- data/lib/test_helpers/matchers.rb +36 -0
- data/pulse-meter.gemspec +39 -0
- data/spec/pulse_meter/mixins/dumper_spec.rb +158 -0
- data/spec/pulse_meter/mixins/utils_spec.rb +134 -0
- data/spec/pulse_meter/sensor/base_spec.rb +97 -0
- data/spec/pulse_meter/sensor/counter_spec.rb +54 -0
- data/spec/pulse_meter/sensor/hashed_counter_spec.rb +39 -0
- data/spec/pulse_meter/sensor/indicator_spec.rb +43 -0
- data/spec/pulse_meter/sensor/timeline_spec.rb +58 -0
- data/spec/pulse_meter/sensor/timelined/average_spec.rb +6 -0
- data/spec/pulse_meter/sensor/timelined/counter_spec.rb +6 -0
- data/spec/pulse_meter/sensor/timelined/hashed_counter_spec.rb +8 -0
- data/spec/pulse_meter/sensor/timelined/max_spec.rb +7 -0
- data/spec/pulse_meter/sensor/timelined/median_spec.rb +7 -0
- data/spec/pulse_meter/sensor/timelined/min_spec.rb +7 -0
- data/spec/pulse_meter/sensor/timelined/percentile_spec.rb +17 -0
- data/spec/pulse_meter/visualize/app_spec.rb +27 -0
- data/spec/pulse_meter/visualize/dsl/layout_spec.rb +64 -0
- data/spec/pulse_meter/visualize/dsl/page_spec.rb +75 -0
- data/spec/pulse_meter/visualize/dsl/sensor_spec.rb +30 -0
- data/spec/pulse_meter/visualize/dsl/widget_spec.rb +127 -0
- data/spec/pulse_meter/visualize/layout_spec.rb +55 -0
- data/spec/pulse_meter/visualize/page_spec.rb +150 -0
- data/spec/pulse_meter/visualize/sensor_spec.rb +120 -0
- data/spec/pulse_meter/visualize/widget_spec.rb +113 -0
- data/spec/pulse_meter/visualizer_spec.rb +42 -0
- data/spec/pulse_meter_spec.rb +16 -0
- data/spec/shared_examples/timeline_sensor.rb +279 -0
- data/spec/shared_examples/timelined_subclass.rb +23 -0
- data/spec/spec_helper.rb +29 -0
- metadata +435 -0
@@ -0,0 +1,120 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe PulseMeter::Visualize::Sensor do
|
4
|
+
let(:interval){ 100 }
|
5
|
+
let(:name) { "some_sensor" }
|
6
|
+
let(:annotation) { 'sensor descr' }
|
7
|
+
let!(:real_sensor){ PulseMeter::Sensor::Timelined::Counter.new(name, ttl: 1000, interval: interval, annotation: annotation) }
|
8
|
+
let(:sensor) { described_class.new(sensor: name) }
|
9
|
+
|
10
|
+
let(:color){ '#ABCDEF' }
|
11
|
+
let(:sensor_with_color) { described_class.new(sensor: name, color: color) }
|
12
|
+
|
13
|
+
let(:bad_sensor) { described_class.new(sensor: "bad_sensor_name") }
|
14
|
+
let(:interval_start){ Time.at((Time.now.to_i / interval) * interval) }
|
15
|
+
|
16
|
+
describe '#last_value' do
|
17
|
+
context "when sensor does not exist" do
|
18
|
+
it "should raise RestoreError" do
|
19
|
+
expect{ bad_sensor.last_value }.to raise_exception(PulseMeter::RestoreError)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
context "when sensor has no data" do
|
25
|
+
it "should return nil" do
|
26
|
+
sensor.last_value.should be_nil
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context "when sensor has data" do
|
31
|
+
context "when need_incomplete arg is true" do
|
32
|
+
it "should return last value" do
|
33
|
+
Timecop.freeze(interval_start) do
|
34
|
+
real_sensor.event(101)
|
35
|
+
end
|
36
|
+
Timecop.freeze(interval_start+1) do
|
37
|
+
sensor.last_value(true).should == 101
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
context "when need_incomplete arg is false" do
|
43
|
+
it "should return last complete value" do
|
44
|
+
Timecop.freeze(interval_start) do
|
45
|
+
real_sensor.event(101)
|
46
|
+
end
|
47
|
+
Timecop.freeze(interval_start + 1) do
|
48
|
+
sensor.last_value.should be_nil
|
49
|
+
end
|
50
|
+
Timecop.freeze(interval_start + interval + 1) do
|
51
|
+
sensor.last_value.should == 101
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe "#last_point_data" do
|
60
|
+
|
61
|
+
context "when sensor does not exist" do
|
62
|
+
it "should raise RestoreError" do
|
63
|
+
expect{ bad_sensor.last_point_data }.to raise_exception(PulseMeter::RestoreError)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
it "should return last value with annotation (and color)" do
|
68
|
+
Timecop.freeze(interval_start) do
|
69
|
+
real_sensor.event(101)
|
70
|
+
end
|
71
|
+
Timecop.freeze(interval_start + 1) do
|
72
|
+
sensor.last_point_data(true).should == {name: annotation, y: 101}
|
73
|
+
sensor.last_point_data.should == {name: annotation, y: nil}
|
74
|
+
sensor_with_color.last_point_data(true).should == {name: annotation, y: 101, color: color}
|
75
|
+
sensor_with_color.last_point_data.should == {name: annotation, y: nil, color: color}
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
describe "#timeline_data" do
|
81
|
+
before(:each) do
|
82
|
+
Timecop.freeze(interval_start) do
|
83
|
+
real_sensor.event(101)
|
84
|
+
end
|
85
|
+
Timecop.freeze(interval_start + interval) do
|
86
|
+
real_sensor.event(55)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
context "when sensor does not exist" do
|
91
|
+
it "should raise RestoreError" do
|
92
|
+
expect{ bad_sensor.timeline_data(interval) }.to raise_exception(PulseMeter::RestoreError)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
|
97
|
+
describe "returned value" do
|
98
|
+
it "should contain sensor annotation" do
|
99
|
+
Timecop.freeze(interval_start + interval + 1) do
|
100
|
+
sensor.timeline_data(interval)[:name].should == annotation
|
101
|
+
end
|
102
|
+
end
|
103
|
+
it "should contain sensor color" do
|
104
|
+
Timecop.freeze(interval_start + interval + 1) do
|
105
|
+
sensor_with_color.timeline_data(interval)[:color].should == color
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
it "should contain [interval_start, value] pairs for each interval" do
|
110
|
+
Timecop.freeze(interval_start + interval + 1) do
|
111
|
+
data = sensor.timeline_data(interval * 2)
|
112
|
+
data[:data].should == [{x: interval_start.to_i * 1000, y: 101}]
|
113
|
+
data = sensor.timeline_data(interval * 2, true)
|
114
|
+
data[:data].should == [{x: interval_start.to_i * 1000, y: 101}, {x: (interval_start + interval).to_i * 1000, y: 55}]
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe PulseMeter::Visualize::Widget do
|
4
|
+
let(:interval){ 100 }
|
5
|
+
let!(:a_sensor){ PulseMeter::Sensor::Timelined::Counter.new(:a_sensor, :ttl => 1000, :interval => interval, annotation: 'A') }
|
6
|
+
let!(:b_sensor){ PulseMeter::Sensor::Timelined::Counter.new(:b_sensor, :ttl => 1000, :interval => interval, annotation: 'B') }
|
7
|
+
|
8
|
+
let(:type) { :some_type }
|
9
|
+
let(:widget_name){ "some_widget" }
|
10
|
+
|
11
|
+
let(:redraw_interval){5}
|
12
|
+
let(:values_label){'xxxx'}
|
13
|
+
let(:width){6}
|
14
|
+
let(:show_last_point){false}
|
15
|
+
let(:timespan){interval * 2}
|
16
|
+
let(:a_color){'#FF0000'}
|
17
|
+
let(:b_color){'#FFFF00'}
|
18
|
+
|
19
|
+
let(:interval_start){ Time.at((Time.now.to_i / interval) * interval) }
|
20
|
+
|
21
|
+
def add_widget_settings(w)
|
22
|
+
w.redraw_interval redraw_interval
|
23
|
+
w.values_label values_label
|
24
|
+
w.width width
|
25
|
+
w.show_last_point show_last_point
|
26
|
+
w.timespan timespan
|
27
|
+
|
28
|
+
w.sensor :a_sensor, color: a_color
|
29
|
+
w.sensor :b_sensor, color: b_color
|
30
|
+
end
|
31
|
+
|
32
|
+
let(:widgets) do
|
33
|
+
[:line, :spline, :area, :pie].each_with_object({}) do |type, h|
|
34
|
+
w = PulseMeter::Visualize::DSL::Widget.new(type, widget_name)
|
35
|
+
add_widget_settings(w)
|
36
|
+
h[type] = w.to_widget
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "#data" do
|
41
|
+
it "should contain type, title, interval, values_label, width, show_last_point attriutes" do
|
42
|
+
widgets.each do |k,w|
|
43
|
+
wdata = w.data
|
44
|
+
wdata[:type].should == k
|
45
|
+
wdata[:title].should == widget_name
|
46
|
+
wdata[:interval].should == redraw_interval
|
47
|
+
wdata[:values_title].should == values_label
|
48
|
+
wdata[:width].should == width
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "series attribute" do
|
53
|
+
before(:each) do
|
54
|
+
Timecop.freeze(interval_start + 1) do
|
55
|
+
a_sensor.event(12)
|
56
|
+
b_sensor.event(33)
|
57
|
+
end
|
58
|
+
Timecop.freeze(interval_start + interval + 1) do
|
59
|
+
a_sensor.event(111)
|
60
|
+
end
|
61
|
+
@current_time = interval_start + 2 * interval - 1
|
62
|
+
|
63
|
+
end
|
64
|
+
context "pie widget" do
|
65
|
+
|
66
|
+
it "should contain valid pie series" do
|
67
|
+
|
68
|
+
Timecop.freeze(@current_time) do
|
69
|
+
widgets[:pie].data[:series].should == [{
|
70
|
+
type: :pie,
|
71
|
+
name: values_label,
|
72
|
+
data: [{
|
73
|
+
name: a_sensor.annotation,
|
74
|
+
color: a_color,
|
75
|
+
y: 12
|
76
|
+
}, {
|
77
|
+
name: b_sensor.annotation,
|
78
|
+
color: b_color,
|
79
|
+
y: 33
|
80
|
+
}]
|
81
|
+
|
82
|
+
}]
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
context "line, spline, area widgets" do
|
88
|
+
|
89
|
+
it "should contain valid series" do
|
90
|
+
|
91
|
+
Timecop.freeze(@current_time) do
|
92
|
+
|
93
|
+
[:line, :spline, :area].each do |type|
|
94
|
+
|
95
|
+
widget = widgets[type]
|
96
|
+
widget.data[:series].should == [{
|
97
|
+
name: a_sensor.annotation,
|
98
|
+
color: a_color,
|
99
|
+
data: [{x: interval_start.to_i * 1000, y: 12}]
|
100
|
+
}, {
|
101
|
+
name: b_sensor.annotation,
|
102
|
+
color: b_color,
|
103
|
+
data: [{x: interval_start.to_i * 1000, y: 33}]
|
104
|
+
}]
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe PulseMeter::Visualizer do
|
4
|
+
describe "::draw" do
|
5
|
+
it "should generate correct layout with passed block" do
|
6
|
+
layout = described_class.draw do |l|
|
7
|
+
|
8
|
+
l.title "My Gauges"
|
9
|
+
|
10
|
+
l.page "Dashboard" do |p|
|
11
|
+
p.spline :convertion do |c|
|
12
|
+
c.sensor :adv_clicks, color: :green
|
13
|
+
c.sensor :adv_shows, color: :red
|
14
|
+
end
|
15
|
+
|
16
|
+
p.pie :agents, title: 'User Agents' do |c|
|
17
|
+
c.sensor :agent_ie
|
18
|
+
c.sensor :agent_chrome
|
19
|
+
c.sensor :agent_ff
|
20
|
+
c.sensor :agent_other
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
l.page "Request stats" do |p|
|
26
|
+
p.spline :rph_total, sensor: :rph_total
|
27
|
+
p.line :rph_main_page, sensor: :rph_main_page
|
28
|
+
p.line :request_time_p95_hour
|
29
|
+
|
30
|
+
p.pie :success_vs_fail_total_hourly do |w|
|
31
|
+
w.sensor :success_total_hourly
|
32
|
+
w.sensor :fail_total_hourly
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
layout.should be_kind_of(PulseMeter::Visualize::Layout)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe PulseMeter do
|
4
|
+
describe "::redis=" do
|
5
|
+
it "should store redis" do
|
6
|
+
PulseMeter.redis = 'redis'
|
7
|
+
PulseMeter.redis.should == 'redis'
|
8
|
+
end
|
9
|
+
end
|
10
|
+
describe "::redis" do
|
11
|
+
it "should retrieve redis" do
|
12
|
+
PulseMeter.redis = 'redis'
|
13
|
+
PulseMeter.redis.should == 'redis'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,279 @@
|
|
1
|
+
shared_examples_for "timeline sensor" do |extra_init_values, default_event|
|
2
|
+
class Dummy
|
3
|
+
include PulseMeter::Mixins::Dumper
|
4
|
+
def name; :dummy end
|
5
|
+
def redis; PulseMeter.redis; end
|
6
|
+
end
|
7
|
+
|
8
|
+
let(:name){ :some_value_with_history }
|
9
|
+
let(:ttl){ 100 }
|
10
|
+
let(:raw_data_ttl){ 30 }
|
11
|
+
let(:interval){ 5 }
|
12
|
+
let(:reduce_delay){ 3 }
|
13
|
+
let(:good_init_values){ {:ttl => ttl, :raw_data_ttl => raw_data_ttl, :interval => interval, :reduce_delay => reduce_delay}.merge(extra_init_values || {}) }
|
14
|
+
let!(:sensor){ described_class.new(name, good_init_values) }
|
15
|
+
let(:dummy) {Dummy.new}
|
16
|
+
let(:base_class){ PulseMeter::Sensor::Base }
|
17
|
+
let(:redis){ PulseMeter.redis }
|
18
|
+
let(:sample_event) {default_event || 123}
|
19
|
+
|
20
|
+
before(:each) do
|
21
|
+
@interval_id = (Time.now.to_i / interval) * interval
|
22
|
+
@raw_data_key = sensor.raw_data_key(@interval_id)
|
23
|
+
@next_raw_data_key = sensor.raw_data_key(@interval_id + interval)
|
24
|
+
@start_of_interval = Time.at(@interval_id)
|
25
|
+
end
|
26
|
+
|
27
|
+
describe "#dump" do
|
28
|
+
it "should be dumped succesfully" do
|
29
|
+
expect {sensor.dump!}.not_to raise_exception
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe ".restore" do
|
34
|
+
before do
|
35
|
+
# no need to call sensor.dump! explicitly for it
|
36
|
+
# will be called automatically after creation
|
37
|
+
@restored = base_class.restore(sensor.name)
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should restore #{described_class} instance" do
|
41
|
+
@restored.should be_instance_of(described_class)
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should restore object with the same data" do
|
45
|
+
def inner_data(obj)
|
46
|
+
obj.instance_variables.sort.map {|v| obj.instance_variable_get(v)}
|
47
|
+
end
|
48
|
+
|
49
|
+
inner_data(sensor).should == inner_data(@restored)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe "#event" do
|
54
|
+
it "should write events to redis" do
|
55
|
+
expect{
|
56
|
+
sensor.event(sample_event)
|
57
|
+
}.to change{ redis.keys('*').count }.by(1)
|
58
|
+
end
|
59
|
+
|
60
|
+
it "should write data so that it totally expires after :raw_data_ttl" do
|
61
|
+
key_count = redis.keys('*').count
|
62
|
+
sensor.event(sample_event)
|
63
|
+
Timecop.freeze(Time.now + raw_data_ttl + 1) do
|
64
|
+
redis.keys('*').count.should == key_count
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should write data to bucket indicated by truncated timestamp" do
|
69
|
+
key = sensor.raw_data_key(@interval_id)
|
70
|
+
expect{
|
71
|
+
Timecop.freeze(@start_of_interval) do
|
72
|
+
sensor.event(sample_event)
|
73
|
+
end
|
74
|
+
}.to change{ redis.ttl(key) }
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe "#summarize" do
|
79
|
+
it "should convert data stored by raw_data_key to a value defined only by stored data" do
|
80
|
+
Timecop.freeze(@start_of_interval) do
|
81
|
+
sensor.event(sample_event)
|
82
|
+
end
|
83
|
+
Timecop.freeze(@start_of_interval + interval) do
|
84
|
+
sensor.event(sample_event)
|
85
|
+
end
|
86
|
+
sensor.summarize(@raw_data_key).should == sensor.summarize(@next_raw_data_key)
|
87
|
+
sensor.summarize(@raw_data_key).should_not be_nil
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe "#reduce" do
|
92
|
+
it "should store summarized value into data_key" do
|
93
|
+
Timecop.freeze(@start_of_interval){ sensor.event(sample_event) }
|
94
|
+
val = sensor.summarize(@raw_data_key)
|
95
|
+
val.should_not be_nil
|
96
|
+
sensor.reduce(@interval_id)
|
97
|
+
redis.get(sensor.data_key(@interval_id)).should == val.to_s
|
98
|
+
end
|
99
|
+
|
100
|
+
it "should remove original raw_data_key" do
|
101
|
+
Timecop.freeze(@start_of_interval){ sensor.event(sample_event) }
|
102
|
+
expect{
|
103
|
+
sensor.reduce(@interval_id)
|
104
|
+
}.to change{ redis.keys(sensor.raw_data_key(@interval_id)).count }.from(1).to(0)
|
105
|
+
end
|
106
|
+
|
107
|
+
it "should expire stored summarized data" do
|
108
|
+
Timecop.freeze(@start_of_interval) do
|
109
|
+
sensor.event(sample_event)
|
110
|
+
sensor.reduce(@interval_id)
|
111
|
+
redis.keys(sensor.data_key(@interval_id)).count.should == 1
|
112
|
+
end
|
113
|
+
Timecop.freeze(@start_of_interval + ttl + 1) do
|
114
|
+
redis.keys(sensor.data_key(@interval_id)).count.should == 0
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
it "should not store data if there is no corresponding raw data" do
|
119
|
+
Timecop.freeze(@start_of_interval) do
|
120
|
+
sensor.reduce(@interval_id)
|
121
|
+
redis.keys(sensor.data_key(@interval_id)).count.should == 0
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
describe "#reduce_all_raw" do
|
127
|
+
it "should reduce all data older than reduce_delay" do
|
128
|
+
Timecop.freeze(@start_of_interval){ sensor.event(sample_event) }
|
129
|
+
val0 = sensor.summarize(@raw_data_key)
|
130
|
+
Timecop.freeze(@start_of_interval + interval){ sensor.event(sample_event) }
|
131
|
+
val1 = sensor.summarize(@next_raw_data_key)
|
132
|
+
expect{
|
133
|
+
Timecop.freeze(@start_of_interval + interval + interval + reduce_delay + 1) { sensor.reduce_all_raw }
|
134
|
+
}.to change{ redis.keys(sensor.raw_data_key('*')).count }.from(2).to(0)
|
135
|
+
|
136
|
+
redis.get(sensor.data_key(@interval_id)).should == val0.to_s
|
137
|
+
redis.get(sensor.data_key(@interval_id + interval)).should == val1.to_s
|
138
|
+
end
|
139
|
+
|
140
|
+
it "should not reduce fresh data" do
|
141
|
+
Timecop.freeze(@start_of_interval){ sensor.event(sample_event) }
|
142
|
+
|
143
|
+
expect{
|
144
|
+
Timecop.freeze(@start_of_interval + interval + reduce_delay - 1) { sensor.reduce_all_raw }
|
145
|
+
}.not_to change{ redis.keys(sensor.raw_data_key('*')).count }
|
146
|
+
|
147
|
+
expect{
|
148
|
+
Timecop.freeze(@start_of_interval + interval + reduce_delay - 1) { sensor.reduce_all_raw }
|
149
|
+
}.not_to change{ redis.keys(sensor.data_key('*')).count }
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
describe ".reduce_all_raw" do
|
154
|
+
it "should silently skip objects without reduce logic" do
|
155
|
+
dummy.dump!
|
156
|
+
expect {described_class.reduce_all_raw}.not_to raise_exception
|
157
|
+
end
|
158
|
+
|
159
|
+
it "should send reduce_all_raw to all dumped objects" do
|
160
|
+
described_class.any_instance.should_receive(:reduce_all_raw)
|
161
|
+
described_class.reduce_all_raw
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
describe "#timeline_within" do
|
166
|
+
it "shoulde raise exception unless both arguments are Time objects" do
|
167
|
+
[:q, nil, -1].each do |bad_value|
|
168
|
+
expect{ sensor.timeline_within(Time.now, bad_value) }.to raise_exception(ArgumentError)
|
169
|
+
expect{ sensor.timeline_within(bad_value, Time.now) }.to raise_exception(ArgumentError)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
it "should return an array of SensorData objects corresponding to stored data for passed interval" do
|
174
|
+
sensor.event(sample_event)
|
175
|
+
now = Time.now
|
176
|
+
timeline = sensor.timeline_within(now - 1, now)
|
177
|
+
timeline.should be_kind_of(Array)
|
178
|
+
timeline.each{|i| i.should be_kind_of(SensorData) }
|
179
|
+
end
|
180
|
+
|
181
|
+
it "should return array of results containing as many results as there are sensor interval beginnings in the passed interval" do
|
182
|
+
Timecop.freeze(@start_of_interval){ sensor.event(sample_event) }
|
183
|
+
Timecop.freeze(@start_of_interval + interval){ sensor.event(sample_event) }
|
184
|
+
|
185
|
+
future = @start_of_interval + 3600
|
186
|
+
Timecop.freeze(future) do
|
187
|
+
sensor.timeline_within(
|
188
|
+
Time.at(@start_of_interval + interval - 1),
|
189
|
+
Time.at(@start_of_interval + interval + 1)
|
190
|
+
).size.should == 1
|
191
|
+
|
192
|
+
sensor.timeline_within(
|
193
|
+
Time.at(@start_of_interval - 1),
|
194
|
+
Time.at(@start_of_interval + interval + 1)
|
195
|
+
).size.should == 2
|
196
|
+
end
|
197
|
+
|
198
|
+
Timecop.freeze(@start_of_interval + interval + 2) do
|
199
|
+
sensor.timeline_within(
|
200
|
+
Time.at(@start_of_interval + interval + 1),
|
201
|
+
Time.at(@start_of_interval + interval + 2)
|
202
|
+
).size.should == 0
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
describe "#timeline" do
|
208
|
+
it "should raise exception if passed interval is not a positive integer" do
|
209
|
+
[:q, nil, -1].each do |bad_interval|
|
210
|
+
expect{ sensor.timeline(bad_interval) }.to raise_exception(ArgumentError)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
it "should request timeline within interval from given number of seconds ago till now" do
|
215
|
+
Timecop.freeze do
|
216
|
+
now = Time.now
|
217
|
+
ago = interval * 100
|
218
|
+
sensor.timeline(ago).should == sensor.timeline_within(now - ago, now)
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
it "should return array of results containing as many results as there are sensor interval beginnings in the passed interval" do
|
223
|
+
Timecop.freeze(@start_of_interval){ sensor.event(sample_event) }
|
224
|
+
Timecop.freeze(@start_of_interval + interval){ sensor.event(sample_event) }
|
225
|
+
|
226
|
+
Timecop.freeze(@start_of_interval + interval + 1) do
|
227
|
+
sensor.timeline(2).size.should == 1
|
228
|
+
end
|
229
|
+
Timecop.freeze(@start_of_interval + interval + 2) do
|
230
|
+
sensor.timeline(1).size.should == 0
|
231
|
+
end
|
232
|
+
Timecop.freeze(@start_of_interval + interval + 1) do
|
233
|
+
sensor.timeline(2 + interval).size.should == 2
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
describe "SensorData value for an interval" do
|
239
|
+
def check_sensor_data(sensor, value)
|
240
|
+
data = sensor.timeline(2).first
|
241
|
+
data.value.should == value
|
242
|
+
data.start_time.to_i.should == @interval_id
|
243
|
+
end
|
244
|
+
|
245
|
+
it "should contain summarized value stored by data_key for reduced intervals" do
|
246
|
+
Timecop.freeze(@start_of_interval){ sensor.event(sample_event) }
|
247
|
+
sensor.reduce(@interval_id)
|
248
|
+
Timecop.freeze(@start_of_interval + 1){
|
249
|
+
check_sensor_data(sensor, redis.get(sensor.data_key(@interval_id)))
|
250
|
+
}
|
251
|
+
end
|
252
|
+
|
253
|
+
it "should contain summarized value based on raw data for intervals not yet reduced" do
|
254
|
+
Timecop.freeze(@start_of_interval){ sensor.event(sample_event) }
|
255
|
+
Timecop.freeze(@start_of_interval + 1){
|
256
|
+
check_sensor_data(sensor, sensor.summarize(@raw_data_key))
|
257
|
+
}
|
258
|
+
end
|
259
|
+
|
260
|
+
it "should contain nil for intervals without any data" do
|
261
|
+
Timecop.freeze(@start_of_interval + 1) {
|
262
|
+
check_sensor_data(sensor, nil)
|
263
|
+
}
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
describe "#cleanup" do
|
268
|
+
it "should remove all sensor data (raw data, reduced data, annotations) from redis" do
|
269
|
+
Timecop.freeze(@start_of_interval){ sensor.event(sample_event) }
|
270
|
+
sensor.reduce(@interval_id)
|
271
|
+
Timecop.freeze(@start_of_interval + interval){ sensor.event(sample_event) }
|
272
|
+
sensor.annotate("Fooo sensor")
|
273
|
+
|
274
|
+
sensor.cleanup
|
275
|
+
redis.keys('*').should be_empty
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
end
|