#!/usr/bin/env ruby
#
# A tool for running the GS2 tests for every new revision and
# uploading formatted results to the central repository.
#
# This is free software released under the MIT licence.
# Written by:
#             Edmund Highcock (edmund.highcock@users.sourceforge.net)
#
# To get help: 
#  $ ./gk_test_utility man

require 'getoptlong'
require 'psych'
require 'haml'

module CommandLineFlunky

  STARTUP_MESSAGE = "" #"\n------Gyrokinetics Test Utility-----"

  MANUAL_HEADER = <<EOF

-------------Gyrokinetics Test Utility Manual---------------

  Written by Edmund Highcock (2014)

NAME

  gk_test_utility


SYNOPSIS

  gk_test_utility <command> [arguments] [options]


DESCRIPTION

  A utility for automatically downloading new commits of GS2/Trinity, running tests and upload results to a central repository.

EXAMPLES

  $ gk_test_utility upload_test -s <system>
EOF

  COMMANDS_WITH_HELP = [
    ['upload_test', 'up', 0,  'Run the tests within the current folder and upload the result. To be run from within the GS2 source folder.', [], [:s, :c, :r]],
    ['run_automated_testing', 'run', 0,  'Periodically check for new revisions. If new revisions have been checked in, download them, test them and upload the results to the repository.', [], [:s, :c, :r]],
    #['display_summary_graph', 'disp', 1,  'Display a summary graph of the file using gnuplot.', ['inputfile',], [:f]],
    #['merge', 'mg', 2,  'Merge data from two input files to create a single output file. ', ['inputfile', 'outputfile(s)'], [:f, :t, :m]],
    #['write_summary_graph', 'wsg', 2,  'Write a summary graph of the file to disk using gnuplot. The file format is determined by the extension of the graph file', ['inputfile', 'graph file'], [:f,:w]],

  ]



  COMMAND_LINE_FLAGS_WITH_HELP = [
    #['--boolean', '-b', GetoptLong::NO_ARGUMENT, 'A boolean option'],
    #['--formats', '-f', GetoptLong::REQUIRED_ARGUMENT, "A list of formats pertaining to the various input and output files (in the order which they appear), separated by commas. If they are all the same, only one value may be given. If a value is left empty (i.e. there are two commas in a row) then the previous value will be used. Currently supported formats are #{SUPPORTED_FORMATS.inspect}. "],
    #['--merge-sources', '-m', GetoptLong::REQUIRED_ARGUMENT, "A list of which bits of information should come from which input file during a merge. Currently supported merge sources are #{SUPPORTED_MERGE_SOURCES.inspect} "],
    ['--code', '-c', GetoptLong::REQUIRED_ARGUMENT, "The name of the code (i.e. 'gs2' or 'trinity')."],
    ['--delete', '-d', GetoptLong::NO_ARGUMENT, "Delete existing test data and start again."],
    ['--folder', '-f', GetoptLong::REQUIRED_ARGUMENT, "The folder in which to download and test the code."],
    ['--results-url', '-r', GetoptLong::REQUIRED_ARGUMENT, "The URL of the repository where the results should be uploaded to."],
    ['--system', '-s', GetoptLong::REQUIRED_ARGUMENT, "The name of the system."],
    ['--url', '-u', GetoptLong::REQUIRED_ARGUMENT, "The URL of the code repository to be tested."],

    ]

  LONG_COMMAND_LINE_OPTIONS = [
  #["--no-short-form", "", GetoptLong::NO_ARGUMENT, %[This boolean option has no short form]],
  ]

  # specifying flag sets a bool to be true

  CLF_BOOLS = [:d]

  CLF_INVERSE_BOOLS = [] # specifying flag sets a bool to be false

  PROJECT_NAME = 'gk_test_utility'

  def self.method_missing(method, *args)
#     p method, args
    GkTestUtility.send(method, *args)
  end

  #def self.setup(copts)
    #CommandLineFlunkyTestUtility.setup(copts)
  #end

  SCRIPT_FILE = __FILE__
end

