causes-hydra 0.21.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.
Files changed (61) hide show
  1. data/.document +5 -0
  2. data/.gitignore +22 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +39 -0
  5. data/Rakefile +56 -0
  6. data/TODO +18 -0
  7. data/VERSION +1 -0
  8. data/bin/warmsnake.rb +76 -0
  9. data/caliper.yml +6 -0
  10. data/hydra-icon-64x64.png +0 -0
  11. data/hydra.gemspec +130 -0
  12. data/hydra_gray.png +0 -0
  13. data/lib/hydra.rb +16 -0
  14. data/lib/hydra/cucumber/formatter.rb +29 -0
  15. data/lib/hydra/hash.rb +16 -0
  16. data/lib/hydra/js/lint.js +5150 -0
  17. data/lib/hydra/listener/abstract.rb +39 -0
  18. data/lib/hydra/listener/minimal_output.rb +24 -0
  19. data/lib/hydra/listener/notifier.rb +17 -0
  20. data/lib/hydra/listener/progress_bar.rb +48 -0
  21. data/lib/hydra/listener/report_generator.rb +30 -0
  22. data/lib/hydra/master.rb +249 -0
  23. data/lib/hydra/message.rb +47 -0
  24. data/lib/hydra/message/master_messages.rb +19 -0
  25. data/lib/hydra/message/runner_messages.rb +52 -0
  26. data/lib/hydra/message/worker_messages.rb +52 -0
  27. data/lib/hydra/messaging_io.rb +46 -0
  28. data/lib/hydra/pipe.rb +61 -0
  29. data/lib/hydra/runner.rb +305 -0
  30. data/lib/hydra/safe_fork.rb +31 -0
  31. data/lib/hydra/spec/autorun_override.rb +3 -0
  32. data/lib/hydra/spec/hydra_formatter.rb +26 -0
  33. data/lib/hydra/ssh.rb +41 -0
  34. data/lib/hydra/stdio.rb +16 -0
  35. data/lib/hydra/sync.rb +99 -0
  36. data/lib/hydra/tasks.rb +342 -0
  37. data/lib/hydra/trace.rb +24 -0
  38. data/lib/hydra/worker.rb +150 -0
  39. data/test/fixtures/assert_true.rb +7 -0
  40. data/test/fixtures/config.yml +4 -0
  41. data/test/fixtures/features/step_definitions.rb +21 -0
  42. data/test/fixtures/features/write_alternate_file.feature +7 -0
  43. data/test/fixtures/features/write_file.feature +7 -0
  44. data/test/fixtures/hello_world.rb +3 -0
  45. data/test/fixtures/js_file.js +4 -0
  46. data/test/fixtures/json_data.json +4 -0
  47. data/test/fixtures/slow.rb +9 -0
  48. data/test/fixtures/sync_test.rb +8 -0
  49. data/test/fixtures/write_file.rb +10 -0
  50. data/test/fixtures/write_file_alternate_spec.rb +10 -0
  51. data/test/fixtures/write_file_spec.rb +9 -0
  52. data/test/fixtures/write_file_with_pending_spec.rb +11 -0
  53. data/test/master_test.rb +152 -0
  54. data/test/message_test.rb +31 -0
  55. data/test/pipe_test.rb +38 -0
  56. data/test/runner_test.rb +153 -0
  57. data/test/ssh_test.rb +14 -0
  58. data/test/sync_test.rb +113 -0
  59. data/test/test_helper.rb +68 -0
  60. data/test/worker_test.rb +60 -0
  61. metadata +209 -0
