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