nodule 0.0.34
Sign up to get free protection for your applications and to get access to all the features.
- 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
|