Skip to content

Commit

Permalink
Added customizable timeout, #kill! method
Browse files Browse the repository at this point in the history
  • Loading branch information
Avdi Grimm committed Aug 1, 2010
1 parent bdb6845 commit 94e4406
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 16 deletions.
1 change: 1 addition & 0 deletions examples/cucumber/adventure.feature
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,4 @@ Feature: play adventure
When I enter "quit"
Then the process should exit succesfully


1 change: 1 addition & 0 deletions examples/cucumber/support/env.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
$:.unshift(File.expand_path('../../../lib', File.dirname(__FILE__)))
require 'rspec'
require 'greenletters'
require 'greenletters/cucumber_steps'

42 changes: 42 additions & 0 deletions greenletters.gemspec
Original file line number Diff line number Diff line change
@@ -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
38 changes: 28 additions & 10 deletions lib/greenletters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 =
Expand All @@ -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
Expand Down Expand Up @@ -318,6 +323,7 @@ def t.<<(*)
t
}
@history = TranscriptHistoryBuffer.new(@transcript)
@timeout = options.fetch(:timeout) { DEFAULT_TIMEOUT }
end

def on(event, *args, &block)
Expand All @@ -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!
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down
27 changes: 21 additions & 6 deletions lib/greenletters/cucumber_steps.rb
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 94e4406

Please sign in to comment.