periodic_calculations 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 (58) hide show
  1. data/.gitignore +9 -0
  2. data/Gemfile +3 -0
  3. data/Gemfile.lock +120 -0
  4. data/MIT-LICENSE +20 -0
  5. data/README.md +8 -0
  6. data/Rakefile +24 -0
  7. data/lib/periodic_calculations/base.rb +36 -0
  8. data/lib/periodic_calculations/engine.rb +5 -0
  9. data/lib/periodic_calculations/lazy_query.rb +36 -0
  10. data/lib/periodic_calculations/query.rb +136 -0
  11. data/lib/periodic_calculations/query_options.rb +61 -0
  12. data/lib/periodic_calculations/version.rb +3 -0
  13. data/lib/periodic_calculations.rb +11 -0
  14. data/periodic_calculations.gemspec +28 -0
  15. data/spec/dummy/README.rdoc +28 -0
  16. data/spec/dummy/Rakefile +6 -0
  17. data/spec/dummy/app/assets/images/.keep +0 -0
  18. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  19. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  20. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  21. data/spec/dummy/app/controllers/concerns/.keep +0 -0
  22. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  23. data/spec/dummy/app/mailers/.keep +0 -0
  24. data/spec/dummy/app/models/.keep +0 -0
  25. data/spec/dummy/app/models/activity.rb +2 -0
  26. data/spec/dummy/app/models/concerns/.keep +0 -0
  27. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  28. data/spec/dummy/config/application.rb +23 -0
  29. data/spec/dummy/config/boot.rb +5 -0
  30. data/spec/dummy/config/database.yml +20 -0
  31. data/spec/dummy/config/environment.rb +5 -0
  32. data/spec/dummy/config/environments/development.rb +29 -0
  33. data/spec/dummy/config/environments/production.rb +80 -0
  34. data/spec/dummy/config/environments/test.rb +36 -0
  35. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  36. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  37. data/spec/dummy/config/initializers/inflections.rb +16 -0
  38. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  39. data/spec/dummy/config/initializers/secret_token.rb +12 -0
  40. data/spec/dummy/config/initializers/session_store.rb +3 -0
  41. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  42. data/spec/dummy/config/locales/en.yml +23 -0
  43. data/spec/dummy/config/routes.rb +56 -0
  44. data/spec/dummy/config.ru +4 -0
  45. data/spec/dummy/db/migrate/20131203172849_create_activities.rb +9 -0
  46. data/spec/dummy/db/schema.rb +26 -0
  47. data/spec/dummy/lib/assets/.keep +0 -0
  48. data/spec/dummy/log/.keep +0 -0
  49. data/spec/dummy/public/404.html +58 -0
  50. data/spec/dummy/public/422.html +58 -0
  51. data/spec/dummy/public/500.html +57 -0
  52. data/spec/dummy/public/favicon.ico +0 -0
  53. data/spec/model_spec.rb +82 -0
  54. data/spec/periodic_calculations/lazy_query_spec.rb +38 -0
  55. data/spec/periodic_calculations/query_options_spec.rb +82 -0
  56. data/spec/periodic_calculations/query_spec.rb +211 -0
  57. data/spec/spec_helper.rb +6 -0
  58. metadata +227 -0
