require 'set' require 'diff/lcs' require 'tork/server' require 'tork/config' module Tork class Engine < Server def initialize super Tork.config :engine @lines_by_file = {} @passed_test_files = Set.new @failed_test_files = Set.new @running_test_files = Set.new @recently_passed_test_files = Set.new @recently_failed_test_files = Set.new end def loop @master = popen('tork-master') super ensure pclose @master end def boot! @master.reconnect # resume running all previously running test files and # all previously failed test files in the new master resumable = @running_test_files + @failed_test_files @running_test_files.clear test resumable end def test test_file, *line_numbers # a list of tests was passed in for the first argument if test_file.respond_to? :each and line_numbers.empty? test_file.each {|args| test(*args) } elsif File.exist? test_file and @running_test_files.add? test_file if line_numbers.empty? line_numbers = find_changed_line_numbers(test_file) else line_numbers.map!(&:to_i) line_numbers.clear if line_numbers.any?(&:zero?) end send @master, [:test, test_file, line_numbers] end end def test? if @running_test_files.empty? tell @client, 'There are no running test files to list.' else tell @client, @running_test_files.sort, false end end def stop signal=nil if @running_test_files.empty? tell @client, 'There are no running test files to stop.' else send @master, [:stop, signal].compact @running_test_files.clear end end def pass! if @passed_test_files.empty? tell @client, 'There are no passed test files to re-run.' else test @passed_test_files end end def pass? if @passed_test_files.empty? tell @client, 'There are no passed test files to list.' else tell @client, @passed_test_files.sort, false end end def fail! if @failed_test_files.empty? tell @client, 'There are no failed test files to re-run.' else test @failed_test_files end end def fail? if @failed_test_files.empty? tell @client, 'There are no failed test files to list.' else tell @client, @failed_test_files.sort, false end end protected def recv client, message case client when @master send @clients, message # propagate downstream event, file, line_numbers = message case event_sym = event.to_sym when :fail, :pass @running_test_files.delete file if event_sym == :fail @recently_failed_test_files.add file was_pass = @passed_test_files.delete? file now_fail = @failed_test_files.add? file send @clients, [:fail!, file, message] if was_pass and now_fail elsif line_numbers.empty? # only whole test file runs should qualify as pass @recently_passed_test_files.add file was_fail = @failed_test_files.delete? file now_pass = @passed_test_files.add? file send @clients, [:pass!, file, message] if was_fail and now_pass end # notify user when all test files have finished running if @running_test_files.empty? passed = @recently_passed_test_files.to_a @recently_passed_test_files.clear failed = @recently_failed_test_files.to_a @recently_failed_test_files.clear tested = passed + failed send @clients, [:done, tested, passed, failed] end end else super end end private def find_changed_line_numbers test_file # cache test file contents for diffing below new_lines = File.readlines(test_file) old_lines = @lines_by_file[test_file] || new_lines @lines_by_file[test_file] = new_lines # find changed line numbers in the test file Diff::LCS.diff(old_lines, new_lines).flatten. # +1 because line numbers start at 1, not 0 map {|change| change.position + 1 }.uniq end end end