@@ -0,0 +1,24 @@
1
+ module Hydra #:nodoc:
2
+ # Trace output when in verbose mode.
3
+ module Trace
4
+ module ClassMethods
5
+ # Make a class traceable. Takes one parameter,
6
+ # which is the prefix for the trace to identify this class
7
+ def traceable(prefix = self.class.to_s)
8
+ include Hydra::Trace::InstanceMethods
9
+ class << self; attr_accessor :_traceable_prefix; end
10
+ self._traceable_prefix = prefix
11
+ $stdout.sync = true
12
+ end
13
+ end
14
+
15
+ module InstanceMethods
16
+ # Trace some output with the class's prefix and a newline.
17
+ # Checks to ensure we're running verbosely.
18
+ def trace(str)
19
+ $stdout.write "#{Time.now.to_f} #{self.class._traceable_prefix}| #{str}\n" if @verbose
20
+ end
21
+ end
22
+ end
23
+ end
24
+ Object.extend(Hydra::Trace::ClassMethods)
@@ -0,0 +1,150 @@
1
+ module Hydra #:nodoc:
2
+ # Hydra class responsible to dispatching runners and communicating with the master.
3
+ #
4
+ # The Worker is never run directly by a user. Workers are created by a
5
+ # Master to delegate to Runners.
6
+ #
7
+ # The general convention is to have one Worker per machine on a distributed
8
+ # network.
9
+ class Worker
10
+ include Hydra::Messages::Worker
11
+ traceable('WORKER')
12
+
13
+ attr_reader :runners
14
+ # Create a new worker.
15
+ # * io: The IO object to use to communicate with the master
16
+ # * num_runners: The number of runners to launch
17
+ def initialize(opts = {})
18
+ @verbose = opts.fetch(:verbose) { false }
19
+ @io = opts.fetch(:io) { raise "No IO Object" }
20
+ @runners = []
21
+ @listeners = []
22
+
23
+ boot_runners(opts.fetch(:runners) { 1 })
24
+ @io.write(Hydra::Messages::Worker::WorkerBegin.new)
25
+
26
+ process_messages
27
+
28
+ @runners.each{|r| Process.wait r[:pid] }
29
+ end
30
+
31
+
32
+ # message handling methods
33
+
34
+ # When a runner wants a file, it hits this method with a message.
35
+ # Then the worker bubbles the file request up to the master.
36
+ def request_file(message, runner)
37
+ @io.write(RequestFile.new)
38
+ runner[:idle] = true
39
+ end
40
+
41
+ # When the master sends a file down to the worker, it hits this
42
+ # method. Then the worker delegates the file down to a runner.
43
+ def delegate_file(message)
44
+ runner = idle_runner
45
+ runner[:idle] = false
46
+ runner[:io].write(RunFile.new(eval(message.serialize)))
47
+ end
48
+
49
+ # When a runner finishes, it sends the results up to the worker. Then the
50
+ # worker sends the results up to the master.
51
+ def relay_results(message, runner)
52
+ runner[:idle] = true
53
+ @io.write(Results.new(eval(message.serialize)))
54
+ end
55
+
56
+ # When a master issues a shutdown order, it hits this method, which causes
57
+ # the worker to send shutdown messages to its runners.
58
+ def shutdown
59
+ @running = false
60
+ trace "Notifying #{@runners.size} Runners of Shutdown"
61
+ @runners.each do |r|
62
+ trace "Sending Shutdown to Runner"
63
+ trace "\t#{r.inspect}"
64
+ r[:io].write(Shutdown.new)
65
+ end
66
+ Thread.exit
67
+ end
68
+
69
+ private
70
+
71
+ def boot_runners(num_runners) #:nodoc:
72
+ trace "Booting #{num_runners} Runners"
73
+ num_runners.times do
74
+ pipe = Hydra::Pipe.new
75
+ child = SafeFork.fork do
76
+ pipe.identify_as_child
77
+ Hydra::Runner.new(:io => pipe, :verbose => @verbose)
78
+ end
79
+ pipe.identify_as_parent
80
+ @runners << { :pid => child, :io => pipe, :idle => false }
81
+ end
82
+ trace "#{@runners.size} Runners booted"
83
+ end
84
+
85
+ # Continuously process messages
86
+ def process_messages #:nodoc:
87
+ trace "Processing Messages"
88
+ @running = true
89
+
90
+ Thread.abort_on_exception = true
91
+
92
+ process_messages_from_master
93
+ process_messages_from_runners
94
+
95
+ @listeners.each{|l| l.join }
96
+ @io.close
97
+ trace "Done processing messages"
98
+ end
99
+
100
+ def process_messages_from_master
101
+ @listeners << Thread.new do
102
+ while @running
103
+ begin
104
+ message = @io.gets
105
+ if message and !message.class.to_s.index("Master").nil?
106
+ trace "Received Message from Master"
107
+ trace "\t#{message.inspect}"
108
+ message.handle(self)
109
+ else
110
+ trace "Nothing from Master, Pinging"
111
+ @io.write Ping.new
112
+ end
113
+ rescue IOError => ex
114
+ trace "Worker lost Master"
115
+ Thread.exit
116
+ end
117
+ end
118
+ end
119
+ end
120
+
121
+ def process_messages_from_runners
122
+ @runners.each do |r|
123
+ @listeners << Thread.new do
124
+ while @running
125
+ begin
126
+ message = r[:io].gets
127
+ if message and !message.class.to_s.index("Runner").nil?
128
+ trace "Received Message from Runner"
129
+ trace "\t#{message.inspect}"
130
+ message.handle(self, r)
131
+ end
132
+ rescue IOError => ex
133
+ trace "Worker lost Runner [#{r.inspect}]"
134
+ Thread.exit
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
140
+
141
+ # Get the next idle runner
142
+ def idle_runner #:nodoc:
143
+ idle_r = nil
144
+ while idle_r.nil?
145
+ idle_r = @runners.detect{|runner| runner[:idle]}
146
+ end
147
+ return idle_r
148
+ end
149
+ end
150
+ 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
@@ -0,0 +1,4 @@
1
+ ---
2
+ workers:
3
+ - type: local
4
+ runners: 2
@@ -0,0 +1,21 @@
1
+ Given /^a target file$/ do
2
+ @target_file = File.expand_path(File.join(Dir.tmpdir, 'hydra_test.txt'))
3
+ end
4
+
5
+ Given /^an alternate target file$/ do
6
+ @target_file = File.expand_path(File.join(Dir.tmpdir, 'alternate_hydra_test.txt'))
7
+ end
8
+
9
+ When /^I write "([^\"]*)" to the file$/ do |text|
10
+ f = File.new(@target_file, 'w')
11
+ f.write text
12
+ f.flush
13
+ f.close
14
+ end
15
+
16
+ Then /^"([^\"]*)" should be written in the file$/ do |text|
17
+ f = File.new(@target_file, 'r')
18
+ raise 'Did not write to file' unless text == f.read
19
+ f.close
20
+ end
21
+
@@ -0,0 +1,7 @@
1
+ Feature: Write a file
2
+
3
+ Scenario: Write to hydra_test.txt
4
+ Given an alternate target file
5
+ When I write "HYDRA" to the file
6
+ Then "HYDRA" should be written in the file
7
+
@@ -0,0 +1,7 @@
1
+ Feature: Write a file
2
+
3
+ Scenario: Write to hydra_test.txt
4
+ Given a target file
5
+ When I write "HYDRA" to the file
6
+ Then "HYDRA" should be written in the file
7
+
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ $stdout.write "{:class=>Hydra::Messages::TestMessage, :text=>\"Hello World\"}\n"
3
+ $stdout.flush
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ var thisvar;
3
+ var thatvar
4
+
@@ -0,0 +1,4 @@
1
+ {
2
+ "var1": "something",
3
+ "var2": "trailing comma",
4
+ }
@@ -0,0 +1,9 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'test_helper')
2
+
3
+ class WriteFileTest < Test::Unit::TestCase
4
+ def test_slow
5
+ sleep(5)
6
+ end
7
+ end
8
+
9
+
@@ -0,0 +1,8 @@
1
+ require 'test/unit'
2
+
3
+ class SyncTest < Test::Unit::TestCase
4
+ def test_truth
5
+ assert true
6
+ end
7
+ end
8
+
@@ -0,0 +1,10 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'test_helper')
2
+
3
+ class WriteFileTest < Test::Unit::TestCase
4
+ def test_write_a_file
5
+ File.open(File.join(Dir.tmpdir, 'hydra_test.txt'), 'a') do |f|
6
+ f.write "HYDRA"
7
+ end
8
+ end
9
+ end
10
+
@@ -0,0 +1,10 @@
1
+ require 'tmpdir'
2
+ require 'rspec'
3
+ context "file writing" do
4
+ it "writes to a file" do
5
+ File.open(File.join(Dir.tmpdir, 'alternate_hydra_test.txt'), 'a') do |f|
6
+ f.write "HYDRA"
7
+ end
8
+ end
9
+ end
10
+
@@ -0,0 +1,9 @@
1
+ require 'tmpdir'
2
+ require 'rspec'
3
+ context "file writing" do
4
+ it "writes to a file" do
5
+ File.open(File.join(Dir.tmpdir, 'hydra_test.txt'), 'a') do |f|
6
+ f.write "HYDRA"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,11 @@
1
+ require 'tmpdir'
2
+ require 'rspec'
3
+ context "file writing" do
4
+ it "writes to a file" do
5
+ File.open(File.join(Dir.tmpdir, 'hydra_test.txt'), 'a') do |f|
6
+ f.write "HYDRA"
7
+ end
8
+ end
9
+ it 'could do so much more' # pending spec
10
+ end
11
+
@@ -0,0 +1,152 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ class MasterTest < Test::Unit::TestCase
4
+ context "with a file to test and a destination to verify" do
5
+ setup do
6
+ # avoid having other tests interfering with us
7
+ sleep(0.2)
8
+ FileUtils.rm_f(target_file)
9
+ end
10
+
11
+ teardown do
12
+ FileUtils.rm_f(target_file)
13
+ end
14
+
15
+ should "run a test" do
16
+ Hydra::Master.new(
17
+ :files => [test_file]
18
+ )
19
+ assert File.exists?(target_file)
20
+ assert_equal "HYDRA", File.read(target_file)
21
+ end
22
+
23
+ should "run a spec with pending examples" do
24
+ progress_bar = Hydra::Listener::ProgressBar.new(StringIO.new)
25
+ Hydra::Master.new(
26
+ :files => [rspec_file_with_pending],
27
+ :listeners => [progress_bar]
28
+ )
29
+ assert File.exists?(target_file)
30
+ assert_equal "HYDRA", File.read(target_file)
31
+ assert_equal false, progress_bar.instance_variable_get('@errors')
32
+ end
33
+
34
+ should "generate a report" do
35
+ Hydra::Master.new(:files => [test_file])
36
+ assert File.exists?(target_file)
37
+ assert_equal "HYDRA", File.read(target_file)
38
+ report_file = File.join(Dir.tmpdir, 'hydra_heuristics.yml')
39
+ assert File.exists?(report_file)
40
+ assert report = YAML.load_file(report_file)
41
+ assert_not_nil report[test_file]
42
+ end
43
+
44
+ should "run a test 6 times on 1 worker with 2 runners" do
45
+ Hydra::Master.new(
46
+ :files => [test_file]*6,
47
+ :workers => [ { :type => :local, :runners => 2 } ]
48
+ )
49
+ assert File.exists?(target_file)
50
+ assert_equal "HYDRA"*6, File.read(target_file)
51
+ end
52
+
53
+ # The test being run sleeps for 5 seconds. So, if this was run in
54
+ # series, it would take at least 50 seconds. This test ensures that
55
+ # in runs in less than that amount of time. Since there are 10
56
+ # runners to run the file 10 times, it should only take 5-10 seconds
57
+ # based on overhead.
58
+ should "run a slow test 10 times on 1 worker with 10 runners quickly" do
59
+ start = Time.now
60
+ Hydra::Master.new(
61
+ :files => [File.join(File.dirname(__FILE__), 'fixtures', 'slow.rb')]*10,
62
+ :workers => [
63
+ { :type => :local, :runners => 10 }
64
+ ]
65
+ )
66
+ finish = Time.now
67
+ assert (finish-start) < 30, "took #{finish-start} seconds"
68
+ end
69
+
70
+ should "run a slow test 10 times on 2 workers with 5 runners each quickly" do
71
+ start = Time.now
72
+ Hydra::Master.new(
73
+ :files => [File.join(File.dirname(__FILE__), 'fixtures', 'slow.rb')]*10,
74
+ :workers => [
75
+ { :type => :local, :runners => 5 },
76
+ { :type => :local, :runners => 5 }
77
+ ]
78
+ )
79
+ finish = Time.now
80
+ assert (finish-start) < 15, "took #{finish-start} seconds"
81
+ end
82
+
83
+ should "run a test via ssh" do
84
+ Hydra::Master.new(
85
+ :files => [test_file],
86
+ :workers => [{
87
+ :type => :ssh,
88
+ :connect => 'localhost',
89
+ :directory => File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')),
90
+ :runners => 1
91
+ }]
92
+ )
93
+ assert File.exists?(target_file)
94
+ assert_equal "HYDRA", File.read(target_file)
95
+ end
96
+
97
+ should "run a test with config from a yaml file" do
98
+ Hydra::Master.new(
99
+ :files => [test_file],
100
+ :config => File.join(File.dirname(__FILE__), 'fixtures', 'config.yml')
101
+ )
102
+ assert File.exists?(target_file)
103
+ assert_equal "HYDRA", File.read(target_file)
104
+ end
105
+
106
+ should "synchronize a test file over ssh with rsync" do
107
+ local = File.join(Dir.tmpdir, 'hydra', 'local')
108
+ remote = File.join(Dir.tmpdir, 'hydra', 'remote')
109
+ sync_test = File.join(File.dirname(__FILE__), 'fixtures', 'sync_test.rb')
110
+ [local, remote].each{|f| FileUtils.rm_rf f; FileUtils.mkdir_p f}
111
+
112
+ # setup the folders:
113
+ # local:
114
+ # - test_a
115
+ # - test_c
116
+ # remote:
117
+ # - test_b
118
+ #
119
+ # add test_c to exludes
120
+ FileUtils.cp(sync_test, File.join(local, 'test_a.rb'))
121
+ FileUtils.cp(sync_test, File.join(local, 'test_c.rb'))
122
+ FileUtils.cp(sync_test, File.join(remote, 'test_b.rb'))
123
+
124
+ # ensure a is not on remote
125
+ assert !File.exists?(File.join(remote, 'test_a.rb')), "A should not be on remote"
126
+ # ensure c is not on remote
127
+ assert !File.exists?(File.join(remote, 'test_c.rb')), "C should not be on remote"
128
+ # ensure b is on remote
129
+ assert File.exists?(File.join(remote, 'test_b.rb')), "B should be on remote"
130
+
131
+ Hydra::Master.new(
132
+ :files => ['test_a.rb'],
133
+ :workers => [{
134
+ :type => :ssh,
135
+ :connect => 'localhost',
136
+ :directory => remote,
137
+ :runners => 1
138
+ }],
139
+ :sync => {
140
+ :directory => local,
141
+ :exclude => ['test_c.rb']
142
+ }
143
+ )
144
+ # ensure a is copied
145
+ assert File.exists?(File.join(remote, 'test_a.rb')), "A was not copied"
146
+ # ensure c is not copied
147
+ assert !File.exists?(File.join(remote, 'test_c.rb')), "C was copied, should be excluded"
148
+ # ensure b is deleted
149
+ assert !File.exists?(File.join(remote, 'test_b.rb')), "B was not deleted"
150
+ end
151
+ end
152
+ end