reportable 1.0.0

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