rhod 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.pryrc +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +164 -0
- data/Rakefile +8 -0
- data/lib/rhod.rb +9 -0
- data/lib/rhod/backoffs.rb +36 -0
- data/lib/rhod/command.rb +46 -0
- data/lib/rhod/version.rb +3 -0
- data/rhod.gemspec +25 -0
- data/test/helper.rb +17 -0
- data/test/test_command.rb +54 -0
- metadata +129 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: deae1b7b10203a606f6ba69985c0b0520e3ed666
|
4
|
+
data.tar.gz: e7485d2cb082e4697c55ac0af77ad59a94f5e9d2
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: fbb5a386678bbdb265d75323df5cbb8c8f77100a4224d814dbe7d3391367f7cbfeaee3b9e203de061a3d2a50eacbd0ce35d49c0d3ae4472c1427b113ea340c37
|
7
|
+
data.tar.gz: 69cab5ac043ac3806c37f47e55f39b5a42b91cc01b9015ddebda5ce91d7258dfe97a999a260225597405aef2ce082d99a4d671e6fd0daac79b7319d420d75580
|
data/.gitignore
ADDED
data/.pryrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require './lib/rhod'
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Paul Bergeron
|
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.
|
data/README.md
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
# Rhod
|
2
|
+
|
3
|
+
A Lightweight High Avalibility framework for Ruby, inspired by [Hystrix](https://github.com/Netflix/Hystrix)
|
4
|
+
|
5
|
+
> Korben Dallas: You guard this with your life, or you're gonna look like this guy here! You green?
|
6
|
+
>
|
7
|
+
> DJ Ruby Rhod: G-green.
|
8
|
+
>
|
9
|
+
> Korben Dallas: Super green?
|
10
|
+
>
|
11
|
+
> DJ Ruby Rhod: Super green.
|
12
|
+
|
13
|
+
Rhod helps you handle failures gracefully, even during a firefight. When your code has to interact with other services, it also means writing code to keep it running in the event of failure. Failures can include exceptions, timeouts, downed hosts, and any number of issues that are caused by events outside of your application.
|
14
|
+
|
15
|
+
Rhod allows you to fully customize how your application reacts when it can't reach a service it needs. but by default it is configured for a 'fail fast' scenario. With some configuration, Rhod can support the following failure scenarios and variations on them:
|
16
|
+
|
17
|
+
- Fail Fast
|
18
|
+
- Retry N times before Fail
|
19
|
+
- Retry N times with progressive backoffs before Fail
|
20
|
+
- Fail Silent
|
21
|
+
- Fail w/ Fallback
|
22
|
+
- Primary / Secondary ("hot spare") switch over
|
23
|
+
|
24
|
+
## Installation
|
25
|
+
|
26
|
+
Rhod requires Ruby 1.9.2 or greater.
|
27
|
+
|
28
|
+
Add this line to your application's Gemfile:
|
29
|
+
|
30
|
+
gem 'rhod'
|
31
|
+
|
32
|
+
And then execute:
|
33
|
+
|
34
|
+
$ bundle
|
35
|
+
|
36
|
+
Or install it yourself as:
|
37
|
+
|
38
|
+
$ gem install rhod
|
39
|
+
|
40
|
+
## Usage
|
41
|
+
|
42
|
+
Rhod has a very simple API. Design your application as you would normally, then enclose network accessing portions of your code with:
|
43
|
+
|
44
|
+
Rhod.execute do
|
45
|
+
...
|
46
|
+
end
|
47
|
+
|
48
|
+
This implements the "Fail Fast" scenario by default.
|
49
|
+
|
50
|
+
Example, open a remote reasource, fail immediately if it fails:
|
51
|
+
|
52
|
+
require 'open-uri'
|
53
|
+
require 'rhod'
|
54
|
+
|
55
|
+
Rhod.execute { open("http://google.com").read }
|
56
|
+
|
57
|
+
### Retries with and without backoffs
|
58
|
+
|
59
|
+
#### Idempotence Caution
|
60
|
+
|
61
|
+
Code within a `Rhod::Command` block with reties in use must be _idempotent_, i.e., safe to run multiple times.
|
62
|
+
|
63
|
+
Rhod supports retying up to N times. By default it uses a logarithmic backoff:
|
64
|
+
|
65
|
+
Rhod::Backoffs.default.take(5)
|
66
|
+
# [0.7570232465074598, 2.403267722339301, 3.444932048942182, 4.208673319629471, 4.811984719351674]
|
67
|
+
|
68
|
+
Rhod also comes with exponential and constant (always the same value) backoffs. You can also supply any Enumerator that produces a series of numbers. See `lib/rhod/backoffs.rb` for examples.
|
69
|
+
|
70
|
+
Example, open a remote reasource, fail once it has failed 10 times, with the default (logarithmic) backoff:
|
71
|
+
|
72
|
+
require 'open-uri'
|
73
|
+
require 'rhod'
|
74
|
+
|
75
|
+
Rhod::Command.execute(:retries => 10) { open("http://google.com").read }
|
76
|
+
|
77
|
+
Example, open a remote reasource, fail once it has failed 10 times, waiting 0.2 seconds between attempts:
|
78
|
+
|
79
|
+
require 'open-uri'
|
80
|
+
require 'rhod'
|
81
|
+
|
82
|
+
Rhod.execute(:retries => 10, :backoffs => Rhod::Backoffs.constant_backoff(0.2)) do
|
83
|
+
open("http://google.com").read
|
84
|
+
end
|
85
|
+
|
86
|
+
Example, open a remote reasource, fail once it has failed 10 times, with an exponetially growing wait time between attempts:
|
87
|
+
|
88
|
+
require 'open-uri'
|
89
|
+
require 'rhod'
|
90
|
+
|
91
|
+
Rhod.execute(:retries => 10, :backoffs => Rhod::Backoffs.expoential_backoffs) do
|
92
|
+
open("http://google.com").read
|
93
|
+
end
|
94
|
+
|
95
|
+
Example, open a remote reasource, fail once it has failed 10 times, with waiting between attempts:
|
96
|
+
|
97
|
+
require 'open-uri'
|
98
|
+
require 'rhod'
|
99
|
+
|
100
|
+
Rhod.execute(:retries => 10, :backoffs => Rhod::Backoffs.constant_backoff(0)) do
|
101
|
+
open("http://google.com").read
|
102
|
+
end
|
103
|
+
|
104
|
+
### Fail Silent
|
105
|
+
|
106
|
+
In the event of a failure, Rhod falls back to a `fallback`. The most basic case is to fall back to a constant value.
|
107
|
+
|
108
|
+
Example, open a remote reasource, if it fails return them empty string.
|
109
|
+
|
110
|
+
require 'open-uri'
|
111
|
+
require 'rhod'
|
112
|
+
|
113
|
+
Rhod.execute(:fallback => -> {""}) do
|
114
|
+
open("http://google.com").read
|
115
|
+
end
|
116
|
+
|
117
|
+
### Fail w/ Fallback
|
118
|
+
|
119
|
+
If there is another network call that can be used to fetch the reasource, it's possible to use another `Rhod::Command` once a failure has occurred.
|
120
|
+
|
121
|
+
require 'open-uri'
|
122
|
+
require 'rhod'
|
123
|
+
|
124
|
+
search_engine_fallback = Rhod::Command.new(
|
125
|
+
:fallback => -> {""} # couldn't get anything
|
126
|
+
) do
|
127
|
+
open("https://yahoo.com").read
|
128
|
+
end
|
129
|
+
|
130
|
+
Rhod.execute(:fallback => -> { search_engine_fallback.execute }) do
|
131
|
+
open("http://google.com").read
|
132
|
+
end
|
133
|
+
|
134
|
+
### Primary / Secondary ("Hot Spare") switch over
|
135
|
+
|
136
|
+
Sometimes the fallback is just a part of normal operation. Just code in the state of which back end to access.
|
137
|
+
|
138
|
+
require 'open-uri'
|
139
|
+
require 'rhod'
|
140
|
+
|
141
|
+
class SearchEngineHTML
|
142
|
+
attr_accessor :secondary
|
143
|
+
|
144
|
+
def fetch
|
145
|
+
url = !@secondary ? "http://google.com" : "https://yahoo.com"
|
146
|
+
|
147
|
+
Rhod.execute(url, :fallback => Proc.new { @secondary = !@secondary; fetch }) do |url|
|
148
|
+
open(url).read
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
search_engine_html = SearchEngineHTML.new
|
154
|
+
|
155
|
+
search_engine_html.fetch
|
156
|
+
|
157
|
+
## Contributing
|
158
|
+
|
159
|
+
1. Fork it
|
160
|
+
2. Create your feature branch (`git checkout -b feature/my-new-feature`)
|
161
|
+
3. Make your changes and add tests, verify they pass with (`bundle exec rake test`)
|
162
|
+
4. Commit your changes (`git commit -am 'Add some feature'`)
|
163
|
+
5. Push to the branch (`git push origin feature/my-new-feature`)
|
164
|
+
6. Create new Pull Request
|
data/Rakefile
ADDED
data/lib/rhod.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
module Rhod::Backoffs
|
2
|
+
|
3
|
+
extend self
|
4
|
+
# Returns a generator of a expoentially increasing series starting at 1
|
5
|
+
def expoential_backoffs
|
6
|
+
Enumerator.new do |yielder|
|
7
|
+
x = 0
|
8
|
+
loop do
|
9
|
+
x += 1
|
10
|
+
yielder << (1.0/2.0*(2.0**x - 1.0)).ceil
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Returns a generator of a logarithmicly increasing series starting at 0.3
|
16
|
+
def logarithmic_backoffs
|
17
|
+
Enumerator.new do |yielder|
|
18
|
+
x = 0.3
|
19
|
+
loop do
|
20
|
+
x += 1
|
21
|
+
yielder << Math.log2(x**2)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Always the same backoff
|
27
|
+
def constant_backoff(i)
|
28
|
+
Enumerator.new do |yielder|
|
29
|
+
loop do
|
30
|
+
yielder << i
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
alias default logarithmic_backoffs
|
36
|
+
end
|
data/lib/rhod/command.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
class Rhod::Command
|
2
|
+
|
3
|
+
EXCEPTIONS = [Exception, StandardError]
|
4
|
+
|
5
|
+
def initialize(*args, &block)
|
6
|
+
opts = args[-1].kind_of?(Hash) ? args.pop : {}
|
7
|
+
@args = args
|
8
|
+
@args ||= []
|
9
|
+
|
10
|
+
@request = block
|
11
|
+
|
12
|
+
@retries = opts[:retries]
|
13
|
+
@retries ||= 0
|
14
|
+
@attempts = 0
|
15
|
+
|
16
|
+
@backoffs = opts[:backoffs]
|
17
|
+
@backoffs ||= Rhod::Backoffs.default
|
18
|
+
|
19
|
+
@fallback = opts[:fallback]
|
20
|
+
end
|
21
|
+
|
22
|
+
### Class methods
|
23
|
+
|
24
|
+
def self.execute(*args, &block)
|
25
|
+
this = self.new(*args, &block)
|
26
|
+
this.execute
|
27
|
+
end
|
28
|
+
|
29
|
+
### Instance methods
|
30
|
+
|
31
|
+
def execute
|
32
|
+
begin
|
33
|
+
@request.call(*@args)
|
34
|
+
rescue *EXCEPTIONS
|
35
|
+
@attempts += 1
|
36
|
+
if @attempts <= @retries
|
37
|
+
sleep(@backoffs.next)
|
38
|
+
retry
|
39
|
+
else
|
40
|
+
return @fallback.call(*@args) if @fallback
|
41
|
+
raise
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
data/lib/rhod/version.rb
ADDED
data/rhod.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'rhod/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "rhod"
|
8
|
+
spec.version = Rhod::VERSION
|
9
|
+
spec.authors = ["Paul Bergeron"]
|
10
|
+
spec.email = ["paul.d.bergeron@gmail.com"]
|
11
|
+
spec.summary = %q{A High Avalibility framework for Ruby}
|
12
|
+
spec.homepage = "https://github.com/dinedal/rhod"
|
13
|
+
spec.license = "MIT"
|
14
|
+
|
15
|
+
spec.files = `git ls-files`.split($/)
|
16
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
21
|
+
spec.add_development_dependency "rake"
|
22
|
+
spec.add_development_dependency "pry"
|
23
|
+
spec.add_development_dependency "minitest"
|
24
|
+
spec.add_development_dependency "turn"
|
25
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'rhod'
|
2
|
+
require 'turn'
|
3
|
+
|
4
|
+
Turn.config do |c|
|
5
|
+
# use one of output formats:
|
6
|
+
# :outline - turn's original case/test outline mode [default]
|
7
|
+
# :progress - indicates progress with progress bar
|
8
|
+
# :dotted - test/unit's traditional dot-progress mode
|
9
|
+
# :pretty - new pretty reporter
|
10
|
+
# :marshal - dump output as YAML (normal run mode only)
|
11
|
+
# :cue - interactive testing
|
12
|
+
c.format = :pretty
|
13
|
+
# turn on invoke/execute tracing, enable full backtrace
|
14
|
+
c.trace = 100
|
15
|
+
# use humanized test names (works only with :outline format)
|
16
|
+
c.natural = true
|
17
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require File.expand_path(File.dirname(__FILE__) + '/helper')
|
3
|
+
|
4
|
+
describe Rhod::Command do
|
5
|
+
describe "self.execute" do
|
6
|
+
it "runs immediately and returns inner value" do
|
7
|
+
Rhod::Command.execute { 1 }.must_equal 1
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
describe "execute" do
|
12
|
+
it "retries requests" do
|
13
|
+
val = 0
|
14
|
+
|
15
|
+
begin
|
16
|
+
Rhod::Command.new(:retries => 1, :backoffs => Rhod::Backoffs.constant_backoff(0)) do
|
17
|
+
val += 1
|
18
|
+
raise StandardError
|
19
|
+
end.execute
|
20
|
+
rescue
|
21
|
+
end
|
22
|
+
|
23
|
+
val.must_equal 2
|
24
|
+
end
|
25
|
+
|
26
|
+
it "takes args" do
|
27
|
+
Rhod::Command.new(1) {|a| 1 + a}.execute.must_equal 2
|
28
|
+
end
|
29
|
+
|
30
|
+
describe "fallbacks" do
|
31
|
+
it "triggers fallback on failure" do
|
32
|
+
Rhod::Command.new(:fallback => -> { 1 }) {raise StandardError}.execute.must_equal 1
|
33
|
+
end
|
34
|
+
|
35
|
+
it "passes args to fallbacks" do
|
36
|
+
Rhod::Command.new(1, :fallback => ->(a) { 1 + a }) {raise StandardError}.execute.must_equal 2
|
37
|
+
end
|
38
|
+
|
39
|
+
it "only uses fallback after all retries" do
|
40
|
+
val = 0
|
41
|
+
|
42
|
+
Rhod::Command.new(
|
43
|
+
:retries => 1,
|
44
|
+
:backoffs => Rhod::Backoffs.constant_backoff(0),
|
45
|
+
:fallback => -> { 1 }) do
|
46
|
+
val += 1
|
47
|
+
raise StandardError
|
48
|
+
end.execute
|
49
|
+
|
50
|
+
val.must_equal 2
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
metadata
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rhod
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Paul Bergeron
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-04-05 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.3'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: pry
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: minitest
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: turn
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description:
|
84
|
+
email:
|
85
|
+
- paul.d.bergeron@gmail.com
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- .gitignore
|
91
|
+
- .pryrc
|
92
|
+
- Gemfile
|
93
|
+
- LICENSE.txt
|
94
|
+
- README.md
|
95
|
+
- Rakefile
|
96
|
+
- lib/rhod.rb
|
97
|
+
- lib/rhod/backoffs.rb
|
98
|
+
- lib/rhod/command.rb
|
99
|
+
- lib/rhod/version.rb
|
100
|
+
- rhod.gemspec
|
101
|
+
- test/helper.rb
|
102
|
+
- test/test_command.rb
|
103
|
+
homepage: https://github.com/dinedal/rhod
|
104
|
+
licenses:
|
105
|
+
- MIT
|
106
|
+
metadata: {}
|
107
|
+
post_install_message:
|
108
|
+
rdoc_options: []
|
109
|
+
require_paths:
|
110
|
+
- lib
|
111
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
112
|
+
requirements:
|
113
|
+
- - '>='
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '0'
|
116
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
117
|
+
requirements:
|
118
|
+
- - '>='
|
119
|
+
- !ruby/object:Gem::Version
|
120
|
+
version: '0'
|
121
|
+
requirements: []
|
122
|
+
rubyforge_project:
|
123
|
+
rubygems_version: 2.0.3
|
124
|
+
signing_key:
|
125
|
+
specification_version: 4
|
126
|
+
summary: A High Avalibility framework for Ruby
|
127
|
+
test_files:
|
128
|
+
- test/helper.rb
|
129
|
+
- test/test_command.rb
|