@@ -0,0 +1,82 @@
1
+ describe PeriodicCalculations::QueryOptions do
2
+
3
+ it "initializes without errors when valid parameters" do
4
+ time = Time.now
5
+ query_options = described_class.new(:count, :id, time, time)
6
+ query_options.window_start.should == time
7
+ end
8
+
9
+ describe "raises ArgumentError when" do
10
+ it "no operation" do
11
+ expect {
12
+ described_class.new(nil, :id, Time.now, Time.now)
13
+ }.to raise_error(ArgumentError)
14
+ end
15
+
16
+ it "invalid operation" do
17
+ expect {
18
+ described_class.new(:xxx, :id, Time.now, Time.now)
19
+ }.to raise_error(ArgumentError)
20
+ end
21
+
22
+ it "no column_name" do
23
+ expect {
24
+ described_class.new(nil, nil, Time.now, Time.now)
25
+ }.to raise_error(ArgumentError)
26
+ end
27
+
28
+ it "no window start" do
29
+ expect {
30
+ described_class.new(:count, :id, nil, Time.now)
31
+ }.to raise_error(ArgumentError)
32
+ end
33
+
34
+ it "no window end" do
35
+ expect {
36
+ described_class.new(:count, :id, Time.now, nil)
37
+ }.to raise_error(ArgumentError)
38
+ end
39
+
40
+ it "window start is not a Time" do
41
+ expect {
42
+ described_class.new(:count, :id, 3, Time.now)
43
+ }.to raise_error(ArgumentError)
44
+ end
45
+
46
+ it "window end is not a Time" do
47
+ expect {
48
+ described_class.new(:count, :id, Time.now, 3)
49
+ }.to raise_error(ArgumentError)
50
+ end
51
+
52
+ it "interval_unit is not valid" do
53
+ expect {
54
+ described_class.new(:count, :id, Time.now, Time.now, :interval_unit => '3days')
55
+ }.to raise_error(ArgumentError)
56
+ end
57
+ end
58
+
59
+ describe "defaults" do
60
+ let(:query_options) do
61
+ time = Time.now
62
+ described_class.new(:count, :id, time, time)
63
+ end
64
+
65
+ it "timestamp_column to :created_at" do
66
+ query_options.timestamp_column.should == :created_at
67
+ end
68
+
69
+ it "interval_unit to :day" do
70
+ query_options.interval_unit.should == :day
71
+ end
72
+
73
+ it "cumulative to false" do
74
+ query_options.cumulative.should be_false
75
+ end
76
+
77
+ it "timezone_offset to Rails timezone offset in seconds" do
78
+ query_options.timezone_offset.should == Time.now.in_time_zone.utc_offset
79
+ end
80
+ end
81
+
82
+ end
@@ -0,0 +1,211 @@
1
+ require 'spec_helper'
2
+
3
+ describe PeriodicCalculations::Query do
4
+
5
+ before { Activity.delete_all }
6
+
7
+ let(:scope) { Activity.all }
8
+ let(:time) { Time.now }
9
+
10
+ let(:operation) { :count }
11
+ let(:column_name) { :id }
12
+ let(:start_time) { time - 1.day }
13
+ let(:end_time) { time + 1.day }
14
+ let(:options) { {} }
15
+
16
+ describe "#to_sql" do
17
+ it "returns the sanitized_sql" do
18
+ query_options = PeriodicCalculations::QueryOptions.new(operation, column_name, start_time, end_time, options)
19
+ query = PeriodicCalculations::Query.new(scope, query_options)
20
+
21
+ query.stub(:sanitized_sql).and_return("wohoo")
22
+ query.to_sql.should == "wohoo"
23
+ end
24
+ end
25
+
26
+ describe "#execute" do
27
+
28
+ def execute(scope, *args)
29
+ query_options = PeriodicCalculations::QueryOptions.new(*args)
30
+ PeriodicCalculations::Query.new(scope, query_options).execute
31
+ end
32
+
33
+ describe "Intervals" do
34
+ it "should add missing values within range" do
35
+ execute(scope, operation, column_name, start_time, end_time, options).should have(3).items
36
+ end
37
+
38
+ it "should return one single day when same dates" do
39
+ start_time = time
40
+ end_time = time
41
+
42
+ execute(scope, operation, column_name, start_time, end_time, options).should have(1).items
43
+ end
44
+
45
+ it "should return two day when consecutive" do
46
+ start_time = time
47
+ end_time = time + 1.day
48
+
49
+ execute(scope, operation, column_name, start_time, end_time, options).should have(2).items
50
+ end
51
+
52
+ it "should operate by day" do
53
+ start_time = time
54
+ end_time = time + 2.day
55
+ options.merge!(:interval_unit => :day)
56
+
57
+ execute(scope, operation, column_name, start_time, end_time, options).should have(3).items
58
+ end
59
+
60
+ it "should operate by week" do
61
+ start_time = time
62
+ end_time = time + 1.week
63
+ options.merge!(:interval_unit => :week)
64
+
65
+ execute(scope, operation, column_name, start_time, end_time, options).should have(2).items
66
+ end
67
+
68
+ it "should operate by month" do
69
+ start_time = time.beginning_of_month
70
+ end_time = time + 4.months
71
+ options.merge!(:interval_unit => :month)
72
+
73
+ execute(scope, operation, column_name, start_time, end_time, options).should have(5).items
74
+ end
75
+ end
76
+
77
+ describe "Time window" do
78
+ it "should consider rows outside current scope" do
79
+ Activity.create(:quantity => 3, :created_at => time)
80
+ scope = Activity.where(:quantity => 3)
81
+
82
+ execute(scope, operation, column_name, start_time, end_time, options).map(&:last).should == [0, 1, 0]
83
+ end
84
+
85
+ it "should NOT consider rows outside current scope" do
86
+ Activity.create(:quantity => 0, :created_at => time)
87
+ scope = Activity.where(:quantity => 3)
88
+
89
+ execute(scope, operation, column_name, start_time, end_time, options).map(&:last).should == [0, 0, 0]
90
+ end
91
+
92
+ it "should operate with a custom timestamp column" do
93
+ Activity.create(:quantity => 3, :finished_at => time)
94
+ options[:timestamp_column] = :finished_at
95
+ execute(scope, operation, column_name, start_time, end_time, options).map(&:last).should == [0, 1, 0]
96
+ end
97
+
98
+ it "should return matching results taking timezone into account" do
99
+ Time.zone = ActiveSupport::TimeZone["Pacific Time (US & Canada)"]
100
+
101
+ Time.zone.name.should == "Pacific Time (US & Canada)" # ensure correctly set
102
+
103
+ # Outside left window limit
104
+ Activity.create(:quantity => 3, :created_at => start_time.beginning_of_day - 1.seconds)
105
+ # Inside by left window limit
106
+ Activity.create(:quantity => 3, :created_at => start_time.beginning_of_day + 1.seconds)
107
+ # Inside by right window limit
108
+ Activity.create(:quantity => 3, :created_at => end_time.end_of_day - 1.seconds)
109
+ # Outside by right window limit
110
+ Activity.create(:quantity => 3, :created_at => end_time.end_of_day + 1.seconds)
111
+
112
+ execute(scope, :count, column_name, start_time, end_time, options).map(&:last).should == [1, 0, 1]
113
+ end
114
+ end
115
+
116
+ describe "Operation: count" do
117
+ it "should count NON cumulatively" do
118
+ Activity.create(:created_at => time - 10.day)
119
+ Activity.create(:created_at => time)
120
+
121
+ execute(scope, operation, column_name, start_time, end_time, options).map(&:last).should == [0, 1, 0]
122
+ end
123
+
124
+ it "should count cumulatively" do
125
+ options[:cumulative] = true
126
+
127
+ # outside interval matters
128
+ Activity.create(:created_at => time - 10.day)
129
+
130
+ Activity.create(:created_at => time - 1.day)
131
+ Activity.create(:created_at => time)
132
+ Activity.create(:created_at => time + 1.day)
133
+
134
+ execute(scope, operation, column_name, start_time, end_time, options).map(&:last).should == [2, 3, 4]
135
+ end
136
+ end
137
+
138
+ describe "Operation: average" do
139
+ it "should calculate the average cumulatively" do
140
+ Activity.create(:quantity => 4, :created_at => time)
141
+ Activity.create(:quantity => 8, :created_at => time)
142
+
143
+ execute(scope, :avg, :quantity, start_time, end_time, options).map(&:last).should == [0, 6, 0]
144
+ end
145
+
146
+ it "should calculate the average NON cumulatively" do
147
+ Activity.create(:quantity => 4, :created_at => time - 10.days)
148
+ Activity.create(:quantity => 8, :created_at => time)
149
+
150
+ options[:cumulative] = true
151
+
152
+ execute(scope, :avg, :quantity, start_time, end_time, options).map(&:last).should == [4, 6, 6]
153
+ end
154
+ end
155
+
156
+ describe "Operation: sum" do
157
+ it "should calculate the sum NON cumulatively" do
158
+ Activity.create(:quantity => 4, :created_at => time)
159
+ Activity.create(:quantity => 8, :created_at => time)
160
+
161
+ execute(scope, :sum, :quantity, start_time, end_time, options).map(&:last).should == [0, 12, 0]
162
+ end
163
+
164
+ it "should calculate the sum cumulatively" do
165
+ options[:cumulative] = true
166
+
167
+ Activity.create(:quantity => 4, :created_at => time - 10.days)
168
+ Activity.create(:quantity => 8, :created_at => time)
169
+
170
+ execute(scope, :sum, :quantity, start_time, end_time, options).map(&:last).should == [4, 12, 12]
171
+ end
172
+ end
173
+
174
+ describe "Operation: minimum" do
175
+ it "should calculate the minimum no cumulatively" do
176
+ Activity.create(:quantity => 4, :created_at => time)
177
+ Activity.create(:quantity => 8, :created_at => time)
178
+
179
+ execute(scope, :min, :quantity, start_time, end_time, options).map(&:last).should == [0, 4, 0]
180
+ end
181
+
182
+ it "should calculate the minimum cumulatively" do
183
+ options[:cumulative] = true
184
+
185
+ Activity.create(:quantity => 8, :created_at => time - 10.days)
186
+ Activity.create(:quantity => 4, :created_at => time)
187
+
188
+ execute(scope, :min, :quantity, start_time, end_time, options).map(&:last).should == [8, 4, 4]
189
+ end
190
+ end
191
+
192
+ describe "Operation: maximum" do
193
+ it "should calculate the maximum NON cumulatively" do
194
+ Activity.create(:quantity => 4, :created_at => time)
195
+ Activity.create(:quantity => 8, :created_at => time)
196
+
197
+ execute(scope, :max, :quantity, start_time, end_time, options).map(&:last).should == [0, 8, 0]
198
+ end
199
+
200
+ it "should calculate the maximum cumulatively" do
201
+ options[:cumulative] = true
202
+
203
+ Activity.create(:quantity => 4, :created_at => time - 10.days)
204
+ Activity.create(:quantity => 8, :created_at => time)
205
+
206
+ execute(scope, :max, :quantity, start_time, end_time, options).map(&:last).should == [4, 8, 8]
207
+ end
208
+ end
209
+
210
+ end
211
+ end
@@ -0,0 +1,6 @@
1
+ require 'coveralls'
2
+ Coveralls.wear!
3
+
4
+ ENV["RAILS_ENV"] = "test"
5
+ require File.expand_path("../dummy/config/environment", __FILE__)
6
+ require 'rspec/rails'
metadata ADDED
@@ -0,0 +1,227 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: periodic_calculations
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Pol
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-12-07 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rails
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '3.2'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '3.2'
30
+ - !ruby/object:Gem::Dependency
31
+ name: railties
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rspec-rails
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: pg
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: coveralls
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ description: Periodic Calculations gem allows you to retrieve periodic results of
95
+ aggregates that can be accumulated over time with PostgreSQL.
96
+ email:
97
+ - polmiro@gmail.com
98
+ executables: []
99
+ extensions: []
100
+ extra_rdoc_files: []
101
+ files:
102
+ - .gitignore
103
+ - Gemfile
104
+ - Gemfile.lock
105
+ - MIT-LICENSE
106
+ - README.md
107
+ - Rakefile
108
+ - lib/periodic_calculations.rb
109
+ - lib/periodic_calculations/base.rb
110
+ - lib/periodic_calculations/engine.rb
111
+ - lib/periodic_calculations/lazy_query.rb
112
+ - lib/periodic_calculations/query.rb
113
+ - lib/periodic_calculations/query_options.rb
114
+ - lib/periodic_calculations/version.rb
115
+ - periodic_calculations.gemspec
116
+ - spec/dummy/README.rdoc
117
+ - spec/dummy/Rakefile
118
+ - spec/dummy/app/assets/images/.keep
119
+ - spec/dummy/app/assets/javascripts/application.js
120
+ - spec/dummy/app/assets/stylesheets/application.css
121
+ - spec/dummy/app/controllers/application_controller.rb
122
+ - spec/dummy/app/controllers/concerns/.keep
123
+ - spec/dummy/app/helpers/application_helper.rb
124
+ - spec/dummy/app/mailers/.keep
125
+ - spec/dummy/app/models/.keep
126
+ - spec/dummy/app/models/activity.rb
127
+ - spec/dummy/app/models/concerns/.keep
128
+ - spec/dummy/app/views/layouts/application.html.erb
129
+ - spec/dummy/config.ru
130
+ - spec/dummy/config/application.rb
131
+ - spec/dummy/config/boot.rb
132
+ - spec/dummy/config/database.yml
133
+ - spec/dummy/config/environment.rb
134
+ - spec/dummy/config/environments/development.rb
135
+ - spec/dummy/config/environments/production.rb
136
+ - spec/dummy/config/environments/test.rb
137
+ - spec/dummy/config/initializers/backtrace_silencers.rb
138
+ - spec/dummy/config/initializers/filter_parameter_logging.rb
139
+ - spec/dummy/config/initializers/inflections.rb
140
+ - spec/dummy/config/initializers/mime_types.rb
141
+ - spec/dummy/config/initializers/secret_token.rb
142
+ - spec/dummy/config/initializers/session_store.rb
143
+ - spec/dummy/config/initializers/wrap_parameters.rb
144
+ - spec/dummy/config/locales/en.yml
145
+ - spec/dummy/config/routes.rb
146
+ - spec/dummy/db/migrate/20131203172849_create_activities.rb
147
+ - spec/dummy/db/schema.rb
148
+ - spec/dummy/lib/assets/.keep
149
+ - spec/dummy/log/.keep
150
+ - spec/dummy/public/404.html
151
+ - spec/dummy/public/422.html
152
+ - spec/dummy/public/500.html
153
+ - spec/dummy/public/favicon.ico
154
+ - spec/model_spec.rb
155
+ - spec/periodic_calculations/lazy_query_spec.rb
156
+ - spec/periodic_calculations/query_options_spec.rb
157
+ - spec/periodic_calculations/query_spec.rb
158
+ - spec/spec_helper.rb
159
+ homepage: https://github.com/polmiro/periodic_calculations
160
+ licenses:
161
+ - MIT
162
+ post_install_message:
163
+ rdoc_options: []
164
+ require_paths:
165
+ - lib
166
+ required_ruby_version: !ruby/object:Gem::Requirement
167
+ none: false
168
+ requirements:
169
+ - - ! '>='
170
+ - !ruby/object:Gem::Version
171
+ version: '0'
172
+ required_rubygems_version: !ruby/object:Gem::Requirement
173
+ none: false
174
+ requirements:
175
+ - - ! '>='
176
+ - !ruby/object:Gem::Version
177
+ version: '0'
178
+ requirements: []
179
+ rubyforge_project:
180
+ rubygems_version: 1.8.23
181
+ signing_key:
182
+ specification_version: 3
183
+ summary: Retrieve periodic stats in periods of time
184
+ test_files:
185
+ - spec/dummy/README.rdoc
186
+ - spec/dummy/Rakefile
187
+ - spec/dummy/app/assets/images/.keep
188
+ - spec/dummy/app/assets/javascripts/application.js
189
+ - spec/dummy/app/assets/stylesheets/application.css
190
+ - spec/dummy/app/controllers/application_controller.rb
191
+ - spec/dummy/app/controllers/concerns/.keep
192
+ - spec/dummy/app/helpers/application_helper.rb
193
+ - spec/dummy/app/mailers/.keep
194
+ - spec/dummy/app/models/.keep
195
+ - spec/dummy/app/models/activity.rb
196
+ - spec/dummy/app/models/concerns/.keep
197
+ - spec/dummy/app/views/layouts/application.html.erb
198
+ - spec/dummy/config.ru
199
+ - spec/dummy/config/application.rb
200
+ - spec/dummy/config/boot.rb
201
+ - spec/dummy/config/database.yml
202
+ - spec/dummy/config/environment.rb
203
+ - spec/dummy/config/environments/development.rb
204
+ - spec/dummy/config/environments/production.rb
205
+ - spec/dummy/config/environments/test.rb
206
+ - spec/dummy/config/initializers/backtrace_silencers.rb
207
+ - spec/dummy/config/initializers/filter_parameter_logging.rb
208
+ - spec/dummy/config/initializers/inflections.rb
209
+ - spec/dummy/config/initializers/mime_types.rb
210
+ - spec/dummy/config/initializers/secret_token.rb
211
+ - spec/dummy/config/initializers/session_store.rb
212
+ - spec/dummy/config/initializers/wrap_parameters.rb
213
+ - spec/dummy/config/locales/en.yml
214
+ - spec/dummy/config/routes.rb
215
+ - spec/dummy/db/migrate/20131203172849_create_activities.rb
216
+ - spec/dummy/db/schema.rb
217
+ - spec/dummy/lib/assets/.keep
218
+ - spec/dummy/log/.keep
219
+ - spec/dummy/public/404.html
220
+ - spec/dummy/public/422.html
221
+ - spec/dummy/public/500.html
222
+ - spec/dummy/public/favicon.ico
223
+ - spec/model_spec.rb
224
+ - spec/periodic_calculations/lazy_query_spec.rb
225
+ - spec/periodic_calculations/query_options_spec.rb
226
+ - spec/periodic_calculations/query_spec.rb
227
+ - spec/spec_helper.rb