hydra 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -21,7 +21,7 @@ end
21
21
  require 'rake/testtask'
22
22
  Rake::TestTask.new(:test) do |test|
23
23
  test.libs << 'lib' << 'test'
24
- test.pattern = 'test/**/test_*.rb'
24
+ test.pattern = 'test/**/*_test.rb'
25
25
  test.verbose = true
26
26
  end
27
27
 
@@ -29,7 +29,7 @@ begin
29
29
  require 'rcov/rcovtask'
30
30
  Rcov::RcovTask.new do |test|
31
31
  test.libs << 'test'
32
- test.pattern = 'test/**/test_*.rb'
32
+ test.pattern = 'test/**/*_test.rb'
33
33
  test.verbose = true
34
34
  end
35
35
  rescue LoadError
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.0
1
+ 0.4.0
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{hydra}
8
- s.version = "0.3.0"
8
+ s.version = "0.4.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Nick Gauthier"]
12
- s.date = %q{2010-01-27}
12
+ s.date = %q{2010-01-28}
13
13
  s.description = %q{Spread your tests over multiple machines to test your code faster.}
14
14
  s.email = %q{nick@smartlogicsolutions.com}
15
15
  s.extra_rdoc_files = [
@@ -27,20 +27,23 @@ Gem::Specification.new do |s|
27
27
  "VERSION",
28
28
  "hydra.gemspec",
29
29
  "lib/hydra.rb",
30
- "lib/hydra/io.rb",
31
30
  "lib/hydra/message.rb",
32
31
  "lib/hydra/message/runner_messages.rb",
32
+ "lib/hydra/message/worker_messages.rb",
33
+ "lib/hydra/messaging_io.rb",
33
34
  "lib/hydra/pipe.rb",
34
35
  "lib/hydra/runner.rb",
35
36
  "lib/hydra/ssh.rb",
36
- "test/echo_the_dolphin.rb",
37
- "test/helper.rb",
38
- "test/sample_tests/assert_true.rb",
39
- "test/sample_tests/write_file.rb",
40
- "test/test_message.rb",
41
- "test/test_pipe.rb",
42
- "test/test_runner.rb",
43
- "test/test_ssh.rb"
37
+ "lib/hydra/worker.rb",
38
+ "test/fixtures/assert_true.rb",
39
+ "test/fixtures/echo_the_dolphin.rb",
40
+ "test/fixtures/write_file.rb",
41
+ "test/message_test.rb",
42
+ "test/pipe_test.rb",
43
+ "test/runner_test.rb",
44
+ "test/ssh_test.rb",
45
+ "test/test_helper.rb",
46
+ "test/worker_test.rb"
44
47
  ]
