#!/usr/bin/env ruby 

# == Synopsis 
#   bast-testrunner is used to run testsuite and import test result to bats-qa-reports.
#
# == Usage 
#   bats-testrunner [Options]
#
#   For help use: bats-testrunner -h
#
# == Options
#   -h, --help                           Displays help message
#   -v, --version                        Display the version, then exit
#   -V, --verbose                        Verbose output
#   -c <bats_test_cnf>, --config <bats_test_cnf>   set <bats_test_cnf> as BATS test config
#
# == Author
#   zhi.dou@nokia.com
#
# == Copyright
#   Copyright (c) 2011 Nokia

require 'optparse' 
require 'rdoc/usage'
require 'ostruct'
require 'date'
require 'ftools'
require 'rubygems'    
require 'active_record'    
require 'yaml'      
require 'fileutils'
require 'net/smtp'
require 'rexml/document'  
include REXML

# Global constant
RT_EXT = ".tr.xml"
PUB_DIR = "public"
REP_DIR = "reports"
APP_NAME = "boss"
GLB_CONF = "/etc/bats.conf"

# Entity class of each test plan config
class TestContext
  attr_accessor :testPlanXml,:testTarget,:testVersion,:testType,:testPlanDir
  attr_accessor :testSession
  def to_s
    str = "\n"
    str += "Test Config: \n"
    str += "  TEST_PLAN_DIR: #{@testPlanDir}\n"
    str += "  TEST_PLAN_XML: #{@testPlanXml}\n"
    str += "  TEST_TARGET: #{@testTarget}\n"
    str += "  TEST_VERSION: #{@testVersion}\n"
    str += "  TEST_TYPE: #{@testType}\n"
  end
end

# Entity class of BATS global config
class BATSContext
  def initialize
    # variable's default value
    @verbose = false
    @testConfigs = Array.new
    @batsRoot = '/srv/www/bats-qa-reports'
    @serverAddress = 'localhost'
  end 
  def addTestConfig(config)
    @testConfigs << config
  end 
  attr_reader :testConfigs
  attr_accessor :config,:verbose,:batsRoot,:serverAddress
  attr_accessor :userEmail,:userPassword,:userName
  def to_s
    str = "\n"
    str += "Global Config: \n"
    str += "  BATS_QA_REPORTS_ROOT: #{@batsRoot}\n"
    str += "  SERVER_ADDRESS: #{@serverAddress}\n"
    str += "  USER_NAME: #{@userName}\n"
    str += "  USER_EMAIL: #{@userEmail}\n"
    str += "  USER_PASSWORD: #{@userPassword}\n"
    str += "Please update '/etc/bats.conf' to change global config.\n"
    str += @testConfigs.to_s
  end
end

# Super class of all working class
class Worker
  @@ctx = BATSContext.new
  def print(s)
    puts s if @@ctx.verbose
  end
end

# Report notification
class TestReporter < Worker
  def getTestSummary(c)
    # email the user with the test report link
    rUrl = "\n================================================================================\n"
    rUrl += "\n\nTest Target : "+c.testTarget+", Test Type : "+c.testType+", Test version : "+c.testVersion
    rUrl += "\n\nTotal Cases : "+c.testSession.total_cases.to_s+", Passed : "+c.testSession.total_passed.to_s
    rUrl += ", Failed : "+c.testSession.total_failed.to_s+", Passed Rate : "+c.testSession.total_pass_rate.to_s
    rUrl += "\n\nhttp://"+@@ctx.serverAddress+":3000/"+APP_NAME+"/"+c.testTarget+"/"+c.testType+"/"+c.testVersion+"/"+c.testSession.id.to_s
    rUrl += "\n\n"
  end
  def run
    str = ''
    @@ctx.testConfigs.each do |c|
      str += getTestSummary(c)
    end
    print("Sending link to #{@@ctx.userEmail} ... ")
    print( str )
    Net::SMTP.start('smtp.nokia.com',25) do |smtp|
      smtp.open_message_stream('bats@nokia.com', @@ctx.userEmail) do |f|
        f.puts 'From: bats@nokia.com'
        f.puts 'To: ' + @@ctx.userEmail
        f.puts 'Subject: BATS Test Report Notification'
        f.puts ""
        f.puts "Test Reports :"
        f.puts str
      end
    end
  end 
end

