delano-tryouts 0.7.1 → 0.7.2
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/CHANGES.txt +10 -0
- data/README.rdoc +28 -17
- data/lib/tryouts/cli/run.rb +3 -3
- data/lib/tryouts/drill.rb +24 -9
- data/lib/tryouts/stats.rb +5 -1
- data/lib/tryouts/tryout.rb +4 -7
- data/lib/tryouts.rb +14 -5
- data/tryouts/01_mixins_tryouts.rb +2 -5
- data/tryouts/10_syntax_tryouts.rb +14 -10
- data/tryouts/50_class_context_tryouts.rb +2 -3
- data/tryouts.gemspec +4 -4
- metadata +4 -4
data/CHANGES.txt
CHANGED
@@ -1,6 +1,16 @@
|
|
1
1
|
TRYOUTS, CHANGES
|
2
2
|
|
3
3
|
|
4
|
+
#### 0.7.2 (2009-06-26) ###############################
|
5
|
+
|
6
|
+
NOTE: You will need to make a syntax change to your tryouts.
|
7
|
+
OLD: dream OUTPUT, :format
|
8
|
+
NEW: dream :format, OUTPUT
|
9
|
+
|
10
|
+
* CHANGE: Order of dream arguments is reversed!
|
11
|
+
* CHANGE: Reduced CLI width to 79
|
12
|
+
|
13
|
+
|
4
14
|
#### 0.7.1 (2009-06-26) ###############################
|
5
15
|
|
6
16
|
* FIXED: Updated manifest in gemspec
|
data/README.rdoc
CHANGED
@@ -11,11 +11,21 @@ A tryout is made up of one of more drills. The return value of the drill block i
|
|
11
11
|
* Drill Sergeant: The class responsible for executing a drill.
|
12
12
|
* Dream: the expected outcome of a drill. There's always one or more dream per drill.
|
13
13
|
|
14
|
-
== Installation
|
14
|
+
== Installation
|
15
15
|
|
16
|
-
{{{
|
17
16
|
$ gem install tryouts
|
18
|
-
|
17
|
+
|
18
|
+
|
19
|
+
== NOTICE (2009-06-26): DSL Syntax Change
|
20
|
+
|
21
|
+
The order of dream arguments has reversed between 0.7.1 and 0.7.2. It is now:
|
22
|
+
|
23
|
+
* <tt>dream :method, 'Expected value'</tt>
|
24
|
+
|
25
|
+
This is a return to the original syntax and I think it's the right way to go because it reads more naturally:
|
26
|
+
|
27
|
+
* <tt>drill 'test name', 'return value', :class, String</tt>
|
28
|
+
|
19
29
|
|
20
30
|
== Examples
|
21
31
|
|
@@ -23,9 +33,9 @@ The examples below are a complete overview of Tryouts syntax.
|
|
23
33
|
|
24
34
|
=== Testing Ruby Codes (:api)
|
25
35
|
|
26
|
-
library :
|
36
|
+
library :gibbler, "../path/to/gibbler/lib"
|
27
37
|
|
28
|
-
tryouts "Common Usage" do
|
38
|
+
tryouts "Common Usage", :api do
|
29
39
|
|
30
40
|
# This drill block should return 3.
|
31
41
|
drill "Maths R Us", 3 do
|
@@ -34,26 +44,26 @@ The examples below are a complete overview of Tryouts syntax.
|
|
34
44
|
|
35
45
|
# You can specify a method to execute
|
36
46
|
# on the return value of the drill block.
|
37
|
-
drill "We want a symbol",
|
47
|
+
drill "We want a symbol", :class, Symbol do
|
38
48
|
orange.methods.first
|
39
49
|
end
|
40
50
|
|
41
51
|
# Dreams can also be specified explicitly which is
|
42
52
|
# important b/c it's possible to specify multiple.
|
43
|
-
dream
|
53
|
+
dream :class, Array
|
44
54
|
dream [:a, :b, :c]
|
45
55
|
drill "Should return a list of 3" do
|
46
56
|
Letters.list(3)
|
47
57
|
end
|
48
58
|
|
49
59
|
# Drills can pass based on an exception too.
|
50
|
-
dream
|
60
|
+
dream :exception, NameError
|
51
61
|
drill "Something failed" do
|
52
62
|
raise NameError
|
53
63
|
end
|
54
64
|
|
55
65
|
# We can even put simple drills on a single line.
|
56
|
-
drill
|
66
|
+
drill 'Small, fast, and furious', 'Muggsy Bogues', :match, /Mug+sy Bogu?es/
|
57
67
|
|
58
68
|
end
|
59
69
|
|
@@ -69,18 +79,18 @@ You can also use Tryouts to run benchmarks. The tryouts method takes a second pa
|
|
69
79
|
@@array = (1..100000).map { rand }
|
70
80
|
end
|
71
81
|
|
72
|
-
dream 3.0
|
73
|
-
dream 0.1
|
82
|
+
dream :mean, 3.0 # The mean should be <= 3.0 seconds
|
83
|
+
dream :sdev, 0.1 # and the standard deviation <= 0.1
|
74
84
|
drill("Array sort!") { @@array.dup.sort! }
|
75
85
|
|
76
86
|
# You can also include a dream inline
|
77
|
-
drill("Array sort", 3.0
|
87
|
+
drill("Array sort", :mean, 3.0) { @@array.dup.sort }
|
78
88
|
|
79
89
|
# The 3rd argument is the number of times to
|
80
90
|
# execute the drill block. The mean and sdev
|
81
91
|
# are calculate based on all iterations. The
|
82
92
|
# default is 5 and the maximum is 30.
|
83
|
-
dream 0.1,
|
93
|
+
dream :sdev, 0.1, 15
|
84
94
|
drill("Array sort") { @@array.dup.sort }
|
85
95
|
|
86
96
|
end
|
@@ -95,17 +105,18 @@ http://github.com/delano/tryouts/raw/gh-pages/screens/tryouts-1-failure.png
|
|
95
105
|
|
96
106
|
The drill that failed looks like this:
|
97
107
|
|
98
|
-
dream :
|
99
|
-
dream 'ab33b9dec202d136d0e695a3a7b06ee678134882'
|
100
|
-
drill
|
108
|
+
dream :respond_to?, :to_gibble
|
109
|
+
dream :to_gibble, 'ab33b9dec202d136d0e695a3a7b06ee678134882'
|
110
|
+
drill Array, "Array"
|
101
111
|
|
102
112
|
|
103
113
|
== BETA Notice
|
104
114
|
|
105
|
-
|
115
|
+
Tryouts is very new (est. 2009-05-19) and has not been vetted by the scrutiny of time. In particular you can expect:
|
106
116
|
|
107
117
|
* The test definition syntax may change in future releases.
|
108
118
|
* Unexpected errors.
|
119
|
+
* Bugs! I love fixing bugs so if you find one let me know.
|
109
120
|
|
110
121
|
|
111
122
|
== On Threads
|
data/lib/tryouts/cli/run.rb
CHANGED
@@ -47,7 +47,7 @@ class Run < Drydock::Command
|
|
47
47
|
|
48
48
|
passed, failed = 0, 0
|
49
49
|
Tryouts.instances.each_pair do |group,tryouts_inst|
|
50
|
-
puts '', ' %-
|
50
|
+
puts '', ' %-79s'.att(:reverse) % group unless Tryouts.verbose < 0
|
51
51
|
puts " #{tryouts_inst.paths.join("\n ")}" if Tryouts.verbose > 0
|
52
52
|
tryouts_inst.tryouts.each_pair do |name,to|
|
53
53
|
begin
|
@@ -63,11 +63,11 @@ class Run < Drydock::Command
|
|
63
63
|
end
|
64
64
|
|
65
65
|
unless tryouts_inst.errors.empty?
|
66
|
-
title = '%-
|
66
|
+
title = '%-78s' % " RUNTIME ERRORS !?"
|
67
67
|
puts $/, ' ' << title.color(:red).att(:reverse).bright
|
68
68
|
tryouts_inst.errors.each do |ex|
|
69
69
|
trace = Tryouts.verbose > 1 ? ex.backtrace : [ex.backtrace.first]
|
70
|
-
puts '%
|
70
|
+
puts '%4s%s: %s' % ['', ex.class, ex.message.to_s.split($/).join($/ + ' '*16)]
|
71
71
|
puts
|
72
72
|
puts '%14s %s' % ["", trace.join($/ + ' '*16)]
|
73
73
|
puts
|
data/lib/tryouts/drill.rb
CHANGED
@@ -32,8 +32,19 @@ class Tryouts
|
|
32
32
|
# A Reality object (the actual output of the test)
|
33
33
|
attr_reader :reality
|
34
34
|
|
35
|
-
@@valid_dtypes = [:
|
35
|
+
@@valid_dtypes = [:api, :benchmark]
|
36
36
|
|
37
|
+
# * +name+ The display name of this drill
|
38
|
+
# * +dtype+ A Symbol representing the drill type. One of: :api, :benchmark
|
39
|
+
# * +args+ These are dependent on the drill type. See the Sergeant classes
|
40
|
+
# * +&drill+ The body of the drill. The return value of this block
|
41
|
+
# is compared to the exepected output of the dreams.
|
42
|
+
#
|
43
|
+
# The DSL syntax:
|
44
|
+
# * dream OUTPUT
|
45
|
+
# * dream FORMAT, OUTPUT
|
46
|
+
# * dream FORMAT, OUTPUT, REPS (benchmark only)
|
47
|
+
#
|
37
48
|
def initialize(name, dtype, *args, &drill)
|
38
49
|
@name, @dtype, @drill, @skip = name, dtype, drill, false
|
39
50
|
@dreams = []
|
@@ -42,14 +53,17 @@ class Tryouts
|
|
42
53
|
@sergeant = Tryouts::Drill::Sergeant::CLI.new *args
|
43
54
|
when :api
|
44
55
|
default_output = drill.nil? ? args.shift : nil
|
56
|
+
dream_output, format = *(args.size == 1 ? args.first : args.reverse)
|
45
57
|
@sergeant = Tryouts::Drill::Sergeant::API.new default_output
|
46
|
-
|
58
|
+
unless args.empty?
|
59
|
+
@dreams << Tryouts::Drill::Dream.new(dream_output, format)
|
60
|
+
end
|
47
61
|
when :benchmark
|
48
|
-
|
49
|
-
@sergeant = Tryouts::Drill::Sergeant::Benchmark.new reps
|
62
|
+
dream_output, format, reps = *(args.size == 1 ? args.first : [args[1], args[0], args[2]])
|
63
|
+
@sergeant = Tryouts::Drill::Sergeant::Benchmark.new reps
|
50
64
|
@dreams << Tryouts::Drill::Dream.new(Tryouts::Stats, :class)
|
51
|
-
unless
|
52
|
-
@dreams << Tryouts::Drill::Dream.new(
|
65
|
+
unless dream_output.nil?
|
66
|
+
@dreams << Tryouts::Drill::Dream.new(dream_output, format)
|
53
67
|
end
|
54
68
|
when :skip
|
55
69
|
@skip = true
|
@@ -63,6 +77,7 @@ class Tryouts
|
|
63
77
|
@reality = Tryouts::Drill::Reality.new
|
64
78
|
end
|
65
79
|
|
80
|
+
def self.valid_dtypes; @@valid_dtypes; end
|
66
81
|
def self.valid_dtype?(t); @@valid_dtypes.member?(t); end
|
67
82
|
|
68
83
|
def skip?; @skip; end
|
@@ -128,9 +143,9 @@ class Tryouts
|
|
128
143
|
|
129
144
|
@dreams.each do |dream|
|
130
145
|
next if dream == reality #? :normal : :red
|
131
|
-
out.puts '%12s: %s'.color(@clr) % ["
|
132
|
-
out.puts '%12s: %s' % ["
|
133
|
-
out.puts '%12s: %s' % ["
|
146
|
+
out.puts '%12s: %s'.color(@clr) % ["failed", dream.test_to_string(@reality)]
|
147
|
+
out.puts '%12s: %s' % ["drill", @reality.comparison_value(dream).inspect]
|
148
|
+
out.puts '%12s: %s' % ["dream", dream.comparison_value.inspect]
|
134
149
|
out.puts
|
135
150
|
end
|
136
151
|
|
data/lib/tryouts/stats.rb
CHANGED
@@ -17,7 +17,7 @@
|
|
17
17
|
# are not stored but instead all the values are calculated on the fly.
|
18
18
|
class Tryouts
|
19
19
|
class Stats
|
20
|
-
attr_reader :sum, :sumsq, :n, :min, :max
|
20
|
+
attr_reader :sum, :sumsq, :n, :min, :max, :name
|
21
21
|
|
22
22
|
def initialize(name=:unknown)
|
23
23
|
@name = name
|
@@ -34,6 +34,8 @@ class Tryouts
|
|
34
34
|
@max = 0.0
|
35
35
|
end
|
36
36
|
|
37
|
+
def samples; @n; end
|
38
|
+
|
37
39
|
# Adds a sampling to the calculations.
|
38
40
|
def sample(s)
|
39
41
|
@sum += s
|
@@ -88,5 +90,7 @@ class Tryouts
|
|
88
90
|
sample(now - @last_time)
|
89
91
|
@last_time = now
|
90
92
|
end
|
93
|
+
|
94
|
+
|
91
95
|
end
|
92
96
|
end
|
data/lib/tryouts/tryout.rb
CHANGED
@@ -57,7 +57,7 @@ class Tryouts
|
|
57
57
|
DrillContext.module_eval &setup if setup.is_a?(Proc)
|
58
58
|
puts "\n %s ".bright % @name unless Tryouts.verbose < 0
|
59
59
|
@drills.each do |drill|
|
60
|
-
print ' %-
|
60
|
+
print ' %-69s ' % "\"#{drill.name}\"" unless Tryouts.verbose < 0
|
61
61
|
drill.run DrillContext.new
|
62
62
|
if drill.skip?
|
63
63
|
@skipped += 1
|
@@ -77,7 +77,7 @@ class Tryouts
|
|
77
77
|
return if Tryouts.verbose < 0
|
78
78
|
failed = @drills.select { |d| !d.skip? && !d.success? }
|
79
79
|
failed.each_with_index do |drill,index|
|
80
|
-
title = ' %-
|
80
|
+
title = ' %-69s %2d/%-2d ' % ["\"#{drill.name}\"", index+1, failed.size]
|
81
81
|
puts $/, ' ' << title.color(:red).att(:reverse)
|
82
82
|
puts drill.report
|
83
83
|
end
|
@@ -136,11 +136,8 @@ class Tryouts
|
|
136
136
|
if args.empty?
|
137
137
|
dobj = Tryouts::Drill::Dream.from_block definition
|
138
138
|
else
|
139
|
-
|
140
|
-
|
141
|
-
else
|
142
|
-
dobj = Tryouts::Drill::Dream.new(*args) # dream 'OUTPUT', :format
|
143
|
-
end
|
139
|
+
args = args.size == 1 ? [args.first] : args.reverse
|
140
|
+
dobj = Tryouts::Drill::Dream.new(*args)
|
144
141
|
end
|
145
142
|
@dream_catcher.push dobj
|
146
143
|
dobj
|
data/lib/tryouts.rb
CHANGED
@@ -30,9 +30,18 @@ class Tryouts
|
|
30
30
|
class Exception < RuntimeError; end
|
31
31
|
# = BadDreams
|
32
32
|
# Raised when there is a problem loading or parsing a Tryouts::Drill::Dream object
|
33
|
-
class
|
33
|
+
class BadDream < Exception; end
|
34
34
|
|
35
|
-
|
35
|
+
class NoDrillType < Exception
|
36
|
+
attr_accessor :tname
|
37
|
+
def initialize(t); @tname = t; end
|
38
|
+
def message
|
39
|
+
vdt = Tryouts::Drill.valid_dtypes
|
40
|
+
"Tryout '#{@tname}' has no drill type. Should be: #{vdt.join(', ')}"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
VERSION = "0.7.2"
|
36
45
|
|
37
46
|
require 'tryouts/mixins'
|
38
47
|
require 'tryouts/tryout'
|
@@ -167,7 +176,7 @@ class Tryouts
|
|
167
176
|
|
168
177
|
# Create a new Tryout object and add it to the list for this Tryouts class.
|
169
178
|
# * +name+ is the name of the Tryout
|
170
|
-
# * +
|
179
|
+
# * +dtype+ is the default drill type for the Tryout.
|
171
180
|
# * +command+ when type is :cli, this is the name of the Rye::Box method that we're testing. Otherwise ignored.
|
172
181
|
# * +b+ is a block definition for the Tryout. See Tryout#from_block
|
173
182
|
#
|
@@ -177,7 +186,7 @@ class Tryouts
|
|
177
186
|
dtype ||= @dtype
|
178
187
|
command ||= @command if dtype == :cli
|
179
188
|
|
180
|
-
raise
|
189
|
+
raise NoDrillType, name if dtype.nil?
|
181
190
|
|
182
191
|
to = find_tryout(name, dtype)
|
183
192
|
if to.nil?
|
@@ -240,6 +249,7 @@ class Tryouts
|
|
240
249
|
file_content = File.read(fpath)
|
241
250
|
to = Tryouts.new
|
242
251
|
begin
|
252
|
+
to.paths << fpath
|
243
253
|
to.instance_eval file_content, fpath
|
244
254
|
# After parsing the DSL, we'll know the group name.
|
245
255
|
# If a Tryouts object already exists for that group
|
@@ -248,7 +258,6 @@ class Tryouts
|
|
248
258
|
to = @@instances[to.group]
|
249
259
|
to.instance_eval file_content, fpath
|
250
260
|
end
|
251
|
-
to.paths << fpath
|
252
261
|
rescue SyntaxError, LoadError, Exception, TypeError,
|
253
262
|
RuntimeError, NoMethodError, NameError => ex
|
254
263
|
to.errors << ex
|
@@ -1,10 +1,7 @@
|
|
1
1
|
|
2
|
-
library :tryouts, File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
|
3
2
|
|
4
3
|
group "Mixins"
|
5
4
|
|
6
|
-
|
7
|
-
|
8
5
|
test_hash = {
|
9
6
|
:level1 => {
|
10
7
|
:level2 => {},
|
@@ -15,12 +12,12 @@ test_hash = {
|
|
15
12
|
}
|
16
13
|
|
17
14
|
|
18
|
-
tryouts "Hash" do
|
15
|
+
tryouts "Hash", :api do
|
19
16
|
setup do
|
20
17
|
|
21
18
|
end
|
22
19
|
|
23
20
|
drill "knows the deepest point", test_hash.deepest_point, 3
|
24
|
-
drill "has a last method", {}
|
21
|
+
drill "has a last method", {} #, :respond_to?, :last
|
25
22
|
|
26
23
|
end
|
@@ -1,17 +1,21 @@
|
|
1
1
|
|
2
2
|
tryout "DSL Syntax", :api do
|
3
3
|
|
4
|
+
drill "can specify a dream inline", 3 do
|
5
|
+
12 / 4
|
6
|
+
end
|
7
|
+
|
4
8
|
dream 4770744
|
5
9
|
drill "can specify dream above the drill" do
|
6
10
|
4770744
|
7
11
|
end
|
8
12
|
|
9
|
-
dream
|
13
|
+
dream :class, Array
|
10
14
|
drill "can pass based on output object class" do
|
11
15
|
[1,2,3]
|
12
16
|
end
|
13
17
|
|
14
|
-
dream
|
18
|
+
dream :exception, NameError
|
15
19
|
drill "can pass based on exception class" do
|
16
20
|
bad_method_call
|
17
21
|
end
|
@@ -22,19 +26,19 @@ tryout "DSL Syntax", :api do
|
|
22
26
|
|
23
27
|
drill "inline true values will pass too", true
|
24
28
|
drill "can specify inline return values", :food, :food
|
25
|
-
drill "can specify match format", 'mahir', /..hi./i
|
29
|
+
drill "can specify match format", 'mahir', :match, /..hi./i
|
26
30
|
|
27
31
|
dream "big"
|
28
|
-
dream
|
29
|
-
dream /\Ab.g\z
|
32
|
+
dream :class, String
|
33
|
+
dream :match, /\Ab.g\z/
|
30
34
|
drill "can handle multiple dreams" do
|
31
35
|
"big"
|
32
36
|
end
|
33
37
|
|
34
|
-
drill "can specify gt (>) format", 2,
|
35
|
-
drill "can specify gte (>=) format", 2,
|
36
|
-
drill "can specify lt (<) format", 1,
|
37
|
-
drill "can specify lte (<=) format", 2,
|
38
|
+
drill "can specify gt (>) format", 2, :gt, 1
|
39
|
+
drill "can specify gte (>=) format", 2, :gte, 2
|
40
|
+
drill "can specify lt (<) format", 1, :lt, 2
|
41
|
+
drill "can specify lte (<=) format", 2, :lte, 2
|
38
42
|
|
39
|
-
drill "can run arbitrary formats", [3,1,2], [1,2,3]
|
43
|
+
drill "can run arbitrary formats", [3,1,2], :sort, [1,2,3]
|
40
44
|
end
|
@@ -8,7 +8,7 @@ tryout "Setting class variables", :api do
|
|
8
8
|
@from_setup = true
|
9
9
|
end
|
10
10
|
|
11
|
-
drill "can't access class var created in setup (1.9 only)",
|
11
|
+
drill "can't access class var created in setup (1.9 only)", :exception, NameError do
|
12
12
|
@@from_setup
|
13
13
|
end
|
14
14
|
|
@@ -25,6 +25,5 @@ tryout "Setting class variables", :api do
|
|
25
25
|
@@from_drill.class.to_s
|
26
26
|
end
|
27
27
|
|
28
|
-
|
29
|
-
drill "Knows where Santa Claus lives", 'H0H 0H0'
|
28
|
+
drill 'Small, fast, and furious', 'Muggsy Bogues', :match, /Mug+sy Bogu?es/
|
30
29
|
end
|
data/tryouts.gemspec
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
@spec = Gem::Specification.new do |s|
|
2
2
|
s.name = "tryouts"
|
3
3
|
s.rubyforge_project = "tryouts"
|
4
|
-
s.version = "0.7.
|
4
|
+
s.version = "0.7.2"
|
5
5
|
s.summary = "Tryouts are high-level tests for your Ruby code. May all your dreams come true!"
|
6
6
|
s.description = s.summary
|
7
7
|
s.author = "Delano Mandelbaum"
|
@@ -24,9 +24,9 @@
|
|
24
24
|
|
25
25
|
# = DEPENDENCIES =
|
26
26
|
# Add all gem dependencies
|
27
|
-
s.add_dependency 'rye'
|
28
|
-
s.add_dependency 'drydock'
|
29
|
-
s.add_dependency 'sysinfo'
|
27
|
+
s.add_dependency 'rye'
|
28
|
+
s.add_dependency 'drydock'
|
29
|
+
s.add_dependency 'sysinfo'
|
30
30
|
|
31
31
|
# = MANIFEST =
|
32
32
|
# The complete list of files to be included in the release. When GitHub packages your gem,
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: delano-tryouts
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.7.
|
4
|
+
version: 0.7.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Delano Mandelbaum
|
@@ -20,7 +20,7 @@ dependencies:
|
|
20
20
|
requirements:
|
21
21
|
- - ">="
|
22
22
|
- !ruby/object:Gem::Version
|
23
|
-
version: 0
|
23
|
+
version: "0"
|
24
24
|
version:
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: drydock
|
@@ -30,7 +30,7 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: 0
|
33
|
+
version: "0"
|
34
34
|
version:
|
35
35
|
- !ruby/object:Gem::Dependency
|
36
36
|
name: sysinfo
|
@@ -40,7 +40,7 @@ dependencies:
|
|
40
40
|
requirements:
|
41
41
|
- - ">="
|
42
42
|
- !ruby/object:Gem::Version
|
43
|
-
version: 0
|
43
|
+
version: "0"
|
44
44
|
version:
|
45
45
|
description: Tryouts are high-level tests for your Ruby code. May all your dreams come true!
|
46
46
|
email: tryouts@solutious.com
|