45
48
  s.homepage = %q{http://github.com/ngauthier/hydra}
46
49
  s.rdoc_options = ["--charset=UTF-8"]
@@ -48,14 +51,15 @@ Gem::Specification.new do |s|
48
51
  s.rubygems_version = %q{1.3.5}
49
52
  s.summary = %q{Distributed testing toolkit}
50
53
  s.test_files = [
51
- "test/test_ssh.rb",
52
- "test/helper.rb",
53
- "test/test_message.rb",
54
- "test/test_pipe.rb",
55
- "test/test_runner.rb",
56
- "test/sample_tests/write_file.rb",
57
- "test/sample_tests/assert_true.rb",
58
- "test/echo_the_dolphin.rb"
54
+ "test/message_test.rb",
55
+ "test/test_helper.rb",
56
+ "test/ssh_test.rb",
57
+ "test/fixtures/write_file.rb",
58
+ "test/fixtures/assert_true.rb",
59
+ "test/fixtures/echo_the_dolphin.rb",
60
+ "test/worker_test.rb",
61
+ "test/runner_test.rb",
62
+ "test/pipe_test.rb"
59
63
  ]
60
64
 
61
65
  if s.respond_to? :specification_version then
@@ -2,4 +2,5 @@ require 'hydra/pipe'
2
2
  require 'hydra/ssh'
3
3
  require 'hydra/message'
4
4
  require 'hydra/runner'
5
+ require 'hydra/worker'
5
6
 
@@ -19,6 +19,7 @@ module Hydra #:nodoc:
19
19
  # are attributes of the message and the values are
20
20
  # set to the attribute.
21
21
  def initialize(opts = {})
22
+ opts.delete :class
22
23
  opts.each do |variable,value|
23
24
  self.send("#{variable}=",value)
24
25
  end
@@ -41,4 +42,5 @@ module Hydra #:nodoc:
41
42
  end
42
43
 
43
44
  require 'hydra/message/runner_messages'
45
+ require 'hydra/message/worker_messages'
44
46
 
@@ -3,6 +3,9 @@ module Hydra #:nodoc:
3
3
  module Runner #:nodoc:
4
4
  # Message indicating that a Runner needs a file to run
5
5
  class RequestFile < Hydra::Message
6
+ def handle(worker, runner) #:nodoc:
7
+ worker.request_file(self, runner)
8
+ end
6
9
  end
7
10
 
8
11
  # Message telling the Runner to run a file
@@ -23,6 +26,9 @@ module Hydra #:nodoc:
23
26
  def serialize #:nodoc:
24
27
  super(:output => @output, :file => @file)
25
28
  end
29
+ def handle(worker, runner) #:nodoc:
30
+ worker.relay_results(self, runner)
31
+ end
26
32
  end
27
33
 
28
34
  # Message to tell the Runner to shut down
@@ -0,0 +1,27 @@
1
+ module Hydra #:nodoc:
2
+ module Messages #:nodoc:
3
+ module Worker #:nodoc:
4
+ # Message indicating that a work needs a file to delegate to a runner
5
+ class RequestFile < Hydra::Message
6
+ end
7
+
8
+ # Message telling a worker to delegate a file to a runner
9
+ class RunFile < Hydra::Messages::Runner::RunFile
10
+ def handle(worker)
11
+ worker.delegate_file(self)
12
+ end
13
+ end
14
+
15
+ # Message relaying the results of a worker up to the master
16
+ class Results < Hydra::Messages::Runner::Results
17
+ end
18
+
19
+ # Message telling the worker to shut down.
20
+ class Shutdown < Hydra::Messages::Runner::Shutdown
21
+ def handle(worker)
22
+ worker.shutdown
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -38,10 +38,6 @@ module Hydra #:nodoc:
38
38
  # because the string is not a message.
39
39
  class UnprocessableMessage < RuntimeError
40
40
  attr_accessor :message
41
- # Allow a custom message for the exception.
42
- def initialize(message = "Message expected")
43
- @message = message
44
- end
45
41
  end
46
42
  end
47
43
  end
@@ -1,4 +1,4 @@
1
- require 'hydra/io'
1
+ require 'hydra/messaging_io'
2
2
  module Hydra #:nodoc:
3
3
  # Read and write between two processes via pipes. For example:
4
4
  # @pipe = Hydra::Pipe.new
@@ -1,5 +1,5 @@
1
1
  require 'open3'
2
- require 'hydra/io'
2
+ require 'hydra/messaging_io'
3
3
  module Hydra #:nodoc:
4
4
  # Read and write with an ssh connection. For example:
5
5
  # @ssh = Hydra::SSH.new(
@@ -0,0 +1,114 @@
1
+ module Hydra #:nodoc:
2
+ # Hydra class responsible to dispatching runners and communicating with the master.
3
+ class Worker
4
+ # Create a new worker.
5
+ # * io: The IO object to use to communicate with the master
6
+ # * num_runners: The number of runners to launch
7
+ def initialize(io, num_runners)
8
+ @io = io
9
+ @runners = []
10
+ @listeners = []
11
+ boot_runners(num_runners)
12
+ process_messages
13
+ @runners.each{|r| Process.wait r[:pid] }
14
+ end
15
+
16
+
17
+ # message handling methods
18
+
19
+ # When a runner wants a file, it hits this method with a message.
20
+ # Then the worker bubbles the file request up to the master.
21
+ def request_file(message, runner)
22
+ @io.write(Hydra::Messages::Worker::RequestFile.new)
23
+ runner[:idle] = true
24
+ end
25
+
26
+ # When the master sends a file down to the worker, it hits this
27
+ # method. Then the worker delegates the file down to a runner.
28
+ def delegate_file(message)
29
+ r = idle_runner
30
+ r[:idle] = false
31
+ r[:io].write(Hydra::Messages::Runner::RunFile.new(eval(message.serialize)))
32
+ end
33
+
34
+ # When a runner finishes, it sends the results up to the worker. Then the
35
+ # worker sends the results up to the master.
36
+ # TODO: when we relay results, it should trigger a RunFile or Shutdown from
37
+ # the master implicitly
38
+ def relay_results(message, runner)
39
+ runner[:idle] = true
40
+ @io.write(Hydra::Messages::Worker::Results.new(eval(message.serialize)))
41
+ end
42
+
43
+ # When a master issues a shutdown order, it hits this method, which causes
44
+ # the worker to send shutdown messages to its runners.
45
+ # TODO: implement a ShutdownComplete message, so that we can kill the
46
+ # processes if necessary.
47
+ def shutdown
48
+ @running = false
49
+ @runners.each do |r|
50
+ r[:io].write(Hydra::Messages::Runner::Shutdown.new)
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def boot_runners(num_runners) #:nodoc:
57
+ num_runners.times do
58
+ pipe = Hydra::Pipe.new
59
+ child = Process.fork do
60
+ pipe.identify_as_child
61
+ Hydra::Runner.new(pipe)
62
+ end
63
+ pipe.identify_as_parent
64
+ @runners << { :pid => child, :io => pipe, :idle => false }
65
+ end
66
+ end
67
+
68
+ # Continuously process messages
69
+ def process_messages #:nodoc:
70
+ @running = true
71
+
72
+ # Abort the worker if one of the runners has an exception
73
+ # TODO: catch this exception, return a dying message to the master
74
+ # then shutdown
75
+ Thread.abort_on_exception = true
76
+
77
+ # Worker listens and handles messages
78
+ @listeners << Thread.new do
79
+ while @running
80
+ message = @io.gets
81
+ message.handle(self) if message
82
+ end
83
+ end
84
+
85
+ # Runners listen, but when they handle they pass themselves
86
+ # so we can reference them when we deal with their messages
87
+ @runners.each do |r|
88
+ @listeners << Thread.new do
89
+ while @running
90
+ begin
91
+ message = r[:io].gets
92
+ message.handle(self, r) if message
93
+ rescue IOError => ex
94
+ # If the other end of the pipe closes
95
+ # we will continue, because we're probably
96
+ # not @running anymore
97
+ end
98
+ end
99
+ end
100
+ end
101
+ @listeners.each{|l| l.join }
102
+ end
103
+
104
+ # Get the next idle runner
105
+ def idle_runner #:nodoc:
106
+ idle_r = nil
107
+ while idle_r.nil?
108
+ idle_r = @runners.detect{|r| r[:idle]}
109
+ sleep(1)
110
+ end
111
+ return idle_r
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,7 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'test_helper')
2
+
3
+ class AssertTrueTest < Test::Unit::TestCase
4
+ should "be true" do
5
+ assert true
6
+ end
7
+ end
@@ -1,6 +1,6 @@
1
- require File.join(File.dirname(__FILE__), '..', 'helper')
1
+ require File.join(File.dirname(__FILE__), '..', 'test_helper')
2
2
 
