computron 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.bundle/config ADDED
@@ -0,0 +1,2 @@
1
+ ---
2
+ BUNDLE_WITHOUT: ""
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ gem 'eventmachine'
2
+ gem 'em-http-request'
3
+ gem 'colored'
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Brad Gessler
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,13 @@
1
+ = computron
2
+
3
+ Computron is like ab on steroids, implemented in Ruby using em-http-request. It was designed out of a need to simulate real users on a website. It also does a few smart things like decode JSON into a Ruby hash so you can use those responses in your next requests.
4
+
5
+ = The DSL
6
+
7
+ Computron is a simple DLS around HTTP em that makes more sense to write user tests.
8
+
9
+ = Report Summary
10
+
11
+ == Copyright
12
+
13
+ Copyright (c) 2010 Brad Gessler. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,48 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "computron"
8
+ gem.summary = %Q{An evented HTTP computron}
9
+ gem.description = %Q{Computron was created to help Poll Everywhere peform load tests that simulate how real users work.}
10
+ gem.email = "brad@bradgessler.com"
11
+ gem.homepage = "http://github.com/bradgessler/computron"
12
+ gem.authors = ["Brad Gessler"]
13
+ gem.add_dependency "eventmachine", ">= 0.12.10"
14
+ gem.add_dependency "em-http-request", ">= 0.2.7"
15
+ gem.add_dependency "colored", ">= 1.1"
16
+ gem.add_development_dependency "rspec", ">= 1.2.9"
17
+ gem.add_development_dependency "yard", ">= 0"
18
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
19
+ end
20
+ Jeweler::GemcutterTasks.new
21
+ rescue LoadError
22
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
23
+ end
24
+
25
+ require 'spec/rake/spectask'
26
+ Spec::Rake::SpecTask.new(:spec) do |spec|
27
+ spec.libs << 'lib' << 'spec'
28
+ spec.spec_files = FileList['spec/**/*_spec.rb']
29
+ end
30
+
31
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
32
+ spec.libs << 'lib' << 'spec'
33
+ spec.pattern = 'spec/**/*_spec.rb'
34
+ spec.rcov = true
35
+ end
36
+
37
+ task :spec => :check_dependencies
38
+
39
+ task :default => :spec
40
+
41
+ begin
42
+ require 'yard'
43
+ YARD::Rake::YardocTask.new
44
+ rescue LoadError
45
+ task :yardoc do
46
+ abort "YARD is not available. In order to run yardoc, you must: sudo gem install yard"
47
+ end
48
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
data/bin/computron ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ computron_dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
4
+ $LOAD_PATH.unshift(computron_dir) unless $LOAD_PATH.include?(computron_dir)
5
+
6
+ require 'rubygems'
7
+ require 'computron'
8
+
9
+ simulation = Computron::DSL.new(File.read(ARGV.last))
10
+
11
+ Signal.trap('INT'){
12
+ EM.stop
13
+ }
14
+
15
+ simulation.run!
data/computron.gemspec ADDED
@@ -0,0 +1,76 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{computron}
8
+ s.version = "0.1.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Brad Gessler"]
12
+ s.date = %q{2010-05-24}
13
+ s.default_executable = %q{computron}
14
+ s.description = %q{Computron was created to help Poll Everywhere peform load tests that simulate how real users work.}
15
+ s.email = %q{brad@bradgessler.com}
16
+ s.executables = ["computron"]
17
+ s.extra_rdoc_files = [
18
+ "LICENSE",
19
+ "README.rdoc"
20
+ ]
21
+ s.files = [
22
+ ".bundle/config",
23
+ ".document",
24
+ ".gitignore",
25
+ "Gemfile",
26
+ "LICENSE",
27
+ "README.rdoc",
28
+ "Rakefile",
29
+ "VERSION",
30
+ "bin/computron",
31
+ "computron.gemspec",
32
+ "lib/computron.rb",
33
+ "lib/computron/client.rb",
34
+ "lib/computron/dsl.rb",
35
+ "lib/computron/report.rb",
36
+ "spec/computron_spec.rb",
37
+ "spec/spec.opts",
38
+ "spec/spec_helper.rb",
39
+ "test.rb"
40
+ ]
41
+ s.homepage = %q{http://github.com/bradgessler/computron}
42
+ s.rdoc_options = ["--charset=UTF-8"]
43
+ s.require_paths = ["lib"]
44
+ s.rubygems_version = %q{1.3.6}
45
+ s.summary = %q{An evented HTTP computron}
46
+ s.test_files = [
47
+ "spec/computron_spec.rb",
48
+ "spec/spec_helper.rb"
49
+ ]
50
+
51
+ if s.respond_to? :specification_version then
52
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
53
+ s.specification_version = 3
54
+
55
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
56
+ s.add_runtime_dependency(%q<eventmachine>, [">= 0.12.10"])
57
+ s.add_runtime_dependency(%q<em-http-request>, [">= 0.2.7"])
58
+ s.add_runtime_dependency(%q<colored>, [">= 1.1"])
59
+ s.add_development_dependency(%q<rspec>, [">= 1.2.9"])
60
+ s.add_development_dependency(%q<yard>, [">= 0"])
61
+ else
62
+ s.add_dependency(%q<eventmachine>, [">= 0.12.10"])
63
+ s.add_dependency(%q<em-http-request>, [">= 0.2.7"])
64
+ s.add_dependency(%q<colored>, [">= 1.1"])
65
+ s.add_dependency(%q<rspec>, [">= 1.2.9"])
66
+ s.add_dependency(%q<yard>, [">= 0"])
67
+ end
68
+ else
69
+ s.add_dependency(%q<eventmachine>, [">= 0.12.10"])
70
+ s.add_dependency(%q<em-http-request>, [">= 0.2.7"])
71
+ s.add_dependency(%q<colored>, [">= 1.1"])
72
+ s.add_dependency(%q<rspec>, [">= 1.2.9"])
73
+ s.add_dependency(%q<yard>, [">= 0"])
74
+ end
75
+ end
76
+
@@ -0,0 +1,186 @@
1
+ require 'logger'
2
+ require 'eventmachine'
3
+ require 'em-http'
4
+ require 'colored'
5
+
6
+ module Computron
7
+ class Client
8
+ class Response
9
+
10
+ attr_reader :status
11
+
12
+ def initialize(opts={})
13
+ @status = {}
14
+ yield self if block_given?
15
+ end
16
+
17
+ def call(http)
18
+ if callback = status[http.response_header.status]
19
+ callback.call(decode_response(http))
20
+ end
21
+ @finished.call if @finished
22
+ end
23
+
24
+ # A shortcut for specifying a callback on an HTTP code if you don't know the status name.
25
+ def code(code, &block)
26
+ status[code] = block
27
+ end
28
+
29
+ # Runs after every HTTP request
30
+ def finished(&block)
31
+ @finished = block
32
+ end
33
+
34
+ # Returns a hash containing the response code to the status like {200 => ok}.
35
+ def self.http_status_codes
36
+ @http_status_codes ||= %(100 continue
37
+ 101 switching_protocols
38
+ 200 ok
39
+ 201 created
40
+ 202 accepted
41
+ 203 non_authoritive_information
42
+ 204 no_content
43
+ 205 reset_content
44
+ 206 partial_content
45
+ 300 multiple_choices
46
+ 301 moved_permanently
47
+ 302 found
48
+ 303 see_other
49
+ 304 not_modified
50
+ 305 use_proxy
51
+ 307 temporary_redirect
52
+ 400 bad_request
53
+ 401 unauthorized
54
+ 402 payment_required
55
+ 403 forbidden
56
+ 404 not_found
57
+ 405 method_not_allowed
58
+ 406 not_acceptable
59
+ 407 proxy_authentication_required
60
+ 408 request_timeout
61
+ 409 conflict
62
+ 410 gone
63
+ 411 length_required
64
+ 412 precondition_failed
65
+ 413 request_entity_too_large
66
+ 414 request_uri_too_long
67
+ 415 unsupported_media_type
68
+ 416 request_range_not_satisfiable
69
+ 417 expectation_failed
70
+ 500 internal_server_error
71
+ 501 not_implemented
72
+ 502 bad_gateway
73
+ 503 service_unavailable
74
+ 504 gateway_timeout
75
+ 505 http_version_not_supported).inject({}) do |hash, line|
76
+ code, status = line.split(' ')
77
+ hash[code.to_i] = status
78
+ hash
79
+ end
80
+ end
81
+
82
+ http_status_codes.each do |code, status|
83
+ eval %(
84
+ def #{status}(&block)
85
+ status[#{code}] = block
86
+ end)
87
+ end
88
+
89
+ private
90
+ def decode_response(http)
91
+ content_type = http.response_header['CONTENT_TYPE']
92
+
93
+ case content_type
94
+ when %r{^application/json}
95
+ response = ActiveSupport::JSON.decode(http.response)
96
+ else
97
+ response = http.response # Do nothing, just return text
98
+ end
99
+
100
+ rescue
101
+ raise StandardError.new("Problem decoding '#{content_type}': \n#{http.response}")
102
+ end
103
+ end
104
+
105
+ attr_accessor :cookie, :name, :logger
106
+
107
+ def initialize
108
+ @logger = Logger.new($stdout)
109
+ yield self if block_given?
110
+ end
111
+
112
+ def long_poll(url, opts={}, &block)
113
+ response = Response.new
114
+ request = EM::HttpRequest.new(url).get(opts)
115
+
116
+ request.callback {|http|
117
+ persist_cookie(http)
118
+ repoll = Struct.new(:url).new(url)
119
+ block.call(response, repoll) if block_given?
120
+ response.call(http)
121
+ log_request(http, "Repolling")
122
+ long_poll(repoll.url, &block)
123
+ }
124
+ request.errback {|http|
125
+ log_request(http, "Long poll timed-out. Repolling.", 'yellow')
126
+ }
127
+
128
+
129
+ # get url do |response|
130
+ # repoll = Struct.new(:url).new(url)
131
+ # block.call(response, repoll)
132
+ # response.finished {
133
+ # logger.info "Repolling".yellow
134
+ # long_poll(repoll.url, &block)
135
+ # }
136
+ # end
137
+ end
138
+
139
+ # HTTP requests
140
+ %w[get put post delete head].each do |http_meth|
141
+ eval %{
142
+ def #{http_meth}(*args, &block)
143
+ request(:#{http_meth}, *args, &block)
144
+ end}
145
+ end
146
+
147
+ private
148
+ def log_request(http, extra=nil, color=nil)
149
+ status = http.response_header.status.to_s
150
+ if color
151
+ status = status.send(color)
152
+ else
153
+ case status
154
+ when 500
155
+ status = status.red
156
+ when 0, 400..499
157
+ status = status.yellow
158
+ else
159
+ status = status.green
160
+ end
161
+ end
162
+ logger.info "#{status.ljust(3)} #{http.method.ljust(4)} #{http.uri}#{" (#{extra})".magenta if extra}\n #{http.response}\n"
163
+ end
164
+
165
+ # Simplifies em-http-client into something more suitable for tests. Interpets JSON, etc.
166
+ def request(http_meth, url, opts={}, &block)
167
+ response = Response.new
168
+ request = EM::HttpRequest.new(url).send(http_meth, opts)
169
+
170
+ request.callback {|http|
171
+ persist_cookie(http)
172
+ block.call(response) if block_given?
173
+ response.call(http)
174
+ log_request(http)
175
+ }
176
+ request.errback {|http|
177
+ log_request(http, "Network error or timeout.")
178
+ }
179
+ end
180
+
181
+ # Maintains session for mock users
182
+ def persist_cookie(http)
183
+ self.cookie = http.response_header['SET_COOKIE'] if http.response_header.include?('SET_COOKIE')
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,47 @@
1
+ module Computron
2
+ class DSL
3
+
4
+ attr_reader :client
5
+
6
+ def initialize(code=nil, &block)
7
+ @client = Client.new
8
+ @simulation = Proc.new { code ? eval(code, self.send(:binding)) : instance_eval(&block) }
9
+ end
10
+
11
+ def run!
12
+ EM::run { @simulation.call }
13
+ end
14
+
15
+ def halt!
16
+ EM::stop
17
+ end
18
+
19
+ def default_host(host=nil)
20
+ host ? @default_host = host : @default_host
21
+ end
22
+
23
+ def every(seconds, opts={}, &block)
24
+ timer = EventMachine::PeriodicTimer.new(0) do
25
+ timer.interval = seconds
26
+ block.call(timer)
27
+ end
28
+ end
29
+
30
+ # HTTP utility methods. These are not meant to behave like a client
31
+ %w[get put post delete head].each do |http_meth|
32
+ eval %{
33
+ def #{http_meth}(*args, &block)
34
+ client.send(:#{http_meth}, *args, &block)
35
+ end}
36
+ end
37
+
38
+ # Deal with paths and URLs
39
+ def url(path)
40
+ url = String.new
41
+ url.concat "http://#{default_host}" unless url =~ %r{^(\w+)://} if default_host
42
+ url.concat path =~ /^\// ? path : "/#{path}" # Deal with leading /\
43
+ url
44
+ end
45
+
46
+ end
47
+ end
@@ -0,0 +1,48 @@
1
+ # TODO integrate this puppy into the runner
2
+ module Computron
3
+ class Report
4
+
5
+ class Request
6
+
7
+ class Response
8
+ attr_accessor :headers, :status, :at
9
+ end
10
+
11
+ attr_accessor :uri, :response, :headers, :at, :method
12
+
13
+ def response
14
+ yield @response if block_given?
15
+ @response
16
+ end
17
+
18
+ def duration
19
+ @duration ||= response.at.to_f - at.to_f if at and response.at
20
+ end
21
+
22
+ def initialize
23
+ @response = Response.new
24
+ yield self if block_given?
25
+ end
26
+
27
+ end
28
+
29
+ attr_reader :requests
30
+
31
+ def initialize
32
+ @requests = []
33
+ end
34
+
35
+ def by_status
36
+ requests.inject({}) do |memo, request|
37
+ memo[request.response.status] ||= []
38
+ memo[request.response.status] << request
39
+ memo
40
+ end
41
+ end
42
+
43
+ def sample(&block)
44
+ requests << Request.new(&block)
45
+ end
46
+
47
+ end
48
+ end
data/lib/computron.rb ADDED
@@ -0,0 +1,5 @@
1
+ $stdout.sync = true
2
+
3
+ require 'computron/client'
4
+ require 'computron/dsl'
5
+ require 'computron/report'
@@ -0,0 +1,7 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe "Computron" do
4
+ it "fails" do
5
+ fail "hey buddy, you should probably rename this file and start specing for real"
6
+ end
7
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,9 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+ require 'computron'
4
+ require 'spec'
5
+ require 'spec/autorun'
6
+
7
+ Spec::Runner.configure do |config|
8
+
9
+ end
data/test.rb ADDED
@@ -0,0 +1,34 @@
1
+ require '/Users/bgessler/Projects/imprezo/web/config/environment.rb'
2
+ require 'lib/computron'
3
+
4
+ clients_count = 2
5
+ post_interval = 10
6
+
7
+ clients = clients_count.times.map{ Computron::Client.new }
8
+
9
+ default_host 'localhost:3000'
10
+
11
+ get url('/events/1.json') do |response|
12
+ response.ok {|json|
13
+ event_id = json['event']['id']
14
+ ticks_url = url("/events/#{event_id}/ticks/stream.json")
15
+
16
+ clients.each do |client|
17
+ client.long_poll ticks_url do |response, repoll|
18
+ response.ok {|ticks|
19
+ if last_tick = ticks.last
20
+ repoll.url = "#{ticks_url}?since_id=#{last_tick['routing']['id']}"
21
+ end
22
+ }
23
+ end
24
+
25
+ every post_interval do
26
+ client.post url("/events/#{event_id}/comments"), :body => Comment.new(:response => 'Hey!').to_json, :head => {'Content-Type' => 'application/json'} do |response|
27
+ # response.ok { p "Super fun" }
28
+ # response.found { p "Redirected" }
29
+ # response.code(405) { p "Method not supported jagoff" }
30
+ end
31
+ end
32
+ end
33
+ }
34
+ end
metadata ADDED
@@ -0,0 +1,147 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: computron
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 0
9
+ version: 0.1.0
10
+ platform: ruby
11
+ authors:
12
+ - Brad Gessler
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-05-24 00:00:00 -07:00
18
+ default_executable: computron
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: eventmachine
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 0
29
+ - 12
30
+ - 10
31
+ version: 0.12.10
32
+ type: :runtime
33
+ version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ name: em-http-request
36
+ prerelease: false
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ segments:
42
+ - 0
43
+ - 2
44
+ - 7
45
+ version: 0.2.7
46
+ type: :runtime
47
+ version_requirements: *id002
48
+ - !ruby/object:Gem::Dependency
49
+ name: colored
50
+ prerelease: false
51
+ requirement: &id003 !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ segments:
56
+ - 1
57
+ - 1
58
+ version: "1.1"
59
+ type: :runtime
60
+ version_requirements: *id003
61
+ - !ruby/object:Gem::Dependency
62
+ name: rspec
63
+ prerelease: false
64
+ requirement: &id004 !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ segments:
69
+ - 1
70
+ - 2
71
+ - 9
72
+ version: 1.2.9
73
+ type: :development
74
+ version_requirements: *id004
75
+ - !ruby/object:Gem::Dependency
76
+ name: yard
77
+ prerelease: false
78
+ requirement: &id005 !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ segments:
83
+ - 0
84
+ version: "0"
85
+ type: :development
86
+ version_requirements: *id005
87
+ description: Computron was created to help Poll Everywhere peform load tests that simulate how real users work.
88
+ email: brad@bradgessler.com
89
+ executables:
90
+ - computron
91
+ extensions: []
92
+
93
+ extra_rdoc_files:
94
+ - LICENSE
95
+ - README.rdoc
96
+ files:
97
+ - .bundle/config
98
+ - .document
99
+ - .gitignore
100
+ - Gemfile
101
+ - LICENSE
102
+ - README.rdoc
103
+ - Rakefile
104
+ - VERSION
105
+ - bin/computron
106
+ - computron.gemspec
107
+ - lib/computron.rb
108
+ - lib/computron/client.rb
109
+ - lib/computron/dsl.rb
110
+ - lib/computron/report.rb
111
+ - spec/computron_spec.rb
112
+ - spec/spec.opts
113
+ - spec/spec_helper.rb
114
+ - test.rb
115
+ has_rdoc: true
116
+ homepage: http://github.com/bradgessler/computron
117
+ licenses: []
118
+
119
+ post_install_message:
120
+ rdoc_options:
121
+ - --charset=UTF-8
122
+ require_paths:
123
+ - lib
124
+ required_ruby_version: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - ">="
127
+ - !ruby/object:Gem::Version
128
+ segments:
129
+ - 0
130
+ version: "0"
131
+ required_rubygems_version: !ruby/object:Gem::Requirement
132
+ requirements:
133
+ - - ">="
134
+ - !ruby/object:Gem::Version
135
+ segments:
136
+ - 0
137
+ version: "0"
138
+ requirements: []
139
+
140
+ rubyforge_project:
141
+ rubygems_version: 1.3.6
142
+ signing_key:
143
+ specification_version: 3
144
+ summary: An evented HTTP computron
145
+ test_files:
146
+ - spec/computron_spec.rb
147
+ - spec/spec_helper.rb