Permalink
Browse files

Added customizable timeout, #kill! method

  • Loading branch information...
1 parent bdb6845 commit 94e4406cee7218c36cc87d4ca71fe708cee219a2 Avdi Grimm committed Aug 1, 2010
@@ -65,3 +65,4 @@ Feature: play adventure
When I enter "quit"
Then the process should exit succesfully
+
@@ -1,4 +1,5 @@
$:.unshift(File.expand_path('../../../lib', File.dirname(__FILE__)))
+require 'rspec'
require 'greenletters'
require 'greenletters/cucumber_steps'
View
@@ -0,0 +1,42 @@
+# -*- encoding: utf-8 -*-
+
+Gem::Specification.new do |s|
+ s.name = %q{greenletters}
+ s.version = "0.1.0"
+
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
+ s.authors = ["Avdi Grimm"]
+ s.date = %q{2010-07-31}
+ s.default_executable = %q{greenletters}
+ s.description = %q{ Greenletterrs is a console automation framework, similar to the classic
+ utility Expect. You give it a command to execute, and tell it which outputs
+ or events to expect and how to respond to them.
+
+ Greenletters also includes a set of Cucumber steps which simplify the task
+ of spcifying interactive command-line applications.
+}
+ s.email = %q{avdi@avdi.org}
+ s.executables = ["greenletters"]
+ s.extra_rdoc_files = ["History.txt", "bin/greenletters", "version.txt"]
+ s.files = ["History.txt", "README.org", "Rakefile", "bin/greenletters", "examples/adventure.rb", "examples/cucumber/adventure.feature", "examples/cucumber/greenletters.log", "examples/cucumber/support/env.rb", "lib/greenletters.rb", "lib/greenletters/cucumber_steps.rb", "script/console", "spec/greenletters_spec.rb", "spec/spec_helper.rb", "test/test_greenletters.rb", "version.txt"]
+ s.homepage = %q{http://github.com/avdi/greenletters}
+ s.rdoc_options = ["--main", "README.org"]
+ s.require_paths = ["lib"]
+ s.rubyforge_project = %q{greenletters}
+ s.rubygems_version = %q{1.3.6}
+ s.summary = %q{A Ruby console automation framework a la Expect}
+ s.test_files = ["test/test_greenletters.rb"]
+
+ if s.respond_to? :specification_version then
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
+ s.specification_version = 3
+
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
+ s.add_development_dependency(%q<bones>, [">= 3.4.7"])
+ else
+ s.add_dependency(%q<bones>, [">= 3.4.7"])
+ end
+ else
+ s.add_dependency(%q<bones>, [">= 3.4.7"])
+ end
+end
View
@@ -103,7 +103,7 @@ def initialize(transcript)
def <<(output)
@buffer << output
@transcript << output
- length = [@buffer.length, 256].min
+ length = [@buffer.length, 512].min
@buffer = @buffer[-length, length]
self
end
@@ -242,7 +242,7 @@ def initialize(pattern=0, options={}, &block)
end
def call(process)
- if pattern === process.status.exitstatus
+ if process.status && pattern === process.status.exitstatus
@block.call(process, process.status)
true
else
@@ -267,8 +267,9 @@ def call(process)
end
class Process
- END_MARKER = '__GREENLETTERS_PROCESS_ENDED__'
- DEFAULT_LOG_LEVEL = ::Logger::WARN
+ END_MARKER = '__GREENLETTERS_PROCESS_ENDED__'
+ DEFAULT_LOG_LEVEL = ::Logger::WARN
+ DEFAULT_TIMEOUT = 1.0
# Shamelessly stolen from Rake
RUBY_EXT =
@@ -289,6 +290,10 @@ class Process
attr_reader :output_buffer # Output ready to be read from process
attr_reader :status # :not_started -> :running -> :ended -> :exited
attr_reader :cwd # Working directory for the command
+ attr_reader :input
+ attr_reader :output
+ attr_reader :pid
+ attr_accessor :timeout
def_delegators :input_buffer, :puts, :write, :print, :printf, :<<
def_delegators :output_buffer, :rest, :rest_size, :check_until
@@ -318,6 +323,7 @@ def t.<<(*)
t
}
@history = TranscriptHistoryBuffer.new(@transcript)
+ @timeout = options.fetch(:timeout) { DEFAULT_TIMEOUT }
end
def on(event, *args, &block)
@@ -327,6 +333,7 @@ def on(event, *args, &block)
def wait_for(event, *args, &block)
raise "Already waiting for #{blocker}" if blocker
t = add_blocking_trigger(event, *args, &block)
+ @logger.debug "Entering wait cycle for #{event}"
process_events
rescue
unblock!
@@ -376,11 +383,10 @@ def start!
cmd = wrapped_command
@logger.debug "executing #{cmd.join(' ')}"
merge_environment(@env) do
- @logger.debug "command environment:\n#{ENV.inspect}"
@output, @input, @pid = PTY.spawn(*cmd)
end
@state = :running
- @logger.debug "spawned pid #{@pid}"
+ @logger.debug "spawned pid #{@pid}; in: #{@input.inspect}; out: #{@output.inspect}"
end
end
@@ -389,6 +395,12 @@ def flush_output_buffer!
@output_buffer.terminate
end
+ def kill!(signal="TERM")
+ handle_child_exit do
+ ::Process.kill(signal, @pid)
+ end
+ end
+
def alive?
::Process.kill(0, @pid)
true
@@ -421,6 +433,10 @@ def time
Time.now
end
+ def to_s
+ "Process<pid: #{pid}; in: #{input.inspect}; out: #{output.inspect}>"
+ end
+
private
attr_reader :triggers
@@ -439,14 +455,15 @@ def process_events
raise StateError, "Process not started!" if not_started?
handle_child_exit do
while blocked?
- @logger.debug "select()"
input_handles = input_buffer.string.empty? ? [] : [@input]
output_handles = [@output]
- error_handles = [@input, @output]
+ error_handles = [@input, @output].uniq
timeout = shortest_timeout
@logger.debug "select() on #{[output_handles, input_handles, error_handles, timeout].inspect}"
+
ready_handles = IO.select(
output_handles, input_handles, error_handles, timeout)
+
if ready_handles.nil?
process_timeout
else
@@ -625,13 +642,14 @@ def process_interruption(reason)
raise SystemError,
"Interrupted (#{reason}) while waiting for #{blocker}.\n" \
"Recent activity:\n" +
- @history.buffer
+ @history.buffer + "\n" + ("-" * 60) + "\n"
end
unblock!
end
end
def catchup_trigger!(trigger)
+ @logger.debug "Catching up trigger #{trigger}"
check_trigger(trigger)
end
@@ -647,7 +665,7 @@ def shortest_timeout
result = triggers.grep(TimeoutTrigger).map{|t|
t.expiration_time - Time.now
}.min
- if result.nil? then result = 1.0 end
+ if result.nil? then result = @timeout end
if result < 0 then result = 0 end
result
end
@@ -1,13 +1,24 @@
require 'cucumber'
+require 'shellwords'
module Greenletters
module CucumberHelpers
def greenletters_prepare_entry(text)
text.chomp + "\n"
end
+
def greenletters_massage_pattern(text)
Regexp.new(Regexp.escape(text.strip.tr_s(" \r\n\t", " ")).gsub('\ ', '\s+'))
end
+
+ # Override this in your Cucumber setup to customize how processes are
+ # constructed.
+ def make_greenletters_process(command_line, options = {})
+ command = Shellwords.shellwords(command_line)
+ options[:logger] = @greenletters_process_log if @greenletters_process_log
+ process_args = command + [options]
+ Greenletters::Process.new(*process_args)
+ end
end
end
@@ -20,18 +31,21 @@ def greenletters_massage_pattern(text)
end
Given /^process activity is logged to "([^\"]*)"$/ do |filename|
- logger = ::Logger.new(open(filename, 'w+'))
+ logger = ::Logger.new(open(filename, 'a+'))
#logger.level = ::Logger::INFO
logger.level = ::Logger::DEBUG
@greenletters_process_log = logger
end
Given /^a process (?:"([^\"]*)" )?from command "([^\"]*)"$/ do |name, command|
name ||= "default"
- options = {
- }
- options[:logger] = @greenletters_process_log if @greenletters_process_log
- @greenletters_process_table[name] = Greenletters::Process.new(command, options)
+ @greenletters_process_table[name] = make_greenletters_process(command)
+end
+
+Given /^the process(?: "([^\"]*)")? has a timeout of ([0-9.]+) seconds$/ do
+ |name, length|
+ name ||= :"default"
+ @greenletters_process_table[name].timeout = length.to_f
end
Given /^I reply "([^\"]*)" to output "([^\"]*)"(?: from process "([^\"]*)")?$/ do
@@ -71,8 +85,9 @@ def greenletters_massage_pattern(text)
Then /^I should see the following output(?: from process "([^\"]*)")?:$/ do
|name, pattern|
name ||= "default"
+ process = @greenletters_process_table[name]
pattern = greenletters_massage_pattern(pattern)
- @greenletters_process_table[name].wait_for(:output, pattern)
+ process.wait_for(:output, pattern)
end
Then /^I should see all the following outputs(?: from process "([^\"]*)")?:$/ do

0 comments on commit 94e4406

Please sign in to comment.