3
- class TestWriteFile < Test::Unit::TestCase
3
+ class WriteFileTest < Test::Unit::TestCase
4
4
  should "write file" do
5
5
  File.open(File.join(Dir.tmpdir, 'hydra_test.txt'), 'w') do |f|
6
6
  f.write "HYDRA"
@@ -1,6 +1,6 @@
1
- require File.join(File.dirname(__FILE__), 'helper')
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
2
 
3
- class TestMessage < Test::Unit::TestCase
3
+ class MessageTest < Test::Unit::TestCase
4
4
  class MyMessage < Hydra::Message
5
5
  attr_accessor :my_var
6
6
  def serialize
@@ -17,7 +17,7 @@ class TestMessage < Test::Unit::TestCase
17
17
  end
18
18
  should "serialize" do
19
19
  assert_equal(
20
- {:class=>TestMessage::MyMessage, :my_var=>"my value"},
20
+ {:class=>MyMessage, :my_var=>"my value"},
21
21
  eval(@m.serialize)
22
22
  )
23
23
  end
@@ -1,6 +1,6 @@
1
- require File.join(File.dirname(__FILE__), 'helper')
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
2
 
3
- class TestPipe < Test::Unit::TestCase
3
+ class PipeTest < Test::Unit::TestCase
4
4
  context "a pipe" do
