servolux 0.1.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.
@@ -0,0 +1,274 @@
1
+
2
+ # == Synopsis
3
+ # A Piper is used to fork a child proces and then establish a communication
4
+ # pipe between the parent and child. This communication pipe is used to pass
5
+ # Ruby objects between the two.
6
+ #
7
+ # == Details
8
+ # When a new piper instance is created, the Ruby process is forked into two
9
+ # porcesses - the parent and the child. Each continues execution from the
10
+ # point of the fork. The piper establishes a pipe for communication between
11
+ # the parent and the child. This communication pipe can be opened as read /
12
+ # write / read-write (from the perspective of the parent).
13
+ #
14
+ # Communication over the pipe is handled by marshalling Ruby objects through
15
+ # the pipe. This means that nearly any Ruby object can be passed between the
16
+ # two processes. For example, exceptions from the child process can be
17
+ # marshalled back to the parent and raised there.
18
+ #
19
+ # Object passing is handled by use of the +puts+ and +gets+ methods defined
20
+ # on the Piper. These methods use a +timeout+ and the Kernel#select method
21
+ # to ensure a timely return.
22
+ #
23
+ # == Examples
24
+ #
25
+ # piper = Servolux::Piper.new('r', :timeout => 5)
26
+ #
27
+ # piper.parent {
28
+ # $stdout.puts "parent pid #{Process.pid}"
29
+ # $stdout.puts "child pid #{piper.pid} [from fork]"
30
+ #
31
+ # child_pid = piper.gets
32
+ # $stdout.puts "child pid #{child_pid} [from child]"
33
+ #
34
+ # msg = piper.gets
35
+ # $stdout.puts "message from child #{msg.inspect}"
36
+ # }
37
+ #
38
+ # piper.child {
39
+ # sleep 2
40
+ # piper.puts Process.pid
41
+ # sleep 3
42
+ # piper.puts "The time is #{Time.now}"
43
+ # }
44
+ #
45
+ # piper.close
46
+ #
47
+ class Servolux::Piper
48
+
49
+ # :stopdoc:
50
+ SEPERATOR = [0xDEAD, 0xBEEF].pack('n*').freeze
51
+ # :startdoc:
52
+
53
+ # call-seq:
54
+ # Piper.daemon( nochdir = false, noclose = false )
55
+ #
56
+ # Creates a new Piper with the child process configured as a daemon. The
57
+ # +pid+ method of the piper returns the PID of the daemon process.
58
+ #
59
+ # Be default a daemon process will release its current working directory
60
+ # and the stdout/stderr/stdin file descriptors. This allows the parent
61
+ # process to exit cleanly. This behavior can be overridden by setting the
62
+ # _nochdir_ and _noclose_ flags to true. The first will keep the current
63
+ # working directory; the second will keep stdout/stderr/stdin open.
64
+ #
65
+ def self.daemon( nochdir = false, noclose = false )
66
+ piper = self.new(:timeout => 1)
67
+ piper.parent {
68
+ pid = piper.gets
69
+ piper.instance_variable_set(:@child_pid, pid)
70
+ }
71
+ piper.child {
72
+ Process.setsid # Become session leader.
73
+ exit!(0) if fork # Zap session leader.
74
+
75
+ Dir.chdir '/' unless nochdir # Release old working directory.
76
+ File.umask 0000 # Ensure sensible umask.
77
+
78
+ unless noclose
79
+ STDIN.reopen '/dev/null' # Free file descriptors and
80
+ STDOUT.reopen '/dev/null', 'a' # point them somewhere sensible.
81
+ STDERR.reopen '/dev/null', 'a'
82
+ end
83
+
84
+ piper.puts Process.pid
85
+ }
86
+ piper
87
+ end
88
+
89
+ # The timeout in seconds to wait for puts / gets commands.
90
+ attr_accessor :timeout
91
+
92
+ # The read end of the pipe.
93
+ attr_reader :read_io
94
+
95
+ # The write end of the pipe.
96
+ attr_reader :write_io
97
+
98
+ # call-seq:
99
+ # Piper.new( mode = 'r', opts = {} )
100
+ #
101
+ # Creates a new Piper instance with the communication pipe configured
102
+ # using the provided _mode_. The default mode is read-only (from the
103
+ # parent, and write-only from the child). The supported modes are as
104
+ # follows:
105
+ #
106
+ # Mode | Parent View | Child View
107
+ # -----+-------------+-----------
108
+ # r read-only write-only
109
+ # w write-only read-only
110
+ # rw read-write read-write
111
+ #
112
+ # The communication timeout can be provided as an option. This is the
113
+ # number of seconds to wait for a +puts+ or +gets+ to succeed.
114
+ #
115
+ def initialize( *args )
116
+ opts = args.last.is_a?(Hash) ? args.pop : {}
117
+ mode = args.first || 'r'
118
+
119
+ unless %w[r w rw].include? mode
120
+ raise ArgumentError, "Unsupported mode #{mode.inspect}"
121
+ end
122
+
123
+ @timeout = opts.getopt(:timeout, 0)
124
+ @read_io, @write_io = IO.pipe
125
+ @child_pid = Kernel.fork
126
+
127
+ if child?
128
+ case mode
129
+ when 'r'; close_read
130
+ when 'w'; close_write
131
+ end
132
+ else
133
+ case mode
134
+ when 'r'; close_write
135
+ when 'w'; close_read
136
+ end
137
+ end
138
+ end
139
+
140
+ # Close both the read and write ends of the communications pipe. This only
141
+ # affects the process from which it was called -- the parent or the child.
142
+ #
143
+ def close
144
+ @read_io.close rescue nil
145
+ @write_io.close rescue nil
146
+ end
147
+
148
+ # Close the read end of the communications pipe. This only affects the
149
+ # process from which it was called -- the parent or the child.
150
+ #
151
+ def close_read
152
+ @read_io.close rescue nil
153
+ end
154
+
155
+ # Close the write end of the communications pipe. This only affects the
156
+ # process from which it was called -- the parent or the child.
157
+ #
158
+ def close_write
159
+ @write_io.close rescue nil
160
+ end
161
+
162
+ # Returns +true+ if the communications pipe is readable from the process
163
+ # and there is data waiting to be read.
164
+ #
165
+ def readable?
166
+ return false if @read_io.closed?
167
+ r,w,e = Kernel.select([@read_io], nil, nil, @timeout)
168
+ return !(r.nil? or r.empty?)
169
+ end
170
+
171
+ # Returns +true+ if the communications pipe is writeable from the process
172
+ # and the write buffer can accept more data.
173
+ #
174
+ def writeable?
175
+ return false if @write_io.closed?
176
+ r,w,e = Kernel.select(nil, [@write_io], nil, @timeout)
177
+ return !(w.nil? or w.empty?)
178
+ end
179
+
180
+ # call-seq:
181
+ # child { block }
182
+ # child {|piper| block }
183
+ #
184
+ # Execute the _block_ only in the child process. This method returns
185
+ # immediately when called from the parent process.
186
+ #
187
+ def child( &block )
188
+ return unless child?
189
+ raise ArgumentError, "A block must be supplied" if block.nil?
190
+
191
+ if block.arity > 0
192
+ block.call(self)
193
+ else
194
+ block.call
195
+ end
196
+ end
197
+
198
+ # Returns +true+ if this is the child prcoess and +false+ otherwise.
199
+ #
200
+ def child?
201
+ @child_pid.nil?
202
+ end
203
+
204
+ # call-seq:
205
+ # parent { block }
206
+ # parent {|piper| block }
207
+ #
208
+ # Execute the _block_ only in the parent process. This method returns
209
+ # immediately when called from the child process.
210
+ #
211
+ def parent( &block )
212
+ return unless parent?
213
+ raise ArgumentError, "A block must be supplied" if block.nil?
214
+
215
+ if block.arity > 0
216
+ block.call(self)
217
+ else
218
+ block.call
219
+ end
220
+ end
221
+
222
+ # Returns +true+ if this is the parent prcoess and +false+ otherwise.
223
+ #
224
+ def parent?
225
+ !@child_pid.nil?
226
+ end
227
+
228
+ # Returns the PID of the child process when called from the parent.
229
+ # Returns +nil+ when called from the child.
230
+ #
231
+ def pid
232
+ @child_pid
233
+ end
234
+
235
+ # Read an object from the communication pipe. Returns +nil+ if the pipe is
236
+ # closed for reading or if no data is available before the timeout
237
+ # expires. If data is available then it is un-marshalled and returned as a
238
+ # Ruby object.
239
+ #
240
+ # This method will block until the +timeout+ is reached or data can be
241
+ # read from the pipe.
242
+ #
243
+ def gets
244
+ return unless readable?
245
+
246
+ data = @read_io.gets SEPERATOR
247
+ return if data.nil?
248
+
249
+ data.chomp! SEPERATOR
250
+ Marshal.load(data) rescue data
251
+ end
252
+
253
+ # Write an object to the communication pipe. Returns +nil+ if the pipe is
254
+ # closed for writing or if the write buffer is full. The _obj_ is
255
+ # marshalled and written to the pipe (therefore, procs and other
256
+ # un-marshallable Ruby objects cannot be passed through the pipe).
257
+ #
258
+ # If the write is successful, then the number of bytes written to the pipe
259
+ # is returned. If this number is zero it means that the _obj_ was
260
+ # unsuccessfully communicated (sorry).
261
+ #
262
+ def puts( obj )
263
+ return unless writeable?
264
+
265
+ bytes = @write_io.write Marshal.dump(obj)
266
+ @write_io.write SEPERATOR if bytes > 0
267
+ @write_io.flush
268
+
269
+ bytes
270
+ end
271
+
272
+ end # class Servolux::Piper
273
+
274
+ # EOF
@@ -0,0 +1,226 @@
1
+
2
+ # == Synopsis
3
+ # The Server class makes it simple to create a server-type application in
4
+ # Ruby. A server in this context is any process that should run for a long
5
+ # period of time either in the foreground or as a daemon.
6
+ #
7
+ # == Details
8
+ # The Server class provides for standard server features: process ID file
9
+ # management, signal handling, run loop, logging, etc. All that you need to
10
+ # provide is a +run+ method that will be called by the server's run loop.
11
+ # Optionally, you can provide a block to the +new+ method and it will be
12
+ # called within the run loop instead of a run method.
13
+ #
14
+ # SIGINT and SIGTERM are handled by default. These signals will gracefully
15
+ # shutdown the server by calling the +shutdown+ method (provided by default,
16
+ # too). A few other signals can be handled by defining a few methods on your
17
+ # server instance. For example, SIGINT is hanlded by the +int+ method (an
18
+ # alias for +shutdown+). Likewise, SIGTERM is handled by the +term+ method
19
+ # (another alias for +shutdown+). The following signal methods are
20
+ # recognized by the Server class:
21
+ #
22
+ # Method | Signal | Default Action
23
+ # --------+----------+----------------
24
+ # hup SIGHUP none
25
+ # int SIGINT shutdown
26
+ # term SIGTERM shutdown
27
+ # usr1 SIGUSR1 none
28
+ # usr2 SIGUSR2 none
29
+ #
30
+ # In order to handle SIGUSR1 you would define a <tt>usr1</tt> method for your
31
+ # server.
32
+ #
33
+ # There are a few other methods that are useful and should be mentioned. Two
34
+ # methods are called before and after the run loop starts: +before_starting+
35
+ # and +after_starting+. The first is called just before the run loop thread
36
+ # is created and started. The second is called just after the run loop
37
+ # thread has been created (no guarantee is made that the run loop thread has
38
+ # actually been scheduled).
39
+ #
40
+ # Likewise, two other methods are called before and after the run loop is
41
+ # shutdown: +before_stopping+ and +after_stopping+. The first is called just
42
+ # before the run loop thread is signaled for shutdown. The second is called
43
+ # just after the run loop thread has died; the +after_stopping+ method is
44
+ # guarnteed to NOT be called till after the run loop thread is well and
45
+ # truly dead.
46
+ #
47
+ # == Usage
48
+ # For simple, quick and dirty servers just pass a block to the Server
49
+ # initializer. This block will be used as the run method.
50
+ #
51
+ # server = Servolux::Server.new('Basic', :interval => 1) {
52
+ # puts "I'm alive and well @ #{Time.now}"
53
+ # }
54
+ # server.startup
55
+ #
56
+ # For more complex services you will need to define your own server methods:
57
+ # the +run+ method, signal handlers, and before/after methods. Any pattern
58
+ # that Ruby provides for defining methods on objects can be used to define
59
+ # these methods. In a nutshell:
60
+ #
61
+ # Inheritance
62
+ #
63
+ # class MyServer < Servolux::Server
64
+ # def run
65
+ # puts "I'm alive and well @ #{Time.now}"
66
+ # end
67
+ # end
68
+ # server = MyServer.new('MyServer', :interval => 1)
69
+ # server.startup
70
+ #
71
+ # Extension
72
+ #
73
+ # module MyServer
74
+ # def run
75
+ # puts "I'm alive and well @ #{Time.now}"
76
+ # end
77
+ # end
78
+ # server = Servolux::Server.new('Module', :interval => 1)
79
+ # server.extend MyServer
80
+ # server.startup
81
+ #
82
+ # Singleton Class
83
+ #
84
+ # server = Servolux::Server.new('Singleton', :interval => 1)
85
+ # class << server
86
+ # def run
87
+ # puts "I'm alive and well @ #{Time.now}"
88
+ # end
89
+ # end
90
+ # server.startup
91
+ #
92
+ # == Examples
93
+ #
94
+ # === Signals
95
+ # This example shows how to change the log level of the server when SIGUSR1
96
+ # is sent to the process. The log level toggles between "debug" and the
97
+ # original log level each time SIGUSR1 is sent to the server process. Since
98
+ # this is a module, it can be used with any Servolux::Server instance.
99
+ #
100
+ # module DebugSignal
101
+ # def usr1
102
+ # if @old_log_level
103
+ # logger.level = @old_log_level
104
+ # @old_log_level = nil
105
+ # else
106
+ # @old_log_level = logger.level
107
+ # logger.level = :debug
108
+ # end
109
+ # end
110
+ # end
111
+ #
112
+ # server = Servolux::Server.new('Debugger', :interval => 2) {
113
+ # logger.info "Running @ #{Time.now}"
114
+ # logger.debug "hey look - a debug message"
115
+ # }
116
+ # server.extend DebugSignal
117
+ # server.startup
118
+ #
119
+ class Servolux::Server
120
+ include ::Servolux::Threaded
121
+
122
+ # :stopdoc:
123
+ SIGNALS = %w[HUP INT TERM USR1 USR2] & Signal.list.keys
124
+ SIGNALS.each {|sig| sig.freeze}.freeze
125
+ # :startdoc:
126
+
127
+ Error = Class.new(::Servolux::Error)
128
+
129
+ attr_reader :name
130
+ attr_writer :logger
131
+ attr_writer :pid_file
132
+
133
+ # call-seq:
134
+ # Server.new( name, options = {} ) { block }
135
+ #
136
+ # Creates a new server identified by _name_ and configured from the
137
+ # _options_ hash. The _block_ is run inside a separate thread that will
138
+ # loop at the configured interval.
139
+ #
140
+ # ==== Options
141
+ # * logger <Logger> :: The logger instance this server will use
142
+ # * pid_file <String> :: Location of the PID file
143
+ # * interval <Numeric> :: Sleep interval between invocations of the _block_
144
+ #
145
+ def initialize( name, opts = {}, &block )
146
+ @name = name
147
+
148
+ self.logger = opts.getopt :logger
149
+ self.pid_file = opts.getopt :pid_file
150
+ self.interval = opts.getopt :interval, 0
151
+
152
+ if block
153
+ eg = class << self; self; end
154
+ eg.__send__(:define_method, :run, &block)
155
+ end
156
+
157
+ ary = %w[name logger pid_file].map { |var|
158
+ self.send(var).nil? ? var : nil
159
+ }.compact
160
+ raise Error, "These variables are required: #{ary.join(', ')}." unless ary.empty?
161
+ end
162
+
163
+ # Start the server running using it's own internal thread. This method
164
+ # will not return until the server is shutdown.
165
+ #
166
+ # Startup involves creating a PID file, registering signal handlers to
167
+ # shutdown the server, starting and joining the server thread. The PID
168
+ # file is deleted when this method returns.
169
+ #
170
+ def startup
171
+ return self if running?
172
+ begin
173
+ create_pid_file
174
+ trap_signals
175
+ start
176
+ join
177
+ ensure
178
+ delete_pid_file
179
+ end
180
+ return self
181
+ end
182
+
183
+ alias :shutdown :stop # for symmetry with the startup method
184
+ alias :int :stop # handles the INT signal
185
+ alias :term :stop # handles the TERM signal
186
+ private :start, :stop
187
+
188
+ # Returns the logger instance used by the server. If none was given, then
189
+ # a logger is created from the Logging framework (see the Logging rubygem
190
+ # for more information).
191
+ #
192
+ def logger
193
+ @logger ||= Logging.logger[self]
194
+ end
195
+
196
+ # Returns the PID file name used by the server. If none was given, then
197
+ # the server name is used to create a PID file name.
198
+ #
199
+ def pid_file
200
+ @pid_file ||= name.downcase.tr(' ','_') + '.pid'
201
+ end
202
+
203
+ private
204
+
205
+ def create_pid_file
206
+ logger.debug "Server #{name.inspect} creating pid file #{pid_file.inspect}"
207
+ File.open(pid_file, 'w') {|fd| fd.write(Process.pid.to_s)}
208
+ end
209
+
210
+ def delete_pid_file
211
+ if test(?f, pid_file)
212
+ logger.debug "Server #{name.inspect} removing pid file #{pid_file.inspect}"
213
+ File.delete(pid_file)
214
+ end
215
+ end
216
+
217
+ def trap_signals
218
+ SIGNALS.each do |sig|
219
+ m = sig.downcase.to_sym
220
+ Signal.trap(sig) { self.send(m) rescue nil } if self.respond_to? m
221
+ end
222
+ end
223
+
224
+ end # class Servolux::Server
225
+
226
+ # EOF
@@ -0,0 +1,133 @@
1
+
2
+ # == Synopsis
3
+ # The Threaded module is used to peform some activity at a specified
4
+ # interval.
5
+ #
6
+ # == Details
7
+ # Sometimes it is useful for an object to have its own thread of execution
8
+ # to perform a task at a recurring interval. The Threaded module
9
+ # encapsulates this functionality so you don't have to write it yourself. It
10
+ # can be used with any object that responds to the +run+ method.
11
+ #
12
+ # The threaded object is run by calling the +start+ method. This will create
13
+ # a new thread that will invoke the +run+ method at the desired interval.
14
+ # Just before the thread is created the +before_starting+ method will be
15
+ # called (if it is defined by the threaded object). Likewise, after the
16
+ # thread is created the +after_starting+ method will be called (if it is
17
+ # defeined by the threaded object).
18
+ #
19
+ # The threaded object is stopped by calling the +stop+ method. This sets an
20
+ # internal flag and then wakes up the thread. The thread gracefully exits
21
+ # after checking the flag. Like the start method, before and after methods
22
+ # are defined for stopping as well. Just before the thread is stopped the
23
+ # +before_stopping+ method will be called (if it is defined by the threaded
24
+ # object). Likewise, after the thread has died the +after_stopping+ method
25
+ # will be called (if it is defeined by the threaded object).
26
+ #
27
+ # Calling the +join+ method on a threaded object will cause the calling
28
+ # thread to wait until the threaded object has stopped. An optional timeout
29
+ # parameter can be given.
30
+ #
31
+ # == Examples
32
+ # Take a look at the Servolux::Server class for an example of a threaded
33
+ # object.
34
+ #
35
+ module Servolux::Threaded
36
+
37
+ # This method will be called by the activity thread at the desired
38
+ # interval. Implementing classes are exptect to provide this
39
+ # functionality.
40
+ #
41
+ def run
42
+ raise NotImplementedError,
43
+ 'This method must be defined by the threaded object.'
44
+ end
45
+
46
+ # Start the activity thread. If already started this method will return
47
+ # without taking any action.
48
+ #
49
+ # If the including class defines a 'before_starting' method, it will be
50
+ # called before the thread is created and run. Likewise, if the
51
+ # including class defines an 'after_starting' method, it will be called
52
+ # after the thread is created.
53
+ #
54
+ def start
55
+ return self if running?
56
+ logger.debug "Starting"
57
+
58
+ before_starting if self.respond_to?(:before_starting)
59
+ @activity_thread_running = true
60
+ @activity_thread = Thread.new {
61
+ begin
62
+ loop {
63
+ sleep interval
64
+ break unless running?
65
+ run
66
+ }
67
+ rescue Exception => e
68
+ logger.fatal e
69
+ end
70
+ }
71
+ after_starting if self.respond_to?(:after_starting)
72
+ self
73
+ end
74
+
75
+ # Stop the activity thread. If already stopped this method will return
76
+ # without taking any action.
77
+ #
78
+ # If the including class defines a 'before_stopping' method, it will be
79
+ # called before the thread is stopped. Likewise, if the including class
80
+ # defines an 'after_stopping' method, it will be called after the thread
81
+ # has stopped.
82
+ #
83
+ def stop
84
+ return self unless running?
85
+ logger.debug "Stopping"
86
+
87
+ before_stopping if self.respond_to?(:before_stopping)
88
+ @activity_thread_running = false
89
+ @activity_thread.wakeup
90
+ @activity_thread.join
91
+ @activity_thread = nil
92
+ after_stopping if self.respond_to?(:after_stopping)
93
+ self
94
+ end
95
+
96
+ # If the activity thread is running, the calling thread will suspend
97
+ # execution and run the activity thread. This method does not return until
98
+ # the activity thread is stopped or until _limit_ seconds have passed.
99
+ #
100
+ # If the activity thread is not running, this method returns immediately
101
+ # with +nil+.
102
+ #
103
+ def join( limit = nil )
104
+ @activity_thread.join limit
105
+ self
106
+ rescue NoMethodError
107
+ return self
108
+ end
109
+
110
+ # Returns +true+ if the activity thread is running. Returns +false+
111
+ # otherwise.
112
+ #
113
+ def running?
114
+ @activity_thread_running
115
+ end
116
+
117
+ # Sets the number of seconds to sleep between invocations of the
118
+ # threaded object's 'run' method.
119
+ #
120
+ def interval=( value )
121
+ @activity_thread_interval = value
122
+ end
123
+
124
+ # Returns the number of seconds to sleep between invocations of the
125
+ # threaded object's 'run' method.
126
+ #
127
+ def interval
128
+ @activity_thread_interval
129
+ end
130
+
131
+ end # module Servolux::Threaded
132
+
133
+ # EOF
data/lib/servolux.rb ADDED
@@ -0,0 +1,49 @@
1
+
2
+ require 'logging'
3
+
4
+ module Servolux
5
+
6
+ # :stopdoc:
7
+ VERSION = '0.1.0'
8
+ LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
9
+ PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
10
+ # :startdoc:
11
+
12
+ # Generic Servolux Error class.
13
+ Error = Class.new(StandardError)
14
+
15
+ # Returns the version string for the library.
16
+ #
17
+ def self.version
18
+ VERSION
19
+ end
20
+
21
+ # Returns the library path for the module. If any arguments are given,
22
+ # they will be joined to the end of the libray path using
23
+ # <tt>File.join</tt>.
24
+ #
25
+ def self.libpath( *args )
26
+ args.empty? ? LIBPATH : ::File.join(LIBPATH, args.flatten)
27
+ end
28
+
29
+ # Returns the lpath for the module. If any arguments are given,
30
+ # they will be joined to the end of the path using
31
+ # <tt>File.join</tt>.
32
+ #
33
+ def self.path( *args )
34
+ args.empty? ? PATH : ::File.join(PATH, args.flatten)
35
+ end
36
+
37
+ # Returns +true+ if the execution platform supports fork.
38
+ #
39
+ def self.fork?
40
+ RUBY_PLATFORM != 'java' and test(?e, '/dev/null')
41
+ end
42
+
43
+ end # module Servolux
44
+
45
+ %w[threaded server piper daemon].each do |lib|
46
+ require Servolux.libpath('servolux', lib)
47
+ end
48
+
49
+ # EOF
@@ -0,0 +1,20 @@
1
+
2
+ require File.join(File.dirname(__FILE__), %w[spec_helper])
3
+
4
+ describe Servolux do
5
+
6
+ before :all do
7
+ @root_dir = File.expand_path(File.join(File.dirname(__FILE__), '..'))
8
+ end
9
+
10
+ it "finds things releative to 'lib'" do
11
+ Servolux.libpath(%w[servolux threaded]).should == File.join(@root_dir, %w[lib servolux threaded])
12
+ end
13
+
14
+ it "finds things releative to 'root'" do
15
+ Servolux.path('Rakefile').should == File.join(@root_dir, 'Rakefile')
16
+ end
17
+
18
+ end
19
+
20
+ # EOF