
require 'monitor'

if RUBY_VERSION.to_i > 2
  require 'rexml'
end

require File.join(File.dirname(__FILE__), "..", "models", "project.rb")
require File.join(File.dirname(__FILE__), "..", "models", "test_file.rb")



class Build < ActiveRecord::Base
  include MonitorMixin

  belongs_to :project, :class_name => "Project"
  has_many :test_files, :class_name => "TestFile",  :dependent => :destroy
  has_many :build_step_executions, :dependent => :destroy
  
  after_create :mark_project_latest_build_time
  before_save :set_duration  
  
  STEP_SCM_UPDATE = "SCM Update"
  STEP_PREPARE = "Prepare"
  STEP_SEND_NOTIFICATIONS = "Send Notifications"
  STEP_CLEANUP = "Cleanup"
  
  def test_framework
    self.test_syntax_framework
  end
  
  def self.mark_ghost_builds(started_before)
    Build.where("status = ?", "building").each do |a_build|
      next if a_build.created_at && a_build.created_at > started_before

      a_build.status = 'terminated'
      a_build.successful = false
      a_build.finish_time = Time.now
      a_build.duration = (a_build.finish_time - a_build.start_time) rescue nil
      a_build.save

      if a_build.pid
        begin
          raise "Mark Ghost Build shall not happen here"
        rescue => e
          puts "[ERROR], Error Trace: #{e.backtrace}"
        end



        a_build.kill_builder_process(a_build.pid)
      end
    end
  end

  def kill_builder_process(pid)
    puts "About to kill process: #{pid}"
    begin
      if RUBY_PLATFORM =~ /mingw/ || RUBY_PLATFORM =~ /mswin/
        output = `taskkill /F /T /PID #{pid}`
      else
        output = `kill -9  #{pid}`
      end
      puts "[INFO] kill build output: |#{output}| "
    rescue => e1
      puts "[Error] Failed to kill the terminated process: #{e1}"
    end
  end

  def all_test_cases
    test_files.includes(:test_cases).collect { |x| x.test_cases }.flatten
  end

  def all_test_cases_in_json
    array = all_test_cases.map{|tc| {:test_case_id => tc.id, :test_case => tc.name, :test_file_id => tc.test_file.id, :test_file => tc.test_file.filename } }
    return array.to_json
  end


  def ui_test_count
    build_test_cases_count = 0
    test_files.pluck(:num_total).compact.each { |x| build_test_cases_count += x || 0 }
    return build_test_cases_count
  end

  def test_execution_time
    ui_test_duration = 0
    test_files.pluck(:duration).each{ |x| ui_test_duration += x if x  }
    return ui_test_duration
  end

  def build_agents
    test_files.pluck("agent").uniq.compact
  end

  def ui_test_error_count
    count = 0
    begin
      test_files.pluck(:num_errors, :num_failures).each { |x| 
        count += x[0] if x[0] 
        count += x[1] if x[1]
      }
    rescue => e

      puts("Failed: #{e}")
    end
    return count
  end

  def set_label
    self.label ||= self.id
  end

  def incomplete?
    status == "building"
  end

  def elapsed_time_in_progress
    Time.now - start_time rescue nil
  end

  def brief_error
    ""
  end

  def duration_display(total_seconds)
    if total_seconds <= 60
      time_display = "#{total_seconds} sec"
    else
      minutes = total_seconds.to_i / 60
      seconds = total_seconds.to_i - minutes * 60
      time_display = "#{minutes} min #{seconds} sec"
    end
    return time_display
  end
  
  def pretty_duration(the_time = nil)
    the_time ||= self.duration
    parse_string = 
        if the_time < 60
            '%S sec'
        elsif the_time < 60 * 10            
          '%-M:%S'             
        elsif the_time < 3600
          '%M min'             
        else
          '%-H h %-M m'
        end

    Time.at(the_time).utc.strftime(parse_string)
  end


  def stage_timings
    if build_step_executions.first && build_step_executions.first.end_time.nil?
      last_stage_end_time = self.finish_time
      self.build_step_executions.reverse.each do |bt|
        next if last_stage_end_time.nil?
        bt.update_column(:end_time, last_stage_end_time)
        bt.update_column(:duration, last_stage_end_time - bt.start_time)
        last_stage_end_time = bt.start_time
      end
    end

    total_percentage = 0.0

    entries = []    
    self.build_step_executions.each do |bt|
      begin
        the_percentage = (bt.duration * 100 / self.duration).round(1)
        total_percentage += the_percentage 
        entries << {:stage_name => bt.task_name, :duration => duration_display(bt.duration.round(1)), :end_time => bt.end_time, :percentage => the_percentage }
      rescue => e        

      end
    end


    last_entry = entries.last



    
    if !self.incomplete? && self.finish_time && last_entry && last_entry[:end_time]
      entries << {:stage_name => "Finalize", :duration => duration_display(self.finish_time - last_entry[:end_time]), :percentage => (100.0 - total_percentage) }
    end

    return entries
  end


  def cancel
    if self.pid
      log(:info, "Cancel build #{self.id},  killing build process #{self.pid}")
      kill_builder_process(self.pid)
      delete_lock
    end
    self.finish_time = Time.now
    self.build_step_executions.each do |bs|
      if bs.start_time && bs.end_time.nil?
        bs.end_time = Time.now
        bs.save
      end
    end    
    self.status = "cancelled"
    self.save
  end

  def delete_lock
    the_lock_file = "#{BUILDWISE_HOME}/work/#{self.project.identifier}/.lock"
    if File.exist?(the_lock_file)
      File.delete(the_lock_file)
    end
  end


  def status_css_for_display
    if self.incomplete?
      "incomplete"
    elsif self.successful
      "successful"
    else
      "failed"
    end

  end


  def artifact(path)
    begin
      File.join(self.artifacts_dir, path)
    rescue => e
      nil
    end
  end


  def custom_artifacts
    begin
      artifact_entries = Dir.entries(self.artifacts_dir).find_all { |artifact| artifact != "ui-tests" && artifact != "." && artifact != ".." }
    rescue 
      artifact_entries = []
    end
    


    sort_me = { "changeset.log" => 1, "notifications.log" => 19}
    sorted = artifact_entries.sort_by { |k| 
      the_score = sort_me[k]
      if the_score.nil?
        if k.include?("step_")
          the_score = 10
        elsif k.include?("rake_parallel")
          the_score = 16
        elsif k.include?("build_")
          the_score = 18
        else # assign big score to user specified, handle them later
          the_score = 200
        end
      end
      the_score 
    }
    

    system_generated = []
    user_specified = []
    split_idx = 0
    sorted.each_with_index do |x, idx|
      if x.start_with?("notifications") && x.end_with?(".log")
        split_idx = idx
        system_generated << x        
        break
      end
      system_generated << x
    end
    
    user_specified = sorted.drop(split_idx + 1).sort  if split_idx > 0    
    final_sorted = (system_generated + user_specified).flatten
    return final_sorted
  end



  def public_artifact(path)
    build_artifact_folder_name = self.to_s.rjust(5, "0")
    File.join(BUILDWISE_ROOT, "public","artifacts", build_artifact_folder_name, path)
  end
  
  def changeset
    @changeset ||= contents_for_display(artifact('changeset.log'))
  end

  def output
    build_log_file = artifact('build-output.log')
    if build_log_file.nil? || !File.exist?(build_log_file)
      puts "[INFO] No log file '#{build_log_file}' detected yet"
      return ""
    end


    @output = contents_for_display(build_log_file)

    unless RUBY_VERSION =~ /1.8/
      @output = @output.force_encoding("UTF-8")
    end

    return @output
  end

  def backtrace_for_display
    unless RUBY_VERSION =~ /1.8/
      @backtrace.force_encoding("UTF-8")
    else
      @backtrace
    end
  end





  def next_grid_test(agent_ip_address)
    log(:debug, "[#{agent_ip_address}] calling next_grid_text =>")
    spec_file = nil

    start_assign_time = Time.now
    ActiveRecord::Base.transaction do
      log(:debug, "[#{agent_ip_address}] About to get next file")

      pending_test_files = TestFile.where(:status => nil, :build_id => self.id).order("priority DESC, filename").select(:id, :filename, :past_agents, :duration, :priority, :successful, :assigned_at, :status).to_a

      if self.project.active_distribution_rules.any? && pending_test_files.any?
        apply_distribution_rules(pending_test_files, agent_ip_address)
      end

      log(:debug, "[#{agent_ip_address}] => found #{pending_test_files.size} tests remainning")
      first_try_spec_file = spec_file = pending_test_files.shift
      log(:debug, "[#{agent_ip_address}] Found next test to assign: #{spec_file.filename} |#{agent_ip_address}|#{spec_file.past_agents}") if spec_file

      if spec_file.nil? then
        log(:info, "[#{agent_ip_address}] No files to test")


      elsif agent_ip_address && agent_ip_address == spec_file.past_agents
        log(:info, "#{agent_ip_address} ran this test file #{spec_file.filename} before, try to find a different file")

        failed_agent_assigned_to_another_ok = false
        while spec_file = pending_test_files.shift do
          next if agent_ip_address == spec_file.past_agents
          log(:info, "[#{agent_ip_address}] A new test #{spec_file.filename} assigned to agent #{agent_ip_address}")
          assign_test_to_agent(spec_file, agent_ip_address)
          failed_agent_assigned_to_another_ok = true
          break
        end

        if !failed_agent_assigned_to_another_ok && first_try_spec_file
          log(:info, "Re-assign this test #{first_try_spec_file.filename} to same agent #{agent_ip_address}, can't find any other tests to test!")


        end

      else # first time assignment
        log(:debug, "[#{agent_ip_address}] Assigned test time => #{Time.now - start_assign_time}")
        assign_test_to_agent(spec_file, agent_ip_address)
      end

    end # end db transaction



    tests_assigned_to_same_agent = TestFile.where(:status => "Assigned", :result => nil, :build_id => self.id, :agent => agent_ip_address).order("assigned_at asc")
    if tests_assigned_to_same_agent.count > 1 then
      log(:info, "[#{agent_ip_address}] oops, assigned more than one tests to same agents, undo it")
      tests_assigned_to_same_agent.each_with_index do |one_test_file, idx|
        next if idx == 0
        unassign_test(one_test_file)
      end
      return nil
    else
      return spec_file
    end
  end





  def apply_distribution_rules(pending_test_files, agent_ip_address)






    begin

      self.project.active_distribution_rules.each do |dr|

        rule_test_file_names = JSON.parse(dr.test_file_names)
        rule_allowed_agent_names =  JSON.parse(dr.build_agent_names) rescue []

        if dr.constraint == "on_same_agent"

          assignments_for_specified_tests = TestFile.where(:build_id => self.id).where("agent IS NOT NULL").where("filename IN (?)", rule_test_file_names).to_a



          if assignments_for_specified_tests.empty?



          else

            the_agent = assignments_for_specified_tests.first.agent

            if agent_ip_address == the_agent



            else

              exclude_filenames = []
              pending_test_files.delete_if {|x| rule_test_file_names.include?(x.filename) }
            end

          end

        elsif dr.constraint == "only_on_agents"



            pending_test_files.delete_if {|x|  rule_test_file_names.include?(x.filename) &&  !rule_allowed_agent_names.include?(agent_ip_address) }

        else

        end

      end

    rescue => ee

    end


    return pending_test_files
  end



  def cancel_assignment(test_file, agent_ip_address)
    matching_files = TestFile.where(:build_id => self.id, :agent => agent_ip_address, :filename => test_file)
    if matching_files && matching_files.count > 0
      unassign_test(matching_files.first)
    end
  end

  def unassign_test(spec_file)
    spec_file.agent = nil
    spec_file.assigned_at = nil
    spec_file.status = nil
    spec_file.result = nil
    spec_file.save
  end

  def assign_test_to_agent(spec_file, agent_ip_address)
    spec_file.agent = agent_ip_address
    spec_file.assigned_at = Time.now
    spec_file.status = "Assigned"
    spec_file.result = nil
    spec_file.save # unless params[:just_looking]
  end

  alias reassign_test_to_agent assign_test_to_agent



  def current_agent_allocations
    result = []
    return result unless status == "building"      
    test_files.where(:status => "Assigned", :result => nil).each do |tf|
      alloc = Allocation.new
      alloc.agent_name = tf.agent
      alloc.assigned_at = tf.assigned_at
      alloc.test_file_name = tf.filename
      alloc.test_file_id = tf.id      
      result << alloc
    end
    return result;
  end
  
  
  

  def determine_test_syntax_framework(ui_test_dir)
    if File.exist?(File.join(ui_test_dir, "step_definitions"))
      return "Cucumber"
    elsif File.exist?(File.join(ui_test_dir, "..", "node_module"))

      return "Mocha"      
    else
      return "RSpec"
    end
  end






  def analyse_ui_test_reports()
    ui_test_dir = artifact_ui_test_dir = File.join(self.artifacts_dir, "ui-tests")
    ui_test_report_dir = determine_ui_test_report_dir

    log(:info, "Finishing build #{self.id}, parse ui test reports dir: #{ui_test_report_dir}")

    if ui_test_report_dir && File.exist?(ui_test_report_dir)

      begin        
        ui_test_framework = self.project.config.builder_ui_test_framework || self.test_syntax_framework        
        if ui_test_framework.nil? || ui_test_framework.empty?
          self.test_syntax_framework = self.determine_test_syntax_framework(ui_test_dir)        

        else
          self.test_syntax_framework = ui_test_framework
          self.save
        end
    
        case ui_test_framework 
          
        when "Cucumber"
          test_files = parse_feature_results(ui_test_dir, ui_test_report_dir, :complete => true, :file_pattern => /^TEST.*\.xml$/i)
          archive_cucumber_feature_html_and_log(ui_test_report_dir)
          
        when "Mocha" # javascript  

          test_files = parse_spec_results(ui_test_dir, ui_test_report_dir, :complete => true, :file_pattern =>  /^*\.xml$/i)

        when "unittest" # python       

          test_files = parse_spec_results(ui_test_dir, ui_test_report_dir, :complete => true, :file_pattern =>  /^*\.xml$/i)
          
        when "RSpec"


          test_files = parse_spec_results(ui_test_dir, ui_test_report_dir, :complete => true, :file_pattern => /^[TEST|SPEC].*\.xml$/i)          

        when "JUnit"
          
          test_files = parse_junit_reports_in_dir(ui_test_report_dir, :complete => true, :file_pattern => /^[TEST|SPEC].*\.xml$/i)          
          
        else  # default to RSpec

          test_files = parse_spec_results(ui_test_dir, ui_test_report_dir, :complete => true, :file_pattern =>  /^*\.xml$/i)

        end
        
        if test_files then
          ActiveRecord::Base.transaction do
            test_files.each do |tf|
              

              if tf.filename && tf.filename =~ /\.xml$/
                begin
                  ui_test_framework = self.project.config.builder_ui_test_framework || self.test_syntax_framework
                  ui_test_framework.downcase!

                  if ui_test_framework =~ /mocha/i
                    tf.update_column(:filename, tf.filename.gsub(".xml", ".js"))
                  elsif ui_test_framework =~ /cucumber/i
                    tf.update_column(:filename, tf.filename.gsub(".xml", ".feature").gsub("TEST-features-", ""))                    
                  elsif ui_test_framework =~ /unittest/i
                    tf.update_column(:filename, tf.filename.gsub(".xml", ".py"))
                    
                  elsif ui_test_framework =~ /playwrighttest/i
                    tf.update_column(:filename, tf.filename.gsub(".xml", ".spec.ts"))
                  end
                  
                rescue => e
                  puts "Failed to update test file's name: #{e}"
                end
              end
              
              
              tf.update_column(:build_id, self.id)

              test_file_successful = tf.num_total > 0 && (tf.num_errors && tf.num_errors == 0) && (tf.num_failures && tf.num_failures == 0)
              tf.update_column(:successful, test_file_successful)

            end
          end
        end

      rescue => e        
        log(:error, "Failed to parse test reports #{e}")
      end

    end

    return "OK|#{ui_test_report_dir}"
  end

  

  def archive_cucumber_feature_html_and_log(ui_test_report_dir) 
    require 'fileutils'
    begin
      features_html_file = File.join(ui_test_report_dir, "features.html")
      if File.exist?(features_html_file)
        FileUtils.cp(features_html_file, artifact("features.html"))
      end    
      features_log_file = File.join(ui_test_report_dir, "features.log")
      if File.exist?(features_log_file)
        FileUtils.cp(features_log_file, artifact("features.log"))
      end    
    rescue => e
      log(:error, "Failed to archive cucumber feature.html and features.log #{e}")      
    end    
  end


  def determine_ui_test_report_dir

    return nil if self.is_parallel

    ui_test_report_dir = self.project.ui_test_report_dir

    if ui_test_report_dir && File.exist?(ui_test_report_dir)
      return ui_test_report_dir
    end

    if test_syntax_framework == "Cucumber" then
      ["test/acceptance/features/reports", "features/reports"].each do |candiate|
        file_path = File.join(self.project.checkout_dir, candiate)
        return file_path if File.exist?(file_path)
      end
      puts "[INFO] no features reports folder found"
    else
      ["test/acceptance/spec/reports", "spec/reports", "rpsec/spec/reports"].each do |candiate|
        file_path = File.join(self.project.checkout_dir, candiate)

        return file_path if File.exist?(file_path)
      end
      puts "[INFO] no spec reports folder found"
    end



    puts "[INFO] try Cucumber test report folders anyway"
    ["test/acceptance/features/reports", "features/reports"].each do |candiate|
      file_path = File.join(self.project.checkout_dir, candiate)
      if File.exist?(file_path)
        self.test_syntax_framework = "Cucumber"
        self.save
        return file_path
      end
    end

    puts "[INFO] can't find ui test report folder, tried all options"
    return nil
  end



  def parse_junit_reports_in_dir(ui_test_report_dir, options = {})
    parse_junit4_reports_in_dir(ui_test_report_dir, options)




  end
  


  def parse_junit4_reports_in_dir(ui_test_report_dir,  options = {})
    result_dir = Dir.open(ui_test_report_dir)
    result_files = result_dir.entries.grep(options[:file_pattern])
    result_dir.close    
        
    screenshots_dir = File.join(ui_test_report_dir, "screenshots")  
    FileUtils.mkdir_p(screenshots_dir) unless File.exist?(screenshots_dir)
      
    build_artifact_folder_name = self.id.to_s.rjust(5, "0")
    buildwise_root = File.expand_path File.join(File.dirname(__FILE__), "..", "..")
    build_artifact_dir = File.join(buildwise_root, "public", "artifacts", build_artifact_folder_name)
    FileUtils.mkdir_p(build_artifact_dir) unless File.exist?(build_artifact_dir)
        
    start_time = Time.now
    test_results = []

    result_files.each do |result_file_name|
      file_name = File.join(ui_test_report_dir, result_file_name)
      next unless File.exist?(file_name)
      test_file = parse_junit_report(file_name, {}, options)

      if File.exist?(File.join(screenshots_dir, test_file.filename)) && test_file.id 
        

        test_file.test_cases.each do |tc|

          tc_screenshot_file = File.join(screenshots_dir, test_file.filename, tc.name + ".png")

          
          if tc_screenshot_file && File.exist?(tc_screenshot_file)
            saved_test_case_screenshot_file = File.join(build_artifact_dir, "screenshot-#{tc.id}.png")
            begin
              FileUtils.cp(tc_screenshot_file, saved_test_case_screenshot_file)
              tc.update_column(:screenshot, saved_test_case_screenshot_file.gsub("#{buildwise_root}/public", ""))
            rescue => e


            end
            
          end          
        end        
      end      
      test_results << test_file
    end    
    
    test_results.sort! { |a, b| b.last_modified <=> a.last_modified }        
    return test_results
  end

  def parse_junit5_reports_in_dir(test_report_dir, options = {})
    
    result_dir = Dir.open(test_report_dir)
    result_files = result_dir.entries.grep(options[:file_pattern])
    result_dir.close   
    
    start_time = Time.now
    test_results = []

    result_files.each do |result_file_name|
      next if result_file_name.end_with?("TEST-junit-vintage.xml")

      file_name = File.join(test_report_dir, result_file_name)
      
      doc = nil
      begin
        doc = Nokogiri::XML(File.read(file_name))
      rescue => e
        log(:warn, "Failed parsing result_file: #{result_file} #{e}")
        return nil
      end
      
      
      classname_to_testfile_lookup = {}

      test_results = []
      begin
      
      doc.xpath("//testcase").each { |element|
        test_case_name = element.attributes["name"].to_s
        classname = element.attributes["classname"].to_s
        
        test_file = classname_to_testfile_lookup[classname]
        if test_file.nil?
          test_file = TestFile.new(:filename => classname, :created_at => Time.now, :build_id => self.id)
          classname_to_testfile_lookup[classname] = test_file
          test_files << test_file
        end
        
        test_case = TestCase.new(:name => test_case_name, :duration => element.attributes["time"].to_s.to_f, :created_at => Time.now, :build_id => self.id, :project_id => self.project_id)
              
        failure_nodes = element.elements.select { |x| x.name == "failure" }
        error_nodes = element.elements.select { |x| x.name == "error" }

        if failure_nodes.any? then
          failure_message = failure_nodes.first["message"]

          test_case.failure = failure_nodes.first.text rescue ""
          if failure_message 
            test_case.failure = failure_message  + "\n" + test_case.failure
          end

          test_case.failure = test_case.failure.force_encoding("UTF-8") unless RUBY_VERSION =~ /1\.8/
          test_case.successful = false
        elsif error_nodes.any?
          test_case.failure = error_nodes.first.text
          test_case.successful = false
        else
          test_case.successful = true
        end
        
        test_file.test_cases << test_case                
      }
      
      if options[:complete]
        test_results.each do |tf|          
          tf.build_id = self.id if tf.build_id.nil?
          tf.save          
        end
      end
      
      rescue => e        

      end
    
      return test_results
    end
    
    
  end
  
  

  def parse_spec_results_with_lookups(ui_test_report_dir, context_to_spec_lookups, options = {})
    result_dir = Dir.open(ui_test_report_dir)
    result_files = result_dir.entries.grep(options[:file_pattern])
    result_dir.close

    screenshots_dir = File.join(ui_test_report_dir, "screenshots")
    build_artifact_folder_name = self.id.to_s.rjust(5, "0")
    buildwise_root = File.expand_path File.join(File.dirname(__FILE__), "..", "..")
    build_artifact_dir = File.join(buildwise_root, "public","artifacts", build_artifact_folder_name)
    FileUtils.mkdir_p(build_artifact_dir) unless File.exist?(build_artifact_dir)

    if options[:complete]
      puts "#{Time.now} [DEBUG] saving the test results in database"
    end
    

    start_time = Time.now
    test_results = []
    result_files.each do |result_file_name|
      file_name = File.join(ui_test_report_dir, result_file_name)
      next unless File.exist?(file_name)

      test_file = parse_junit_report(file_name, context_to_spec_lookups, options)
      next if test_file.nil?
      
      if File.exist?(File.join(screenshots_dir, test_file.filename)) && test_file.id && !self.is_parallel

        test_file.test_cases.each do |tc|
          tc_screenshot_file = File.join(screenshots_dir, test_file.filename, tc.name + ".png")

          if File.exist?(tc_screenshot_file)
            test_file_artifact_folder_name = test_file.id.to_s.rjust(5, "0")
            build_spec_artifact_dir = File.join(build_artifact_dir, test_file_artifact_folder_name)  
            FileUtils.mkdir_p(build_spec_artifact_dir) unless File.exist?(build_spec_artifact_dir)

            saved_test_case_screenshot_file = File.join(build_spec_artifact_dir, "screenshot-#{tc.id}.png")
            begin
              FileUtils.mv(tc_screenshot_file, saved_test_case_screenshot_file)
              tc.update_column(:screenshot, saved_test_case_screenshot_file.gsub("#{buildwise_root}/public", ""))
            rescue => e
              log(:info, "Failed to move the file over to /public/artifacts, might have bee moved already")
            end
            
          end          
        end        
      end
      
      test_results << test_file
    end











    log(:debug, "Parse test results, cost #{Time.now - start_time}") if options[:complete]
    
    test_results.sort! { |a, b| b.last_modified <=> a.last_modified }
    return test_results
  end



  def parse_junit_report(file_name, context_to_spec_lookups = {}, options = {})

    require 'nokogiri'


    doc = nil
    begin
      doc = Nokogiri::XML(File.read(file_name))
    rescue => e
      log(:warn, "Failed parsing result_file: #{result_file} #{e}")
      return nil
    end

    suite_count = doc.xpath("/testsuites/testsuite").count
    first_test_suite = doc.xpath("/testsuites/testsuite").first
    
    if suite_count == 1
      begin      
        first_test_suite.attributes # if not valid
      rescue => e
        first_test_suite = doc.root
      end
    else

      second_test_suite = doc.xpath("/testsuites/testsuite")[1]
      
      if first_test_suite && second_test_suite && first_test_suite["tests"].to_s == "0" && second_test_suite["tests"].to_s.to_i > 0
        first_test_suite = doc.xpath("/testsuites/testsuite")[1]
      end
      
      begin      
        first_test_suite.attributes # if not valid
      rescue => e
        first_test_suite = doc.root
      end
    
    end
        
    suite_name, test_count, duration, test_failures, test_errors = first_test_suite.attributes['name'].to_s, first_test_suite.attributes['tests'].to_s, first_test_suite.attributes['time'].to_s.to_f, first_test_suite.attributes['failures'].to_s, first_test_suite.attributes['errors'].to_s
    
    exeuction_time = first_test_suite.attributes["timestamp"]
    file_path = context_to_spec_lookups[suite_name] || file_name
    test_file = TestFile.new(:result_filename => file_name, :file_path => file_path, :filename => File.basename(file_path), :description => suite_name, :created_at => Time.now)
    test_file.last_modified = File.mtime(file_name)
    test_file.stdout = doc.xpath("//system-out").text rescue nil
    test_file.stderr = doc.xpath("//system-err").text rescue nil


    if test_file.stdout &&  test_file.stdout.size > 65000
      test_file.stdout = test_file.stdout.truncate(1024) + "\n...\n" + test_file.stdout.last(1024)
    end
    if test_file.stderr &&  test_file.stderr.size > 65000
      test_file.stderr = test_file.stderr.truncate(1024) + "\n...\n" + test_file.stderr.last(1024)
    end
      
    doc.xpath("//testcase").each { |element|
      test_case_name = element.attributes["name"].to_s
      
      test_case = TestCase.new(:name => test_case_name, :duration => element.attributes["time"].to_s.to_f, :created_at => Time.now, :build_id => self.id, :project_id => self.project_id)

      if test_file && suite_name && suite_name.strip.size >= 3 && self.project.config.remove_suite_name_from_test_case
         test_case.remove_prefix(suite_name)
      end
      
      failure_nodes = element.elements.select { |x| x.name == "failure" }
      error_nodes = element.elements.select { |x| x.name == "error" }

      if failure_nodes.any? then
        failure_message = failure_nodes.first["message"]

        test_case.failure = failure_nodes.first.text rescue ""
        if failure_message 
          test_case.failure = failure_message  + "\n" + test_case.failure
        end

        test_case.failure = test_case.failure.force_encoding("UTF-8") unless RUBY_VERSION =~ /1\.8/
        test_case.successful = false
      elsif error_nodes.any?
        test_case.failure = error_nodes.first.text
        test_case.successful = false
      else
        test_case.successful = true
      end
      test_file.num_total = test_count.to_i
      test_file.num_failures = test_failures.to_i
      test_file.num_errors = test_errors.to_i
      test_file.duration = duration.to_f

      if options[:complete]
        unless test_file.build_id
          test_file.build_id = self.id
          test_file.save!
        end
        test_case.test_file_id = test_file.id
        test_case.save!
      else
        test_file.test_cases << test_case
      end
    }
    return test_file
  end


  def parse_feature_results(ui_test_dir, ui_test_report_dir, options = {})
    require 'rexml/document'
    test_results = nil
    begin
      test_results = parse_spec_results_with_lookups(ui_test_report_dir, {}, options)
    rescue => e
      log(:warn, "failed to parse feature results for #{self.id} in #{ui_test_report_dir}: #{e}")
      test_results ||= []
    end
    
    return test_results
  end

  def parse_spec_results(ui_test_dir, ui_test_report_dir, options = {})
    require 'rexml/document'
    test_results = nil
    begin
      log(:info, "About to parse spec files: #{ui_test_dir}")
      unless File.exist?(ui_test_dir)

        log(:warn, "UI test dir not exists: #{ui_test_dir}")
        copy_ui_tests_to_artifacts
      end

      spec_file_to_context_lookups = build_context_to_file_lookups(ui_test_dir)
      log(:info, "Parsing spec files with lookups: #{spec_file_to_context_lookups.size}")
      
      if spec_file_to_context_lookups.empty? 

        log(:info, "Might not be rspec, try to analyse test folder #{ui_test_report_dir} anyway.")
        test_results = parse_junit_reports_in_dir(ui_test_report_dir, options)

      else

        test_results = parse_spec_results_with_lookups(ui_test_report_dir, spec_file_to_context_lookups, options)
        
      end
        
    rescue => e
      if options[:logger]
        options[:logger].warn("failed to parse spec results: #{e}, #{e.backtrace}")
      end
      log(:warn, "failed to parse spec results for #{self.id} in #{ui_test_report_dir}: #{e}, #{e.backtrace}")
      test_results ||= []
    end
    return test_results
  end


  def copy_ui_tests_to_artifacts
    project_config = self.project.config(false) # no need refresh
    project_ui_tests_dir = project_config.ui_tests_dir
    if project_ui_tests_dir.nil? || project_ui_tests_dir.strip == ""
      project_ui_tests_dir = "."
    end

    if self.test_syntax_framework == "Cucumber"
      ui_test_dir = File.expand_path(File.join(self.project.working_dir, "sources", project_ui_tests_dir))
    else
      ui_test_dir = File.expand_path(File.join(self.project.working_dir, "sources", project_ui_tests_dir))
    end

    begin
      log(:info, "** Copy UI tests |#{ui_test_dir}|  to artifacts.....")
      require 'rake' # for FileList
      FileUtils.mkdir(File.join(self.artifacts_dir, "ui-tests"))

      FileUtils.cp_r(FileList[ui_test_dir + "/*"], self.artifacts_dir + "/ui-tests")

    rescue => e
      log(:warn, "failed to copy acceptance test dir: #{ui_test_dir} to #{self.artifacts_dir},  #{e}")
    end
  end
  
  
  def build_feature_to_file_lookups(ui_test_dir)
    the_lookups = {}
    Dir.glob("#{ui_test_dir}/*.feature").each do |feature_file|
      f = File.open(feature_file, 'r')
      f.each do |line| # modify lines
        if line =~ /^\s*Feature:\s*(.*)/ then
          the_lookups[$1] = feature_file
        end
      end
      f.close
    end

    return the_lookups
  end



  def build_context_to_file_lookups(ui_test_dir)
    ui_test_dir = File.expand_path(ui_test_dir)


    if File.exist?(File.join(ui_test_dir, "spec"))
      log(:info, "Settting UI_TEST_DIR => #{ui_test_dir}, spec folder under")
      ui_test_dir = File.join(ui_test_dir, "spec")
      log(:info, "Settting UI_TEST_DIR channged to => #{ui_test_dir}")
    end


    the_lookups = {}
    spec_folders = ui_test_dir.class == Array ? ui_test_dir : [ui_test_dir]
    spec_folders.each do |spec_folder|

      ["*_spec.rb", "*_test.rb"].each do |file_pattern|
        Dir.glob("#{spec_folder}/#{file_pattern}").each do |spec_file|
          begin