5
5
  setup do
6
6
  @pipe = Hydra::Pipe.new
@@ -0,0 +1,70 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ TARGET = File.join(Dir.tmpdir, 'hydra_test.txt')
4
+ TESTFILE = File.join(File.dirname(__FILE__), 'fixtures', 'write_file.rb')
5
+
6
+ class RunnerTest < Test::Unit::TestCase
7
+ context "with a file to test and a destination to verify" do
8
+ setup do
9
+ FileUtils.rm_f(TARGET)
10
+ end
11
+
12
+ teardown do
13
+ FileUtils.rm_f(TARGET)
14
+ end
15
+
16
+
17
+ should "run a test" do
18
+ # flip it around to the parent is in the fork, this gives
19
+ # us more direct control over the runner and proper test
20
+ # coverage output
21
+ pipe = Hydra::Pipe.new
22
+ parent = Process.fork do
23
+ request_a_file_and_verify_completion(pipe)
24
+ end
25
+ run_the_runner(pipe)
26
+ Process.wait(parent)
27
+ end
28
+
29
+ # this flips the above test, so that the main process runs a bit of the parent
30
+ # code, but only with minimal assertion
31
+ should "be able to tell a runner to run a test" do
32
+ pipe = Hydra::Pipe.new
33
+ child = Process.fork do
34
+ run_the_runner(pipe)
35
+ end
36
+ request_a_file_and_verify_completion(pipe)
37
+ Process.wait(child)
38
+ end
39
+ end
40
+
41
+ module RunnerTestHelper
42
+ def request_a_file_and_verify_completion(pipe)
43
+ pipe.identify_as_parent
44
+
45
+ # make sure it asks for a file, then give it one
46
+ assert pipe.gets.is_a?(Hydra::Messages::Runner::RequestFile)
47
+ pipe.write(Hydra::Messages::Runner::RunFile.new(:file => TESTFILE))
48
+
49
+ # grab its response. This makes us wait for it to finish
50
+ response = pipe.gets
51
+
52
+ # tell it to shut down
53
+ pipe.write(Hydra::Messages::Runner::Shutdown.new)
54
+
55
+ # ensure it ran
56
+ assert File.exists?(TARGET)
57
+ assert_equal "HYDRA", File.read(TARGET)
58
+
59
+ pipe.close
60
+ end
61
+
62
+ def run_the_runner(pipe)
63
+ pipe.identify_as_child
64
+ Hydra::Runner.new(pipe)
65
+ pipe.close
66
+ end
67
+ end
68
+ include RunnerTestHelper
69
+ end
70
+
@@ -1,12 +1,12 @@
1
- require File.join(File.dirname(__FILE__), 'helper')
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
2
 
