require File.join(File.dirname(__FILE__), 'pipe_dream')
require File.join(File.dirname(__FILE__), 'safe_fork')
require 'test/unit/testresult'
require 'test/unit'

# The ability to test multiple files across multiple cores
# simultaneously
class Multitest
  @@cores = 2
  def self.cores
    @@cores
  end
  def self.cores=(c)
    @@cores = c
  end
  # Takes an options hash.
  #   :files => files to test
  #   :cores => number of cores to use
  def initialize(files, cores = Multitest.cores)
    @files = files.sort do |a,b|
      File.size(b) <=> File.size(a)
    end
    @cores = cores
    @children = []
    @threads = []
    @pipes = []
    
    Test::Unit.run = true
    @files.each{|f| load f}
  end

  # Run the tests that have been setup
  def run
    return if @files.empty?
    $stderr.write @files.inspect+"\n"; $stderr.flush
    @cores.times do |c|
      @pipes << PipeDream.new
      @children << SafeFork.fork do
        Signal.trap("TERM") { exit }
        Signal.trap("HUP") { exit }
        pipe = @pipes.last
        pipe.identify_as_child
        pipe.write("[Worker #{c}] Booted\n")
        @result = Test::Unit::TestResult.new
        @result.add_listener(Test::Unit::TestResult::FAULT) do |value|
          $stderr.write value
          $stderr.write "\n\n"
          $stderr.flush
        end
        while !pipe.eof?
          file = pipe.gets.chomp
          begin
            pipe.write "[Worker #{c} Starting: #{file}\n"
            start = Time.now

            klasses = Multitest.find_classes_in_file(file)
            klasses.each{|k| k.suite.run(@result){|status, name| ;}}
            
            finish = Time.now
            pipe.write "[Worker #{c}] Completed: #{file} (#{finish-start})\n"
          rescue => ex
            pipe.write "[Worker #{c}] Failed: #{file} - #{ex.to_s}\n"
          end
        end
        $stderr.write @result.to_s
        $stderr.write "\n"
        $stderr.flush
        pipe.close
      end
    end

    total_files = @files.size
    @threads = []
    @pipes.each do |_p|
      @threads << Thread.new(_p) do |p|
        Signal.trap("TERM") { exit }
        p.identify_as_parent
        # boot message
        p.gets
        while !@files.empty? 
          # puts "#{total_files - @files.size}/#{total_files}"
          # send a file
          p.write("#{@files.pop}\n")
          # print the start message
          msg = p.gets
          # $stdout.write msg; $stdout.flush
          # wait for the complete message
          msg = p.gets
          # print complete message
          if msg =~ /Completed/
            # $stdout.write msg; $stdout.flush
          else
            $stderr.write msg; $stderr.flush
          end
        end
        p.close
      end
    end

    Signal.trap("TERM") do
      puts "Exiting"
      @children.each{|c| Process.kill("TERM",c)}
      @threads.each{|t| Thread.kill(t)}
    end

    @threads.each{|t| t.join}
    @children.each{|c| Process.wait(c)}
  end

  def self.find_classes_in_file(f)
    code = ""
    File.open(f) {|buffer| code = buffer.read}
    matches = code.scan(/class\s+([\S]+)/)
    klasses = matches.collect do |c|
      begin
        if c.first.respond_to? :constantize
          c.first.constantize
        else
          eval(c.first)
        end
      rescue NameError
        # $stderr.write "Could not load [#{c.first}] from [#{f}]\n"
        nil
      rescue SyntaxError
        # $stderr.write "Could not load [#{c.first}] from [#{f}]\n"
        nil
      end
    end
    return klasses.select{|k| k.respond_to? 'suite'}
  end

end