hydra 0.3.0 → 0.4.0

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/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