3
- class TestSSH < Test::Unit::TestCase
3
+ class SSHTest < Test::Unit::TestCase
4
4
  context "an ssh connection" do
5
5
  setup do
6
6
  @ssh = Hydra::SSH.new(
7
7
  'localhost', # connect to this machine
8
8
  File.expand_path(File.join(File.dirname(__FILE__))), # move to the test directory
9
- "ruby ./echo_the_dolphin.rb"
9
+ "ruby fixtures/echo_the_dolphin.rb"
10
10
  )
11
11
  @message = Hydra::Messages::TestMessage.new
12
12
  end
File without changes
@@ -0,0 +1,65 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ TARGET = File.join(Dir.tmpdir, 'hydra_test.txt')
4
+ TESTFILE = File.join(File.dirname(__FILE__), 'fixtures', 'write_file.rb')
5
+
6
+ class WorkerTest < Test::Unit::TestCase
7
+ context "with a file to test and a destination to verify" do
8
+ setup do
9
+ FileUtils.rm_f(TARGET)
10
+ end
11
+
12
+ teardown do
13
+ FileUtils.rm_f(TARGET)
14
+ end
15
+
16
+ # run the worker in the foreground and the requests in the background
17
+ should "run a test" do
18
+ num_runners = 4
19
+ pipe = Hydra::Pipe.new
20
+ child = Process.fork do
21
+ request_a_file_and_verify_completion(pipe, num_runners)
22
+ pipe.close
23
+ end
24
+ run_the_worker(pipe, num_runners)
25
+ Process.wait(child)
26
+ end
27
+
28
+ # inverse of the above test to run the worker in the background
29
+ should "be able to tell a worker to run a test" do
30
+ num_runners = 4
31
+ pipe = Hydra::Pipe.new
32
+ child = Process.fork do
33
+ run_the_worker(pipe, num_runners)
34
+ end
35
+ request_a_file_and_verify_completion(pipe, num_runners)
36
+ Process.wait(child)
37
+ pipe.close
38
+ end
39
+ end
40
+
41
+ module WorkerTestHelper
42
+ def run_the_worker(pipe, num_runners)
43
+ pipe.identify_as_child
44
+ Hydra::Worker.new(pipe, num_runners)
45
+ pipe.close
46
+ end
47
+
48
+ def request_a_file_and_verify_completion(pipe, num_runners)
49
+ pipe.identify_as_parent
50
+ num_runners.times do
51
+ assert pipe.gets.is_a?(Hydra::Messages::Worker::RequestFile)
52
+ end
53
+ pipe.write(Hydra::Messages::Worker::RunFile.new(:file => TESTFILE))
54
+
55
+ response = pipe.gets
56
+ assert response.is_a?(Hydra::Messages::Worker::Results)
57
+
58
+ pipe.write(Hydra::Messages::Worker::Shutdown.new)
59
+
60
+ assert File.exists?(TARGET)
61
+ assert_equal "HYDRA", File.read(TARGET)
62
+ end
63
+ end
64
+ include WorkerTestHelper
65
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hydra
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nick Gauthier
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2010-01-27 00:00:00 -05:00
12
+ date: 2010-01-28 00:00:00 -05:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -52,20 +52,23 @@ files:
52
52
  - VERSION
53
53
  - hydra.gemspec
54
54
  - lib/hydra.rb
55
- - lib/hydra/io.rb
56
55
  - lib/hydra/message.rb
57
56
  - lib/hydra/message/runner_messages.rb
57
+ - lib/hydra/message/worker_messages.rb
58
+ - lib/hydra/messaging_io.rb
58
59
  - lib/hydra/pipe.rb
59
60
  - lib/hydra/runner.rb
60
61
  - lib/hydra/ssh.rb
