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,163 @@
1
+ module Saulabs
2
+
3
+ module Reportable
4
+
5
+ # The +ReportCache+ class is a regular +ActiveRecord+ model and represents cached results for single {Saulabs::Reportable::ReportingPeriod}s.
6
+ # +ReportCache+ instances are identified by the combination of +model_name+, +report_name+, +grouping+, +aggregation+ and +reporting_period+.
7
+ #
8
+ class ReportCache < ActiveRecord::Base
9
+
10
+ set_table_name :reportable_cache
11
+
12
+ self.skip_time_zone_conversion_for_attributes = [:reporting_period]
13
+
14
+ # Clears the cache for the specified +klass+ and +report+
15
+ #
16
+ # @param [Class] klass
17
+ # the model the report to clear the cache for works on
18
+ # @param [Symbol] report
19
+ # the name of the report to clear the cache for
20
+ #
21
+ # @example Clearing the cache for a report
22
+ #
23
+ # class User < ActiveRecord::Base
24
+ # reportable :registrations
25
+ # end
26
+ #
27
+ # Saulabs::Reportable::ReportCache.clear_for(User, :registrations)
28
+ #
29
+ def self.clear_for(klass, report)
30
+ self.delete_all(:conditions => {
31
+ :model_name => klass.name,
32
+ :report_name => report.to_s
33
+ })
34
+ end
35
+
36
+ # Processes the report using the respective cache.
37
+ #
38
+ # @param [Saulabe::Reportable::Report] report
39
+ # the report to process
40
+ # @param [Hash] options
41
+ # options for the report
42
+ #
43
+ # @option options [Symbol] :grouping (:day)
44
+ # the period records are grouped in (+:hour+, +:day+, +:week+, +:month+); <b>Beware that <tt>reportable</tt> treats weeks as starting on monday!</b>
45
+ # @option options [Fixnum] :limit (100)
46
+ # the number of reporting periods to get (see +:grouping+)
47
+ # @option options [Hash] :conditions ({})
48
+ # conditions like in +ActiveRecord::Base#find+; only records that match these conditions are reported;
49
+ # @option options [Boolean] :live_data (false)
50
+ # specifies whether data for the current reporting period is to be read; <b>if +:live_data+ is +true+, you will experience a performance hit since the request cannot be satisfied from the cache alone</b>
51
+ # @option options [DateTime, Boolean] :end_date (false)
52
+ # when specified, the report will only include data for the +:limit+ reporting periods until this date.
53
+ #
54
+ # @return [Array<Array<DateTime, Float>>]
55
+ # the result of the report as pairs of {DateTime}s and {Float}s
56
+ #
57
+ def self.process(report, options, &block)
58
+ raise ArgumentError.new('A block must be given') unless block_given?
59
+ self.transaction do
60
+ cached_data = read_cached_data(report, options)
61
+ new_data = read_new_data(cached_data, options, &block)
62
+ prepare_result(new_data, cached_data, report, options)
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ def self.prepare_result(new_data, cached_data, report, options)
69
+ new_data = new_data.map { |data| [ReportingPeriod.from_db_string(options[:grouping], data[0]), data[1]] }
70
+ cached_data.map! { |cached| [ReportingPeriod.new(options[:grouping], cached.reporting_period), cached.value] }
71
+ current_reporting_period = ReportingPeriod.new(options[:grouping])
72
+ reporting_period = get_first_reporting_period(options)
73
+ result = []
74
+ while reporting_period < (options[:end_date] ? ReportingPeriod.new(options[:grouping], options[:end_date]).next : current_reporting_period)
75
+ if cached = cached_data.find { |cached| reporting_period == cached[0] }
76
+ result << [cached[0].date_time, cached[1]]
77
+ else
78
+ new_cached = build_cached_data(report, options[:grouping], options[:conditions], reporting_period, find_value(new_data, reporting_period))
79
+ new_cached.save!
80
+ result << [reporting_period.date_time, new_cached.value]
81
+ end
82
+ reporting_period = reporting_period.next
83
+ end
84
+ if options[:live_data]
85
+ result << [current_reporting_period.date_time, find_value(new_data, current_reporting_period)]
86
+ end
87
+ result
88
+ end
89
+
90
+ def self.find_value(data, reporting_period)
91
+ data = data.detect { |d| d[0] == reporting_period }
92
+ data ? data[1] : 0.0
93
+ end
94
+
95
+ def self.build_cached_data(report, grouping, condition, reporting_period, value)
96
+ self.new(
97
+ :model_name => report.klass.to_s,
98
+ :report_name => report.name.to_s,
99
+ :grouping => grouping.identifier.to_s,
100
+ :aggregation => report.aggregation.to_s,
101
+ :condition => condition.to_s,
102
+ :reporting_period => reporting_period.date_time,
103
+ :value => value
104
+ )
105
+ end
106
+
107
+ def self.read_cached_data(report, options)
108
+ conditions = [
109
+ 'model_name = ? AND report_name = ? AND grouping = ? AND aggregation = ? AND `condition` = ?',
110
+ report.klass.to_s,
111
+ report.name.to_s,
112
+ options[:grouping].identifier.to_s,
113
+ report.aggregation.to_s,
114
+ options[:conditions].to_s
115
+ ]
116
+ first_reporting_period = get_first_reporting_period(options)
117
+ last_reporting_period = get_last_reporting_period(options)
118
+ if last_reporting_period
119
+ conditions.first << ' AND reporting_period BETWEEN ? AND ?'
120
+ conditions << first_reporting_period.date_time
121
+ conditions << last_reporting_period.date_time
122
+ else
123
+ conditions.first << ' AND reporting_period >= ?'
124
+ conditions << first_reporting_period.date_time
125
+ end
126
+ self.all(
127
+ :conditions => conditions,
128
+ :limit => options[:limit],
129
+ :order => 'reporting_period ASC'
130
+ )
131
+ end
132
+
133
+ def self.read_new_data(cached_data, options, &block)
134
+ if !options[:live_data] && cached_data.length == options[:limit]
135
+ []
136
+ else
137
+ first_reporting_period_to_read = if cached_data.length < options[:limit]
138
+ get_first_reporting_period(options)
139
+ else
140
+ ReportingPeriod.new(options[:grouping], cached_data.last.reporting_period).next
141
+ end
142
+ last_reporting_period_to_read = options[:end_date] ? ReportingPeriod.new(options[:grouping], options[:end_date]).last_date_time : nil
143
+ yield(first_reporting_period_to_read.date_time, last_reporting_period_to_read)
144
+ end
145
+ end
146
+
147
+ def self.get_first_reporting_period(options)
148
+ if options[:end_date]
149
+ ReportingPeriod.first(options[:grouping], options[:limit] - 1, options[:end_date])
150
+ else
151
+ ReportingPeriod.first(options[:grouping], options[:limit])
152
+ end
153
+ end
154
+
155
+ def self.get_last_reporting_period(options)
156
+ return ReportingPeriod.new(options[:grouping], options[:end_date]) if options[:end_date]
157
+ end
158
+
159
+ end
160
+
161
+ end
162
+
163
+ end
@@ -0,0 +1,181 @@
1
+ module Saulabs
2
+
3
+ module Reportable
4
+
5
+ # A reporting period is a specific hour or a specific day etc. depending on the used {Saulabs::Reportable::Grouping}.
6
+ #
7
+ class ReportingPeriod
8
+
9
+ # The actual +DateTime the reporting period represents
10
+ #
11
+ attr_reader :date_time
12
+
13
+ # The {Saulabs::Reportable::Grouping} of the reporting period
14
+ #
15
+ attr_reader :grouping
16
+
17
+ # Initializes a new reporting period.
18
+ #
19
+ # @param [Saulabs::Reportable::Grouping] grouping
20
+ # the grouping the generate the reporting period for
21
+ # @param [DateTime] date_time
22
+ # the +DateTime+ to generate the reporting period for
23
+ #
24
+ def initialize(grouping, date_time = nil)
25
+ @grouping = grouping
26
+ @date_time = parse_date_time(date_time || DateTime.now)
27
+ end
28
+
29
+ # Gets a reporting period relative to the current one.
30
+ #
31
+ # @param [Fixnum] offset
32
+ # the offset to get the reporting period for
33
+ #
34
+ # @return [Saulabs::Reportable::ReportingPeriod]
35
+ # the reporting period relative by offset to the current one
36
+ #
37
+ # @example Getting the reporting period one week later
38
+ #
39
+ # reporting_period = Saulabs::Reportable::ReportingPeriod.new(:week, DateTime.now)
40
+ # next_week = reporting_period.offset(1)
41
+ #
42
+ def offset(offset)
43
+ self.class.new(@grouping, @date_time + offset.send(@grouping.identifier))
44
+ end
45
+
46
+ # Gets the first reporting period for a grouping and a limit (optionally relative to and end date).
47
+ #
48
+ # @param [Saulabs::ReportingPeriod::Grouping] grouping
49
+ # the grouping to get the first reporting period for
50
+ # @param [Fixnum] limit
51
+ # the limit to get the first reporting period for
52
+ # @param [DateTime] end_date
53
+ # the end date to get the first reporting period for (the first reporting period is then +end_date+ - +limit+ * +grouping+)
54
+ #
55
+ # @return [Saulabs::Reportable::ReportingPeriod]
56
+ # the first reporting period for the grouping, limit and optionally end date
57
+ #
58
+ def self.first(grouping, limit, end_date = nil)
59
+ self.new(grouping, end_date).offset(-limit)
60
+ end
61
+
62
+ # Gets a reporting period from a DB date string.
63
+ #
64
+ # @param [Saulabs::Reportable::Grouping] grouping
65
+ # the grouping to get the reporting period for
66
+ # @param [String] db_string
67
+ # the DB string to parse and get the reporting period for
68
+ #
69
+ # @return [Saulabs::Reportable::ReportingPeriod]
70
+ # the reporting period for the {Saulabs::Reportable::Grouping} as parsed from the db string
71
+ #
72
+ def self.from_db_string(grouping, db_string)
73
+ parts = grouping.date_parts_from_db_string(db_string)
74
+ result = case grouping.identifier
75
+ when :hour
76
+ self.new(grouping, DateTime.new(parts[0], parts[1], parts[2], parts[3], 0, 0))
77
+ when :day
78
+ self.new(grouping, Date.new(parts[0], parts[1], parts[2]))
79
+ when :week
80
+ self.new(grouping, Date.commercial(parts[0], parts[1], 1))
81
+ when :month
82
+ self.new(grouping, Date.new(parts[0], parts[1], 1))
83
+ end
84
+ result
85
+ end
86
+
87
+ # Gets the next reporting period.
88
+ #
89
+ # @return [Saulabs::Reportable::ReportingPeriod]
90
+ # the reporting period after the current one
91
+ #
92
+ def next
93
+ self.offset(1)
94
+ end
95
+
96
+ # Gets the previous reporting period.
97
+ #
98
+ # @return [Saulabs::Reportable::ReportingPeriod]
99
+ # the reporting period before the current one
100
+ #
101
+ def previous
102
+ self.offset(-1)
103
+ end
104
+
105
+ # Gets whether the reporting period +other+ is equal to the current one.
106
+ #
107
+ # @param [Saulabs::Reportable::ReportingPeriod] other
108
+ # the reporting period to check for whether it is equal to the current one
109
+ #
110
+ # @return [Boolean]
111
+ # true if +other+ is equal to the current reporting period, false otherwise
112
+ #
113
+ def ==(other)
114
+ if other.is_a?(Saulabs::Reportable::ReportingPeriod)
115
+ @date_time.to_s == other.date_time.to_s && @grouping.identifier.to_s == other.grouping.identifier.to_s
116
+ elsif other.is_a?(Time) || other.is_a?(DateTime)
117
+ @date_time == parse_date_time(other)
118
+ else
119
+ raise ArgumentError.new("Can only compare instances of #{self.class.name}")
120
+ end
121
+ end
122
+
123
+ # Gets whether the reporting period +other+ is smaller to the current one.
124
+ #
125
+ # @param [Saulabs::Reportable::ReportingPeriod] other
126
+ # the reporting period to check for whether it is smaller to the current one
127
+ #
128
+ # @return [Boolean]
129
+ # true if +other+ is smaller to the current reporting period, false otherwise
130
+ #
131
+ def <(other)
132
+ if other.is_a?(Saulabs::Reportable::ReportingPeriod)
133
+ return @date_time < other.date_time
134
+ elsif other.is_a?(Time) || other.is_a?(DateTime)
135
+ @date_time < parse_date_time(other)
136
+ else
137
+ raise ArgumentError.new("Can only compare instances of #{self.class.name}")
138
+ end
139
+ end
140
+
141
+ # Gets the latest point in time that is included the reporting period. The latest point in time included in a reporting period
142
+ # for grouping hour would be that hour and 59 minutes and 59 seconds.
143
+ #
144
+ # @return [DateTime]
145
+ # the latest point in time that is included in the reporting period
146
+ #
147
+ def last_date_time
148
+ case @grouping.identifier
149
+ when :hour
150
+ DateTime.new(@date_time.year, @date_time.month, @date_time.day, @date_time.hour, 59, 59)
151
+ when :day
152
+ DateTime.new(@date_time.year, @date_time.month, @date_time.day, 23, 59, 59)
153
+ when :week
154
+ date_time = (@date_time - @date_time.wday.days) + 7.days
155
+ Date.new(date_time.year, date_time.month, date_time.day)
156
+ when :month
157
+ Date.new(@date_time.year, @date_time.month, (Date.new(@date_time.year, 12, 31) << (12 - @date_time.month)).day)
158
+ end
159
+ end
160
+
161
+ private
162
+
163
+ def parse_date_time(date_time)
164
+ case @grouping.identifier
165
+ when :hour
166
+ DateTime.new(date_time.year, date_time.month, date_time.day, date_time.hour)
167
+ when :day
168
+ date_time.to_date
169
+ when :week
170
+ date_time = (date_time - date_time.wday.days) + 1.day
171
+ Date.new(date_time.year, date_time.month, date_time.day)
172
+ when :month
173
+ Date.new(date_time.year, date_time.month, 1)
174
+ end
175
+ end
176
+
177
+ end
178
+
179
+ end
180
+
181
+ end
@@ -0,0 +1,62 @@
1
+ module Saulabs
2
+
3
+ module Reportable
4
+
5
+ module SparklineTagHelper
6
+
7
+ # Renders a sparkline with the given data.
8
+ #
9
+ # @param [Array<Array<DateTime, Float>>] data
10
+ # an array of report data as returned by {Saulabs::Reportable::Report#run}
11
+ # @param [Hash] options
12
+ # options for the sparkline
13
+ #
14
+ # @option options [Fixnum] :width (300)
15
+ # the width of the generated image
16
+ # @option options [Fixnum] :height (34)
17
+ # the height of the generated image
18
+ # @option options [String] :line_color ('0077cc')
19
+ # the line color of the generated image
20
+ # @option options [String] :fill_color ('e6f2fa')
21
+ # the fill color of the generated image
22
+ # @option options [Array<Symbol>] :labels ([])
23
+ # the axes to render lables for (Array of +:x+, +:y+, +:r+, +:t+; this is x axis, y axis, right, top)
24
+ # @option options [String] :alt ('')
25
+ # the alt attribute for the generated image
26
+ # @option options [String] :title ('')
27
+ # the title attribute for the generated image
28
+ #
29
+ # @return [String]
30
+ # an image tag showing a sparkline for the passed +data+
31
+ #
32
+ # @example Rendering a sparkline tag for report data
33
+ #
34
+ # <%= sparkline_tag(User.registrations_report, :width => 200, :height => 100, :color => '000') %>
35
+ #
36
+ def sparkline_tag(data, options = {})
37
+ options.reverse_merge!({ :width => 300, :height => 34, :line_color => '0077cc', :fill_color => 'e6f2fa', :labels => [], :alt => '', :title => '' })
38
+ data = data.collect { |d| d[1] }
39
+ labels = ''
40
+ unless options[:labels].empty?
41
+ chxr = {}
42
+ options[:labels].each_with_index do |l, i|
43
+ chxr[l] = "#{i}," + ([:x, :t].include?(l) ? "0,#{data.length}" : "#{[data.min, 0].min},#{data.max}")
44
+ end
45
+ labels = "&chxt=#{options[:labels].map(&:to_s).join(',')}&chxr=#{options[:labels].collect{|l| chxr[l]}.join('|')}"
46
+ end
47
+ title = ''
48
+ unless options[:title].empty?
49
+ title = "&chtt=#{options[:title]}"
50
+ end
51
+ image_tag(
52
+ "http://chart.apis.google.com/chart?cht=ls&chs=#{options[:width]}x#{options[:height]}&chd=t:#{data.join(',')}&chco=#{options[:line_color]}&chm=B,#{options[:fill_color]},0,0,0&chls=1,0,0&chds=#{data.min},#{data.max}#{labels}#{title}",
53
+ :alt => options[:alt],
54
+ :title => options[:title]
55
+ )
56
+ end
57
+
58
+ end
59
+
60
+ end
61
+
62
+ end
@@ -0,0 +1,9 @@
1
+ require 'saulabs/reportable'
2
+
3
+ ActiveRecord::Base.class_eval do
4
+ include Saulabs::Reportable
5
+ end
6
+
7
+ ActionView::Base.class_eval do
8
+ include Saulabs::Reportable::SparklineTagHelper
9
+ end
@@ -0,0 +1,25 @@
1
+ plugin_root = File.join(File.dirname(__FILE__), '..')
2
+
3
+ gem 'rails'
4
+ require 'active_record'
5
+ require 'active_support'
6
+ require 'action_controller'
7
+ require 'action_view'
8
+
9
+ $:.unshift "#{plugin_root}/lib"
10
+
11
+ RAILS_ROOT = File.expand_path(File.dirname(__FILE__) + '/../')
12
+ Rails::Initializer.run(:set_load_path)
13
+ Rails::Initializer.run(:set_autoload_paths)
14
+ Rails::Initializer.run(:initialize_time_zone) do |config|
15
+ config.time_zone = 'Pacific Time (US & Canada)'
16
+ end
17
+
18
+ require File.join(File.dirname(__FILE__), '..', 'rails', 'init.rb')
19
+
20
+ FileUtils.mkdir_p File.join(File.dirname(__FILE__), 'log')
21
+ ActiveRecord::Base.logger = Logger.new(File.join(File.dirname(__FILE__), 'log', 'spec.log'))
22
+
23
+ databases = YAML::load(IO.read(File.join(File.dirname(__FILE__), 'db', 'database.yml')))
24
+ ActiveRecord::Base.establish_connection(databases['sqlite3'])
25
+ load(File.join(File.dirname(__FILE__), 'db', 'schema.rb'))
@@ -0,0 +1,169 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'spec_helper')
2
+
3
+ describe Saulabs::Reportable::CumulatedReport do
4
+
5
+ before do
6
+ @report = Saulabs::Reportable::CumulatedReport.new(User, :cumulated_registrations)
7
+ end
8
+
9
+ describe '#run' do
10
+
11
+ it 'should cumulate the data' do
12
+ @report.should_receive(:cumulate).once
13
+
14
+ @report.run
15
+ end
16
+
17
+ it 'should return an array of the same length as the specified limit when :live_data is false' do
18
+ @report = Saulabs::Reportable::CumulatedReport.new(User, :cumulated_registrations, :limit => 10, :live_data => false)
19
+
20
+ @report.run.length.should == 10
21
+ end
22
+
23
+ it 'should return an array of the same length as the specified limit + 1 when :live_data is true' do
24
+ @report = Saulabs::Reportable::CumulatedReport.new(User, :cumulated_registrations, :limit => 10, :live_data => true)
25
+
26
+ @report.run.length.should == 11
27
+ end
28
+
29
+ for grouping in [:hour, :day, :week, :month] do
30
+
31
+ describe "for grouping #{grouping.to_s}" do
32
+
33
+ [true, false].each do |live_data|
34
+
35
+ describe "with :live_data = #{live_data}" do
36
+
37
+ before(:all) do
38
+ User.delete_all
39
+ User.create!(:login => 'test 1', :created_at => Time.now, :profile_visits => 2)
40
+ User.create!(:login => 'test 2', :created_at => Time.now - 1.send(grouping), :profile_visits => 1)
41
+ User.create!(:login => 'test 3', :created_at => Time.now - 3.send(grouping), :profile_visits => 2)
42
+ User.create!(:login => 'test 4', :created_at => Time.now - 3.send(grouping), :profile_visits => 3)
43
+ end
44
+
45
+ describe 'the returned result' do
46
+
47
+ before do
48
+ @grouping = Saulabs::Reportable::Grouping.new(grouping)
49
+ @report = Saulabs::Reportable::CumulatedReport.new(User, :cumulated_registrations,
50
+ :grouping => grouping,
51
+ :limit => 10,
52
+ :live_data => live_data
53
+ )
54
+ @result = @report.run
55
+ end
56
+
57
+ it "should be an array starting reporting period (Time.now - limit.#{grouping.to_s})" do
58
+ @result.first[0].should == Saulabs::Reportable::ReportingPeriod.new(@grouping, Time.now - 10.send(grouping)).date_time
59
+ end
60
+
61
+ if live_data
62
+ it "should be data ending with the current reporting period" do
63
+ @result.last[0].should == Saulabs::Reportable::ReportingPeriod.new(@grouping).date_time
64
+ end
65
+ else
66
+ it "should be data ending with the reporting period before the current" do
67
+ @result.last[0].should == Saulabs::Reportable::ReportingPeriod.new(@grouping).previous.date_time
68
+ end
69
+ end
70
+
71
+ end
72
+
73
+ it 'should return correct data for aggregation :count' do
74
+ @report = Saulabs::Reportable::CumulatedReport.new(User, :registrations,
75
+ :aggregation => :count,
76
+ :grouping => grouping,
77
+ :limit => 10,
78
+ :live_data => live_data
79
+ )
80
+ result = @report.run
81
+
82
+ result[10][1].should == 4.0 if live_data
83
+ result[9][1].should == 3.0
84
+ result[8][1].should == 2.0
85
+ result[7][1].should == 2.0
86
+ result[6][1].should == 0.0
87
+ end
88
+
89
+ it 'should return correct data for aggregation :sum' do
90
+ @report = Saulabs::Reportable::CumulatedReport.new(User, :registrations,
91
+ :aggregation => :sum,
92
+ :grouping => grouping,
93
+ :value_column => :profile_visits,
94
+ :limit => 10,
95
+ :live_data => live_data
96
+ )
97
+ result = @report.run()
98
+
99
+ result[10][1].should == 8.0 if live_data
100
+ result[9][1].should == 6.0
101
+ result[8][1].should == 5.0
102
+ result[7][1].should == 5.0
103
+ result[6][1].should == 0.0
104
+ end
105
+
106
+ it 'should return correct data for aggregation :count when custom conditions are specified' do
107
+ @report = Saulabs::Reportable::CumulatedReport.new(User, :registrations,
108
+ :aggregation => :count,
109
+ :grouping => grouping,
110
+ :limit => 10,
111
+ :live_data => live_data
112
+ )
113
+ result = @report.run(:conditions => ['login IN (?)', ['test 1', 'test 2', 'test 4']])
114
+
115
+ result[10][1].should == 3.0 if live_data
116
+ result[9][1].should == 2.0
117
+ result[8][1].should == 1.0
118
+ result[7][1].should == 1.0
119
+ result[6][1].should == 0.0
120
+ end
121
+
122
+ it 'should return correct data for aggregation :sum when custom conditions are specified' do
123
+ @report = Saulabs::Reportable::CumulatedReport.new(User, :registrations,
124
+ :aggregation => :sum,
125
+ :grouping => grouping,
126
+ :value_column => :profile_visits,
127
+ :limit => 10,
128
+ :live_data => live_data
129
+ )
130
+ result = @report.run(:conditions => ['login IN (?)', ['test 1', 'test 2', 'test 4']])
131
+
132
+ result[10][1].should == 6.0 if live_data
133
+ result[9][1].should == 4.0
134
+ result[8][1].should == 3.0
135
+ result[7][1].should == 3.0
136
+ result[6][1].should == 0.0
137
+ end
138
+
139
+ end
140
+
141
+ after(:all) do
142
+ User.destroy_all
143
+ end
144
+
145
+ end
146
+
147
+ end
148
+
149
+ end
150
+
151
+ after(:each) do
152
+ Saulabs::Reportable::ReportCache.destroy_all
153
+ end
154
+
155
+ end
156
+
157
+ describe '#cumulate' do
158
+
159
+ it 'should correctly cumulate the given data' do
160
+ first = (Time.now - 1.week).to_s
161
+ second = Time.now.to_s
162
+ data = [[first, 1], [second, 2]]
163
+
164
+ @report.send(:cumulate, data, @report.send(:options_for_run, {})).should == [[first, 1.0], [second, 3.0]]
165
+ end
166
+
167
+ end
168
+
169
+ end