nodule 0.0.34

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