computron 0.1.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.
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