class GkTestUtility
  class << self
    # This function gets called before every command
    # and allows arbitrary manipulation of the command
    # options (copts) hash
    def setup(copts)
      # None neededed
    end
    def verbosity
      2
    end
    def check_copts(copts)
      unless ['gs2', 'trinity'].include? copts[:c]
        raise "Unknown code: #{copts[:c].inspect}"
      end
      #systems = `./build_#{copts[:c]} ls`.split(/\s+/)
      #unless systems.include? copts[:s]
        #puts "This system: #{copts[:s].inspect} is not supported by the automated system"
        #raise
      #end
    end
    def check_folder(command, copts)
      unless Dir.entries(Dir.pwd).include? copts[:c]+'.f90'
        raise "The #{command} command must be run from within the top level of source repository"
      end
    end
    def fetch_utility(copts)
      GkTestUtility.new(copts)
    end
    def run_automated_testing(copts)
      raise "Please specify a testing folder using the -f flag. This should typically be an empty folder on the first run." unless copts[:f]
      raise "Please specify a code repository using the -u flag." unless copts[:u]
      check_copts(copts)
      utility = fetch_utility(copts)
      utility.run_automated_testing
    end
    def upload_test(copts)
      check_folder("upload_test", copts)
      check_copts(copts)
      utility = fetch_utility(copts)
      utility.add_test_result(TestResult.test(copts, Dir.pwd))
      utility.upload_new_data
    end
  end

  ###################
  # Instance Methods 
  ###################

  attr_reader :code
  attr_reader :results
  attr_reader :url
  def initialize(copts)
    @copts=copts
    @code=copts[:c]
    @url = copts[:u]
    retrieve_existing_data
  end
  def add_test_result(test_result)
    @existing_data[test_result.build_identifier] = test_result.data
    @results.push test_result
    @results.each_with_index{|r,i| r.id=i}
  end
  def retrieve_existing_data
    FileUtils.makedirs('.tmp_repository')
    Dir.chdir('.tmp_repository') do
      raise "ERROR: please specify repository using the -r flag" unless @copts[:r]
      unless (not @copts[:d]) and system "scp #{@copts[:r]}/test_data.yaml ."
        File.open('test_data.yaml', 'w'){|f| f.puts Psych.dump(Hash.new)}
      end
      @existing_data = Psych.load(File.read('test_data.yaml'))
    end
    @results = @existing_data.values.map{|hsh| TestResult.new(hsh)}
    @results.each_with_index{|r,i| r.id=i}
  end
  def get_revisions
    Dir.chdir(@copts[:f]) do
      head_revision = Revision.new("HEAD", @copts[:u], '')
      head_revision.checkout
      Dir.chdir("HEAD") do
        system "svn log > log.txt" 
        log = File.read("log.txt")
        @revisions = log.split(/^-{20,}$/).map{|s| 
          Revision.new(s.scan(/^r(\d+)/).flatten[0], @copts[:u], s)
        }.find_all{|r|
          case @copts[:c]
          when 'gs2'
            r.revision.to_i >= 3205
            #r.revision.to_i < 3201
          when 'trinity'
            r.revision.to_i >= 3211
          else
            raise "unknown code #{@copts[:c]}"
          end
        }
        
        #p @revisions
      end
      #@revisions.push head_revision
    end 
  end
  def untested_revisions
    tested = @results.map{|r| r.svn_revision}
    untested_revisions = @revisions.find_all{|rev|
      not tested.include? rev.revision.to_i
    }
    untested_revisions
  end
  def run_automated_testing
    get_revisions
    untested = untested_revisions
    p "Testing Revisions", untested.map{|r| r.revision}
    untested.each do |r|
      Dir.chdir @copts[:f] do
        r.checkout
        Dir.chdir r.revision do
          #puts 'Dir.pwd', Dir.pwd
          add_test_result(TestResult.test(@copts, r, Dir.pwd))
        end
      end
    end
    upload_new_data
  end
  def sorted_results
    @results.sort_by{|r| -r.svn_revision}
  end
  def upload_new_data
    FileUtils.makedirs('.tmp_repository')
    dir = File.dirname(File.expand_path(__FILE__))
    engine = Haml::Engine.new(File.read(dir+'/results_list.haml'))
    Dir.chdir('.tmp_repository') do
      raise "ERROR: please specify repository using the -r flag" unless @copts[:r]
      File.open('test_data.yaml', 'w'){|f| f.puts Psych.dump(@existing_data)}
      File.open('index.html', 'w'){|f| f.puts engine.render(self)}
      system "scp test_data.yaml #{@copts[:r]}/."
      system "scp index.html #{@copts[:r]}/."
    end
  end
