red_trend 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .rspec
19
+ .rvmrc
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in red_trend.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ Copyright (c) 2012 Brendon Murphy
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+
data/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # RedTrend
2
+
3
+ Store your trend data in redis.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'red_trend'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install red_trend
18
+
19
+ ## Usage
20
+
21
+ ```ruby
22
+ require 'red_trend'
23
+
24
+ red_trend = RedTrend.new(:prefix => "project:9")
25
+ red_trend.record("post_views", 42)
26
+ red_trend.record("post_views", 53)
27
+ red_trend.record("post_views", 53)
28
+ red_trend.top("post_views") # => ["53", "42"]
29
+ ```
30
+
31
+ TODO add configuration examples here
32
+
33
+ ## How it works
34
+
35
+ TODO
36
+
37
+ ## Todos
38
+
39
+ Rip ActiveSupport entirely if possible; otherwise narrow the inclusion.
40
+
41
+ ## Contributing
42
+
43
+ 1. Fork it
44
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
45
+ 3. Create your specs and feature code
46
+ 4. Commit your changes (`git commit -am 'Added some feature'`)
47
+ 5. Push to the branch (`git push origin my-new-feature`)
48
+ 6. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
@@ -0,0 +1,3 @@
1
+ class RedTrend
2
+ VERSION = "0.0.1"
3
+ end
data/lib/red_trend.rb ADDED
@@ -0,0 +1,132 @@
1
+ require 'redis'
2
+ require "red_trend/version"
3
+ require 'active_support/all'
4
+
5
+ class RedTrend
6
+ CYCLE_UNITS = {
7
+ :minute => 60,
8
+ :hour => 3600,
9
+ :day => 86400
10
+ }.freeze
11
+
12
+ class << self
13
+ attr_writer :redis
14
+
15
+ def redis
16
+ @redis ||= Redis.new
17
+ end
18
+ end
19
+
20
+ attr_reader :options
21
+
22
+ def initialize(options = {})
23
+ @options = options
24
+ unless CYCLE_UNITS.include?(cycle_unit)
25
+ raise ArgumentError, "cycle unit must be in #{CYCLE_UNITS.keys}"
26
+ end
27
+ end
28
+
29
+ # How many cycles we'll persist data for
30
+ def cycles_count
31
+ @options.fetch(:cycles_count, 3)
32
+ end
33
+
34
+ def cycle_length
35
+ CYCLE_UNITS[cycle_unit]
36
+ end
37
+
38
+ # The length of 1 cycle
39
+ def cycle_unit
40
+ @options.fetch(:cycle_unit, :hour)
41
+ end
42
+
43
+ # The interval length of each cycle, probably
44
+ # days for production
45
+ def cycle_interval
46
+ cycle_length * cycles_count
47
+ end
48
+
49
+ def current_cycle
50
+ method = case cycle_unit
51
+ when :minute then :min
52
+ when :hour then :hour
53
+ when :day then :yday
54
+ end
55
+
56
+ time = @time || Time.now
57
+ (time.send(method) % cycles_count) + 1
58
+ end
59
+
60
+ def cycle_positions
61
+ 1.upto(cycles_count - 1).inject([current_cycle]) do |n_cycles, offset|
62
+ n = current_cycle - offset
63
+ n = n + cycles_count if n < 1
64
+ n_cycles << n
65
+ end
66
+ end
67
+
68
+ # Increment the leaderboard score for the object on
69
+ # the sorted set for the current cycle. Make the key
70
+ # volitile so that it doesn't persist past the number
71
+ # of cycles. Store the union score set after each
72
+ # new score
73
+ def record(key, member)
74
+ @time = Time.now
75
+
76
+ n_key = make_key(key, current_cycle)
77
+
78
+ zcard = redis.zcard(n_key)
79
+
80
+ redis.multi do
81
+ redis.zincrby n_key, 1, member
82
+ # FIXME I'm not certain the second redis.ttl is working in the multi
83
+ if zcard < 1 || redis.ttl(n_key) == -1
84
+ redis.expire n_key, calculate_expire_seconds
85
+ end
86
+ unionize_sets(key)
87
+ end
88
+
89
+ @time = nil
90
+
91
+ true
92
+ end
93
+
94
+ # Returns top scoring ids for the current interval
95
+ def top(key, limit = 10)
96
+ redis.zrevrange make_key(key), 0, limit - 1
97
+ end
98
+
99
+ def weight_offset
100
+ ("%0.1f" % (1 / cycles_count.to_f)).to_f
101
+ end
102
+
103
+ def cycle_weights
104
+ cycles_count.times.inject([]) do |weights, n|
105
+ weights << 1 - n * weight_offset
106
+ end
107
+ end
108
+
109
+ private
110
+
111
+ def make_key(*args)
112
+ key_parts = [options[:prefix]]
113
+ key_parts.concat Array(args)
114
+ key_parts.compact.join(":")
115
+ end
116
+
117
+ def redis
118
+ self.class.redis
119
+ end
120
+
121
+ # The time in seconds which the current cycle key should expire in
122
+ def calculate_expire_seconds
123
+ ((@time.to_f / cycle_length).floor * cycle_length + cycle_interval) - @time.to_i
124
+ end
125
+
126
+ # Take the scores from all current cycle sets and store
127
+ # them as a union
128
+ def unionize_sets(key)
129
+ keys = cycle_positions.collect { |n| make_key(key, n) }
130
+ redis.zunionstore(make_key(key), keys, :weights => cycle_weights)
131
+ end
132
+ end
data/red_trend.gemspec ADDED
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/red_trend/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Brendon Murphy"]
6
+ gem.email = ["xternal1+github@gmail.com"]
7
+ gem.description = %q{Store your trend data in redis}
8
+ gem.summary = gem.description
9
+ gem.homepage = ""
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "red_trend"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = RedTrend::VERSION
17
+
18
+ gem.add_dependency "redis"
19
+ gem.add_dependency "active_support"
20
+ gem.add_dependency "i18n"
21
+
22
+ gem.add_development_dependency "rake"
23
+ gem.add_development_dependency "rspec"
24
+ gem.add_development_dependency "integration_test_redis"
25
+ gem.add_development_dependency "timecop"
26
+ end
@@ -0,0 +1,275 @@
1
+ require 'spec_helper'
2
+
3
+ describe RedTrend, "redis connection config at the class level" do
4
+ let(:redis) { stub }
5
+
6
+ it "can instantiate a default localhost redis instance" do
7
+ RedTrend.redis = nil
8
+ Redis.should_receive(:new).with().and_return(redis)
9
+ RedTrend.redis.should == redis
10
+ end
11
+
12
+ it "can be overrided with a redis connection" do
13
+ RedTrend.redis = redis
14
+ RedTrend.redis.should == redis
15
+ end
16
+ end
17
+
18
+ describe RedTrend, "defaults" do
19
+ subject { RedTrend.new }
20
+
21
+ it "cycles_count to 3" do
22
+ subject.cycles_count.should == 3
23
+ end
24
+
25
+ it "cycle_unit to :hour" do
26
+ subject.cycle_unit.should == :hour
27
+ end
28
+
29
+ it "cycle_length to 3 hours" do
30
+ subject.cycle_interval.to_i.should == 3 * 60 * 60
31
+ end
32
+ end
33
+
34
+ describe RedTrend, "overriding defaults" do
35
+ subject { RedTrend.new(:cycles_count => 2, :cycle_unit => :minute) }
36
+
37
+ it "allows setting the cycles_count" do
38
+ subject.cycles_count.should == 2
39
+ end
40
+
41
+ it "allows setting the cycle_length" do
42
+ subject.cycle_length.should == 60
43
+ end
44
+
45
+ it "accepts the cycle_length of :minute, :hour, or :day" do
46
+ [:minute, :hour, :day].each do |unit|
47
+ lambda {
48
+ RedTrend.new(:cycle_length => unit)
49
+ }.should_not raise_error
50
+ end
51
+ end
52
+
53
+ it "raises an argument error for an unknown cycle length" do
54
+ lambda {
55
+ RedTrend.new(:cycle_unit => :foobar)
56
+ }.should raise_error(ArgumentError)
57
+ end
58
+ end
59
+
60
+ describe RedTrend, "cycle interval" do
61
+ context "for a cycle_count of 2 and cycle_unit of :minute" do
62
+ it "is 120" do
63
+ subject = RedTrend.new(:cycles_count => 2, :cycle_unit => :minute)
64
+ subject.cycle_interval.should == 120
65
+ end
66
+ end
67
+
68
+ context "for a cycle_count of 4 and cycle_length of 1 day" do
69
+ it "is 345600" do
70
+ subject = RedTrend.new(:cycles_count => 4, :cycle_unit => :day)
71
+ subject.cycle_interval.should == 345600
72
+ end
73
+ end
74
+ end
75
+
76
+ describe RedTrend, "getting the current cycle" do
77
+ before do
78
+ Timecop.freeze("2012-07-30 18:00:00 -0700")
79
+ end
80
+
81
+ context "for a default RedTrend" do
82
+ subject { RedTrend.new }
83
+
84
+ it "cycles from 1 through 3 on intervals based on the hour" do
85
+ subject.current_cycle.should == 1
86
+ Timecop.travel(3600)
87
+ subject.current_cycle.should == 2
88
+ Timecop.travel(3600)
89
+ subject.current_cycle.should == 3
90
+ Timecop.travel(3600)
91
+ subject.current_cycle.should == 1
92
+ end
93
+
94
+ it "cycles from 2, 3, 1 on intervals based on the hour" do
95
+ Timecop.freeze("2012-07-30 19:00:00 -0700")
96
+ subject.current_cycle.should == 2
97
+ Timecop.travel(3600)
98
+ subject.current_cycle.should == 3
99
+ Timecop.travel(3600)
100
+ subject.current_cycle.should == 1
101
+ Timecop.travel(3600)
102
+ subject.current_cycle.should == 2
103
+ end
104
+
105
+ it "behaves when the time does not fall on the boundary" do
106
+ subject.current_cycle.should == 1
107
+ Timecop.travel(1800)
108
+ subject.current_cycle.should == 1
109
+ Timecop.travel(1800)
110
+ subject.current_cycle.should == 2
111
+ end
112
+ end
113
+
114
+ context "for a RedTrend of 4 cycles of 60 seconds" do
115
+ subject { RedTrend.new(:cycles_count => 4, :cycle_unit => :minute) }
116
+
117
+ it "cycles from 1 through 4 on intervals based on the minute" do
118
+ subject.current_cycle.should == 1
119
+ Timecop.travel(60)
120
+ subject.current_cycle.should == 2
121
+ Timecop.travel(60)
122
+ subject.current_cycle.should == 3
123
+ Timecop.travel(60)
124
+ subject.current_cycle.should == 4
125
+ Timecop.travel(60)
126
+ subject.current_cycle.should == 1
127
+ end
128
+ end
129
+
130
+ context "for a RedTrend of 2 cycles of 1 day" do
131
+ subject { RedTrend.new(:cycles_count => 2, :cycle_unit => :day) }
132
+
133
+ it "cycles from 1 through 2 on intervals based on the day" do
134
+ subject.current_cycle.should == 1
135
+ Timecop.travel(86400)
136
+ subject.current_cycle.should == 2
137
+ Timecop.travel(86400)
138
+ subject.current_cycle.should == 1
139
+ end
140
+ end
141
+
142
+ context "for a RedTrend of 3 cycles of 1 day starting with Dec 31" do
143
+ subject { RedTrend.new(:cycles_count => 3, :cycle_unit => :day) }
144
+
145
+ it "cycles from 1 through 2 on intervals based on the day" do
146
+ Timecop.freeze("2012-12-31 18:00:00 -0700")
147
+ subject.current_cycle.should == 1
148
+ Timecop.travel(86400)
149
+ subject.current_cycle.should == 2
150
+ Timecop.travel(86400)
151
+ subject.current_cycle.should == 3
152
+ Timecop.travel(86400)
153
+ subject.current_cycle.should == 1
154
+ end
155
+ end
156
+ end
157
+
158
+ describe RedTrend, "getting an array of cycle numbers" do
159
+ subject { RedTrend.new :cycles_count => 4 }
160
+
161
+ before do
162
+ Timecop.freeze("2012-07-30 18:00:00 -0700")
163
+ end
164
+
165
+ it "starts with the current cycle to the last cycle and wraps back around" do
166
+ subject.cycle_positions.should == [3, 2, 1, 4]
167
+ end
168
+ end
169
+
170
+ describe RedTrend, "recording" do
171
+ subject { RedTrend.new }
172
+ let(:redis) { IntegrationTestRedis.client }
173
+
174
+ before do
175
+ Timecop.freeze("2012-07-30 18:00:00 -0700")
176
+ end
177
+
178
+ it "increments the score for the member at the current zset key" do
179
+ subject.record("foobar", 42)
180
+ redis.zscore("foobar:1", "42").should == 1
181
+ subject.record("foobar", 42)
182
+ redis.zscore("foobar:1", "42").should == 2
183
+ end
184
+
185
+ it "prefixes the key if the prefix option was set" do
186
+ subject.options[:prefix] = "fizz:buzz"
187
+ subject.record("foobar", 42)
188
+ redis.zscore("fizz:buzz:foobar:1", "42").should == 1
189
+ end
190
+
191
+ it "sets the zset to expire when the cycle wraps back around to it" do
192
+ subject.record("foobar", 42)
193
+ redis.ttl("foobar:1").should == 10800
194
+
195
+ Timecop.travel(1800)
196
+ subject.record("foobar", 42)
197
+ redis.ttl("foobar:1").should == 10800
198
+
199
+ Timecop.travel(1800)
200
+ subject.record("foobar", 42)
201
+ redis.ttl("foobar:2").should == 10800
202
+ end
203
+
204
+ it "creates a union set from the current zsets" do
205
+ redis.exists("foobar").should be_false
206
+ subject.record("foobar", 42)
207
+ redis.exists("foobar").should be_true
208
+ end
209
+ end
210
+
211
+ describe RedTrend, "storing a union off all cycles" do
212
+ subject { RedTrend.new }
213
+ let(:redis) { IntegrationTestRedis.client }
214
+
215
+ before do
216
+ Timecop.freeze("2012-07-30 18:00:00 -0700")
217
+
218
+ # Cycle 1
219
+ subject.record("foobar", 1)
220
+ Timecop.travel(1800)
221
+ subject.record("foobar", 2)
222
+
223
+ # Cycle 2
224
+ Timecop.travel(1800)
225
+ subject.record("foobar", 3)
226
+
227
+ # Cycle 3
228
+ Timecop.travel(3600)
229
+ subject.record("foobar", 4)
230
+
231
+ # Cycle 1
232
+ Timecop.travel(3600)
233
+ subject.record("foobar", 5)
234
+
235
+ # Cycle 2
236
+ Timecop.travel(3600)
237
+ subject.record("foobar", 1)
238
+ end
239
+
240
+ it "unionizes the data from all the available cycles" do
241
+ redis.zrange("foobar", 0, -1).should =~ %w[1 2 3 4 5]
242
+ end
243
+
244
+ it "weights the unionization to prefer newer data" do
245
+ redis.zrevrangebyscore("foobar", '+inf', '-inf').should == %w[1 3 5 2 4]
246
+ end
247
+
248
+ it "builds a weight offset by dividing 1 by the cycles count and rounding to 1/10 precision" do
249
+ subject.weight_offset.should == 0.3
250
+ subject.options[:cycles_count] = 4
251
+ subject.weight_offset.should == 0.2
252
+ end
253
+
254
+ it "uses the weight offset to build decreasing weights by subtracting multiples of the offset" do
255
+ redis.zrevrangebyscore("foobar", '+inf', '-inf', :with_scores => true).map(&:last).should == [1.7, 1, 0.7, 0.7, 0.4]
256
+ end
257
+ end
258
+
259
+ describe RedTrend, "getting the current top ids" do
260
+ subject { RedTrend.new }
261
+
262
+ before do
263
+ 1.upto(11) do |n|
264
+ n.times { subject.record("foobar", n) }
265
+ end
266
+ end
267
+
268
+ it "returns the top 10 zrevrange of the union set" do
269
+ subject.top("foobar").should == %w[11 10 9 8 7 6 5 4 3 2]
270
+ end
271
+
272
+ it "can be given an optional limit" do
273
+ subject.top("foobar", 3).should == %w[11 10 9]
274
+ end
275
+ end
@@ -0,0 +1,16 @@
1
+ require 'rspec'
2
+ require 'timecop'
3
+
4
+ require File.expand_path("../../lib/red_trend", __FILE__)
5
+
6
+ require "integration_test_redis"
7
+ IntegrationTestRedis.start
8
+
9
+ RSpec.configure do |config|
10
+ config.mock_with :rspec
11
+
12
+ config.before(:each) do
13
+ RedTrend.redis = IntegrationTestRedis.client
14
+ IntegrationTestRedis.client.flushdb
15
+ end
16
+ end
metadata ADDED
@@ -0,0 +1,140 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: red_trend
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Brendon Murphy
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-08-03 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: redis
16
+ requirement: &2164433440 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *2164433440
25
+ - !ruby/object:Gem::Dependency
26
+ name: active_support
27
+ requirement: &2164433000 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *2164433000
36
+ - !ruby/object:Gem::Dependency
37
+ name: i18n
38
+ requirement: &2164432580 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *2164432580
47
+ - !ruby/object:Gem::Dependency
48
+ name: rake
49
+ requirement: &2164657580 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *2164657580
58
+ - !ruby/object:Gem::Dependency
59
+ name: rspec
60
+ requirement: &2164657120 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *2164657120
69
+ - !ruby/object:Gem::Dependency
70
+ name: integration_test_redis
71
+ requirement: &2164656140 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: *2164656140
80
+ - !ruby/object:Gem::Dependency
81
+ name: timecop
82
+ requirement: &2164655580 !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ! '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ type: :development
89
+ prerelease: false
90
+ version_requirements: *2164655580
91
+ description: Store your trend data in redis
92
+ email:
93
+ - xternal1+github@gmail.com
94
+ executables: []
95
+ extensions: []
96
+ extra_rdoc_files: []
97
+ files:
98
+ - .gitignore
99
+ - Gemfile
100
+ - LICENSE
101
+ - README.md
102
+ - Rakefile
103
+ - lib/red_trend.rb
104
+ - lib/red_trend/version.rb
105
+ - red_trend.gemspec
106
+ - spec/red_trend_spec.rb
107
+ - spec/spec_helper.rb
108
+ homepage: ''
109
+ licenses: []
110
+ post_install_message:
111
+ rdoc_options: []
112
+ require_paths:
113
+ - lib
114
+ required_ruby_version: !ruby/object:Gem::Requirement
115
+ none: false
116
+ requirements:
117
+ - - ! '>='
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ segments:
121
+ - 0
122
+ hash: 3032661298513805062
123
+ required_rubygems_version: !ruby/object:Gem::Requirement
124
+ none: false
125
+ requirements:
126
+ - - ! '>='
127
+ - !ruby/object:Gem::Version
128
+ version: '0'
129
+ segments:
130
+ - 0
131
+ hash: 3032661298513805062
132
+ requirements: []
133
+ rubyforge_project:
134
+ rubygems_version: 1.8.15
135
+ signing_key:
136
+ specification_version: 3
137
+ summary: Store your trend data in redis
138
+ test_files:
139
+ - spec/red_trend_spec.rb
140
+ - spec/spec_helper.rb