reportable 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,155 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'spec_helper')
2
+
3
+ describe Saulabs::Reportable::Grouping do
4
+
5
+ describe '#new' do
6
+
7
+ it 'should raise an error if an unsupported grouping is specified' do
8
+ lambda { Saulabs::Reportable::Grouping.new(:unsupported) }.should raise_error(ArgumentError)
9
+ end
10
+
11
+ end
12
+
13
+ describe '#to_sql' do
14
+
15
+ describe 'for MySQL' do
16
+
17
+ before do
18
+ ActiveRecord::Base.connection.stub!(:adapter_name).and_return('MySQL')
19
+ end
20
+
21
+ it 'should use DATE_FORMAT with format string "%Y/%m/%d/%H" for grouping :hour' do
22
+ Saulabs::Reportable::Grouping.new(:hour).send(:to_sql, 'created_at').should == "DATE_FORMAT(created_at, '%Y/%m/%d/%H')"
23
+ end
24
+
25
+ it 'should use DATE_FORMAT with format string "%Y/%m/%d" for grouping :day' do
26
+ Saulabs::Reportable::Grouping.new(:day).send(:to_sql, 'created_at').should == "DATE_FORMAT(created_at, '%Y/%m/%d')"
27
+ end
28
+
29
+ it 'should use YEARWEEK with mode 3 for grouping :week' do
30
+ Saulabs::Reportable::Grouping.new(:week).send(:to_sql, 'created_at').should == "YEARWEEK(created_at, 3)"
31
+ end
32
+
33
+ it 'should use DATE_FORMAT with format string "%Y/%m" for grouping :month' do
34
+ Saulabs::Reportable::Grouping.new(:month).send(:to_sql, 'created_at').should == "DATE_FORMAT(created_at, '%Y/%m')"
35
+ end
36
+
37
+ end
38
+
39
+ describe 'for PostgreSQL' do
40
+
41
+ before do
42
+ ActiveRecord::Base.connection.stub!(:adapter_name).and_return('PostgreSQL')
43
+ end
44
+
45
+ for grouping in [:hour, :day, :week, :month] do
46
+
47
+ it "should use date_trunc with truncation identifier \"#{grouping.to_s}\" for grouping :#{grouping.to_s}" do
48
+ Saulabs::Reportable::Grouping.new(grouping).send(:to_sql, 'created_at').should == "date_trunc('#{grouping.to_s}', created_at)"
49
+ end
50
+
51
+ end
52
+
53
+ end
54
+
55
+ describe 'for SQLite3' do
56
+
57
+ before do
58
+ ActiveRecord::Base.connection.stub!(:adapter_name).and_return('SQLite')
59
+ end
60
+
61
+ it 'should use strftime with format string "%Y/%m/%d/%H" for grouping :hour' do
62
+ Saulabs::Reportable::Grouping.new(:hour).send(:to_sql, 'created_at').should == "strftime('%Y/%m/%d/%H', created_at)"
63
+ end
64
+
65
+ it 'should use strftime with format string "%Y/%m/%d" for grouping :day' do
66
+ Saulabs::Reportable::Grouping.new(:day).send(:to_sql, 'created_at').should == "strftime('%Y/%m/%d', created_at)"
67
+ end
68
+
69
+ it 'should use date with mode "weekday 0" for grouping :week' do
70
+ Saulabs::Reportable::Grouping.new(:week).send(:to_sql, 'created_at').should == "date(created_at, 'weekday 0')"
71
+ end
72
+
73
+ it 'should use strftime with format string "%Y/%m" for grouping :month' do
74
+ Saulabs::Reportable::Grouping.new(:month).send(:to_sql, 'created_at').should == "strftime('%Y/%m', created_at)"
75
+ end
76
+
77
+ end
78
+
79
+ end
80
+
81
+ describe '#date_parts_from_db_string' do
82
+
83
+ describe 'for SQLite3' do
84
+
85
+ before do
86
+ ActiveRecord::Base.connection.stub!(:adapter_name).and_return('SQLite')
87
+ end
88
+
89
+ for grouping in [[:hour, '2008/12/31/12'], [:day, '2008/12/31'], [:month, '2008/12']] do
90
+
91
+ it "should split the string with '/' for grouping :#{grouping[0].to_s}" do
92
+ Saulabs::Reportable::Grouping.new(grouping[0]).date_parts_from_db_string(grouping[1]).should == grouping[1].split('/').map(&:to_i)
93
+ end
94
+
95
+ end
96
+
97
+ it 'should split the string with "-" and return teh calendar year and week for grouping :week' do
98
+ db_string = '2008-2-1'
99
+ expected = [2008, 5]
100
+
101
+ Saulabs::Reportable::Grouping.new(:week).date_parts_from_db_string(db_string).should == expected
102
+ end
103
+
104
+ end
105
+
106
+ describe 'for PostgreSQL' do
107
+
108
+ before do
109
+ ActiveRecord::Base.connection.stub!(:adapter_name).and_return('PostgreSQL')
110
+ end
111
+
112
+ it 'should split the date part of the string with "-" and read out the hour for grouping :hour' do
113
+ Saulabs::Reportable::Grouping.new(:hour).date_parts_from_db_string('2008-12-03 06:00:00').should == [2008, 12, 03, 6]
114
+ end
115
+
116
+ it 'should split the date part of the string with "-" for grouping :day' do
117
+ Saulabs::Reportable::Grouping.new(:day).date_parts_from_db_string('2008-12-03 00:00:00').should == [2008, 12, 03]
118
+ end
119
+
120
+ it 'should split the date part of the string with "-" and calculate the calendar week for grouping :week' do
121
+ Saulabs::Reportable::Grouping.new(:week).date_parts_from_db_string('2008-12-01 00:00:00').should == [2008, 49]
122
+ end
123
+
124
+ it 'should split the date part of the string with "-" and return year and month for grouping :month' do
125
+ Saulabs::Reportable::Grouping.new(:month).date_parts_from_db_string('2008-12-01 00:00:00').should == [2008, 12]
126
+ end
127
+
128
+ end
129
+
130
+ describe 'for MySQL' do
131
+
132
+ before do
133
+ ActiveRecord::Base.connection.stub!(:adapter_name).and_return('MySQL')
134
+ end
135
+
136
+ for grouping in [[:hour, '2008/12/31/12'], [:day, '2008/12/31'], [:month, '2008/12']] do
137
+
138
+ it "should split the string with '/' for grouping :#{grouping[0].to_s}" do
139
+ Saulabs::Reportable::Grouping.new(grouping[0]).date_parts_from_db_string(grouping[1]).should == grouping[1].split('/').map(&:to_i)
140
+ end
141
+
142
+ end
143
+
144
+ it 'should use the first 4 numbers for the year and the last 2 numbers for the week for grouping :week' do
145
+ db_string = '200852'
146
+ expected = [2008, 52]
147
+
148
+ Saulabs::Reportable::Grouping.new(:week).date_parts_from_db_string(db_string).should == expected
149
+ end
150
+
151
+ end
152
+
153
+ end
154
+
155
+ end
@@ -0,0 +1,296 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'spec_helper')
2
+
3
+ describe Saulabs::Reportable::ReportCache do
4
+
5
+ before do
6
+ @report = Saulabs::Reportable::Report.new(User, :registrations, :limit => 10)
7
+ end
8
+
9
+ describe '.clear_for' do
10
+
11
+ it 'should delete all entries in the cache for the klass and report name' do
12
+ Saulabs::Reportable::ReportCache.should_receive(:delete_all).once.with(:conditions => {
13
+ :model_name => User.name,
14
+ :report_name => 'registrations'
15
+ })
16
+
17
+ Saulabs::Reportable::ReportCache.clear_for(User, :registrations)
18
+ end
19
+
20
+ end
21
+
22
+ describe '.process' do
23
+
24
+ before do
25
+ Saulabs::Reportable::ReportCache.stub!(:find).and_return([])
26
+ Saulabs::Reportable::ReportCache.stub!(:prepare_result).and_return([])
27
+ end
28
+
29
+ it 'should raise an ArgumentError if no block is given' do
30
+ lambda do
31
+ Saulabs::Reportable::ReportCache.process(@report, @report.options)
32
+ end.should raise_error(ArgumentError)
33
+ end
34
+
35
+ it 'sould start a transaction' do
36
+ Saulabs::Reportable::ReportCache.should_receive(:transaction)
37
+
38
+ Saulabs::Reportable::ReportCache.process(@report, @report.options) {}
39
+ end
40
+
41
+ describe 'with :live_data = true' do
42
+
43
+ before do
44
+ @options = @report.options.merge(:live_data => true)
45
+ end
46
+
47
+ it 'should yield to the given block' do
48
+ lambda {
49
+ Saulabs::Reportable::ReportCache.process(@report, @options) { raise YieldMatchException.new }
50
+ }.should raise_error(YieldMatchException)
51
+ end
52
+
53
+ it 'should yield the first reporting period if not all required data could be retrieved from the cache' do
54
+ reporting_period = Saulabs::Reportable::ReportingPeriod.new(
55
+ @report.options[:grouping],
56
+ Time.now - 3.send(@report.options[:grouping].identifier)
57
+ )
58
+ Saulabs::Reportable::ReportCache.stub!(:all).and_return([Saulabs::Reportable::ReportCache.new])
59
+
60
+ Saulabs::Reportable::ReportCache.process(@report, @options) do |begin_at, end_at|
61
+ begin_at.should == Saulabs::Reportable::ReportingPeriod.first(@report.options[:grouping], @report.options[:limit]).date_time
62
+ end_at.should == nil
63
+ []
64
+ end
65
+ end
66
+
67
+ it 'should yield the reporting period after the last one in the cache if all required data could be retrieved from the cache' do
68
+ reporting_period = Saulabs::Reportable::ReportingPeriod.new(
69
+ @report.options[:grouping],
70
+ Time.now - @report.options[:limit].send(@report.options[:grouping].identifier)
71
+ )
72
+ cached = Saulabs::Reportable::ReportCache.new
73
+ cached.stub!(:reporting_period).and_return(reporting_period.date_time)
74
+ Saulabs::Reportable::ReportCache.stub!(:all).and_return(Array.new(@report.options[:limit] - 1, Saulabs::Reportable::ReportCache.new), cached)
75
+
76
+ Saulabs::Reportable::ReportCache.process(@report, @options) do |begin_at, end_at|
77
+ begin_at.should == reporting_period.date_time
78
+ end_at.should == nil
79
+ []
80
+ end
81
+ end
82
+
83
+ end
84
+
85
+ describe 'with :live_data = false' do
86
+
87
+ it 'should not yield if all required data could be retrieved from the cache' do
88
+ Saulabs::Reportable::ReportCache.stub!(:all).and_return(Array.new(@report.options[:limit], Saulabs::Reportable::ReportCache.new))
89
+
90
+ lambda {
91
+ Saulabs::Reportable::ReportCache.process(@report, @report.options) { raise YieldMatchException.new }
92
+ }.should_not raise_error(YieldMatchException)
93
+ end
94
+
95
+ it 'should yield to the block if no data could be retrieved from the cache' do
96
+ Saulabs::Reportable::ReportCache.stub!(:all).and_return([])
97
+
98
+ lambda {
99
+ Saulabs::Reportable::ReportCache.process(@report, @report.options) { raise YieldMatchException.new }
100
+ }.should raise_error(YieldMatchException)
101
+ end
102
+
103
+ describe 'with :end_date = <some date>' do
104
+
105
+ before do
106
+ @options = @report.options.merge(:end_date => Time.now)
107
+ end
108
+
109
+ it 'should yield the last date and time of the reporting period for the specified end date' do
110
+ reporting_period = Saulabs::Reportable::ReportingPeriod.new(@report.options[:grouping], @options[:end_date])
111
+
112
+ Saulabs::Reportable::ReportCache.process(@report, @options) do |begin_at, end_at|
113
+ end_at.should == reporting_period.last_date_time
114
+ []
115
+ end
116
+ end
117
+
118
+ end
119
+
120
+ end
121
+
122
+ it 'should read existing data from the cache' do
123
+ Saulabs::Reportable::ReportCache.should_receive(:all).once.with(
124
+ :conditions => [
125
+ 'model_name = ? AND report_name = ? AND grouping = ? AND aggregation = ? AND `condition` = ? AND reporting_period >= ?',
126
+ @report.klass.to_s,
127
+ @report.name.to_s,
128
+ @report.options[:grouping].identifier.to_s,
129
+ @report.aggregation.to_s,
130
+ @report.options[:conditions].to_s,
131
+ Saulabs::Reportable::ReportingPeriod.first(@report.options[:grouping], 10).date_time
132
+ ],
133
+ :limit => 10,
134
+ :order => 'reporting_period ASC'
135
+ ).and_return([])
136
+
137
+ Saulabs::Reportable::ReportCache.process(@report, @report.options) { [] }
138
+ end
139
+
140
+ it 'should utilize the end_date in the conditions' do
141
+ end_date = Time.now
142
+ Saulabs::Reportable::ReportCache.should_receive(:all).once.with(
143
+ :conditions => [
144
+ 'model_name = ? AND report_name = ? AND grouping = ? AND aggregation = ? AND `condition` = ? AND reporting_period BETWEEN ? AND ?',
145
+ @report.klass.to_s,
146
+ @report.name.to_s,
147
+ @report.options[:grouping].identifier.to_s,
148
+ @report.aggregation.to_s,
149
+ @report.options[:conditions].to_s,
150
+ Saulabs::Reportable::ReportingPeriod.first(@report.options[:grouping], 9).date_time,
151
+ Saulabs::Reportable::ReportingPeriod.new(@report.options[:grouping], end_date).date_time
152
+ ],
153
+ :limit => 10,
154
+ :order => 'reporting_period ASC'
155
+ ).and_return([])
156
+
157
+ Saulabs::Reportable::ReportCache.process(@report, @report.options.merge(:end_date => end_date)) { [] }
158
+ end
159
+
160
+ it "should read existing data from the cache for the correct grouping if one other than the report's default grouping is specified" do
161
+ grouping = Saulabs::Reportable::Grouping.new(:month)
162
+ Saulabs::Reportable::ReportCache.should_receive(:find).once.with(
163
+ :all,
164
+ :conditions => [
165
+ 'model_name = ? AND report_name = ? AND grouping = ? AND aggregation = ? AND `condition` = ? AND reporting_period >= ?',
166
+ @report.klass.to_s,
167
+ @report.name.to_s,
168
+ grouping.identifier.to_s,
169
+ @report.aggregation.to_s,
170
+ @report.options[:conditions].to_s,
171
+ Saulabs::Reportable::ReportingPeriod.first(grouping, 10).date_time
172
+ ],
173
+ :limit => 10,
174
+ :order => 'reporting_period ASC'
175
+ ).and_return([])
176
+
177
+ Saulabs::Reportable::ReportCache.process(@report, { :limit => 10, :grouping => grouping }) { [] }
178
+ end
179
+
180
+ it 'should yield the first reporting period if the cache is empty' do
181
+ Saulabs::Reportable::ReportCache.process(@report, @report.options) do |begin_at, end_at|
182
+ begin_at.should == Saulabs::Reportable::ReportingPeriod.first(@report.options[:grouping], 10).date_time
183
+ end_at.should == nil
184
+ []
185
+ end
186
+ end
187
+ end
188
+
189
+ describe '.prepare_result' do
190
+
191
+ before do
192
+ @current_reporting_period = Saulabs::Reportable::ReportingPeriod.new(@report.options[:grouping])
193
+ @new_data = [[@current_reporting_period.previous.date_time, 1.0]]
194
+ Saulabs::Reportable::ReportingPeriod.stub!(:from_db_string).and_return(@current_reporting_period.previous)
195
+ @cached = Saulabs::Reportable::ReportCache.new
196
+ @cached.stub!(:save!)
197
+ Saulabs::Reportable::ReportCache.stub!(:build_cached_data).and_return(@cached)
198
+ end
199
+
200
+ it 'should create :limit instances of Saulabs::Reportable::ReportCache with value 0.0 if no new data has been read and nothing was cached' do
201
+ Saulabs::Reportable::ReportCache.should_receive(:build_cached_data).exactly(10).times.with(
202
+ @report,
203
+ @report.options[:grouping],
204
+ @report.options[:conditions],
205
+ anything(),
206
+ 0.0
207
+ ).and_return(@cached)
208
+
209
+ Saulabs::Reportable::ReportCache.send(:prepare_result, [], [], @report, @report.options)
210
+ end
211
+
212
+ it 'should create a new Saulabs::Reportable::ReportCache with the correct value if new data has been read' do
213
+ Saulabs::Reportable::ReportCache.should_receive(:build_cached_data).exactly(9).times.with(
214
+ @report,
215
+ @report.options[:grouping],
216
+ @report.options[:conditions],
217
+ anything(),
218
+ 0.0
219
+ ).and_return(@cached)
220
+ Saulabs::Reportable::ReportCache.should_receive(:build_cached_data).once.with(
221
+ @report,
222
+ @report.options[:grouping],
223
+ @report.options[:conditions],
224
+ @current_reporting_period.previous,
225
+ 1.0
226
+ ).and_return(@cached)
227
+
228
+ Saulabs::Reportable::ReportCache.send(:prepare_result, @new_data, [], @report, @report.options)
229
+ end
230
+
231
+ it 'should save the created Saulabs::Reportable::ReportCache' do
232
+ @cached.should_receive(:save!).once
233
+
234
+ Saulabs::Reportable::ReportCache.send(:prepare_result, @new_data, [], @report, @report.options)
235
+ end
236
+
237
+ it 'should return an array of arrays of Dates and Floats' do
238
+ result = Saulabs::Reportable::ReportCache.send(:prepare_result, @new_data, [], @report, @report.options)
239
+
240
+ result.should be_kind_of(Array)
241
+ result[0].should be_kind_of(Array)
242
+ result[0][0].should be_kind_of(Date)
243
+ result[0][1].should be_kind_of(Float)
244
+ end
245
+
246
+ describe 'with :live_data = false' do
247
+
248
+ before do
249
+ @result = Saulabs::Reportable::ReportCache.send(:prepare_result, @new_data, [], @report, @report.options)
250
+ end
251
+
252
+ it 'should return an array of length :limit' do
253
+ @result.length.should == 10
254
+ end
255
+
256
+ it 'should not include an entry for the current reporting period' do
257
+ @result.find { |row| row[0] == @current_reporting_period.date_time }.should be_nil
258
+ end
259
+
260
+ end
261
+
262
+ describe 'with :live_data = true' do
263
+
264
+ before do
265
+ options = @report.options.merge(:live_data => true)
266
+ @result = Saulabs::Reportable::ReportCache.send(:prepare_result, @new_data, [], @report, options)
267
+ end
268
+
269
+ it 'should return an array of length (:limit + 1)' do
270
+ @result.length.should == 11
271
+ end
272
+
273
+ it 'should include an entry for the current reporting period' do
274
+ @result.find { |row| row[0] == @current_reporting_period.date_time }.should_not be_nil
275
+ end
276
+
277
+ end
278
+ end
279
+
280
+ describe '.find_value' do
281
+
282
+ before do
283
+ @data = [[Saulabs::Reportable::ReportingPeriod.new(Saulabs::Reportable::Grouping.new(:day)), 3.0]]
284
+ end
285
+
286
+ it 'should return the correct value when new data has been read for the reporting period' do
287
+ Saulabs::Reportable::ReportCache.send(:find_value, @data, @data[0][0]).should == 3.0
288
+ end
289
+
290
+ it 'should return 0.0 when no data has been read for the reporting period' do
291
+ Saulabs::Reportable::ReportCache.send(:find_value, @data, @data[0][0].next).should == 0.0
292
+ end
293
+
294
+ end
295
+
296
+ end