end

class Revision
  attr_reader :revision
  attr_reader :message 
  def initialize(revision, url, message)
    @revision = revision
    @url = url
    @message = message
  end
  def checkout
    if FileTest.exist? @revision
      #puts "svn up -r #@revision"
      Dir.chdir @revision do
        raise "svn update #@revision failed" unless  system "svn up -r #@revision"
      end
    else
      raise "svn failed" unless system "svn co -r #@revision #@url #@revision"
    end
  end
end

class TestResult
  attr_reader :failed
  attr_reader :overall_result
  attr_reader :metadata
  attr_reader :build_identifier
  attr_reader :svn_commiter
  attr_reader :svn_revision
  attr_reader :svn_log_message
  attr_reader :results
  attr_reader :error_message
  attr_reader :build_succeeded
  attr_reader :output_message
  attr_accessor :id
  attr_reader :data
  def initialize(data)
    @build_identifier = data[:build_identifier]
    @results = data[:results]
    @metadata = data[:metadata]
    @svn_commiter = data[:svn_commiter]
    @svn_revision = data[:svn_revision]
    @svn_log_message = data[:svn_log_message]
    @error_message = data[:error_message]
    @build_succeeded = data[:build_succeeded]
    @output_message = data[:output_message]
    @data = data
    if not @build_succeeded
      @overall_result = 'FAILED'
      @failed = true
    else
      if @results =~ /FAILED/
        @overall_result = 'FAILED'
        @failed = true
      else
        @overall_result = 'passed'
        @failed = false
      end
    end
  end
  #def svn_revision
    #build_identifier.scan(/^(\d+)/).flatten[0].to_i
  #end
  def self.test(copts, revision, folder)
    build_succeeded = system "./build_#{copts[:c]} -s #{copts[:s]} tests > .tmp_output 2> .tmp_error"
    results = File.read('.tmp_output').sub(/\A.*(^=====.*Test Results.*)\Z/m, '\1')
    error_message = File.read('.tmp_error')
    output_message = File.read('.tmp_output')
    FileUtils.rm('.tmp_output')
    FileUtils.rm('.tmp_error')
    #puts results
    case copts[:c]
    when 'gs2'
      mdata = `ncdump linear_tests/cyclone_itg/cyclone_itg_low_res.cdf -c`.sub(/\A.*(:svn_revision.*)data.*\Z/m, '\1').gsub(/^\s*:/, '')
    when 'trinity'
      mdata = ''
    end
    build_identifier = revision.revision + '.' + copts[:s] 
    #puts mdata, build_identifier
    svn_commiter = `svn info`.scan(/Changed Author:\s+(.*)/).flatten[0]
    results_hash = {
        :build_identifier => build_identifier,
        :svn_commiter => svn_commiter,
        :svn_revision => revision.revision.to_i,
        :svn_log_message => revision.message,
        :results => results, 
        :metadata => mdata,
        :build_succeeded => build_succeeded,
        :error_message => error_message,
        :output_message => output_message,
    }
    #pp results_hash
    return new(results_hash)
  end
end

class ResultsList
  attr_reader :code
  attr_reader :results
  def initialize(code, results)
    @code = code
    @results = results
  end
end


######################################
# This must be at the end of the file
#
require 'command-line-flunky'
###############################
#

begin
  CommandLineFlunky.run_script
#rescue => error
  #puts error
  #exit 1
end