61
- - test/echo_the_dolphin.rb
62
- - test/helper.rb
63
- - test/sample_tests/assert_true.rb
64
- - test/sample_tests/write_file.rb
65
- - test/test_message.rb
66
- - test/test_pipe.rb
67
- - test/test_runner.rb
68
- - test/test_ssh.rb
62
+ - lib/hydra/worker.rb
63
+ - test/fixtures/assert_true.rb
64
+ - test/fixtures/echo_the_dolphin.rb
65
+ - test/fixtures/write_file.rb
66
+ - test/message_test.rb
67
+ - test/pipe_test.rb
68
+ - test/runner_test.rb
69
+ - test/ssh_test.rb
70
+ - test/test_helper.rb
71
+ - test/worker_test.rb
69
72
  has_rdoc: true
70
73
  homepage: http://github.com/ngauthier/hydra
71
74
  licenses: []
@@ -95,11 +98,12 @@ signing_key:
95
98
  specification_version: 3
96
99
  summary: Distributed testing toolkit
97
100
  test_files:
98
- - test/test_ssh.rb
99
- - test/helper.rb
100
- - test/test_message.rb
101
- - test/test_pipe.rb
102
- - test/test_runner.rb
103
- - test/sample_tests/write_file.rb
104
- - test/sample_tests/assert_true.rb
105
- - test/echo_the_dolphin.rb
101
+ - test/message_test.rb
102
+ - test/test_helper.rb
103
+ - test/ssh_test.rb
104
+ - test/fixtures/write_file.rb
105
+ - test/fixtures/assert_true.rb
106
+ - test/fixtures/echo_the_dolphin.rb
107
+ - test/worker_test.rb
108
+ - test/runner_test.rb
109
+ - test/pipe_test.rb
@@ -1,7 +0,0 @@
1
- require File.join(File.dirname(__FILE__), '..', 'helper')
2
-
3
- class TestAssertTrue < Test::Unit::TestCase
4
- should "be true" do
5
- assert true
6
- end
7
- end
@@ -1,42 +0,0 @@
1
- require File.join(File.dirname(__FILE__), 'helper')
2
-
3
- class TestRunner < Test::Unit::TestCase
4
- context "a test runner" do
5
- setup do
6
- @pipe = Hydra::Pipe.new
7
- @child = Process.fork do
8
- @pipe.identify_as_child
9
- Hydra::Runner.new(@pipe)
10
- end
11
- @pipe.identify_as_parent
12
- end
13
- teardown do
14
- @pipe.close
15
- Process.wait(@child)
16
- end
17
- should "boot and run a file and shut down" do
18
- assert @pipe.gets.is_a?(Hydra::Messages::Runner::RequestFile)
19
-
20
- file = File.join(File.dirname(__FILE__), 'sample_tests', 'assert_true.rb')
21
- @pipe.write(Hydra::Messages::Runner::RunFile.new(:file => file))
22
- response = @pipe.gets
23
- assert response.is_a?(Hydra::Messages::Runner::Results)
24
- assert response.output =~ /Finished/
25
- assert_equal file, response.file
26
- @pipe.write(Hydra::Messages::Runner::Shutdown.new)
27
- end
28
-
29
- should "run a test" do
30
- target = File.join(Dir.tmpdir, 'hydra_test.txt')
31
- FileUtils.rm_f(target)
32
- assert !File.exists?(target)
33
- file = File.join(File.dirname(__FILE__), 'sample_tests', 'write_file.rb')
34
- assert @pipe.gets.is_a?(Hydra::Messages::Runner::RequestFile)
35
- @pipe.write(Hydra::Messages::Runner::RunFile.new(:file => file))
36
- response = @pipe.gets
37
- @pipe.write(Hydra::Messages::Runner::Shutdown.new)
38
- assert File.exists?(target)
39
- assert_equal "HYDRA", File.read(target)
40
- end
41
- end
42
- end