require "shellwords"
require "rbconfig"

begin
  require "rake/tasklib"
rescue LoadError => e
  warn e.message
  return
end

module Minitest # :nodoc:

  ##
  # Minitest::TestTask is a rake helper that generates several rake
  # tasks under the main test task's name-space.
  #
  #   task <name>      :: the main test task
  #   task <name>:cmd  :: prints the command to use
  #   task <name>:deps :: runs each test file by itself to find dependency errors
  #   task <name>:slow :: runs the tests and reports the slowest 25 tests.
  #
  # Examples:
  #
  #   Minitest::TestTask.create
  #
  # The most basic and default setup.
  #
  #   Minitest::TestTask.create :my_tests
  #
  # The most basic/default setup, but with a custom name
  #
  #   Minitest::TestTask.create :unit do |t|
  #     t.test_globs = ["test/unit/**/*_test.rb"]
  #     t.warning = false
  #   end
  #
  # Customize the name and only run unit tests.
  #
  # NOTE: To hook this task up to the default, make it a dependency:
  #
  #   task default: :unit

  class TestTask < Rake::TaskLib
    WINDOWS = RbConfig::CONFIG["host_os"] =~ /mswin|mingw/ # :nodoc:

    ##
    # Create several test-oriented tasks under +name+ (default:
    # "test"). Takes an optional block to customize variables.
    # Examples:
    #
    #   Minitest::TestTask.create # named "test", all defaults
    #
    #   Minitest::TestTask.create do |t|
    #     t.warning = false # my tests are noisy
    #     t.framework = %(require_relative "./test/helper.rb")
    #   end

    def self.create name = :test, &block
      task = new name
      task.instance_eval(&block) if block
      task.process_env
      task.define
      task
    end

    ##
    # Extra arguments to pass to the tests. Defaults empty but gets
    # populated by a number of enviroment variables:
    #
    # N (-n flag) :: a string or regexp of tests to run.
    # X (-e flag) :: a string or regexp of tests to exclude.
    # A (arg)     :: quick way to inject an arbitrary argument (eg A=--help).
    #
    # See #process_env

    attr_accessor :extra_args

    ##
    # The code to load the framework. Defaults to requiring
    # minitest/autorun.
    #
    # If you have a test helper file that requires minitest/autorun
    # and anything else your project standardizes on, then you'll
    # probably want to change this to:
    #
    #   Minitest::TestTask.create do |t|
    #     t.framework = %(require_relative "./test/helper.rb")
    #   end
    #
    # or something similar. NOTE: if you do this, then helper must
    # require "minitest/autorun" at the top to start the tests.

    attr_accessor :framework

    ##
    # Extra library directories to include. Defaults to %w[lib test
    # .]. Also uses $MT_LIB_EXTRAS allowing you to dynamically
    # override/inject directories for custom runs.

    attr_accessor :libs

    ##
    # The name of the task and base name for the other tasks generated.

    attr_accessor :name

    ##
    # File globs to find test files. Defaults to something sensible to
    # find test files under the test directory.

    attr_accessor :test_globs

    ##
    # Turn on ruby warnings (-w flag). Defaults to true.

    attr_accessor :warning

    ##
    # Optional: Additional ruby to run before the test framework is loaded.
    # Example:
    #
    #   Minitest::TestTask.create "test:cov" do |t|
    #     t.test_prelude = %(require "simplecov")
    #   end

    attr_accessor :test_prelude

    ##
    # Print out commands as they run. Defaults to Rake's +trace+ (-t
    # flag) option.

    attr_accessor :verbose

    ##
    # Use TestTask.create instead.

    def initialize name = :test # :nodoc:
      self.extra_args   = []
      self.framework    = %(require "minitest/autorun")
      self.libs         = %w[lib test .]
      self.name         = name
      self.test_globs   = ["test/**/test_*.rb",
                           "test/**/*_test.rb"]
      self.test_prelude = nil
      self.verbose      = Rake.application.options.trace || Rake.verbose == true
      self.warning      = true
    end

    ##
    # Extract variables from the environment and convert them to
    # command line arguments. See #extra_args.
    #
    # Environment Variables:
    #
    # MT_LIB_EXTRAS :: Extra libs to dynamically override/inject for custom runs.
    # N             :: Tests to run (string or /regexp/).
    # X             :: Tests to exclude (string or /regexp/).
    # A             :: Any extra arguments. Honors shell quoting.
    #
    # Deprecated:
    #
    # TESTOPTS      :: For argument passing, use +A+.
    # N             :: For parallel testing, use +MT_CPU+.
    # FILTER        :: Same as +TESTOPTS+.

    def process_env
      warn "TESTOPTS is deprecated in Minitest::TestTask. Use A instead" if
        ENV["TESTOPTS"]
      warn "FILTER is deprecated in Minitest::TestTask. Use A instead" if
        ENV["FILTER"]

      lib_extras = (ENV["MT_LIB_EXTRAS"] || "").split File::PATH_SEPARATOR
      self.libs[0, 0] = lib_extras

      extra_args << "-i" << ENV["N"]                      if ENV["N"]
      extra_args << "-i" << ENV["I"]                      if ENV["I"]
      extra_args << "-x" << ENV["X"]                      if ENV["X"]
      extra_args << "-x" << ENV["E"]                      if ENV["E"]
      extra_args.concat Shellwords.split(ENV["TESTOPTS"]) if ENV["TESTOPTS"]
      extra_args.concat Shellwords.split(ENV["FILTER"])   if ENV["FILTER"]
      extra_args.concat Shellwords.split(ENV["A"])        if ENV["A"]

      # TODO? RUBY_DEBUG = ENV["RUBY_DEBUG"]
      # TODO? ENV["RUBY_FLAGS"]

      extra_args.compact!
    end

    def define # :nodoc:
      desc "Run the test suite. Use I, X, and A to add flags/args."
      task name do
        ruby make_test_cmd, verbose: verbose
      end

      desc "Run the test suite, filtering for 'FU' in name (focused units?)."
      task "#{name}:fu" do
        ruby make_test_cmd(include:"/FU/"), verbose: verbose
      end

      desc "Print out the test command. Good for profiling and other tools."
      task "#{name}:cmd" do
        puts "ruby #{make_test_cmd}"
      end

      desc "Show which test files fail when run in isolation."
      task "#{name}:isolated" do
        tests = Dir[*self.test_globs].uniq

        # 3 seems to be the magic number... (tho not by that much)
        bad, good, n = {}, [], (ENV.delete("K") || 3).to_i
        file = ENV.delete "F"
        times = {}

        tt0 = Time.now

        n.threads_do tests.sort do |path|
          t0 = Time.now
          output = `#{Gem.ruby} #{make_test_cmd path:path} 2>&1`
          t1 = Time.now - t0

          times[path] = t1

          if $?.success?
            $stderr.print "."
            good << path
          else
            $stderr.print "x"
            bad[path] = output
          end
        end

        puts "done"
        puts "Ran in %.2f seconds" % [ Time.now - tt0 ]

        if file then
          require "json"
          File.open file, "w" do |io|
            io.puts JSON.pretty_generate times
          end
        end

        unless good.empty?
          puts
          puts "# Good tests:"
          puts
          good.sort.each do |path|
            puts "%.2fs: %s" % [times[path], path]
          end
        end

        unless bad.empty?
          puts
          puts "# Bad tests:"
          puts
          bad.keys.sort.each do |path|
            puts "%.2fs: %s" % [times[path], path]
          end
          puts
          puts "# Bad Test Output:"
          puts
          bad.sort.each do |path, output|
            puts
            puts "# #{path}:"
            puts output
          end
          exit 1
        end
      end

      task "#{name}:deps" => "#{name}:isolated" # now just an alias

      desc "Run the test suite and report the slowest 25 tests."
      task "#{name}:slow" do
        sh ["rake #{name} A=-v",
            "egrep '#test_.* s = .'",
            "sort -n -k2 -t=",
            "tail -25"].join " | "
      end
    end

    ##
    # Generate the test command-line.

    def make_test_cmd **option
      globs = option[:path] || test_globs

      tests = []
      tests.concat Dir[*globs].sort.shuffle # TODO: SEED -> srand first?
      tests.map! { |f| %(require "#{f}") }

      runner = []
      runner << test_prelude if test_prelude
      runner << framework
      runner.concat tests
      runner = runner.join "; "

      extra_args << "-i" << option[:include] if option[:include]

      args  = []
      args << "-I#{libs.join File::PATH_SEPARATOR}" unless libs.empty?
      args << "-w" if warning
      args << "-e"
      args << "'#{runner}'"
      args << "--"
      args << extra_args.map(&:shellescape)

      args.join " "
    end
  end
end

class Work < Queue # :nodoc:
  def initialize jobs # :nodoc:
    super
    close
  end
end

class Integer # :nodoc:
  def threads_do jobs # :nodoc:
    q = Work.new jobs

    Array.new(self) {
      Thread.new do
        while job = q.pop # go until quit value
          yield job
        end
      end
    }.each(&:join)
  end
end
