# Copyright (c) 2021-2022 Huawei Device Co., Ltd.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

module TestRunner
  ERROR_NODATA = 86
  ERROR_CANNOT_CREATE_PROCESS = 87
  ERROR_TIMEOUT = 88

  @@plugins = []
  @@runner_class = nil
  @@target = 'Host'

  def self.plugins
    @@plugins
  end

  def self.runner_class=(value)
    @@runner_class = value
  end

  def self.target
    @@target
  end

  def self.target=(value)
    @@target = value
  end

  class Plugin
    def name
      raise NotImplementedError, "#{self.class} does not implement name()."
    end

    def add_options(opts, options)
      false
    end

    def process(options)
      false
    end
  end

  def self.log(level, *args)
    puts args if level <= $VERBOSITY
  end

  def self.print_exception(exception)
    puts "Exception: exception class   : #{exception.class}"
    puts "           exception message : #{exception.message}"
    exception.backtrace.each do |t|
      puts "                       trace : #{t}"
    end
  end

  def self.split_separated_by_colon(string)
    string.split(':')
          .drop(1)
          .flat_map { |s| s.split(',').map(&:strip) }
  end

  def self.create_runner(file, id, reporter_factory, root_dir, report_dir)
    @@runner_class.new(
          file, id, reporter_factory, root_dir, report_dir)
  end

  class CommandRunner
    def initialize(command, reporter)
      @command = command
      @reporter = reporter
    end

    def dump_output(t, output_err, output)
      start = Time.now

      while (Time.now - start) <= $TIMEOUT
        Kernel.select([output_err], nil, nil, 1)
        begin
          output << output_err.read_nonblock(2048)
        rescue IO::WaitReadable
        rescue EOFError
          return true # finished normally
        rescue StandardError => e
          output << "\nUnexpected exception when reading from pipe: #{e.class.name}, #{e.message}"
          break
        end
      end
      !t.alive? # finished on timeout
    end

    def start_process_timeout
      input, output_err, t = if $enable_core
                               Open3.popen2e(@command, pgroup: true)
                             else
                               Open3.popen2e(@command, pgroup: true, rlimit_core: 0)
                             end
      pid = t[:pid]
      output = ''
      finished = dump_output t, output_err, output

      input.close
      output_err.close

      unless finished
        output << "\nProcess hangs for #{$TIMEOUT}s '#{@command}'" \
                  "\nKilling pid:#{pid}"
        begin
          Process.kill('-TERM', Process.getpgid(pid))
        rescue Errno::ESRCH
        rescue Exception => e
          TestRunner.print_exception e
        end
        return output.strip, ERROR_TIMEOUT, false
      end

      if t.value.exited?
        exitstatus = t.value.exitstatus
      elsif t.value.signaled?
        output << t.value.inspect
        exitstatus = 128 + t.value.termsig
      else
        output << t.value.inspect
        exitstatus = 254 # fallback exit code for an unexpected abnormal exit
      end

      [output.strip, exitstatus, t.value.coredump?]
    rescue Errno::ENOENT => e
      ["Failed to start #{@command} - no executable", ERROR_CANNOT_CREATE_PROCESS, false]
    rescue StandardError
      ["Failed to start #{@command}", ERROR_CANNOT_CREATE_PROCESS, false]
    end

    def run_cmd
      @reporter.log_start_command @command
      start_process_timeout
    end
  end # Runner
end # module