=begin 

            f = File.open(spec_file, 'r')
            f.each do |line| # modify lines
              if line =~ /^\s*(context|describe|spec|specification|test_suite)\s+('|")(.*)('|")\s+do/ then
                the_lookups[$3] = spec_file
              end
            end
            f.close
=end
            File.readlines(spec_file, :encoding => 'UTF-8').each do |line|
              if line =~ /^\s*(context|describe|spec|specification|test_suite)\s+('|")(.*)('|")\s+do/ then
                the_lookups[$3] = spec_file
              end
            end            
            
          rescue => e
            log(:warn, "Failed to read spec file => #{spec_file}, maybe invalid char set. Error: #{e}")
          end
        end
      end

    end


    return the_lookups
  end




  def refresh_ui_test_status

    self.reload
    
    if self.is_waiting_mode
      log(:info, "[INFO] !!! Waiting mode, return")
      return
    end

    self.test_files.reload    
    

    if self.test_files.empty? &&  (Time.now - self.start_time < 15)
      self.update_column(:ui_test_status, "Pending")
      return
    end 
      
    
    if self.test_files.all? { |x| (x.status && x.num_total && x.num_total > 0) || x.result }

      if self.test_files.all? {|x| x.result == "OK"} then      
        self.update_column(:ui_test_status, "OK")
      
      elsif self.test_files.size >= 1 # if not tests assigned, it will mark failed.
        self.update_column(:ui_test_status, "Failed")
      end
      
    else


      self.update_column(:ui_test_status, "Pending")
    end
    
  end




  def increment_test_file_priority_for_recent_checkins(scm)
    
    begin
      
      unless scm.class.name =~ /git/i
        log(:info, "[INFO] test file priority check only works for Git, not for #{scm.class.name}")
        return
      end
    
      last_build = self.project.builds.where("id < ?", self.id).last
      if last_build && last_build.revision
        recent_changed_files_str = scm.changed_files_since(last_build.revision)  
      else
        recent_changed_files_str = scm.changed_files_since(3) # recent 3 checkins   
      end
    
      project_ui_tests_dir = self.project.config.ui_tests_dir
  
      ui_test_extension = ".rb"
      if self.project.config.builder_ui_test_framework 
      
        case self.project.config.builder_ui_test_framework
        when "Cucumber"
          ui_test_extension = ".feature"
        when "Mocha"
          ui_test_extension = ".js"        
        when "unittest"
          ui_test_extension = ".py"        
        end
      end
    
      recent_changed_files = recent_changed_files_str.split
      recent_changed_files.select!{|x| x.include?(project_ui_tests_dir) }
      recent_changed_files = recent_changed_files.collect{|x| File.basename(x)}
      recent_changed_files.select!{|x| x.include?(ui_test_extension) }
    
      log(:info, "recent_changed_files: #{recent_changed_files}")

      priority_changed_count = 0
      recent_changed_files.each do |filename|
        next if filename.nil?
        tf = TestFile.joins(:build).where(filename: filename, builds: {project_id: self.project_id} ).order(id: :desc).first
        if tf
          the_priority = tf.priority
          if the_priority.nil? || the_priority.to_i <= 0
            the_priority = 10
          end
          tf.update_column(:priority, the_priority + 5)  # the incremenet interval
          priority_changed_count += 1

        end                
      end
            
      log(:info, "Updated priority for #{priority_changed_count} test script files")
    rescue => e1 
      log(:warn, "Failed in builder.rb on function increment_test_file_priority_for_recent_checkins, #{e1}")
      log(:warn, e1.backtrace)      
    end
    
  end
  
  def triggered_user
    the_user = User.where(:id => self.triggered_by_user_id).first
    if the_user
      return the_user.username
    else
      return "API"
    end
  end
  

  def chart_data
    hash = {}
    hash["id"] = self.id
    hash["time"] = self.start_time.localtime.strftime("%Y-%m-%d %H:%M")
    if self.status == "complete"  
        hash["color"] = self.successful ? "#8BC34A" : "#ff6b68"
    elsif self.status == '#building'
        hash["color"] = '#058DC7'
    else # cancelled
        hash["color"] = '#9E9E9E'        
    end
    
    
    if self.duration      
      hash["y"] = self.duration ? self.duration : 0      
      hash["duration"] = self.pretty_duration rescue ""
      if self.ui_test_error_count == 0
        hash["pass_rate"] = "100%"
      else 
        hash["pass_rate"] = ((self.ui_test_count - self.ui_test_error_count) * 100.0 / self.ui_test_count).round(1).to_s + "%" rescue ""
      end
    else
      hash["y"] = self.elapsed_time_in_progress ? self.elapsed_time_in_progress : 0      
      hash["duration"] = self.pretty_duration(elapsed_time_in_progress) rescue ""
      hash["pass_rate"] = ""
    end
    return hash
  end
  
  
  def artifact_for_display(artifact_file_name)
    if artifact_file_name.nil?
      return "N/A"
    end
        
    if artifact_file_name =~ /^step_(\d+)\.log$/
      step_id = $1.to_i
      the_step = self.build_step_executions.where(:id => step_id).first      
      if the_step && the_step.task_name        
        return the_step.task_name + ".log"    
      end
    elsif artifact_file_name =~ /^build_(\d+)\.log$/
      return "build.log"
    end
    
    return artifact_file_name
    
  end
  
  def is_ui_test_stage?    
    self.stage && (self.stage == "ui_test_task" || self.stage.downcase == "ui test" || self.stage.downcase == "api test")
  end
  
  
  def prepare_steps(includes_pending = false)
    if includes_pending    
      the_steps = self.build_step_executions.where("task_name = ? OR task_name = ?", STEP_SCM_UPDATE, STEP_PREPARE).to_a 
      if self.incomplete? && the_steps.count < 2

        existing_step_names = the_steps.collect{|x| x.task_name }
        if !existing_step_names.include?(STEP_SCM_UPDATE)
          the_steps << BuildStepExecution.new(:build_id => self.id, :task_name => STEP_SCM_UPDATE)
        end
        
        build_prepare_command = self.project.config.builder_prepare_command
        if !existing_step_names.include?(STEP_PREPARE) && build_prepare_command && !build_prepare_command.strip.empty? 
          the_steps << BuildStepExecution.new(:build_id => self.id, :task_name => STEP_PREPARE)
        end
      end

      return the_steps

    else
      self.build_step_executions.where("task_name = ? OR task_name = ?", STEP_SCM_UPDATE, STEP_PREPARE)     
    end
    
  end
  
  def finalize_steps(includes_pending = false)    
    if includes_pending
      the_steps = self.build_step_executions.where("task_name = ?", STEP_SEND_NOTIFICATIONS).to_a      
      if self.incomplete? && the_steps.count < 1
        the_steps << BuildStepExecution.new(:build_id => self.id, :task_name => STEP_SEND_NOTIFICATIONS)
      end
      return the_steps       
    else
      self.build_step_executions.where("task_name = ?", STEP_SEND_NOTIFICATIONS)      
    end
  end

  def user_steps(includes_pending = false)    
    if includes_pending
      the_steps = self.build_step_executions.where("task_name != ? AND task_name != ? AND task_name != ?", STEP_SCM_UPDATE, STEP_PREPARE, STEP_SEND_NOTIFICATIONS).to_a
      
      existing_step_names = the_steps.collect{|x| x.task_name }

      planned_step_names = self.project.build_steps.select{|x| x.enabled }.collect{|y| y.name}
      planned_step_names.delete(STEP_PREPARE)
      
      if self.incomplete? && existing_step_names.count < planned_step_names.count
        (planned_step_names - existing_step_names).each do |planed_step_name|
            the_steps <<  BuildStepExecution.new(:build_id => self.id, :task_name => planed_step_name)
        end
      end
      
      return the_steps
      
    else
      self.build_step_executions.where("task_name != ? AND task_name != ? AND task_name != ?", STEP_SCM_UPDATE, STEP_PREPARE, STEP_SEND_NOTIFICATIONS)      
    end
  end
  
  def has_ui_test_execution?
    self.user_steps.any?{|x| x.is_ui_test?}
  end
  
  
  def builder_log_file
    if self.artifacts_dir
      File.join(self.artifacts_dir, "build_#{self.id.to_s.rjust(5, '0')}.log")
    else
      nil
    end
  end
  
  def builder_log_output

    the_output = contents_for_display(builder_log_file)

    unless RUBY_VERSION =~ /1.8/
      the_output = the_output.force_encoding("UTF-8")
    end

    return the_output
  end
  

  def false_alarms
    self.test_files.where("past_agents IS NOT NULL AND result = ?", "OK")
  end
  
  
  def analyse_load_results
    if self.is_load_testing && self.load_vu_count.nil?          
      build_load_test_results = LoadTestResult.where("build_id = ?", self.id)
      if build_load_test_results.size > 1
        self.load_duration = ((build_load_test_results.last.end_timestamp - build_load_test_results.first.start_timestamp ) / 1000.0).round(2)
      else
        self.load_duration = self.duration.round(1)
      end      
      
      self.load_hits = build_load_test_results.count
      self.load_hits_per_second = ( ( (self.load_hits * 1.0) / self.load_duration).round(2) ) rescue 0
      self.load_vu_count = build_load_test_results.pluck("agent_name").uniq.count
      self.load_peak_hits_per_second = determine_peak_hits()
      self.save
    end
  end
  
  
  def determine_peak_hits
    build_load_test_results = LoadTestResult.where("build_id = ?", self.id).to_a
    return if build_load_test_results.size < 10

    load_start_time = build_load_test_results.first.start_timestamp
    load_end_time = build_load_test_results.last.end_timestamp
    total_vu_count = self.load_vu_count
    check_time_duration = total_vu_count * 4 # for 4 agents, for every 16 seconds
    
    peak_hits = 0;
    build_load_test_results.each do |x|
      sub_array =  build_load_test_results.select{|y| y.start_timestamp < (check_time_duration * 1000 + x.start_timestamp)}
      if sub_array.count > peak_hits
        peak_hits = sub_array.count
      end
    end 

    return (peak_hits * 1.0 / check_time_duration)
  end
  
    
  def load_execution_duration
    if build_load_test_results.size > 1
      hash[:build_duration] = ((build_load_test_results.last.end_timestamp - build_load_test_results.first.start_timestamp ) / 1000.0).round(2)
    else
      hash[:build_duration] = a_build.duration.round(1)
    end      
  
  end
  

  def parse_performance_results
    the_test_files = TestFile.where(:build_id => self.id).where("stdout IS NOT NULL")
    the_test_files.each do |tf|
      the_test_file_stdout = tf.stdout.strip
      the_test_file_stdout.each_line do |line|
        line.strip!
        if line.start_with?("|") && line.end_with?("|")



          data_tokens = line.split("|")
          if data_tokens.size >= 2
            msg = data_tokens[-2]
            timing = data_tokens[-1]          
            PerformanceTestResult.create!(:build_id => self.id, :test_file_id => tf.id, :operation => msg, :starts_at => tf.created_at, :duration => timing)
          end

        elsif line.start_with?("[") && line.end_with?("]")



          the_data = eval(line) rescue []
          if the_data.size == 7
            msg = the_data[1]
            timing = the_data[4] / 1000.0 rescue 0
            PerformanceTestResult.create!(:build_id => self.id, :test_file_id => tf.id, :operation => msg, :starts_at => tf.created_at, :duration => timing)            
          end
          
        end
      end
    end
    



  end
  
  
  
  def determine_load_build_successful
    if self.is_parallel && self.is_load_testing
      build_load_test_results = LoadTestResult.where("build_id = ?", self.id)
      error_count = build_load_test_results.where("success != ?", true).count 
      self.builder_output ||= ""
      

      project_load_criteria_error_rate = self.project.config.load_criteria_error_rate.to_f
      if error_count > 0 && !project_load_criteria_error_rate.nil?
          build_error_rate = (error_count * 100.0 / build_load_test_results.count).round(2)
          if build_error_rate > project_load_criteria_error_rate
            puts("{Load Testing failed}: error rate: #{build_error_rate} > #{project_load_criteria_error_rate}  (#{error_count} out of #{build_load_test_results.count}).")
            update_column(:successful, false)      
            self.builder_output += "\n Load Tests failure rate: #{build_error_rate}%.\n"
            self.builder_output += "Exceeds #{project_load_criteria_error_rate}% set for the project!"
            update_column(:builder_output, self.builder_output)
            return false;
          end
      end
      

      project_load_operation_criteria = self.project.config.load_operation_criteria
      if project_load_operation_criteria.empty?
        return true;
      end      
      
      raw_operation_timings = build_load_test_results.group_by(&:operation)
      timings = []
      raw_operation_timings.each do |key, val|
        duration_array =  val.collect{|x| (x.duration).round(1)} 
        timings << {:operation => key, :count => val.size,  
                   :average_time => (duration_array.sum / val.count).round(2),   
                   :fastest_time => duration_array.min,
                   :slowest_time => duration_array.max }        
      end      
      
      over_threshold = false      
      project_load_operation_criteria.each do |key, entry|
        next unless entry[:is_active].to_s == "true"
        operation_name = key
        project_operation_min_average_time = entry[:average_time].to_i
        project_operation_min_slowest_time = entry[:slowest_time].to_i
              
        build_operation_average_time = timings.select{|x| x[:operation] == operation_name}.first[:average_time] rescue -1
        build_operation_slowest_time = timings.select{|x| x[:operation] == operation_name}.first[:slowest_time] rescue -1
        
        puts "Comparing average #{operation_name}: #{build_operation_average_time} <=> #{project_operation_min_average_time}"
        if build_operation_average_time < 0 || (project_operation_min_average_time > 0 && build_operation_average_time > project_operation_min_average_time)        
          self.builder_output += "Average execution time of Operation '#{operation_name}': #{build_operation_average_time} exceeds the criteria: #{project_operation_min_average_time} \n"          
          over_threshold = true;
        end
        
        
        puts "Comparing slowest #{operation_name}: #{build_operation_slowest_time} <=> #{project_operation_min_slowest_time}"
        if build_operation_slowest_time < 0 || (project_operation_min_slowest_time > 0 && build_operation_slowest_time > project_operation_min_slowest_time)
          self.builder_output += "Longest execution time of Operation '#{operation_name}': #{build_operation_slowest_time} exceeds the criteria: #{project_operation_min_slowest_time} \n"          
          over_threshold = true;
        end        
      end
      
      if over_threshold
        puts("{Load Testing failed}: over threshold")
        update_column(:successful, false)      
        self.update_column(:builder_output, self.builder_output)
        return false
      end
      
      update_column(:successful, true)      
      
      return true  # pass all critiera
    end
    return nil # not applicable
  end
  
  


  private

  def set_duration
    if self.finish_time && self.start_time
      self.duration = self.finish_time - self.start_time
    end
    set_label  
  end
  


  def log(level, message)
    begin
      if (level.to_s == "info")
        logger.info(message)
      elsif (level.to_s == "warn")
        logger.warn(message)
      elsif (level.to_s == "debug")
        logger.debug(message)
      else
        logger.error(message)
      end
    rescue => e

      puts "#{Time.now} [#{level.to_s.upcase}] #{message}"
    end
  end

  def contents_for_display(file)
    return '' unless file && File.file?(file) && File.readable?(file)
    file_size_kbytes = File.size(file) / 1024
    if file_size_kbytes < MAX_DISPLAY_LOG_KB
      File.read(file)
    else

      contents = File.read(file, 100 * 1024)
      contents = contents[0..100*1024] + "\n...\n" + contents[-100*1024..-1]
      response = "#{file} is #{file_size_kbytes} kbytes - too big to display in the dashboard, the output is truncated\n\n\n"
      response += contents
    end
  end
  
  def mark_project_latest_build_time
    if self.project
      self.update_column(:server_digest, self.project.server_digest)
      
      self.update_column(:is_parallel, self.project.config.is_parallel)
      self.update_column(:is_load_testing, self.project.config.is_load_testing)
      self.update_column(:is_performance_testing, self.project.config.is_performance_testing)
      self.project.update_column(:last_build_time, Time.now)
    end
  end
  

  
end