lib/sprout/tasks/fdb_task.rb in sprout-as3-bundle-0.2.9 vs lib/sprout/tasks/fdb_task.rb in sprout-as3-bundle-1.0.8
- old
+ new
@@ -23,33 +23,136 @@
# t.break = 'SomeFile:23'
# t.continue
# end
#
# You can also point the FDBTask at HTML pages. These pages will be
- # launched in your default browswer. You will need to manually install
+ # launched in your default browser. You will need to manually install
# a debug Flash Player in that particular browser.
- # To use a browser instead of the desktop Flash Player, simply point
+ #
+ # To use a browser instead of the desktop Flash Player, simply point the
# file argument at an HTML document or remote URL. The SWF file loaded
- # must be compiled using the -debug flag in order to connect to the
- # debugger.
+ # must be compiled using the -debug flag, and executed in a debug Flash Player
+ # in order to connect to properly connect to the debugger.
# fdb :debug do |t|
# t.file = 'bin/SomeProject-debug.html'
# t.run
# t.continue
# end
#
+ # .h3 Continuous Integration
+ #
+ # The FDBTask is also the only effective way to execute SWF content
+ # in front of a CI (continuous integration) tool like Cruise Control.
+ # The biggest problem facing SWF execution for CI is uncaught
+ # runtime exceptions. The debug Flash Player throws uncaught exceptions
+ # up to the operating system GUI layer where a user must manually dismiss
+ # a dialog. In addition to blocking the CI process indefinitely, these
+ # messages are also difficult to capture and log.
+ #
+ # Using Sprouts and the FDBTask, we can capture these messages along
+ # with additonal information (e.g. local variables and a complete stack trace)
+ # about the state of the SWF file, and then cleanly exit the Flash Player
+ # and log this information.
+ #
+ # The FDBTask has also been configured to work with the ASUnit XMLPrinter
+ # so that an XML artifact is created and written to disk that includes
+ # the results of running your test suites.
+ #
+ # To use FDB with a CI tool do the following:
+ #
+ # 1) Create a new base runner class (we usually name this XMLRunner.as)
+ # and make it look like the following:
+ #
+ # package {
+ # import asunit.textui.TestRunner;
+ # import asunit.textui.XMLResultPrinter;
+ #
+ # public class XMLRunner extends TestRunner {
+ #
+ # public function XMLRunner() {
+ # setPrinter(new XMLResultPrinter());
+ # start(AllTests, null, TestRunner.SHOW_TRACE);
+ # }
+ # }
+ # }
+ #
+ # 2) Create a new MXMLCTask to compile the newly created runner.
+ # NOTE: Be sure you set +debug+ to true, otherwise the SWF will
+ # not connect to the debugger properly.
+ #
+ # library :asunit3
+ #
+ # desc 'Compile the CI SWF'
+ # mxmlc 'bin/XMLRunner.swf' => :asunit3 do |t|
+ # t.input = 'src/XMLRunner.as'
+ # t.debug = true
+ # t.source_path << 'test'
+ # # Add additional configuration here.
+ # end
+ #
+ # 3) Create a new FDBTask and set +kill_on_fault+ to true.
+ #
+ # desc 'Execute the test harness for CI'
+ # fdb :cruise do |t|
+ # t.kill_on_fault = true
+ # t.file = 'bin/XMLRunner.swf'
+ # t.run
+ # t.continue
+ # end
+ #
+ # 4) Configure your CI task to call:
+ #
+ # rake cruise
+ #
+ # 5) That's it!
+ #
class FDBTask < ToolTask
- # The SWF file to debug.
- attr_accessor :swf
+ TEST_RESULT_PRELUDE = '<XMLResultPrinter>'
+ TEST_RESULT_CLOSING = '</XMLResultPrinter>'
+ TEST_RESULT_FILE = 'AsUnitResults.xml'
+
+ # Relative or absolute path to where unit test results
+ # should be written to disk.
+ # This field can be used in conjunction with the AsUnit
+ # XMLResultPrinter which will trace out JUnit style XML
+ # test results.
+ # By telling fdb where to write those test results, it
+ # will scan the trace output stream looking for +test_result_prelude+,
+ # and +test_result_closing+. Once the closing is encountered, the
+ # prelude and closing (and everything in between) will be written
+ # to disk in the file identified by +test_result_file+, and fdb
+ # will be closed down.
+ attr_writer :test_result_file
+ # String that indicates the beginning of printable test results
+ # Default value is '<XMLResultPrinter>'
+ attr_writer :test_result_prelude
+
+ # String that indicates the closing of printable test results
+ # Default value is '</XMLResultPrinter>'
+ # See test_result_prelude for more info.
+ attr_writer :test_result_closing
+
+ # Boolean value that tells fdb whether or not it should automatically
+ # shut down when an exception is encountered. This feature is used to
+ # prevent GUI prompts for unhandled exceptions, especially when running
+ # a test harness under a continuous integration tool - like cruise control.
+ # If an exception is encountered, fdb will automatically print the exception,
+ # a full stack trace and all local variables in the function where the failure
+ # occured.
+ attr_writer :kill_on_fault
+
def initialize_task # :nodoc:
@default_gem_name = 'sprout-flex3sdk-tool'
@default_gem_path = 'bin/fdb'
+ @kill_on_fault = false
@queue = []
end
def define # :nodoc:
+ super
+ CLEAN.add(test_result_file)
self
end
def stdout=(out) # :nodoc:
@stdout = out
@@ -57,20 +160,39 @@
def stdout # :nodoc:
@stdout ||= $stdout
end
+ def validate_swf(swf)
+ # TODO: Ensure the SWF has been compiled with debugging
+ # turned on.
+ # I believe this will require actually parsing the SWF file and
+ # scanning for the EnableDebugger2 tag.
+ # http://www.adobe.com/devnet/swf/pdf/swf_file_format_spec_v9.pdf
+ end
+
def execute(*args) # :nodoc:
- # TODO: First check the SWF file to ensure that debugging is enabled!
+ # Ensure that if we load a SWF it's been compiled with debugging turned on!
+ file_name = @file
+
+ if(file_name.match(/\.swf$/))
+ validate_swf(file_name)
+ end
+
buffer = FDBBuffer.new(get_executable, stdout)
+ buffer.test_result_file = test_result_file
+ buffer.test_result_prelude = test_result_prelude
+ buffer.test_result_closing = test_result_closing
+ buffer.kill_on_fault = kill_on_fault?
buffer.wait_for_prompt
@queue.each do |command|
handle_command(buffer, command)
end
- buffer.join
+ buffer.join # wait here until the buffer is closed.
+
self
end
def handle_command(buffer, command) # :nodoc:
parts = command.split(' ')
@@ -93,10 +215,26 @@
def command_queue # :nodoc:
@queue
end
+ def kill_on_fault?
+ @kill_on_fault
+ end
+
+ def test_result_file
+ @test_result_file ||= TEST_RESULT_FILE
+ end
+
+ def test_result_prelude
+ @test_result_prelude ||= TEST_RESULT_PRELUDE
+ end
+
+ def test_result_closing
+ @test_result_closing ||= TEST_RESULT_CLOSING
+ end
+
# Print backtrace of all stack frames
def bt
@queue << "bt"
end
@@ -172,12 +310,18 @@
# Specify application to be debugged.
def file=(file)
@prerequisites << file
@queue << "file #{file}"
+ @file = file
end
+ # alias for self.file=
+ def input=(file)
+ self.file = file
+ end
+
# Execute until current function returns
def finish
@queue << "finish"
end
@@ -301,11 +445,11 @@
# Set the value of a variable
def set=(value)
@queue << "set #{value}"
end
- # Sleep until some 'str' String is sent to the output
+ # Sleep until some 'str' String is sent to the output,
def sleep_until(str)
@queue << "sleep #{str}"
end
# Read fdb commands from a file
@@ -345,10 +489,15 @@
end
# A buffer that provides clean blocking support for the fdb command shell
class FDBBuffer #:nodoc:
+ attr_accessor :test_result_file
+ attr_accessor :test_result_prelude
+ attr_accessor :test_result_closing
+ attr_writer :kill_on_fault
+
PLAYER_TERMINATED = 'Player session terminated'
EXIT_PROMPT = 'The program is running. Exit anyway? (y or n)'
PROMPT = '(fdb) '
QUIT = 'quit'
@@ -361,10 +510,14 @@
@found_search = false
@pending_expression = nil
listen exe
end
+ def kill_on_fault?
+ @kill_on_fault
+ end
+
def user_input
@user_input ||= $stdin
end
def create_input(exe)
@@ -386,10 +539,13 @@
@input = create_input(exe)
def puts(msg)
$stdout.puts msg
end
+ @inside_test_result = false
+ full_output = ''
+ test_result = ''
char = ''
line = ''
while true do
begin
char = @input.readpartial 1
@@ -401,18 +557,53 @@
if(char == "\n")
line = ''
else
line << char
+ full_output << char
end
+
+ if(@inside_test_result)
+ test_result << char
+ else
+ @output.print char
+ @output.flush
+ end
- @output.print char
- @output.flush
+ if(!test_result_prelude.nil? && line.index(test_result_prelude))
+ test_result = test_result_prelude
+ @inside_test_result = true
+ end
+
+ if(@inside_test_result && line.index(test_result_closing))
+ write_test_result(test_result)
+ @inside_test_result = false
+ Thread.new {
+ write("\n")
+ write('y')
+ write('kill')
+ write('y')
+ write('quit')
+ }
+ end
if(line == PROMPT || line.match(/\(y or n\) $/))
- @prompted = true
+ full_output_cache = full_output
line = ''
+ full_output = ''
+ @prompted = true
+ if(should_kill?(full_output_cache))
+ Thread.new {
+ wait_for_prompt
+ write('info stack') # Output the full stack trace
+ write('info locals') # Output local variables
+ write('kill') # Kill the running SWF file
+ write('y') # Confirm killing SWF
+ write('quit') # Quit FDB safely
+ }
+ end
+
elsif(@pending_expression && line.match(/#{@pending_expression}/))
@found_search = true
@pending_expression = nil
elsif(line == PLAYER_TERMINATED)
puts ""
@@ -422,23 +613,44 @@
end
end
end
end
+
+ def should_kill?(message)
+ return (@kill_on_fault && fault_found?(message))
+ end
+
+ def fault_found?(message)
+ match = message.match(/\[Fault\]\s.*,.*$/)
+ return !match.nil?
+ end
+
+ def clean_test_result(result)
+ return result.gsub(/^\[trace\]\s/m, '')
+ end
+
+ def write_test_result(result)
+ result = clean_test_result result
+ FileUtils.makedirs(File.dirname(test_result_file))
+ File.open(test_result_file, File::CREAT|File::TRUNC|File::RDWR) do |f|
+ f.puts(result)
+ end
+ end
# Block for the life of the input process
def join
puts ">> Entering FDB interactive mode, type 'help' for more info."
print PROMPT
$stdout.flush
- Thread.new do
+ t = Thread.new {
while true do
msg = user_input.gets.chomp!
@input.puts msg
wait_for_prompt
end
- end
+ }
@listener.join
end
# Block until prompted returns true