pulse-meter 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (96) hide show
  1. data/.gitignore +19 -0
  2. data/.rbenv-version +1 -0
  3. data/.rspec +1 -0
  4. data/.rvmrc +1 -0
  5. data/.travis.yml +4 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE +22 -0
  8. data/Procfile +3 -0
  9. data/README.md +440 -0
  10. data/Rakefile +53 -0
  11. data/bin/pulse +6 -0
  12. data/examples/basic.ru +109 -0
  13. data/examples/basic_sensor_data.rb +38 -0
  14. data/examples/full/Procfile +2 -0
  15. data/examples/full/client.rb +82 -0
  16. data/examples/full/server.ru +114 -0
  17. data/examples/minimal/Procfile +2 -0
  18. data/examples/minimal/client.rb +16 -0
  19. data/examples/minimal/server.ru +20 -0
  20. data/examples/readme_client_example.rb +52 -0
  21. data/lib/cmd.rb +150 -0
  22. data/lib/pulse-meter.rb +17 -0
  23. data/lib/pulse-meter/mixins/dumper.rb +72 -0
  24. data/lib/pulse-meter/mixins/utils.rb +91 -0
  25. data/lib/pulse-meter/sensor.rb +44 -0
  26. data/lib/pulse-meter/sensor/base.rb +75 -0
  27. data/lib/pulse-meter/sensor/counter.rb +36 -0
  28. data/lib/pulse-meter/sensor/hashed_counter.rb +31 -0
  29. data/lib/pulse-meter/sensor/indicator.rb +33 -0
  30. data/lib/pulse-meter/sensor/timeline.rb +180 -0
  31. data/lib/pulse-meter/sensor/timelined/average.rb +26 -0
  32. data/lib/pulse-meter/sensor/timelined/counter.rb +16 -0
  33. data/lib/pulse-meter/sensor/timelined/hashed_counter.rb +22 -0
  34. data/lib/pulse-meter/sensor/timelined/max.rb +25 -0
  35. data/lib/pulse-meter/sensor/timelined/median.rb +14 -0
  36. data/lib/pulse-meter/sensor/timelined/min.rb +25 -0
  37. data/lib/pulse-meter/sensor/timelined/percentile.rb +31 -0
  38. data/lib/pulse-meter/version.rb +3 -0
  39. data/lib/pulse-meter/visualize/app.rb +43 -0
  40. data/lib/pulse-meter/visualize/dsl.rb +0 -0
  41. data/lib/pulse-meter/visualize/dsl/errors.rb +46 -0
  42. data/lib/pulse-meter/visualize/dsl/layout.rb +55 -0
  43. data/lib/pulse-meter/visualize/dsl/page.rb +50 -0
  44. data/lib/pulse-meter/visualize/dsl/sensor.rb +21 -0
  45. data/lib/pulse-meter/visualize/dsl/widget.rb +84 -0
  46. data/lib/pulse-meter/visualize/layout.rb +54 -0
  47. data/lib/pulse-meter/visualize/page.rb +30 -0
  48. data/lib/pulse-meter/visualize/public/css/application.css +19 -0
  49. data/lib/pulse-meter/visualize/public/css/bootstrap.css +4883 -0
  50. data/lib/pulse-meter/visualize/public/css/bootstrap.min.css +729 -0
  51. data/lib/pulse-meter/visualize/public/favicon.ico +0 -0
  52. data/lib/pulse-meter/visualize/public/img/glyphicons-halflings-white.png +0 -0
  53. data/lib/pulse-meter/visualize/public/img/glyphicons-halflings.png +0 -0
  54. data/lib/pulse-meter/visualize/public/js/application.coffee +262 -0
  55. data/lib/pulse-meter/visualize/public/js/application.js +279 -0
  56. data/lib/pulse-meter/visualize/public/js/backbone-min.js +38 -0
  57. data/lib/pulse-meter/visualize/public/js/bootstrap.js +1835 -0
  58. data/lib/pulse-meter/visualize/public/js/highcharts.js +203 -0
  59. data/lib/pulse-meter/visualize/public/js/jquery-1.7.2.min.js +4 -0
  60. data/lib/pulse-meter/visualize/public/js/json2.js +487 -0
  61. data/lib/pulse-meter/visualize/public/js/underscore-min.js +32 -0
  62. data/lib/pulse-meter/visualize/sensor.rb +60 -0
  63. data/lib/pulse-meter/visualize/views/main.haml +40 -0
  64. data/lib/pulse-meter/visualize/widget.rb +68 -0
  65. data/lib/pulse-meter/visualizer.rb +30 -0
  66. data/lib/test_helpers/matchers.rb +36 -0
  67. data/pulse-meter.gemspec +39 -0
  68. data/spec/pulse_meter/mixins/dumper_spec.rb +158 -0
  69. data/spec/pulse_meter/mixins/utils_spec.rb +134 -0
  70. data/spec/pulse_meter/sensor/base_spec.rb +97 -0
  71. data/spec/pulse_meter/sensor/counter_spec.rb +54 -0
  72. data/spec/pulse_meter/sensor/hashed_counter_spec.rb +39 -0
  73. data/spec/pulse_meter/sensor/indicator_spec.rb +43 -0
  74. data/spec/pulse_meter/sensor/timeline_spec.rb +58 -0
  75. data/spec/pulse_meter/sensor/timelined/average_spec.rb +6 -0
  76. data/spec/pulse_meter/sensor/timelined/counter_spec.rb +6 -0
  77. data/spec/pulse_meter/sensor/timelined/hashed_counter_spec.rb +8 -0
  78. data/spec/pulse_meter/sensor/timelined/max_spec.rb +7 -0
  79. data/spec/pulse_meter/sensor/timelined/median_spec.rb +7 -0
  80. data/spec/pulse_meter/sensor/timelined/min_spec.rb +7 -0
  81. data/spec/pulse_meter/sensor/timelined/percentile_spec.rb +17 -0
  82. data/spec/pulse_meter/visualize/app_spec.rb +27 -0
  83. data/spec/pulse_meter/visualize/dsl/layout_spec.rb +64 -0
  84. data/spec/pulse_meter/visualize/dsl/page_spec.rb +75 -0
  85. data/spec/pulse_meter/visualize/dsl/sensor_spec.rb +30 -0
  86. data/spec/pulse_meter/visualize/dsl/widget_spec.rb +127 -0
  87. data/spec/pulse_meter/visualize/layout_spec.rb +55 -0
  88. data/spec/pulse_meter/visualize/page_spec.rb +150 -0
  89. data/spec/pulse_meter/visualize/sensor_spec.rb +120 -0
  90. data/spec/pulse_meter/visualize/widget_spec.rb +113 -0
  91. data/spec/pulse_meter/visualizer_spec.rb +42 -0
  92. data/spec/pulse_meter_spec.rb +16 -0
  93. data/spec/shared_examples/timeline_sensor.rb +279 -0
  94. data/spec/shared_examples/timelined_subclass.rb +23 -0
  95. data/spec/spec_helper.rb +29 -0
  96. 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