offshore 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +19 -0
- data/.rvmrc +2 -0
- data/Gemfile +6 -0
- data/LICENSE +22 -0
- data/README.md +111 -0
- data/Rakefile +2 -0
- data/lib/offshore/client/factory_offshore.rb +8 -0
- data/lib/offshore/client/host.rb +110 -0
- data/lib/offshore/client/suite.rb +33 -0
- data/lib/offshore/client/test.rb +39 -0
- data/lib/offshore/client.rb +4 -0
- data/lib/offshore/server/controller.rb +81 -0
- data/lib/offshore/server/database.rb +111 -0
- data/lib/offshore/server/errors.rb +5 -0
- data/lib/offshore/server/logger.rb +22 -0
- data/lib/offshore/server/middleware.rb +70 -0
- data/lib/offshore/server/mutex.rb +154 -0
- data/lib/offshore/server/railtie.rb +5 -0
- data/lib/offshore/server/redis.rb +45 -0
- data/lib/offshore/server/snapshot.rb +175 -0
- data/lib/offshore/server.rb +10 -0
- data/lib/offshore/tasks.rb +35 -0
- data/lib/offshore/version.rb +3 -0
- data/lib/offshore.rb +29 -0
- data/offshore.gemspec +23 -0
- metadata +150 -0
data/.gitignore
ADDED
data/.rvmrc
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Brian Leonard
|
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,111 @@
|
|
1
|
+
# Offshore
|
2
|
+
|
3
|
+
For when you need a remote factory.
|
4
|
+
Let's say you love testing, but your app heavily depends on another app and it's data in a certain format. You have API and/or database access to this data.
|
5
|
+
|
6
|
+
Offshore allows you use the factories of that app within your test suite as well as adds transactional support to the database of that app so that each test starts with the same fixtures.
|
7
|
+
|
8
|
+
#### Notes
|
9
|
+
|
10
|
+
* For now, this only works on MySQL databases
|
11
|
+
* Redis is needed to run as well
|
12
|
+
|
13
|
+
## Server
|
14
|
+
|
15
|
+
The server app is the one with the factories and the database that your app needs to work.
|
16
|
+
For Rails, add this to your Gemfile:
|
17
|
+
|
18
|
+
group :test do
|
19
|
+
gem 'offshore'
|
20
|
+
end
|
21
|
+
|
22
|
+
You might need something like this to your test.rb application config:
|
23
|
+
|
24
|
+
Offshore.redis = "localhost:6379"
|
25
|
+
|
26
|
+
Then run something like this on the command line
|
27
|
+
|
28
|
+
rails s thin -e test -p 4111
|
29
|
+
|
30
|
+
In you want it anything but blank, you must create a rake task called offshore:seed that creates the test database referenced in the database.yml file in the test environment.
|
31
|
+
Something like this would work.
|
32
|
+
|
33
|
+
namespace :offshore do
|
34
|
+
task :preload do
|
35
|
+
ENV['RAILS_ENV'] = "test"
|
36
|
+
end
|
37
|
+
task :setup => :environment
|
38
|
+
|
39
|
+
desc "seeds the db for offshore gem"
|
40
|
+
task :seed do
|
41
|
+
Rake::Task['db:migrate'].invoke
|
42
|
+
Rake::Task['db:test:prepare'].invoke
|
43
|
+
Rake::Task['db:seed'].invoke
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
The :preload and :setup steps will be invoked in that order before your :seed call. They are actually unnecessary here, but shown in case you have something more complex to do.
|
48
|
+
|
49
|
+
#### Notes
|
50
|
+
|
51
|
+
* For other Rack apps, you should be able to use Offshore::Middleware directly.
|
52
|
+
* This middleware is included automatically in Rails via Railties.
|
53
|
+
* The server is meant to be a singular resource accessed by many test threads (parallelization of tests). It accomplishes this through a mutex and polling its availability.
|
54
|
+
|
55
|
+
|
56
|
+
|
57
|
+
## Client
|
58
|
+
|
59
|
+
The client app is the one running the tests.
|
60
|
+
|
61
|
+
The same thing in the Gemfile:
|
62
|
+
|
63
|
+
group :test do
|
64
|
+
gem 'offshore'
|
65
|
+
end
|
66
|
+
|
67
|
+
The Rspec config looks likes this:
|
68
|
+
|
69
|
+
config.before(:suite) do
|
70
|
+
Offshore.suite.start(:host => "localhost", :port => 4111)
|
71
|
+
end
|
72
|
+
|
73
|
+
config.before(:each) do
|
74
|
+
Offshore.test.start(example)
|
75
|
+
end
|
76
|
+
|
77
|
+
config.after(:each) do
|
78
|
+
Offshore.test.stop(example)
|
79
|
+
end
|
80
|
+
|
81
|
+
config.after(:suite) do
|
82
|
+
Offshore.suite.stop
|
83
|
+
end
|
84
|
+
|
85
|
+
Then in your test you can do:
|
86
|
+
|
87
|
+
user = FactoryOffshore.create(:user, :first_name => "Johnny")
|
88
|
+
user.first_name.should == "Johnny"
|
89
|
+
user.class.should == ::User
|
90
|
+
|
91
|
+
This assumes that you have a class by the same name the the :user factory made from the server that responds to find() with the id created on the server.
|
92
|
+
|
93
|
+
You can send :snapshot => false to Offshore.suite.start to prevent rolling back before the test.
|
94
|
+
Note, this will leave your suite in a somewhat unpredictable state especially when you consider there are other suites that might be rolling that database back.
|
95
|
+
However, this may be preferable if your database is very large. On small (50 tables / 1000 rows) databases, the difference in time seems to be noise. Some efforts are taken (checksum) to not rollback if the test did not change the database.
|
96
|
+
|
97
|
+
|
98
|
+
#### Notes
|
99
|
+
|
100
|
+
* You can also make API requests.
|
101
|
+
* You get a fresh database each time by default.
|
102
|
+
|
103
|
+
## TODO
|
104
|
+
|
105
|
+
* Use binlogs if enabled for even faster MySQL rollback
|
106
|
+
* Configure custom lookups for the hash returned with the created data
|
107
|
+
* Configure custom paths (defaults to /offshore_tests now)
|
108
|
+
* Anything else need to be cleared out each test? i.e. redis, memcache, etc
|
109
|
+
* Other DB support
|
110
|
+
* Other MySQL adapter support
|
111
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
require "faraday"
|
2
|
+
|
3
|
+
module Offshore
|
4
|
+
class Host
|
5
|
+
# def client
|
6
|
+
# return @http if @http
|
7
|
+
# @http = Net::HTTP.new(host, port)
|
8
|
+
# # @http.use_ssl = true
|
9
|
+
# # @http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
10
|
+
# @http
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# def post(append_path, attributes)
|
14
|
+
# request = Net::HTTP::Post.new()
|
15
|
+
# request.set_form_data(attributes)
|
16
|
+
# response = client.request(request)
|
17
|
+
# response.value
|
18
|
+
# JSON.parse(response.body)
|
19
|
+
# end
|
20
|
+
|
21
|
+
def base_uri
|
22
|
+
with_http = host
|
23
|
+
with_http = "http://#{host}" unless (host =~ /^https?:/) == 0
|
24
|
+
need_port = port || 80
|
25
|
+
"#{with_http}:#{need_port}"
|
26
|
+
end
|
27
|
+
|
28
|
+
def connection
|
29
|
+
return @connection if @connection
|
30
|
+
connection_class = Faraday.respond_to?(:new) ? ::Faraday : ::Faraday::Connection
|
31
|
+
|
32
|
+
timeout_seconds = 5*60 # 5 minutes
|
33
|
+
@connection = connection_class.new(base_uri, :timeout => timeout_seconds) do |builder|
|
34
|
+
builder.use Faraday::Request::UrlEncoded if defined?(Faraday::Request::UrlEncoded)
|
35
|
+
builder.adapter Faraday.default_adapter
|
36
|
+
end
|
37
|
+
@connection
|
38
|
+
end
|
39
|
+
|
40
|
+
def post(append_path, attributes={})
|
41
|
+
attributes[:hostname] = `hostname` # always send hostname
|
42
|
+
|
43
|
+
while true do
|
44
|
+
http_response = connection.post("#{self.path}/#{append_path}", attributes)
|
45
|
+
if http_response.success?
|
46
|
+
return MultiJson.decode(http_response.body)
|
47
|
+
elsif http_response.status.to_s == Offshore::WAIT_CODE.to_s
|
48
|
+
sleep 2 # two seconds and try again
|
49
|
+
else
|
50
|
+
begin
|
51
|
+
hash = MultiJson.decode(http_response.body)
|
52
|
+
message = "Error in offshore connection (#{append_path}): #{hash}"
|
53
|
+
rescue
|
54
|
+
message = "Error in offshore connection (#{append_path}): #{http_response.status}"
|
55
|
+
end
|
56
|
+
raise message
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
attr_reader :host, :port
|
62
|
+
def initialize(options)
|
63
|
+
@host = options[:host]
|
64
|
+
@port = options[:port]
|
65
|
+
@path = options[:path]
|
66
|
+
@snapshot = options[:snapshot]
|
67
|
+
end
|
68
|
+
|
69
|
+
def snapshot_name
|
70
|
+
return nil if @snapshot == true
|
71
|
+
return "none" if @snapshot == false
|
72
|
+
@snapshot
|
73
|
+
end
|
74
|
+
|
75
|
+
def path
|
76
|
+
@path ||= "/offshore_tests"
|
77
|
+
end
|
78
|
+
|
79
|
+
def suite_start
|
80
|
+
attributes = {}
|
81
|
+
attributes[:snapshot] = snapshot_name
|
82
|
+
hash = post(:suite_start, attributes)
|
83
|
+
end
|
84
|
+
|
85
|
+
def suite_stop
|
86
|
+
hash = post(:suite_stop)
|
87
|
+
end
|
88
|
+
|
89
|
+
def test_start(name)
|
90
|
+
hash = post(:test_start, { :name => name })
|
91
|
+
end
|
92
|
+
|
93
|
+
def test_stop(name)
|
94
|
+
hash = post(:test_stop, { :name => name })
|
95
|
+
end
|
96
|
+
|
97
|
+
def factory_create(name, attributes={})
|
98
|
+
data = { :name => name }
|
99
|
+
data[:attributes] = attributes
|
100
|
+
hash = post(:factory_create, data)
|
101
|
+
factory_object(hash)
|
102
|
+
end
|
103
|
+
|
104
|
+
# TODO: move this to a config block
|
105
|
+
def factory_object(hash)
|
106
|
+
hash["class_name"].constantize.find(hash["id"])
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Offshore
|
2
|
+
class Suite
|
3
|
+
def hosts
|
4
|
+
@hosts ||= []
|
5
|
+
end
|
6
|
+
|
7
|
+
def default
|
8
|
+
hosts.first
|
9
|
+
end
|
10
|
+
|
11
|
+
def all_hosts!(method, *args)
|
12
|
+
hosts.each do |host|
|
13
|
+
host.send(method, *args)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def start(server_array_or_hash)
|
18
|
+
server_array = server_array_or_hash.is_a?(Array) ? server_array_or_hash : [server_array_or_hash]
|
19
|
+
raise "Need one server" if server_array.size != 1
|
20
|
+
|
21
|
+
server_array.each do |hash|
|
22
|
+
hosts << Host.new(hash)
|
23
|
+
end
|
24
|
+
|
25
|
+
all_hosts!(:suite_start)
|
26
|
+
end
|
27
|
+
|
28
|
+
def stop
|
29
|
+
Offshore.test # if the last one failed this will stop it
|
30
|
+
all_hosts!(:suite_stop)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Offshore
|
2
|
+
class Test
|
3
|
+
def uuid
|
4
|
+
@uid ||= rand(99999999).to_s
|
5
|
+
end
|
6
|
+
|
7
|
+
def get_name(example)
|
8
|
+
name = nil
|
9
|
+
name = example.full_description if example.respond_to?(:full_description)
|
10
|
+
name ||= "unknown"
|
11
|
+
"#{uuid} #{name}"
|
12
|
+
end
|
13
|
+
|
14
|
+
def run!(example)
|
15
|
+
raise "already run test: #{get_name(@run_example)}" if @run_example
|
16
|
+
@run_example = example
|
17
|
+
end
|
18
|
+
|
19
|
+
def run?
|
20
|
+
!!@run_example
|
21
|
+
end
|
22
|
+
|
23
|
+
def start(example=nil)
|
24
|
+
run!(example)
|
25
|
+
Offshore.suite.all_hosts!(:test_start, get_name(example))
|
26
|
+
end
|
27
|
+
|
28
|
+
def stop(example=nil)
|
29
|
+
Offshore.suite.all_hosts!(:test_stop, get_name(example))
|
30
|
+
Offshore.send(:internal_test_ended)
|
31
|
+
end
|
32
|
+
|
33
|
+
def failed
|
34
|
+
raise "have not run!" unless @run_example
|
35
|
+
stop(@run_example)
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Offshore
|
2
|
+
class Controller
|
3
|
+
attr_reader :params
|
4
|
+
def initialize(hash)
|
5
|
+
@params = hash
|
6
|
+
end
|
7
|
+
|
8
|
+
def suite_start
|
9
|
+
suite_key_set(params)
|
10
|
+
[200, {"todo" => "log that the suite is running"}]
|
11
|
+
end
|
12
|
+
|
13
|
+
def suite_stop
|
14
|
+
suite_key_clear
|
15
|
+
[200, {"action" => "cleared"}]
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_start
|
19
|
+
Logger.info(" Start: #{params["name"]}")
|
20
|
+
Offshore::Database.lock
|
21
|
+
Offshore::Database.rollback if suite_key_options["snapshot"] != "none"
|
22
|
+
[200, {"action" => "reset / lock"}]
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_stop
|
26
|
+
Logger.info(" Stop: #{params["name"]}")
|
27
|
+
Offshore::Database.unlock
|
28
|
+
[200, {"action" => "unlocked"}]
|
29
|
+
end
|
30
|
+
|
31
|
+
def factory_create
|
32
|
+
name = params["name"]
|
33
|
+
raise "Need a factory name" unless name
|
34
|
+
attributes = params["attributes"] || {}
|
35
|
+
|
36
|
+
clazz = factory_girl
|
37
|
+
require "factory_girl" unless clazz
|
38
|
+
clazz = factory_girl
|
39
|
+
|
40
|
+
begin
|
41
|
+
val = clazz.create(name, attributes)
|
42
|
+
out = {:class_name => val.class.name, :id => val.id, :attributes => val.attributes}
|
43
|
+
status = 200
|
44
|
+
rescue ActiveRecord::RecordInvalid => invalid
|
45
|
+
out = {:error => invalid.record.errors.full_messages.join(",") }
|
46
|
+
status = 422
|
47
|
+
end
|
48
|
+
[status, out]
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def factory_girl
|
54
|
+
if defined? FactoryGirl
|
55
|
+
return FactoryGirl
|
56
|
+
elsif defined? Factory
|
57
|
+
return Factory
|
58
|
+
end
|
59
|
+
return nil
|
60
|
+
end
|
61
|
+
|
62
|
+
def suite_key_set(val)
|
63
|
+
Offshore.redis.set(suite_key, MultiJson.encode(val))
|
64
|
+
@suite_key_options = nil
|
65
|
+
end
|
66
|
+
|
67
|
+
def suite_key_clear
|
68
|
+
Offshore.redis.del(suite_key)
|
69
|
+
end
|
70
|
+
|
71
|
+
def suite_key_options
|
72
|
+
@suite_key_options ||= MultiJson.decode(Offshore.redis.get(suite_key) || "{}")
|
73
|
+
end
|
74
|
+
|
75
|
+
def suite_key
|
76
|
+
raise "need a hostname" unless params["hostname"]
|
77
|
+
"suite:#{params["hostname"]}:options"
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
module Offshore
|
2
|
+
module Database
|
3
|
+
extend self
|
4
|
+
|
5
|
+
GENERATE_KEY = "database:generate"
|
6
|
+
LOCK_KEY = "database:lock"
|
7
|
+
SHUTDOWN_KEY = "database:shutdown"
|
8
|
+
SUITES_LIST_KEY = "suites:list"
|
9
|
+
|
10
|
+
def redis
|
11
|
+
Offshore.redis
|
12
|
+
end
|
13
|
+
|
14
|
+
def lock
|
15
|
+
Logger.info(" Database.lock")
|
16
|
+
if Offshore::Mutex.lock(LOCK_KEY)
|
17
|
+
Logger.info(" .... lock acquired")
|
18
|
+
else
|
19
|
+
Logger.info(" .... locked")
|
20
|
+
raise Offshore::CheckBackLater.new("Database in use")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def unlock
|
25
|
+
Logger.info(" Database.unlock")
|
26
|
+
Offshore::Mutex.unlock!(LOCK_KEY)
|
27
|
+
end
|
28
|
+
|
29
|
+
def reset
|
30
|
+
Logger.info(" Database.reset")
|
31
|
+
redis.del(LOCK_KEY)
|
32
|
+
redis.del(GENERATE_KEY)
|
33
|
+
redis.del(SHUTDOWN_KEY)
|
34
|
+
redis.del(SUITES_LIST_KEY)
|
35
|
+
end
|
36
|
+
|
37
|
+
def startup(background=true)
|
38
|
+
Logger.info(" Database.startup")
|
39
|
+
reset
|
40
|
+
create_schema(background)
|
41
|
+
end
|
42
|
+
|
43
|
+
def shutdown
|
44
|
+
Logger.info(" Database.shutdown")
|
45
|
+
redis.incr(SHUTDOWN_KEY, 1)
|
46
|
+
end
|
47
|
+
|
48
|
+
def init
|
49
|
+
# TODO: how to let these finish and also stall startup.
|
50
|
+
# should it be per test
|
51
|
+
# what about db reconnections?
|
52
|
+
Logger.info(" Database.init")
|
53
|
+
if redis.get(SHUTDOWN_KEY)
|
54
|
+
Logger.info(" .... databse shutting down. Exiting.")
|
55
|
+
raise "Database shutting down. No new connections, please."
|
56
|
+
else
|
57
|
+
create_schema(true)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def schema_snapshot
|
62
|
+
Logger.info(" Database.schema_snapshot")
|
63
|
+
snapshot.create
|
64
|
+
end
|
65
|
+
|
66
|
+
def rollback
|
67
|
+
Logger.info(" Database.rollback")
|
68
|
+
snapshot.rollback
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def create_schema(background)
|
74
|
+
times = redis.incr(GENERATE_KEY) # get the incremented counter value
|
75
|
+
unless times == 1
|
76
|
+
Logger.info(" Database.create_schema: already created")
|
77
|
+
return
|
78
|
+
end
|
79
|
+
|
80
|
+
Logger.info(" Database.create_schema: creating....")
|
81
|
+
lock
|
82
|
+
if background
|
83
|
+
build_in_fork
|
84
|
+
else
|
85
|
+
build_in_process
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
|
90
|
+
def build_in_fork
|
91
|
+
Logger.info(" ..... building in fork")
|
92
|
+
otherProcess = fork { `bundle exec rake offshore:seed_schema --trace`; `bundle exec rake offshore:unlock --trace`; }
|
93
|
+
Process.detach(otherProcess)
|
94
|
+
end
|
95
|
+
|
96
|
+
def build_in_process
|
97
|
+
Logger.info(" ..... building in process")
|
98
|
+
#TODO: configurable, what rake job if not exist
|
99
|
+
%x(bundle exec rake offshore:seed_schema --trace)
|
100
|
+
unless $? == 0
|
101
|
+
reset
|
102
|
+
raise "rake offshore:seed_schema failed!"
|
103
|
+
end
|
104
|
+
unlock
|
105
|
+
end
|
106
|
+
|
107
|
+
def snapshot
|
108
|
+
Offshore::Snapshot::Template
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Offshore
|
2
|
+
module Logger
|
3
|
+
extend self
|
4
|
+
|
5
|
+
def say(type, message)
|
6
|
+
message = "[Offshore][#{type}][#{Time.now.to_i}][#{Time.now}] #{message}"
|
7
|
+
if defined?(Rails)
|
8
|
+
Rails.logger.send(type, message)
|
9
|
+
else
|
10
|
+
puts message
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def info(message)
|
15
|
+
say(:info, message)
|
16
|
+
end
|
17
|
+
|
18
|
+
def error(message)
|
19
|
+
say(:error, message)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Offshore
|
2
|
+
class Middleware
|
3
|
+
def initialize(app)
|
4
|
+
@app = app
|
5
|
+
end
|
6
|
+
|
7
|
+
def init_server
|
8
|
+
Offshore::Database.init # has it's own singleton code
|
9
|
+
end
|
10
|
+
|
11
|
+
def init_thread
|
12
|
+
return if @init_thread
|
13
|
+
@init_thread = true
|
14
|
+
|
15
|
+
# TODO: move this to a config block
|
16
|
+
if defined?(Rails)
|
17
|
+
begin
|
18
|
+
require Rails.root.join("spec","spec_helper")
|
19
|
+
rescue
|
20
|
+
raise
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def init
|
26
|
+
init_thread
|
27
|
+
init_server
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
def call(env)
|
32
|
+
if (env["PATH_INFO"] =~ /^\/offshore_tests\/(.*)/) == 0
|
33
|
+
offshore_call($1, env)
|
34
|
+
else
|
35
|
+
@app.call(env)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def offshore_call(method, env)
|
40
|
+
status = 500
|
41
|
+
headers = {}
|
42
|
+
hash = {"error" => "Unknown method: #{method}"}
|
43
|
+
|
44
|
+
Logger.info("Offshore Tests Action: #{method}")
|
45
|
+
|
46
|
+
begin
|
47
|
+
case method
|
48
|
+
when "factory_create", "suite_start", "suite_stop", "test_start", "test_stop"
|
49
|
+
init if method == "suite_start"
|
50
|
+
controller = Controller.new(Rack::Request.new(env).params)
|
51
|
+
status, hash = controller.send(method)
|
52
|
+
end
|
53
|
+
rescue CheckBackLater => e
|
54
|
+
hash = {"error" => e.message}
|
55
|
+
status = Offshore::WAIT_CODE
|
56
|
+
rescue StandardError => e
|
57
|
+
hash = {"error" => e.message}
|
58
|
+
status = 500
|
59
|
+
raise # for now
|
60
|
+
end
|
61
|
+
|
62
|
+
content = hash.to_json
|
63
|
+
headers['Content-Type'] = "application/json"
|
64
|
+
headers['Content-Length'] = content.length.to_s
|
65
|
+
out = [status, headers, [content]]
|
66
|
+
Logger.info("Offshore Tests #{method}... returns: #{out.to_s}")
|
67
|
+
out
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
# Adapted from...
|
2
|
+
# https://github.com/kenn/redis-mutex/blob/master/lib/redis/mutex.rb
|
3
|
+
# https://github.com/kenn/redis-classy/blob/master/lib/redis/classy.rb
|
4
|
+
|
5
|
+
module Offshore
|
6
|
+
#
|
7
|
+
# Options
|
8
|
+
#
|
9
|
+
# :block => Specify in seconds how long you want to wait for the lock to be released. Speficy 0
|
10
|
+
# if you need non-blocking sematics and return false immediately. (default: 1)
|
11
|
+
# :sleep => Specify in seconds how long the polling interval should be when :block is given.
|
12
|
+
# It is recommended that you do NOT go below 0.01. (default: 0.1)
|
13
|
+
# :expire => Specify in seconds when the lock should forcibly be removed when something went wrong
|
14
|
+
# with the one who held the lock. (default: 10)
|
15
|
+
#
|
16
|
+
class Mutex
|
17
|
+
|
18
|
+
DEFAULT_EXPIRE = 10*60*1000 # 10 minutes (I think)
|
19
|
+
LockError = Class.new(StandardError)
|
20
|
+
UnlockError = Class.new(StandardError)
|
21
|
+
AssertionError = Class.new(StandardError)
|
22
|
+
|
23
|
+
attr_reader :key
|
24
|
+
def initialize(object, options={})
|
25
|
+
@key = (object.is_a?(String) || object.is_a?(Symbol) ? object.to_s : "#{object.class.name}:#{object.id}")
|
26
|
+
@block = options[:block] || 0 # defaults to not blocking (returns false)
|
27
|
+
@sleep = options[:sleep] || 0.1
|
28
|
+
@expire = options[:expire] || DEFAULT_EXPIRE
|
29
|
+
end
|
30
|
+
|
31
|
+
# get, del, etc
|
32
|
+
def method_missing(command, *args, &block)
|
33
|
+
self.class.send(command, @key, *args, &block)
|
34
|
+
end
|
35
|
+
|
36
|
+
def lock
|
37
|
+
self.class.raise_assertion_error if block_given?
|
38
|
+
@locking = false
|
39
|
+
|
40
|
+
if @block > 0
|
41
|
+
# Blocking mode
|
42
|
+
start_at = Time.now
|
43
|
+
while Time.now - start_at < @block
|
44
|
+
@locking = true and break if try_lock
|
45
|
+
sleep @sleep
|
46
|
+
end
|
47
|
+
else
|
48
|
+
# Non-blocking mode
|
49
|
+
@locking = try_lock
|
50
|
+
end
|
51
|
+
@locking
|
52
|
+
end
|
53
|
+
|
54
|
+
def try_lock
|
55
|
+
now = Time.now.to_f
|
56
|
+
@expires_at = now + @expire # Extend in each blocking loop
|
57
|
+
return true if setnx(@expires_at) # Success, the lock has been acquired
|
58
|
+
return false if get.to_f > now # Check if the lock is still effective
|
59
|
+
|
60
|
+
# The lock has expired but wasn't released... BAD!
|
61
|
+
return true if getset(@expires_at).to_f <= now # Success, we acquired the previously expired lock
|
62
|
+
return false # Dammit, it seems that someone else was even faster than us to remove the expired lock!
|
63
|
+
end
|
64
|
+
|
65
|
+
def unlock(force = false)
|
66
|
+
# Since it's possible that the operations in the critical section took a long time,
|
67
|
+
# we can't just simply release the lock. The unlock method checks if expires_at
|
68
|
+
# is the one we though, and do not release when the lock timestamp was overwritten.
|
69
|
+
|
70
|
+
# FIXME TODO: bug here if this wasn't the one to lock it, but I don't have @expires_at in different threads, etc
|
71
|
+
force = true
|
72
|
+
if force
|
73
|
+
# Redis#del with a single key returns '1' or nil
|
74
|
+
!!del
|
75
|
+
else
|
76
|
+
false
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def with_lock
|
81
|
+
if lock!
|
82
|
+
begin
|
83
|
+
@result = yield
|
84
|
+
ensure
|
85
|
+
unlock
|
86
|
+
end
|
87
|
+
end
|
88
|
+
@result
|
89
|
+
end
|
90
|
+
|
91
|
+
def lock!
|
92
|
+
lock or raise LockError, "failed to acquire lock #{key.inspect}"
|
93
|
+
end
|
94
|
+
|
95
|
+
def unlock!(force = false)
|
96
|
+
unlock(force) or raise UnlockError, "failed to release lock #{key.inspect}"
|
97
|
+
end
|
98
|
+
|
99
|
+
class << self
|
100
|
+
def db
|
101
|
+
Offshore.redis
|
102
|
+
end
|
103
|
+
|
104
|
+
def method_missing(command, *args, &block)
|
105
|
+
db.send(command, *args, &block)
|
106
|
+
end
|
107
|
+
|
108
|
+
def sweep
|
109
|
+
return 0 if (all_keys = keys).empty?
|
110
|
+
|
111
|
+
now = Time.now.to_f
|
112
|
+
values = mget(*all_keys)
|
113
|
+
|
114
|
+
expired_keys = all_keys.zip(values).select do |key, time|
|
115
|
+
time && time.to_f <= now
|
116
|
+
end
|
117
|
+
|
118
|
+
expired_keys.each do |key, _|
|
119
|
+
# Make extra sure that anyone haven't extended the lock
|
120
|
+
del(key) if getset(key, now + DEFAULT_EXPIRE).to_f <= now
|
121
|
+
end
|
122
|
+
|
123
|
+
expired_keys.size
|
124
|
+
end
|
125
|
+
|
126
|
+
def lock(object, options = {})
|
127
|
+
raise_assertion_error if block_given?
|
128
|
+
new(object, options).lock
|
129
|
+
end
|
130
|
+
|
131
|
+
def unlock(object, options = {})
|
132
|
+
raise_assertion_error if block_given?
|
133
|
+
new(object, options).unlock
|
134
|
+
end
|
135
|
+
|
136
|
+
def lock!(object, options = {})
|
137
|
+
new(object, options).lock!
|
138
|
+
end
|
139
|
+
|
140
|
+
def unlock!(object, options = {})
|
141
|
+
raise_assertion_error if block_given?
|
142
|
+
new(object, options).unlock!
|
143
|
+
end
|
144
|
+
|
145
|
+
def with_lock(object, options = {}, &block)
|
146
|
+
new(object, options).with_lock(&block)
|
147
|
+
end
|
148
|
+
|
149
|
+
def raise_assertion_error
|
150
|
+
raise AssertionError, 'block syntax has been removed from #lock, use #with_lock instead'
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Offshore
|
2
|
+
extend self
|
3
|
+
|
4
|
+
# Accepts:
|
5
|
+
# 1. A 'hostname:port' String
|
6
|
+
# 2. A 'hostname:port:db' String (to select the Redis db)
|
7
|
+
# 3. A 'hostname:port/namespace' String (to set the Redis namespace)
|
8
|
+
# 4. A Redis URL String 'redis://host:port'
|
9
|
+
# 5. An instance of `Redis`, `Redis::Client`, `Redis::DistRedis`,
|
10
|
+
# or `Redis::Namespace`.
|
11
|
+
def redis=(server)
|
12
|
+
case server
|
13
|
+
when String
|
14
|
+
if server =~ /redis\:\/\//
|
15
|
+
redis = Redis.connect(:url => server, :thread_safe => true)
|
16
|
+
else
|
17
|
+
server, namespace = server.split('/', 2)
|
18
|
+
host, port, db = server.split(':')
|
19
|
+
redis = Redis.new(:host => host, :port => port,
|
20
|
+
:thread_safe => true, :db => db)
|
21
|
+
end
|
22
|
+
namespace ||= default_namespace
|
23
|
+
|
24
|
+
@redis = Redis::Namespace.new(namespace, :redis => redis)
|
25
|
+
when Redis::Namespace
|
26
|
+
@redis = server
|
27
|
+
else
|
28
|
+
@redis = Redis::Namespace.new(default_namespace, :redis => server)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns the current Redis connection. If none has been created, will
|
33
|
+
# create a new one from the Reqsue one (with a different namespace)
|
34
|
+
def redis
|
35
|
+
return @redis if @redis
|
36
|
+
self.redis = Redis.respond_to?(:connect) ? Redis.connect : "localhost:6379"
|
37
|
+
self.redis
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def default_namespace
|
43
|
+
:offshore
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
require 'erb'
|
2
|
+
require 'digest/md5'
|
3
|
+
require 'mysql2'
|
4
|
+
|
5
|
+
module Offshore
|
6
|
+
module Snapshot
|
7
|
+
extend self
|
8
|
+
|
9
|
+
def config
|
10
|
+
return @config if @config
|
11
|
+
|
12
|
+
raise "Only supported in Rails for now" unless defined?(Rails)
|
13
|
+
|
14
|
+
yml = Rails.root.join("config", "database.yml")
|
15
|
+
hash = YAML.load(ERB.new(File.read(yml)).result)["test"]
|
16
|
+
|
17
|
+
@config = {}
|
18
|
+
['username', 'password', 'host', 'port', 'database', 'collation', 'charset'].each do |key|
|
19
|
+
if hash['master'] && hash['master'].has_key?(key)
|
20
|
+
@config[key] = hash['master'][key]
|
21
|
+
else
|
22
|
+
@config[key] = hash[key]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
@config
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
module Offshore
|
32
|
+
module Snapshot
|
33
|
+
module Template
|
34
|
+
extend self
|
35
|
+
|
36
|
+
SNAPSHOT_KEY = "snapshot:checksum"
|
37
|
+
|
38
|
+
def create
|
39
|
+
use_db(test_db)
|
40
|
+
use_db(template_db)
|
41
|
+
clone(test_db, template_db)
|
42
|
+
record_checksum(test_db)
|
43
|
+
end
|
44
|
+
|
45
|
+
def rollback
|
46
|
+
use_db(test_db)
|
47
|
+
stored = Offshore.redis.get(SNAPSHOT_KEY)
|
48
|
+
sum = db_checksum(test_db)
|
49
|
+
|
50
|
+
if stored == sum
|
51
|
+
Logger.info(" Snapshot checksum equal - not cloning")
|
52
|
+
else
|
53
|
+
Logger.info(" Snapshot checksum not equal - cloning")
|
54
|
+
use_db(template_db)
|
55
|
+
clone(template_db, test_db)
|
56
|
+
record_checksum(test_db) unless stored
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
def rollback2
|
62
|
+
use_db(test_db)
|
63
|
+
use_db(template_db)
|
64
|
+
if equal_checksum?(template_db, test_db)
|
65
|
+
Logger.info(" Snapshot checksum equal - not cloning")
|
66
|
+
else
|
67
|
+
Logger.info(" Snapshot checksum not equal - cloning")
|
68
|
+
clone(template_db, test_db)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def record_checksum(db)
|
75
|
+
sum = db_checksum(db)
|
76
|
+
Logger.info(" Snapshot checksum: #{sum}")
|
77
|
+
Offshore.redis.set(SNAPSHOT_KEY, sum)
|
78
|
+
end
|
79
|
+
|
80
|
+
def equal_checksum?(from, to)
|
81
|
+
tem = db_checksum(from)
|
82
|
+
sum = db_checksum(to)
|
83
|
+
Logger.info(" Snapshot checksum (calculated) compare: #{sum} == #{tem}")
|
84
|
+
tem == sum
|
85
|
+
end
|
86
|
+
|
87
|
+
def test_db
|
88
|
+
@test_db ||= make_conn
|
89
|
+
end
|
90
|
+
|
91
|
+
def template_db
|
92
|
+
@template_db ||= make_conn("_offshore_template")
|
93
|
+
end
|
94
|
+
|
95
|
+
def clone(from, to)
|
96
|
+
empty_tables(to)
|
97
|
+
copy_tables(from, to)
|
98
|
+
copy_data(from, to)
|
99
|
+
end
|
100
|
+
|
101
|
+
def db_checksum(db)
|
102
|
+
string = ""
|
103
|
+
name = current_db(db)
|
104
|
+
get_tables(db).each do |table|
|
105
|
+
row = db.query("CHECKSUM TABLE `#{name}`.`#{table}`")
|
106
|
+
string << ":"
|
107
|
+
string << row.first["Checksum"].to_s
|
108
|
+
end
|
109
|
+
|
110
|
+
Digest::MD5.hexdigest(string)
|
111
|
+
end
|
112
|
+
|
113
|
+
def make_conn(append="")
|
114
|
+
ar = Offshore::Snapshot.config
|
115
|
+
config = {}
|
116
|
+
config['reconnect'] = true
|
117
|
+
config['flags'] = Mysql2::Client::MULTI_STATEMENTS
|
118
|
+
|
119
|
+
['username', 'password', 'host', 'port'].each do |key|
|
120
|
+
config[key.to_sym] = ar[key]
|
121
|
+
end
|
122
|
+
|
123
|
+
client = Mysql2::Client.new(config)
|
124
|
+
client.instance_variable_set("@offshore_db_name", "#{ar['database']}#{append}")
|
125
|
+
client
|
126
|
+
end
|
127
|
+
|
128
|
+
def use_db(db)
|
129
|
+
original_db = current_db(db)
|
130
|
+
begin
|
131
|
+
db.query("create database `#{original_db}` DEFAULT CHARACTER SET `utf8`") # TODO forcing UTF8
|
132
|
+
rescue
|
133
|
+
end
|
134
|
+
db.query("use `#{original_db}`")
|
135
|
+
end
|
136
|
+
|
137
|
+
def empty_tables(db)
|
138
|
+
name = current_db(db)
|
139
|
+
get_tables(db).each do |table|
|
140
|
+
db.query("drop table if exists `#{name}`.`#{table}`")
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def copy_tables(from, to)
|
145
|
+
from_db = current_db(from)
|
146
|
+
to_db = current_db(to)
|
147
|
+
get_tables(from).each do |table|
|
148
|
+
to.query("create table `#{to_db}`.`#{table}` like `#{from_db}`.`#{table}`")
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def copy_data(from, to)
|
153
|
+
from_db = current_db(from)
|
154
|
+
to_db = current_db(to)
|
155
|
+
get_tables(from).each do |table|
|
156
|
+
to.query("insert into `#{to_db}`.`#{table}` select * from `#{from_db}`.`#{table}`")
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def current_db(db)
|
161
|
+
db.instance_variable_get("@offshore_db_name")
|
162
|
+
# db.query("select database()").first.values[0]
|
163
|
+
end
|
164
|
+
|
165
|
+
def get_tables(db)
|
166
|
+
table_array = []
|
167
|
+
db.query("show tables").each do |row|
|
168
|
+
table_array << row.values[0]
|
169
|
+
end
|
170
|
+
table_array
|
171
|
+
end
|
172
|
+
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require "redis"
|
2
|
+
require "offshore/server/errors"
|
3
|
+
require "offshore/server/logger"
|
4
|
+
require "offshore/server/redis"
|
5
|
+
require "offshore/server/mutex"
|
6
|
+
require "offshore/server/snapshot"
|
7
|
+
require "offshore/server/database"
|
8
|
+
require "offshore/server/controller"
|
9
|
+
require "offshore/server/middleware"
|
10
|
+
require "offshore/server/railtie" if defined?(Rails)
|
@@ -0,0 +1,35 @@
|
|
1
|
+
namespace :offshore do
|
2
|
+
task :setup
|
3
|
+
task :seed
|
4
|
+
|
5
|
+
task :preload do
|
6
|
+
require "offshore"
|
7
|
+
end
|
8
|
+
|
9
|
+
task :schema_snapshot => [:preload, :setup] do
|
10
|
+
Offshore::Database.schema_snapshot
|
11
|
+
end
|
12
|
+
|
13
|
+
task :schema_rollback => [:preload, :setup] do
|
14
|
+
Offshore::Database.snapshoter.rollback
|
15
|
+
end
|
16
|
+
|
17
|
+
task :unlock => [:preload, :setup] do
|
18
|
+
Offshore::Database.unlock
|
19
|
+
end
|
20
|
+
|
21
|
+
task :shutdown => [:preload, :setup] do
|
22
|
+
Offshore::Database.shutdown
|
23
|
+
end
|
24
|
+
|
25
|
+
task :startup => [:preload, :setup] do
|
26
|
+
Offshore::Database.startup(false)
|
27
|
+
end
|
28
|
+
|
29
|
+
task :reset => [:preload, :setup] do
|
30
|
+
Offshore::Database.reset
|
31
|
+
end
|
32
|
+
|
33
|
+
task :seed_schema => [ :preload, :setup, :seed, :schema_snapshot ]
|
34
|
+
|
35
|
+
end
|
data/lib/offshore.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require "offshore/version"
|
2
|
+
|
3
|
+
module Offshore
|
4
|
+
extend self
|
5
|
+
|
6
|
+
WAIT_CODE = 499
|
7
|
+
|
8
|
+
def suite
|
9
|
+
@suite ||= Suite.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def test
|
13
|
+
if @test && @test.run?
|
14
|
+
# run again because it crashed in execution
|
15
|
+
@test.failed
|
16
|
+
end
|
17
|
+
@test ||= Test.new
|
18
|
+
end
|
19
|
+
|
20
|
+
protected
|
21
|
+
|
22
|
+
def internal_test_ended
|
23
|
+
# called from test.failed and test.stop
|
24
|
+
@test = nil
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
require "offshore/client"
|
29
|
+
require "offshore/server"
|
data/offshore.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/offshore/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Brian Leonard"]
|
6
|
+
gem.email = ["brian@bleonard.com"]
|
7
|
+
gem.description = %q{For handling remote factories and tests}
|
8
|
+
gem.summary = %q{For handling remote factories and tests}
|
9
|
+
gem.homepage = ""
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "offshore"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Offshore::VERSION
|
17
|
+
|
18
|
+
gem.add_dependency "multi_json"
|
19
|
+
gem.add_dependency "faraday"
|
20
|
+
gem.add_dependency "redis"
|
21
|
+
gem.add_dependency "redis-namespace"
|
22
|
+
gem.add_dependency "mysql2"
|
23
|
+
end
|
metadata
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: offshore
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Brian Leonard
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-12-14 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: multi_json
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: faraday
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: redis
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: redis-namespace
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: mysql2
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :runtime
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
description: For handling remote factories and tests
|
95
|
+
email:
|
96
|
+
- brian@bleonard.com
|
97
|
+
executables: []
|
98
|
+
extensions: []
|
99
|
+
extra_rdoc_files: []
|
100
|
+
files:
|
101
|
+
- .gitignore
|
102
|
+
- .rvmrc
|
103
|
+
- Gemfile
|
104
|
+
- LICENSE
|
105
|
+
- README.md
|
106
|
+
- Rakefile
|
107
|
+
- lib/offshore.rb
|
108
|
+
- lib/offshore/client.rb
|
109
|
+
- lib/offshore/client/factory_offshore.rb
|
110
|
+
- lib/offshore/client/host.rb
|
111
|
+
- lib/offshore/client/suite.rb
|
112
|
+
- lib/offshore/client/test.rb
|
113
|
+
- lib/offshore/server.rb
|
114
|
+
- lib/offshore/server/controller.rb
|
115
|
+
- lib/offshore/server/database.rb
|
116
|
+
- lib/offshore/server/errors.rb
|
117
|
+
- lib/offshore/server/logger.rb
|
118
|
+
- lib/offshore/server/middleware.rb
|
119
|
+
- lib/offshore/server/mutex.rb
|
120
|
+
- lib/offshore/server/railtie.rb
|
121
|
+
- lib/offshore/server/redis.rb
|
122
|
+
- lib/offshore/server/snapshot.rb
|
123
|
+
- lib/offshore/tasks.rb
|
124
|
+
- lib/offshore/version.rb
|
125
|
+
- offshore.gemspec
|
126
|
+
homepage: ''
|
127
|
+
licenses: []
|
128
|
+
post_install_message:
|
129
|
+
rdoc_options: []
|
130
|
+
require_paths:
|
131
|
+
- lib
|
132
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
133
|
+
none: false
|
134
|
+
requirements:
|
135
|
+
- - ! '>='
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '0'
|
138
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
139
|
+
none: false
|
140
|
+
requirements:
|
141
|
+
- - ! '>='
|
142
|
+
- !ruby/object:Gem::Version
|
143
|
+
version: '0'
|
144
|
+
requirements: []
|
145
|
+
rubyforge_project:
|
146
|
+
rubygems_version: 1.8.24
|
147
|
+
signing_key:
|
148
|
+
specification_version: 3
|
149
|
+
summary: For handling remote factories and tests
|
150
|
+
test_files: []
|