sauce 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/VERSION +1 -1
- data/lib/job.rb +68 -62
- data/lib/rest.rb +9 -6
- data/test/test_jobs.rb +107 -27
- data/test/test_jobs_old.rb +58 -0
- data/test/test_tunnels.rb +2 -2
- metadata +3 -2
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.4.0
|
data/lib/job.rb
CHANGED
@@ -7,21 +7,19 @@ module Sauce
|
|
7
7
|
# Interact with a Sauce Labs selenium jobs as if it were a ruby object
|
8
8
|
class Job
|
9
9
|
|
10
|
-
|
10
|
+
class CannotDeleteJobError < StandardError; end #:nodoc
|
11
11
|
|
12
|
-
attr_accessor :
|
13
|
-
attr_accessor :
|
14
|
-
attr_accessor :creation_time, :
|
15
|
-
attr_accessor :
|
12
|
+
attr_accessor :id, :owner, :status, :error
|
13
|
+
attr_accessor :name, :browser, :browser_version, :os
|
14
|
+
attr_accessor :creation_time, :start_time, :end_time
|
15
|
+
attr_accessor :public, :video_url, :log_url, :tags
|
16
16
|
|
17
|
-
# TODO: Buckets for logs and videos
|
18
|
-
|
19
17
|
# Get the class @@client.
|
20
18
|
# TODO: Consider metaprogramming this away
|
21
19
|
def self.client
|
22
20
|
@@client
|
23
21
|
end
|
24
|
-
|
22
|
+
|
25
23
|
# Set the class @@client.
|
26
24
|
# TODO: Consider metaprogramming this away
|
27
25
|
def self.client=(client)
|
@@ -48,42 +46,28 @@ module Sauce
|
|
48
46
|
self.all.last
|
49
47
|
end
|
50
48
|
|
51
|
-
|
52
|
-
|
53
|
-
|
49
|
+
# Misnomer: Gets the most recent 100 jobs
|
50
|
+
# TODO: Allow/automate paging
|
51
|
+
def self.all(options={})
|
52
|
+
responses = JSON.parse @@client["jobs/full"].get
|
54
53
|
return responses.collect{|response| Sauce::Job.new(response)}
|
55
|
-
=end
|
56
|
-
return self.complete_jobs + self.in_progress_jobs
|
57
54
|
end
|
58
55
|
|
59
56
|
def self.destroy
|
60
57
|
self.all.each { |tunnel| tunnel.destroy }
|
61
58
|
end
|
62
59
|
|
63
|
-
def self.find
|
60
|
+
def self.find(options={})
|
61
|
+
if options.class == String
|
62
|
+
id = options
|
63
|
+
elsif options.class == Hash
|
64
|
+
id = options[:id]
|
65
|
+
end
|
66
|
+
|
64
67
|
#puts "GET-URL: #{@@client.url}jobs/#{id}"
|
65
68
|
Sauce::Job.new JSON.parse(@@client["jobs/#{id}"].get)
|
66
69
|
end
|
67
70
|
|
68
|
-
def self.complete_jobs
|
69
|
-
responses = JSON.parse @@client["complete-jobs"].get
|
70
|
-
start = Time.now
|
71
|
-
jobs = responses["jobs"].collect{|response| Sauce::Job.find(response["id"])}
|
72
|
-
lapsed = Time.now - start
|
73
|
-
puts "Took #{lapsed} seconds"
|
74
|
-
return jobs
|
75
|
-
end
|
76
|
-
|
77
|
-
def self.in_progress_jobs
|
78
|
-
responses = JSON.parse @@client["in-progress-jobs"].get
|
79
|
-
return [] if responses == []
|
80
|
-
start = Time.now
|
81
|
-
jobs = responses["jobs"].collect{|response| Sauce::Job.find(response["id"])}
|
82
|
-
lapsed = Time.now - start
|
83
|
-
puts "Took #{lapsed} seconds"
|
84
|
-
return jobs
|
85
|
-
end
|
86
|
-
|
87
71
|
# Creates an instance representing a job.
|
88
72
|
def initialize(options)
|
89
73
|
build!(options)
|
@@ -92,42 +76,64 @@ module Sauce
|
|
92
76
|
# Retrieves the latest information on this job from the Sauce Labs' server
|
93
77
|
def refresh!
|
94
78
|
response = JSON.parse @@client["jobs/#{@id}"].get
|
95
|
-
puts "\tjob refresh with: #{response
|
79
|
+
#puts "\tjob refresh with: #{response}"
|
96
80
|
build! response
|
97
81
|
self
|
98
82
|
end
|
99
83
|
|
100
|
-
|
84
|
+
# Save/update the current information for the job
|
85
|
+
def save
|
86
|
+
response = JSON.parse(@@client["jobs/#{@id}"]. self.to_json, :content_type => :json, :accept => :json)
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.to_json(options={})
|
90
|
+
json = {
|
91
|
+
:id => @id,
|
92
|
+
:owner => @owner,
|
93
|
+
:status => @status,
|
94
|
+
:error => @error,
|
95
|
+
:name => @name,
|
96
|
+
:browser => @browser,
|
97
|
+
:browser_version => @browser_version,
|
98
|
+
:os => @os,
|
99
|
+
:creation_time => @creation_time,
|
100
|
+
:start_time => @start_time,
|
101
|
+
:end_time => @end_time,
|
102
|
+
:video_url => @video_url,
|
103
|
+
:log_url => @log_url,
|
104
|
+
:public => @public,
|
105
|
+
:tags => @tags
|
106
|
+
}
|
107
|
+
|
108
|
+
options[:except].each { |key| json.delete(key) } if options[:except]
|
109
|
+
json = json.select { |key,value| options[:only].include? key } if options[:only]
|
110
|
+
|
111
|
+
return json
|
112
|
+
end
|
113
|
+
|
114
|
+
def delete
|
115
|
+
raise CannonDeleteJobError("Cannot delete jobs via Sauce Labs' REST API currently")
|
116
|
+
end
|
117
|
+
|
118
|
+
protected
|
101
119
|
|
102
120
|
# Sets all internal variables from a hash
|
103
121
|
def build!(options)
|
104
|
-
|
105
|
-
|
106
|
-
@
|
107
|
-
@
|
108
|
-
@
|
109
|
-
@
|
110
|
-
@
|
111
|
-
@
|
112
|
-
@
|
113
|
-
|
114
|
-
@
|
115
|
-
@
|
116
|
-
@
|
117
|
-
@
|
118
|
-
@
|
119
|
-
|
120
|
-
@os = options["OS"]
|
121
|
-
@browser = options["Browser"]
|
122
|
-
@browser_version = options["BrowserVersion"]
|
123
|
-
|
124
|
-
# TODO: Should this be created_at and updated_at? Probably.
|
125
|
-
@creation_time = options["CreationTime"]
|
126
|
-
@assignment_time = options["AssignmentTime"]
|
127
|
-
@chef_start_time = options["ChefStartTime"]
|
128
|
-
@end_time = options["EndTime"]
|
129
|
-
@modification_time = options["ModificationTime"]
|
130
|
-
@start_time = options["StartTime"]
|
122
|
+
@id = options["id"]
|
123
|
+
@owner = options["owner"]
|
124
|
+
@status = options["status"]
|
125
|
+
@error = options["error"]
|
126
|
+
@name = options["name"]
|
127
|
+
@browser = options["browser"]
|
128
|
+
@browser_version = options["browser_version"]
|
129
|
+
@os = options["os"]
|
130
|
+
@creation_time = options["creation_time"].to_i
|
131
|
+
@start_time = options["start_time"].to_i
|
132
|
+
@end_time = options["end_time"].to_i
|
133
|
+
@video_url = options["video_url"]
|
134
|
+
@log_url = options["log_url"]
|
135
|
+
@public = options["public"]
|
136
|
+
@tags = options["tags"]
|
131
137
|
|
132
138
|
raise NoIDError if @id.nil? or @id.empty?
|
133
139
|
end
|
data/lib/rest.rb
CHANGED
@@ -4,8 +4,8 @@ require 'json'
|
|
4
4
|
module Sauce
|
5
5
|
# The module that brokers most communication with Sauce Labs' REST API
|
6
6
|
class Client
|
7
|
-
class BadAccessError < StandardError; end
|
8
|
-
class MisconfiguredError < StandardError; end
|
7
|
+
class BadAccessError < StandardError; end #:nodoc
|
8
|
+
class MisconfiguredError < StandardError; end #:nodoc
|
9
9
|
|
10
10
|
attr_accessor :username, :access_key, :client, :ip, :api_url
|
11
11
|
attr_accessor :tunnels, :jobs
|
@@ -16,20 +16,23 @@ module Sauce
|
|
16
16
|
@ip = options[:ip]
|
17
17
|
|
18
18
|
raise MisconfiguredError if @username.nil? or @access_key.nil?
|
19
|
-
@api_url = "https://#{@username}:#{@access_key}@saucelabs.com/
|
19
|
+
@api_url = "https://#{@username}:#{@access_key}@saucelabs.com/api/v1/#{@username}/"
|
20
20
|
@client = RestClient::Resource.new @api_url
|
21
21
|
|
22
22
|
@tunnels = Sauce::Tunnel
|
23
23
|
@tunnels.client = @client
|
24
|
-
@tunnels.account = {
|
24
|
+
@tunnels.account = {
|
25
|
+
:username => @username,
|
25
26
|
:access_key => @access_key,
|
26
27
|
:ip => @ip}
|
27
28
|
|
28
29
|
@jobs = Sauce::Job
|
29
30
|
@jobs.client = @client
|
30
|
-
@jobs.account = {
|
31
|
+
@jobs.account = {
|
32
|
+
:username => @username,
|
31
33
|
:access_key => @access_key,
|
32
|
-
:ip => @ip
|
34
|
+
:ip => @ip
|
35
|
+
}
|
33
36
|
end
|
34
37
|
end
|
35
38
|
end
|
data/test/test_jobs.rb
CHANGED
@@ -2,7 +2,7 @@ require 'helper'
|
|
2
2
|
require 'json'
|
3
3
|
|
4
4
|
class TestSauce < Test::Unit::TestCase
|
5
|
-
context "A jobs instance" do
|
5
|
+
context "A V1 jobs instance" do
|
6
6
|
setup do
|
7
7
|
# Create this file and put in your details to run the tests
|
8
8
|
account = YAML.load_file "live_account.yml"
|
@@ -11,44 +11,124 @@ class TestSauce < Test::Unit::TestCase
|
|
11
11
|
@ip = account["ip"]
|
12
12
|
@client = Sauce::Client.new(:username => @username,
|
13
13
|
:access_key => @access_key)
|
14
|
+
|
15
|
+
@example_data = YAML.load_file('example_data.yml')
|
14
16
|
end
|
15
17
|
|
16
18
|
should "initialize with passed variables" do
|
17
|
-
job_json = JSON.parse '{"BrowserVersion": "3.", "Name": "example_job/name.rb", "_rev": "5-228269313", "CreationTime": 1266698090, "AssignmentTime": 1266698097, "Server": "192.168.0.1:4443", "AssignedTo": "f663372ba04444ce8cb3e6f61503f304", "ChefStartTime": 1266698101, "EndTime": 1266698139, "Type": "job", "Interactive": "true", "Status": "complete", "SeleniumServerLogUploadRequest": {"bucket": "sauce-userdata", "key": "sgrove/6337fe576deba0ba278dc1b5dfceac5f/selenium-server.log"}, "Tags": ["tag_1", "tag_2"], "ResultId": "6337fe576deba0ba278dc1b5dfceac5f", "AttachmentRequests": {}, "ModificationTime": 1266698139, "Browser": "firefox", "StartTime": 1266698101, "Owner": "sgrove", "_id": "01fc48caba6d15b46fad79e1b0562bbe", "OS": "Linux", "VideoUploadRequest": {"bucket": "sauce-userdata", "key": "sgrove/6337fe576deba0ba278dc1b5dfceac5f/video.flv"}}'
|
18
|
-
|
19
19
|
client = Sauce::Client.new(:username => "test_user",
|
20
20
|
:access_key => "abc123")
|
21
21
|
|
22
|
-
job = client.jobs.new(
|
22
|
+
job = client.jobs.new(JSON.parse(@example_data["example_job"]))
|
23
|
+
|
24
|
+
assert_equal "501aca56282545a9a21ad2fc592b03fa", job.id
|
25
|
+
assert_equal "joe", job.owner
|
26
|
+
assert_equal "complete", job.status
|
27
|
+
assert_equal "job-name", job.name
|
28
|
+
|
29
|
+
assert_equal "firefox", job.browser
|
30
|
+
assert_equal "3.5.", job.browser_version
|
31
|
+
assert_equal "Windows 2003", job.os
|
32
|
+
|
33
|
+
assert_equal 1253856281, job.creation_time
|
34
|
+
assert_equal 1253856366, job.start_time
|
35
|
+
assert_equal 1253856465, job.end_time
|
36
|
+
|
37
|
+
assert_equal "http://saucelabs.com/video/8b6bf8d360cc338cc7cf7f6e093264d0/video.flv", job.video_url
|
38
|
+
assert_equal "http://saucelabs.com/video/8b6bf8d360cc338cc7cf7f6e093264d0/selenium-server.log", job.log_url
|
39
|
+
|
40
|
+
assert_equal false, job.public
|
41
|
+
assert_equal ["test", "example", "python_is_fun"], job.tags
|
42
|
+
end
|
23
43
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
assert_equal "5-228269313", job._rev
|
28
|
-
assert_equal "192.168.0.1:4443", job.server
|
29
|
-
assert_equal "f663372ba04444ce8cb3e6f61503f304", job.assigned_to
|
44
|
+
# Note: This won't pass until we have a canonical "test job" and its data
|
45
|
+
should "retrieve and parse a job via the api" do
|
46
|
+
job = @client.jobs.find("501aca56282545a9a21ad2fc592b03fa")
|
30
47
|
|
31
|
-
assert_equal "
|
32
|
-
assert_equal "
|
48
|
+
assert_equal "501aca56282545a9a21ad2fc592b03fa", job.id
|
49
|
+
assert_equal "joe", job.owner
|
33
50
|
assert_equal "complete", job.status
|
34
|
-
assert_equal
|
35
|
-
|
51
|
+
assert_equal "job-name", job.name
|
52
|
+
|
53
|
+
assert_equal "firefox", job.browser
|
54
|
+
assert_equal "3.5.", job.browser_version
|
55
|
+
assert_equal "Windows 2003", job.os
|
56
|
+
|
57
|
+
assert_equal 1253856281, job.creation_time
|
58
|
+
assert_equal 1253856366, job.start_time
|
59
|
+
assert_equal 1253856465, job.end_time
|
60
|
+
|
61
|
+
assert_equal "http://saucelabs.com/video/8b6bf8d360cc338cc7cf7f6e093264d0/video.flv", job.video_url
|
62
|
+
assert_equal "http://saucelabs.com/video/8b6bf8d360cc338cc7cf7f6e093264d0/selenium-server.log", job.log_url
|
63
|
+
|
64
|
+
assert_equal false, job.public
|
65
|
+
assert_equal ["test", "example", "python_is_fun"], job.tags
|
66
|
+
end
|
67
|
+
|
68
|
+
should "update writable properties" do
|
69
|
+
job = @client.jobs.find("501aca56282545a9a21ad2fc592b03fa")
|
70
|
+
|
71
|
+
# Make sure it's in the expected condition before changing
|
72
|
+
assert_equal false, job.public
|
73
|
+
assert_equal ["test", "example", "python_is_fun"], job.tags
|
74
|
+
assert_equal "job-name", job.name
|
36
75
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
76
|
+
job.public = true
|
77
|
+
job.tags = ["changed", "updated", "ruby_is_also_fun"]
|
78
|
+
job.name = "changed-job-name", job.name
|
79
|
+
job.save
|
41
80
|
|
81
|
+
# Fresh copy of the same job
|
82
|
+
job2 = @client.jobs.find("501aca56282545a9a21ad2fc592b03fa")
|
83
|
+
|
84
|
+
assert_equal true, job.public
|
85
|
+
assert_equal ["changed", "updated", "ruby_is_also_fun"], job.tags
|
86
|
+
assert_equal "changed-job-name", job.name
|
87
|
+
|
88
|
+
# Return the job to its original state and check it out
|
89
|
+
job.public = false
|
90
|
+
job.tags = ["test", "example", "python_is_fun"]
|
91
|
+
job.name = "job-name", job.name
|
92
|
+
job.save
|
93
|
+
|
94
|
+
# Check to see if the change took
|
95
|
+
job2.refresh!
|
96
|
+
assert_equal job.public, job2.public
|
97
|
+
assert_equal job.tags, job2.tags
|
98
|
+
assert_equal job.name, job2.name
|
99
|
+
end
|
100
|
+
|
101
|
+
should "not update read-only properties" do
|
102
|
+
job = @client.jobs.find("501aca56282545a9a21ad2fc592b03fa")
|
103
|
+
|
104
|
+
# Make sure it's in the expected condition before changing
|
105
|
+
assert_equal "complete", job.status
|
106
|
+
assert_equal "joe", job.owner
|
42
107
|
assert_equal "Linux", job.os
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
assert_equal
|
48
|
-
assert_equal
|
49
|
-
assert_equal
|
50
|
-
|
51
|
-
|
108
|
+
|
109
|
+
job.status = "groggy"
|
110
|
+
job.owner = "sjobs"
|
111
|
+
job.os = "darwin" # In a perfect world...
|
112
|
+
assert_equal "groggy", job.status
|
113
|
+
assert_equal "joe", job.owner
|
114
|
+
assert_equal "darwin", job.os
|
115
|
+
job.save
|
116
|
+
|
117
|
+
# Changes should go away when refreshed
|
118
|
+
job.refresh!
|
119
|
+
assert_equal "complete", job.status
|
120
|
+
assert_equal "joe", job.owner
|
121
|
+
assert_equal "Linux", job.os
|
122
|
+
end
|
123
|
+
|
124
|
+
should "list the 100 most recent jobs" do
|
125
|
+
jobs = @client.jobs.all
|
126
|
+
|
127
|
+
assert_equal 5, jobs.count
|
128
|
+
end
|
129
|
+
|
130
|
+
should "show the full job information on index if requested" do
|
131
|
+
flunk "TODO: implement this"
|
52
132
|
end
|
53
133
|
|
54
134
|
def teardown
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
class TestSauce < Test::Unit::TestCase
|
5
|
+
context "A jobs instance" do
|
6
|
+
setup do
|
7
|
+
# Create this file and put in your details to run the tests
|
8
|
+
account = YAML.load_file "live_account.yml"
|
9
|
+
@username = account["username"]
|
10
|
+
@access_key = account["access_key"]
|
11
|
+
@ip = account["ip"]
|
12
|
+
@client = Sauce::Client.new(:username => @username,
|
13
|
+
:access_key => @access_key)
|
14
|
+
end
|
15
|
+
|
16
|
+
should "initialize with passed variables" do
|
17
|
+
job_json = JSON.parse '{"BrowserVersion": "3.", "Name": "example_job/name.rb", "_rev": "5-228269313", "CreationTime": 1266698090, "AssignmentTime": 1266698097, "Server": "192.168.0.1:4443", "AssignedTo": "f663372ba04444ce8cb3e6f61503f304", "ChefStartTime": 1266698101, "EndTime": 1266698139, "Type": "job", "Interactive": "true", "Status": "complete", "SeleniumServerLogUploadRequest": {"bucket": "sauce-userdata", "key": "sgrove/6337fe576deba0ba278dc1b5dfceac5f/selenium-server.log"}, "Tags": ["tag_1", "tag_2"], "ResultId": "6337fe576deba0ba278dc1b5dfceac5f", "AttachmentRequests": {}, "ModificationTime": 1266698139, "Browser": "firefox", "StartTime": 1266698101, "Owner": "sgrove", "_id": "01fc48caba6d15b46fad79e1b0562bbe", "OS": "Linux", "VideoUploadRequest": {"bucket": "sauce-userdata", "key": "sgrove/6337fe576deba0ba278dc1b5dfceac5f/video.flv"}}'
|
18
|
+
|
19
|
+
client = Sauce::Client.new(:username => "test_user",
|
20
|
+
:access_key => "abc123")
|
21
|
+
|
22
|
+
job = client.jobs.new(job_json)
|
23
|
+
|
24
|
+
assert_equal "sgrove", job.owner
|
25
|
+
assert_equal "01fc48caba6d15b46fad79e1b0562bbe", job.id
|
26
|
+
assert_equal "example_job/name.rb", job.name
|
27
|
+
assert_equal "5-228269313", job._rev
|
28
|
+
assert_equal "192.168.0.1:4443", job.server
|
29
|
+
assert_equal "f663372ba04444ce8cb3e6f61503f304", job.assigned_to
|
30
|
+
|
31
|
+
assert_equal "job", job.sauce_type
|
32
|
+
assert_equal "true", job.interactive
|
33
|
+
assert_equal "complete", job.status
|
34
|
+
assert_equal ["tag_1", "tag_2"], job.tags
|
35
|
+
assert_equal "6337fe576deba0ba278dc1b5dfceac5f", job.result_id
|
36
|
+
|
37
|
+
# TODO: Buckets
|
38
|
+
#assert_equal , job.selenium_server_log_upload_request
|
39
|
+
#assert_equal , job.attachment_requests
|
40
|
+
#assert_equal , job.videoupload_request
|
41
|
+
|
42
|
+
assert_equal "Linux", job.os
|
43
|
+
assert_equal "firefox", job.browser
|
44
|
+
assert_equal "3.", job.browser_version
|
45
|
+
|
46
|
+
assert_equal 1266698090, job.creation_time
|
47
|
+
assert_equal 1266698097, job.assignment_time
|
48
|
+
assert_equal 1266698101, job.chef_start_time
|
49
|
+
assert_equal 1266698139, job.end_time
|
50
|
+
assert_equal 1266698139, job.modification_time
|
51
|
+
assert_equal 1266698101, job.start_time
|
52
|
+
end
|
53
|
+
|
54
|
+
def teardown
|
55
|
+
@client.tunnels.destroy
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
data/test/test_tunnels.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
require 'helper'
|
2
2
|
|
3
3
|
class TestSauce < Test::Unit::TestCase
|
4
|
-
context "A
|
4
|
+
context "A V1 tunnel instance" do
|
5
5
|
setup do
|
6
6
|
# Create this file and put in your details to run the tests
|
7
7
|
account = YAML.load_file "live_account.yml"
|
@@ -16,7 +16,7 @@ class TestSauce < Test::Unit::TestCase
|
|
16
16
|
should "initialize with passed variables" do
|
17
17
|
client = Sauce::Client.new(:username => "test_user",
|
18
18
|
:access_key => "abc123")
|
19
|
-
assert_equal
|
19
|
+
assert_equal "https://test_user:abc123@saucelabs.com/api/v1/test_user/", client.api_url
|
20
20
|
end
|
21
21
|
|
22
22
|
should "create a tunnel with the current user" do
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sauce
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sean Grove
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2010-02
|
12
|
+
date: 2010-03-02 00:00:00 -08:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -88,5 +88,6 @@ test_files:
|
|
88
88
|
- test/helper.rb
|
89
89
|
- test/irb_boot.rb
|
90
90
|
- test/test_jobs.rb
|
91
|
+
- test/test_jobs_old.rb
|
91
92
|
- test/test_tunnels.rb
|
92
93
|
- test/test_videos.rb
|