# Envrionment clean
class TestEnvCleaner < Worker
  def clean(c)
    # clean the tmp dir and result file
    if File::exists?( PUB_DIR )
      FileUtils.rm_rf PUB_DIR
    end
    tr = File.join( File.dirname(__FILE__) , File.basename(c.testPlanXml) + RT_EXT)
    if File::exists?( tr )
      File.delete( tr )
    end
  end
  def run
    @@ctx.testConfigs.each do |c|
      clean(c)
    end
  end 
end

# User model class of bats-qa-reports
class User < ActiveRecord::Base
  attr_accessible :name, :password, :email, :password_confirmation, :remember_me
end

# Import test result to bats-qa-reports
class TestImporter < Worker
  # add the user to db if user doesn't exist
  def addUser
    str = "User.create! :password => '"+@@ctx.userPassword+"',"
    str += ":email => '"+@@ctx.userEmail+"',:name => '"+@@ctx.userName+"'"
    str += " unless User.exists? :email => '"+@@ctx.userEmail+"'"
    File.open(File.join( @@ctx.batsRoot, 'db','seeds.rb' ),'w') {|f| f.write(str) }
    cstr = "cd " + @@ctx.batsRoot + "\n"
    cstr += "rake db:seed\n"
    system(cstr)
  end
  # import test result to bats-qa-reports
  def import(c)
    tr = c.testPlanXml + RT_EXT
    print("Import #{tr} ... ")
    addUser
    ur = User.find_by_email(@@ctx.userEmail)
    file_path = File.join(tr)
    if File::exists?( file_path )
      require 'meego_test_session'
      require 'meego_test_set'
      require 'meego_test_case'
      # prepare a dir for xml
      rdir = File.join(PUB_DIR,REP_DIR)
      if !File.exists?( rdir )
        Dir.mkdir(PUB_DIR)
        Dir.mkdir(rdir)
      end
      session = MeegoTestSession.new(
        "uploaded_files" => [file_path],
        "testtype" => c.testType,
        "hwproduct" => c.testVersion,
        "target" => c.testTarget,
        "release_version" => APP_NAME
      )
      session.generate_defaults!
      session.tested_at = Time.now
      session.author = ur
      session.editor = ur
      session.published = true
      session.save
      c.testSession = session
    end
  end
  def run
    # prepare db access env
    $LOAD_PATH.unshift( File.join( @@ctx.batsRoot, 'lib' ) )
    $LOAD_PATH.unshift( File.join( @@ctx.batsRoot , 'app' , 'models' ) )
    dbconfig = YAML::load(File.open(File.join(@@ctx.batsRoot, "config","database.yml")))    
    if dbconfig["development"]["adapter"] == "sqlite3"
      dbconfig["development"]["database"] = File.join(@@ctx.batsRoot , dbconfig["development"]["database"])      
    end
    ActiveRecord::Base.establish_connection(dbconfig["development"])
    @@ctx.testConfigs.each do |c|
      import(c)
    end
  end
end

# Invoke testrunner-lite to generate test results
class TestInvoker < Worker
  def run
    @@ctx.testConfigs.each do |c|
      print("Run tests #{c.testPlanXml}... ")
      tr = c.testPlanXml + RT_EXT
      str = "cd " + c.testPlanDir + "\n"
      str += "testrunner-lite -f " + c.testPlanXml 
      str += " -o "+ tr + "\n"
      print(str)
      system(str)
    end
  end
end

# Revise the config's value
class ConfigReviser < Worker
  def reviseDir(dir)
    dir = File.expand_path(dir)
  end
  def reviseName(s)
    s = s.gsub('.','_')
    s = s.gsub('-','_')
  end
  def run
    @@ctx.batsRoot = reviseDir(@@ctx.batsRoot)
    @@ctx.testConfigs.each do |c|
      c.testPlanDir = reviseDir(c.testPlanDir)
      c.testPlanXml = reviseDir(c.testPlanXml)
      c.testTarget = reviseName(c.testTarget)
      c.testVersion = reviseName(c.testVersion)
      c.testType = reviseName(c.testType)
    end
    print(@@ctx)
  end
end

