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.
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