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 +6 -0
- data/.yardopts +1 -0
- data/Gemfile +5 -0
- data/README.md +100 -0
- data/Rakefile +15 -0
- data/ci_jobs/nodule-units/run.sh +25 -0
- data/examples/cat_test.rb +45 -0
- data/examples/wget.rb +36 -0
- data/lib/nodule/alarm.rb +25 -0
- data/lib/nodule/base.rb +258 -0
- data/lib/nodule/cassandra.rb +292 -0
- data/lib/nodule/console.rb +87 -0
- data/lib/nodule/line_io.rb +74 -0
- data/lib/nodule/monkeypatch.rb +8 -0
- data/lib/nodule/process.rb +386 -0
- data/lib/nodule/tempfile.rb +57 -0
- data/lib/nodule/topology.rb +195 -0
- data/lib/nodule/unixsocket.rb +54 -0
- data/lib/nodule/util.rb +56 -0
- data/lib/nodule/version.rb +3 -0
- data/lib/nodule/zeromq.rb +280 -0
- data/lib/nodule.rb +10 -0
- data/nodule.gemspec +28 -0
- data/test/helper.rb +1 -0
- data/test/nodule_cassandra_test.rb +31 -0
- data/test/nodule_console_test.rb +11 -0
- data/test/nodule_lineio_test.rb +32 -0
- data/test/nodule_process_test.rb +25 -0
- data/test/nodule_tempfile_test.rb +22 -0
- data/test/nodule_topology_test.rb +11 -0
- data/test/nodule_unixsocket_test.rb +11 -0
- data/test/nodule_util_test.rb +25 -0
- data/test/nodule_zeromq_test.rb +44 -0
- metadata +163 -0
data/.gitignore
ADDED
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--no-private --protected
|
data/Gemfile
ADDED
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
|
+
|
data/lib/nodule/alarm.rb
ADDED
@@ -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
|
data/lib/nodule/base.rb
ADDED
@@ -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
|