# Parse the test flat config
class FlatTestConfigParser < Worker
  def run
    file = File.new(@@ctx.config, "r")
    keyValue = Array.new(2)
    while (line = file.gets)
      keyValue = line.split("=")
      if keyValue.nitems != 2
        next
      end
      if keyValue[0].strip.upcase == "TEST_PLAN_DIR"
        tCtx = TestContext.new
        tCtx.testPlanDir = keyValue[1].strip
      elsif keyValue[0].strip.upcase == "TEST_PLAN_XML"
        tCtx.testPlanXml = keyValue[1].strip
      elsif keyValue[0].strip.upcase == "TEST_TARGET"
        tCtx.testTarget = keyValue[1].strip
      elsif keyValue[0].strip.upcase == "TEST_VERSION"
        tCtx.testVersion = keyValue[1].strip
      elsif keyValue[0].strip.upcase == "TEST_TYPE"
        tCtx.testType = keyValue[1].strip
        @@ctx.addTestConfig(tCtx)
      end
    end
    file.close
  end
end

# Parse the test xml config
class XmlTestConfigParser < Worker
  def run
    file = File.new(@@ctx.config, "r")
    doc = Document.new(file)  
    doc.elements.each("test_plans/test_plan") do |tp|
      tCtx = TestContext.new
      tCtx.testPlanDir = tp.elements["test_plan_dir"].text
      tCtx.testPlanXml = tp.elements["test_plan_xml"].text
      tCtx.testTarget = tp.elements["test_target"].text
      tCtx.testVersion = tp.elements["test_version"].text
      tCtx.testType = tp.elements["test_type"].text
      @@ctx.addTestConfig(tCtx)
    end 
    file.close
  end
end

# Parse the bats config
class ConfigParser < Worker
  def loadGlobalConfig
    if File::exists?( GLB_CONF )
      file = File.new(GLB_CONF, "r")
      keyValue = Array.new(2)
      while (line = file.gets)
        keyValue = line.split("=")
        if keyValue.nitems != 2
          next
        end
        if keyValue[0].strip.upcase == "USER_NAME"
          @@ctx.userName = keyValue[1].strip
        elsif keyValue[0].strip.upcase == "USER_EMAIL"
          @@ctx.userEmail = keyValue[1].strip
        elsif keyValue[0].strip.upcase == "USER_PASSWORD"
          @@ctx.userPassword = keyValue[1].strip
        elsif keyValue[0].strip.upcase == "BATS_QA_REPORTS_ROOT"
          @@ctx.batsRoot = keyValue[1].strip
        end
      end	
    end
    # load web server's ip address from local db config
    dbconfig = YAML::load(File.open(File.join(@@ctx.batsRoot, "config","database.yml")))    
    if dbconfig["development"]["adapter"] == "mysql"
      @@ctx.serverAddress = dbconfig["development"]["host"] 
    end
  end
  def run
    loadGlobalConfig
    if( File.extname(@@ctx.config) == ".xml" )
      XmlTestConfigParser.new.run
    else
      FlatTestConfigParser.new.run
    end
  end
end

# Main class of bats-testrunner
class BATSTestRunner < Worker
  VERSION = '0.0.1'
  attr_reader :options
  def initialize(arguments, stdin)
    @arguments = arguments
    @stdin = stdin
    # Set defaults
    @options = OpenStruct.new
    @options.verbose = false
    @options.config = 'bats.cnf'
  end
  def run
    if parsed_options?
      puts "Start at #{DateTime.now}\n\n" if @options.verbose
      output_options if @options.verbose # [Optional]
      @@ctx.verbose = @options.verbose
      @@ctx.config = @options.config
      ConfigParser.new.run
      ConfigReviser.new.run
      TestInvoker.new.run
      TestImporter.new.run
      TestReporter.new.run
      TestEnvCleaner.new.run
      puts "\nFinished at #{DateTime.now}" if @options.verbose
    else
      output_help
    end
  end
  # parse options and run all the working classes
  def parsed_options?
    # Specify options
    opts = OptionParser.new 
    opts.on('-v', '--version')    { output_version ; exit 0 }
    opts.on('-h', '--help')       { output_help }
    opts.on('-V', '--verbose')    { @options.verbose = true }  
    opts.on('-c cnf', '--config cnf') do |cnf|
      options.config = cnf
    end
    opts.parse!(@arguments) rescue return false
    true      
  end
  def output_options
    puts "Options:\n"
    @options.marshal_dump.each do |name, val|        
      puts "  #{name} = #{val}"
    end
  end
  def output_help
    output_version
    RDoc::usage()
  end
  def output_version
    puts "#{File.basename(__FILE__)} version #{VERSION}"
  end
end

# Create and run the application
runner = BATSTestRunner.new(ARGV, STDIN)
runner.run
