nodule 0.0.34

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ .yardoc/*
6
+ doc/*
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --no-private --protected
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source "http://rubygems.org"
2
+ source "http://gems.sv2" # For prerelease cassandra gem
3
+
4
+ # Specify your gem's dependencies in nodule.gemspec
5
+ gemspec
data/README.md ADDED
@@ -0,0 +1,100 @@
1
+ Warning
2
+ -------
3
+
4
+ Nodule is really new software (all of 2 weeks old). It was extracted from a larger project
5
+ where it is used for some really large integration tests. We'll remove this message once
6
+ Nodule has a reasonable set of tests of its own.
7
+
8
+ Overview
9
+ --------
10
+
11
+ Nodule is an integration test harness that simplifies the setup and teardown of multi-process
12
+ or large multi-component applications.
13
+
14
+ When setting up complex tests with multiple processes, a lot of work ends up going into generating
15
+ configuration, command-line parameters, and other fiddly things that are their own sources
16
+ of annoying bugs. Nodule tries to tidy some of that up by providing a way to define a test
17
+ topology that automatically injects data in the right places lazily so it doesn't all have to
18
+ be done up-front.
19
+
20
+ Symbols
21
+ -------
22
+
23
+ In as many places as possible, Nodule allows you to use a symbol as a placeholder for a value,
24
+ which it will automatically resolve when it's needed and no earlier, so you aren't forced to worry
25
+ about ordering.
26
+
27
+ Saying ":file => Nodule::Tempfile.new" means that :file will resolve to a temporary file's path
28
+ (via .to_s) in any place it appears as a placeholder. :file can be used before it's associated with
29
+ a tempfile.
30
+
31
+ Procs
32
+ -----
33
+
34
+ In many places, procs can be used as placeholders and will be automatically called when needed.
35
+
36
+ Arrays
37
+ ------
38
+
39
+ The process module requires commands to be specified as argv-style arrays. The array is scanned
40
+ for placeholders, which are converted as already described. The one addition is sub-arrays, which
41
+ are resolved and concatenated without padding. This is useful for parameters that use '=' without
42
+ spaces, for example, "dd if=<filename>" would be specified as ['dd', ['if=', :file]]. These sub-arrays
43
+ are resolved recursively, so multiple levels are allowed.
44
+
45
+ Example
46
+ -------
47
+
48
+ #!/usr/bin/env ruby
49
+
50
+ require "test/unit"
51
+ require 'nodule/process'
52
+ require 'nodule/topology'
53
+ require 'nodule/tempfile'
54
+ require 'nodule/console'
55
+
56
+ class NoduleSimpleTest < Test::Unit::TestCase
57
+ def setup
58
+ @topo = Nodule::Topology.new(
59
+ :greenio => Nodule::Console.new(:fg => :green),
60
+ :redio => Nodule::Console.new(:fg => :red),
61
+ :file1 => Nodule::Tempfile.new(".html"),
62
+ :wget => Nodule::Process.new(
63
+ '/usr/bin/wget', '-O', :file1, 'http://www.ooyala.com',
64
+ :stdout => :greenio, :stderr => :redio
65
+ )
66
+ )
67
+
68
+ @topo.run_serially
69
+ end
70
+
71
+ def teardown
72
+ @topo.cleanup
73
+ end
74
+
75
+ def test_heartbeat
76
+ filename = @topo[:file1].to_s
77
+ assert File.exists? filename
78
+ end
79
+ end
80
+
81
+ Authors
82
+ -------
83
+
84
+ * Viet Nguyen
85
+ * Noah Gibbs
86
+ * Al Tobey
87
+ * Jay Bhat
88
+
89
+ Dependencies
90
+ ------------
91
+
92
+ * Ruby 1.9 (tested on 1.9.2p290)
93
+ * rainbow
94
+ * ffi-rzmq (for Nodule::ZeroMQ, optional)
95
+
96
+ License
97
+ -------
98
+
99
+ Nodule is released under [the MIT license](http://www.opensource.org/licenses/mit-license.php).
100
+
data/Rakefile ADDED
@@ -0,0 +1,15 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ namespace "test" do
5
+ desc "Unit tests"
6
+ Rake::TestTask.new(:units) do |t|
7
+ t.libs += ["test"] # require from test subdir
8
+ t.test_files = Dir["test/nodule_*_test.rb"]
9
+ t.verbose = true
10
+ end
11
+ end
12
+
13
+ task :test => ["test:units"] do
14
+ puts "All tests completed..."
15
+ end
@@ -0,0 +1,25 @@
1
+ #!/bin/bash -l
2
+
3
+ if [[ -z $WORKSPACE ]] ; then
4
+ # Support execution from the shell
5
+ export PROJECT_DIR=$(pwd);
6
+ else
7
+ export PROJECT_DIR=$WORKSPACE/nodule;
8
+ fi
9
+
10
+ cd $PROJECT_DIR
11
+
12
+ echo "Loading RVM..."
13
+ source $HOME/.rvm/scripts/rvm || echo "couldn't load RVM script"
14
+ echo "Using Ruby 1.9.2"
15
+ rvm use --create 1.9.2-p290@nodule
16
+
17
+ echo "Starting bundle update"
18
+ bundle update
19
+
20
+ # Disallow errors
21
+ set -e
22
+
23
+ echo START TASK: tests
24
+ bundle exec rake test
25
+ echo END TASK
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env ruby
2
+ $:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
3
+ require "fileutils"
4
+
5
+ require "test/unit"
6
+ require 'nodule/process'
7
+ require 'nodule/topology'
8
+ require 'nodule/tempfile'
9
+ require 'nodule/console'
10
+
11
+ class NoduleDDCatTest < Test::Unit::TestCase
12
+ BYTES=65536
13
+
14
+ def setup
15
+ @topo = Nodule::Topology.new(
16
+ :redio => Nodule::Console.new(:fg => :red),
17
+ :greenio => Nodule::Console.new(:fg => :green),
18
+ :file1 => Nodule::Tempfile.new(:suffix => ".rand"),
19
+ :file2 => Nodule::Tempfile.new(:suffix => ".copy"),
20
+ :dd => Nodule::Process.new(
21
+ '/bin/dd', 'if=/dev/urandom', ['of=', :file1], "bs=#{BYTES}", 'count=1',
22
+ :stderr => :redio, :verbose => :greenio
23
+ ),
24
+ :ls => Nodule::Process.new('ls', '-l', :stdout => :greenio),
25
+ :copy => Nodule::Process.new('/bin/cp', :file1, :file2, :stderr => :redio),
26
+ )
27
+
28
+ # start up and run in order
29
+ @topo.run_serially
30
+ end
31
+
32
+ def teardown
33
+ @topo.cleanup
34
+ end
35
+
36
+ def test_heartbeat
37
+ file1 = @topo[:file1]
38
+ file2 = @topo[:file2]
39
+
40
+ assert_equal BYTES, File.new(file1.to_s).size
41
+ assert_equal BYTES, File.new(file2.to_s).size
42
+ assert FileUtils.cmp(file1.to_s, file2.to_s)
43
+ end
44
+ end
45
+
data/examples/wget.rb ADDED
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env ruby
2
+ $:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
3
+
4
+ require "test/unit"
5
+ require 'nodule/process'
6
+ require 'nodule/topology'
7
+ require 'nodule/tempfile'
8
+ require 'nodule/console'
9
+ require 'multi_json'
10
+
11
+ class NoduleSimpleTest < Test::Unit::TestCase
12
+ def setup
13
+ @topo = Nodule::Topology.new(
14
+ :greenio => Nodule::Console.new(:fg => :green),
15
+ :redio => Nodule::Console.new(:fg => :red),
16
+ :file1 => Nodule::Tempfile.new(:suffix => ".html"),
17
+ :wget => Nodule::Process.new(
18
+ '/usr/bin/wget', '-O', :file1, 'http://www.ooyala.com',
19
+ :stdout => :greenio
20
+ )
21
+ )
22
+
23
+ @topo.start_all
24
+ end
25
+
26
+ def teardown
27
+ @topo.cleanup
28
+ end
29
+
30
+ def test_heartbeat
31
+ @topo[:wget].wait
32
+ filename = @topo[:file1].to_s
33
+ assert File.exists? filename
34
+ end
35
+ end
36
+
@@ -0,0 +1,25 @@
1
+ # Import alarm() from libc.
2
+
3
+ require 'nodule/base'
4
+ require 'ffi'
5
+
6
+ module Nodule
7
+ module PosixAlarmImport
8
+ extend FFI::Library
9
+ ffi_lib FFI::Library::LIBC
10
+ # unistd.h: unsigned alarm(unsigned seconds);
11
+ attach_function :alarm, [ :uint ], :uint
12
+ end
13
+
14
+ class Alarm < Base
15
+ def initialize(opts={})
16
+ if opts[:timeout]
17
+ PosixAlarmImport.alarm(opts[:timeout])
18
+ end
19
+
20
+ Signal.trap("ALRM") { abort "Got SIGALRM. Aborting."; }
21
+
22
+ super(opts)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,258 @@
1
+ require 'nodule/version'
2
+ require 'nodule/topology'
3
+
4
+ module Nodule
5
+ @sequence = 0
6
+ def self.next_seq
7
+ @sequence += 1
8
+ end
9
+
10
+ class Base
11
+ attr_reader :readers, :running, :read_count, :topology
12
+ attr_accessor :prefix
13
+
14
+ #
15
+ # Create a new Nodule handler. This is meant to be a bass class for higher-level
16
+ # Nodule types.
17
+ #
18
+ # @param [Hash{Symbol => String,Symbol,Proc}] opts
19
+ # @option opts [String] :prefix text prefix for output/logs/etc.
20
+ # @option opts [Symbol, Proc] :reader a symbol for a built-in reader, e.g. ":drain" or a proc
21
+ # @option opts [Enumerable] :readers a list of readers instead of one :reader
22
+ # @option opts [TrueClass] :run run immediately
23
+ # @option opts [TrueClass] :verbose print verbose information to STDERR
24
+ #
25
+ def initialize(opts={})
26
+ @read_count = 0
27
+ @readers ||= []
28
+ @output ||= []
29
+ @prefix = opts[:prefix] || ''
30
+ @verbose = opts[:verbose]
31
+ @done = false
32
+ @topology = nil
33
+ @capture_enabled = false
34
+
35
+ add_readers(opts[:reader]) if opts[:reader]
36
+
37
+ run if opts[:run]
38
+ end
39
+
40
+ def join_topology!(t, name='')
41
+ @topology = t
42
+ @prefix = name if @prefix.nil? || @prefix.empty?
43
+ end
44
+
45
+ def run
46
+ @done = false
47
+
48
+ unless @topology
49
+ @toplogy = Nodule::Topology.new(:auto => self)
50
+ end
51
+
52
+ # automatically determine a prefix for console output based on the key name known to the topology
53
+ if name = @topology.key(self)
54
+ @prefix = "[#{name}]: "
55
+ end
56
+ end
57
+
58
+ def stop
59
+ @done = true
60
+ end
61
+
62
+ def stop!
63
+ @done = true
64
+ end
65
+
66
+ def done?
67
+ @done
68
+ end
69
+
70
+ def wait(timeout=nil)
71
+ end
72
+
73
+ #
74
+ # Returns whether or not any output has been captured.
75
+ # Will raise an exception if capture is not enabled.
76
+ # @return [TrueClass,FalseClass]
77
+ #
78
+ def output?
79
+ raise "output is not captured unless you enable :capture" unless @capture_enabled
80
+ @output.any?
81
+ end
82
+
83
+ #
84
+ # Returns a copy of currently captured output. Line data is not chomped or anything of the sort.
85
+ # Will raise an exception if capture is not enabled.
86
+ # @return [Array<String>]
87
+ #
88
+ def output
89
+ raise "output is not captured unless you enable :capture" unless @capture_enabled
90
+ @output.clone
91
+ end
92
+
93
+ #
94
+ # Returns the captured output and clears the buffer (just like clear! but with a return value).
95
+ # Will raise an exception if capture is not enabled.
96
+ # @return [Array<String>]
97
+ #
98
+ def output!
99
+ raise "output is not captured unless you enable :capture" unless @capture_enabled
100
+ out = @output.clone
101
+ clear!
102
+ out
103
+ end
104
+
105
+ #
106
+ # Reset the read count to zero and clear any captured output.
107
+ #
108
+ def clear!
109
+ @read_count = 0
110
+ @output.clear
111
+ end
112
+
113
+ #
114
+ # A dead-simple backoff loop. Calls your block every @sleeptime seconds, increasing the sleep
115
+ # time by sleeptime every iteration, up to timeout, at which point false will be returned. If
116
+ # the block returns an non-false/nil value, it is returned.
117
+ # @param [Float,Fixnum] timeout
118
+ # @param [Float,Fixnum] sleeptime
119
+ # @yield block that returns false to continue waiting, non-false to return that value
120
+ # @return block return value or false if timeout
121
+ #
122
+ def wait_with_backoff(timeout, sleeptime=0.01)
123
+ raise "a block to execute on each iteration is required" unless block_given?
124
+ started = Time.now
125
+ loop do
126
+ val = yield
127
+ return val if val
128
+ return false if Time.now - started > timeout
129
+ sleep sleeptime
130
+ if sleeptime < timeout / 4
131
+ sleeptime += sleeptime
132
+ end
133
+ end
134
+ end
135
+
136
+ #
137
+ # Wait in a sleep loop until a condition is true or the timeout is
138
+ # reached. On timeout an exception is raised. Has no impact on
139
+ # normal readers.
140
+ #
141
+ # @param [Hash] opts Options for how to read and wait
142
+ # @option opts [Float] :max_sleep maximum number of seconds to wait
143
+ # @option opts [Float] :sleep_by how long to sleep by each time, default 0.1
144
+ # @yield block to call to check condition, returns true or false
145
+ # @example act.read_until(:max_sleep => 1.0) { File.exist?("/tmp/file") }
146
+ #
147
+ def read_until(opts = {}, &block)
148
+ raise "No block given to read_until!" unless block_given?
149
+ started = Time.now
150
+ until block.call
151
+ sleep (opts[:sleep_by] || 0.1)
152
+ if Time.now - started >= (opts[:max_sleep] || 10.0)
153
+ raise "Timeout!"
154
+ end
155
+ end
156
+ end
157
+
158
+ #
159
+ # Wait in a sleep(0.1) loop for the number of reads on the handler to reach <count>.
160
+ # Returns when the number of reads is given. On timeout, if a block was provided,
161
+ # it's called before return. Otherwise, an exception is raised.
162
+ # Has no impact on normal readers.
163
+ #
164
+ # @param [Fixnum] count how many reads to wait for
165
+ # @param [Float] max_sleep maximum number of seconds to wait for the count
166
+ # @yield optional block to run on timeout
167
+ # @example act.require_read_timeout 1, 10 { fail }
168
+ # @example act.require_read_timeout 1, 10 rescue nil
169
+ #
170
+ def require_read_count(count, max_sleep=10)
171
+ read_until(:max_sleep => max_sleep) { @read_count == count }
172
+ end
173
+
174
+ #
175
+ # Add a reader action. Can be a block which will be executed for each unit of input, :capture
176
+ # to capture all items emitted by the target to a list (accessible with .output), :ignore, or
177
+ # nil (which will be ignored).
178
+ # @param [Symbol, Proc] action Action to take on each item read from the handler
179
+ # @option action [Symbol] :capture capture the items into an array (access with .output)
180
+ # @option action [Symbol] :drain read items but throw them away
181
+ # @option action [Symbol] :stderr print the item to stderr (with prefix, in color)
182
+ # @option action [Symbol] :ignore don't do anything
183
+ # @option action [Proc] run the block, passing it the item (e.g. a line of stdout)
184
+ # @yield optionally pass a proc in with normal block syntax
185
+ #
186
+ def add_reader(action=nil, &block)
187
+ if block_given?
188
+ @readers << block
189
+ return unless action
190
+ end
191
+
192
+ if action.respond_to? :call
193
+ @readers << action
194
+ elsif action == :capture
195
+ @capture_enabled = true
196
+ @readers << proc { |item| @output.push(item) }
197
+ elsif action == :drain
198
+ @readers << proc { |_| }
199
+ elsif action == :ignore
200
+ # nothing to do here
201
+ # if it's an unrecognized symbol, defer resolution against the containing topology
202
+ elsif action.kind_of? Symbol
203
+ @readers << proc do |item|
204
+ raise "Topology is not set up!" unless @topology
205
+ raise ":#{action} is not a valid topology symbol in #{@topology.to_hash.inspect}" unless @topology.has_key?(action)
206
+ @topology[action].run_readers(item, self)
207
+ end
208
+ else
209
+ raise ArgumentError.new "Invalid add_reader class: #{action.class}"
210
+ end
211
+ end
212
+
213
+ #
214
+ # Add reader arguments with add_reader. Can be a single item or list.
215
+ # @param [Array<Symbol,Proc,Nodule::Base>] args
216
+ #
217
+ def add_readers(*args)
218
+ args.flatten.each do |reader|
219
+ add_reader(reader)
220
+ end
221
+ end
222
+
223
+ #
224
+ # Run all of the registered reader blocks. The block should expect a single argument
225
+ # that is an item of input. If the block has an arity of two, it will also be handed
226
+ # the nodule object provided to run_readers (if it was provided; no guarantee is made that
227
+ # it will be available). The arity-2 version is provided mostly as a clean way for
228
+ # Nodule::Console to add prefixes to output, but could be useful elsewhere.
229
+ # @param [Object] item the item to pass to the readers, often a String (but could be anything)
230
+ # @param [Nodule::Base] handler that generated the item, optional, untyped
231
+ #
232
+ def run_readers(item, src=nil)
233
+ @read_count += 1
234
+ verbose "READ(#{@read_count}): #{item}"
235
+ @readers.each do |reader|
236
+ if reader.arity == 2
237
+ reader.call(item, src)
238
+ else
239
+ reader.call(item)
240
+ end
241
+ end
242
+ end
243
+
244
+ #
245
+ # Verbose Nodule output.
246
+ # @param [Array<String>] out strings to output, will be joined with ' '
247
+ #
248
+ def verbose(*out)
249
+ if @verbose
250
+ if @topology.respond_to? :[] and @topology[@verbose]
251
+ @topology[@verbose].run_readers out.join(' ')
252
+ else
253
+ STDERR.print "#{out.join(' ')}\n"
254
+ end
255
+ end
256
+ end
257
+ end
258
+ end