require File.expand_path(File.dirname(__FILE__) + "/../test_helper") 

class TestUnitBeforeAfter < Test::Unit::TestCase

  #
  # after_teardown
  #

  USING_RUBY_1_9 = RUBY_VERSION =~ /^1\.9/

  it "adds TestCase.after_teardown hook for appending post-teardown actions" do
    write_and_run_test :use_after_teardown => true

    see_in_order "Loaded suite",
      "THE SETUP",
      "A TEST",
      "THE TEARDOWN",
      "1st after_teardown",
      "2nd after_teardown",
      "Finished in"
    see_results :tests => 1, :assertions => 0, :failures => 0, :errors => 0
  end

  should "execute all after_teardowns, even if the main teardown flunks" do
    write_and_run_test :use_after_teardown => true, :flunk_in_teardown => true
    if USING_RUBY_1_9
      see_in_order "Loaded suite",
        "THE SETUP",
        "A TEST",
        "1st after_teardown",
        "2nd after_teardown",
        "Finished in",
        "1) Failure:",
        "test_something(MyExampleTest) [_test_file_temp.rb:20]:",
        "FLUNK IN TEARDOWN"
    else
      see_in_order "Loaded suite",
        "THE SETUP",
        "A TEST",
        "F",
        "1st after_teardown",
        "2nd after_teardown",
        "Finished in",
        "1) Failure:",
        "test_something(MyExampleTest) [_test_file_temp.rb:20]:",
        "FLUNK IN TEARDOWN"
    end
    see_results :tests => 1, :assertions => 1, :failures => 1, :errors => 0
  end

  should "execute all after_teardowns, even if the main teardown explodes" do
    write_and_run_test :use_after_teardown => true, :raise_in_teardown => true
    see_in_order "Loaded suite",
      "THE SETUP",
      "A TEST",
      "E",
      "1st after_teardown",
      "2nd after_teardown",
      "Finished in",
      "RuntimeError: ERROR IN TEARDOWN"
    see_results :tests => 1, :assertions => 0, :failures => 0, :errors => 1
  end

  should "execute all after_teardowns, even if some of them flunk" do
    write_and_run_test :use_after_teardown => true, :flunk_in_after_teardown => true
    see_in_order "Loaded suite",
      "THE SETUP",
      "A TEST",
      "THE TEARDOWN",
      "1st after_teardown",
      "2nd after_teardown",
      "Finished in",
      "1) Failure:",
      "test_something(MyExampleTest) [_test_file_temp.rb:7]:",
      "Flunk in first after_teardown",
      "2) Failure:",
      "test_something(MyExampleTest) [_test_file_temp.rb:10]:",
      "Flunk in second after_teardown"
    see_results :tests => 1, :assertions => 2, :failures => 2, :errors => 0
  end

  should "execute all after_teardowns, even if some of them explode" do
    write_and_run_test :use_after_teardown => true, :raise_in_after_teardown => true
    if USING_RUBY_1_9
      see_in_order "Loaded suite",
        "THE SETUP",
        "A TEST",
        "THE TEARDOWN",
        "1st after_teardown",
        "2nd after_teardown",
        "Finished in",
        "RuntimeError: Error in first after_teardown",
        "RuntimeError: Error in second after_teardown"
    else
      see_in_order "Loaded suite",
        "THE SETUP",
        "A TEST",
        "THE TEARDOWN",
        "1st after_teardown",
        "E",
        "2nd after_teardown",
        "Finished in",
        "RuntimeError: Error in first after_teardown",
        "RuntimeError: Error in second after_teardown"
    end
    see_results :tests => 1, :assertions => 0, :failures => 0, :errors => 2
  end

  it "will run after_teardowns in the absence of a regular teardown" do
    write_and_run_test :omit_teardown => true, :use_after_teardown => true
    see_in_order "Loaded suite",
      "THE SETUP",
      "A TEST",
      "1st after_teardown",
      "2nd after_teardown",
      "Finished in"
    see_results :tests => 1, :assertions => 0, :failures => 0, :errors => 0
  end

  should "not interfere with normal test writing" do
    write_and_run_test
    see_in_order "Loaded suite",
      "THE SETUP",
      "A TEST",
      "THE TEARDOWN",
      "Finished in"
    see_results :tests => 1, :assertions => 0, :failures => 0, :errors => 0
  end

  it "provides a cleaned-up backtrace" do
    write_and_run_test :with_failure => true
    if USING_RUBY_1_9
      see_in_order "Loaded suite",
        "THE SETUP",
        "A FAILING TEST",
        "THE TEARDOWN",
        "F\n", 
        "Finished in",
        "1) Failure:",
        "test_something(MyExampleTest) [_test_file_temp.rb:17]:",
        "Instrumented failure"
    else
      see_in_order "Loaded suite",
        "THE SETUP",
        "A FAILING TEST",
        "F", 
        "THE TEARDOWN",
        "Finished in",
        "1) Failure:",
        "test_something(MyExampleTest) [_test_file_temp.rb:17]:",
        "Instrumented failure.",
        "<false> is not true."
    end
    see_results :tests => 1, :assertions => 1, :failures => 1, :errors => 0
  end

  it "provides a cleaned-up backtrace, but not TOO cleaned up" do
    write_and_run_test :with_failure => true, :use_helpers => true
    if USING_RUBY_1_9
      see_in_order "Loaded suite",
        "THE SETUP",
        "A FAILING TEST",
        "F", "THE TEARDOWN",
        "Finished in",
        "1) Failure:",
        "test_something(MyExampleTest)",
        "Instrumented failure"
    else
      see_in_order "Loaded suite",
        "THE SETUP",
        "A FAILING TEST",
        "F", "THE TEARDOWN",
        "Finished in",
        "1) Failure:",
        "test_something(MyExampleTest)\n",
        "[_test_file_temp.rb:25:in `tripwire'",
        "_test_file_temp.rb:21:in `my_helper'",
        "_test_file_temp.rb:17:in `test_something']:",
        "Instrumented failure.",
        "<false> is not true."
    end
    see_results :tests => 1, :assertions => 1, :failures => 1, :errors => 0
  end

  should "not interfere with passthrough exception types" do
    if is_modern_test_unit?
      write_and_run_test :raise_nasty_in_test => true
      see_in_no_particular_order "Loaded suite", 
        "THE TEARDOWN",
        "_test_file_temp.rb:16:in `test_something': NASTY ERROR (NoMemoryError)"
      see_no_results
    end
  end

  #
  # before_setup
  #

  it "adds TestCase.before_setup hook for prepending pre-setup actions" do
    write_and_run_test :use_before_setup => true
    see_in_order "Loaded suite",
      "3rd before_setup",
      "2nd before_setup",
      "1st before_setup",
      "THE SETUP",
      "A TEST",
      "THE TEARDOWN",
      "Finished in"
    see_results :tests => 1, :assertions => 0, :failures => 0, :errors => 0
  end

  should "stop executing the test on the first failure withing a before_setup action" do 
    write_and_run_test :use_before_setup => true, :flunk_in_before_setup => true
    see_in_order "Loaded suite",
      "3rd before_setup",
      "2nd before_setup",
      "THE TEARDOWN",
      "1) Failure:",
      "test_something(MyExampleTest) [_test_file_temp.rb:10]:",
      "Flunk in 2nd before_setup"
    see_results :tests => 1, :assertions => 1, :failures => 1, :errors => 0
  end

  should "stop executing the test on the first error within a before_setup action" do
    write_and_run_test :use_before_setup => true, :raise_in_before_setup => true
    see_in_order "Loaded suite",
      "3rd before_setup",
      "2nd before_setup",
      "THE TEARDOWN",
      "Finished in",
      "test_something(MyExampleTest):",
      "RuntimeError: Error in 2nd before_setup",
      "_test_file_temp.rb:10",
      "/hardmock/lib/test_unit_before_after.rb:"
    see_results :tests => 1, :assertions => 0, :failures => 0, :errors => 1
  end

  it "will run before_setup actions in the absence of a regular setup" do
    write_and_run_test :omit_setup => true, :use_before_setup => true
    see_in_order "Loaded suite",
      "3rd before_setup",
      "2nd before_setup",
      "1st before_setup",
      "A TEST",
      "THE TEARDOWN",
      "Finished in"
    see_results :tests => 1, :assertions => 0, :failures => 0, :errors => 0
  end

  it "allows before_setup and after_teardown to be used at the same time" do
    write_and_run_test :use_before_setup => true, :use_after_teardown => true
    see_in_order "Loaded suite",
      "3rd before_setup",
      "2nd before_setup",
      "1st before_setup",
      "A TEST",
      "THE TEARDOWN",
      "1st after_teardown",
      "2nd after_teardown",
      "Finished in"
    see_results :tests => 1, :assertions => 0, :failures => 0, :errors => 0
  end

  #
  # HELPERS
  #

  def teardown
    remove_test
  end

  def test_filename
    "_test_file_temp.rb"
  end 

  def remove_test
    rm_f test_filename
  end

  def write_and_run_test(opts={})
    write(test_filename, generate_test_code(opts))
    run_test
  end

  def run_test
    @output = `ruby #{test_filename} 2>&1`
  end


  def write(fname, code)
    File.open(fname,"w") do |f|
      f.print code     
    end
  end

  def show_output
    puts "-- BEGIN TEST OUTPUT"
    puts @output
    puts "-- END TEST OUTPUT"
  end

  def see_in_order(*phrases)
    idx = 0
    phrases.each do |txt|
      idx = @output.index(txt, idx)
      if idx.nil?
        if @output.index(txt)
          flunk "Phrase '#{txt}' is out-of-order in test output:\n#{@output}"
        else
          flunk "Phrase '#{txt}' not found in test output:\n#{@output}"
        end
      end
    end
  end

  def see_in_no_particular_order(*phrases)
    phrases.each do |txt|
      assert_not_nil @output.index(txt), "Didn't see '#{txt}' in test output:\n#{@output}"
    end
  end

  def see_results(opts)
    if @output =~ /(\d+) tests, (\d+) assertions, (\d+) failures, (\d+) errors/
      tests, assertions, failures, errors = [ $1, $2, $3, $4 ]
      [:tests, :assertions, :failures, :errors].each do |key|
        eval %{assert_equal(opts[:#{key}].to_s, #{key}, "Wrong number of #{key} in report") if opts[:#{key}]}
      end
    else
      flunk "Didn't see the test results report line"
    end
  end

  def see_no_results
    if @output =~ /\d+ tests, \d+ assertions, \d+ failures, \d+ errors/
      flunk "Should not have had a results line:\n#{@output}"
    end
  end

  def lib_dir
    File.expand_path(File.dirname(__FILE__) + "/../../lib")
  end

  def generate_test_code(opts={})

    if opts[:with_failure] or opts[:raise_nasty_in_test]
      test_method_code = generate_failing_test("test_something", opts)
    else
      test_method_code = generate_passing_test("test_something")
    end


    requires_for_ext = ''
    if opts[:use_before_setup] or opts[:use_after_teardown]
      requires_for_ext =<<-RFE
        $: << "#{lib_dir}"
        require 'test_unit_before_after'
      RFE
    end

    before_setups = ''
    if opts[:use_before_setup]
      add_on_two = ""
      if opts[:flunk_in_before_setup]
        add_on_two = %{; test.flunk "Flunk in 2nd before_setup"}
      elsif opts[:raise_in_before_setup]
        add_on_two = %{; raise "Error in 2nd before_setup"}
      end
      before_setups =<<-BSTS
        Test::Unit::TestCase.before_setup do |test| 
          puts "1st before_setup"
        end
        Test::Unit::TestCase.before_setup do |test| 
          puts "2nd before_setup" #{add_on_two}
        end
        Test::Unit::TestCase.before_setup do |test| 
          puts "3rd before_setup"
        end

      BSTS
    end


    setup_code =<<-SC
      def setup
        puts "THE SETUP"
      end
    SC
    if opts[:omit_setup]
      setup_code = ""
    end

    after_teardowns = ''
    if opts[:use_after_teardown]
      add_on_one = ""
      add_on_two = ""
      if opts[:flunk_in_after_teardown]
        add_on_one = %{; test.flunk "Flunk in first after_teardown"}
        add_on_two = %{; test.flunk "Flunk in second after_teardown"}
      elsif opts[:raise_in_after_teardown]
        add_on_one = %{; raise "Error in first after_teardown"}
        add_on_two = %{; raise "Error in second after_teardown"}
      end
      after_teardowns =<<-ATDS
        Test::Unit::TestCase.after_teardown do |test| 
          puts "1st after_teardown" #{add_on_one}
        end
        Test::Unit::TestCase.after_teardown do |test| 
          puts "2nd after_teardown" #{add_on_two}
        end
      ATDS
    end

    teardown_code =<<-TDC
      def teardown
        puts "THE TEARDOWN"
      end
    TDC
    if opts[:flunk_in_teardown]
      teardown_code =<<-TDC
        def teardown
          flunk "FLUNK IN TEARDOWN"
        end
      TDC
    elsif opts[:raise_in_teardown]
      teardown_code =<<-TDC
        def teardown
          raise "ERROR IN TEARDOWN"
        end
      TDC
    end
    if opts[:omit_teardown]
      teardown_code = ""
    end

    str = <<-TCODE
    require 'test/unit'
    #{requires_for_ext}

    #{before_setups} #{after_teardowns}
    
    class MyExampleTest < Test::Unit::TestCase
      #{setup_code}
      #{teardown_code}
      #{test_method_code}
    end
    TCODE
  end

  def generate_passing_test(tname)
    str = <<-TMETH
      def #{tname}
        puts "A TEST"
      end
    TMETH
  end

  def generate_failing_test(tname, opts={})
    str = "NOT DEFINED?"
    if opts[:raise_nasty_in_test]
      str = <<-TMETH
        def #{tname}
          raise NoMemoryError, "NASTY ERROR"
        end
      TMETH

    elsif opts[:use_helpers]
      str = <<-TMETH
        def #{tname}
          puts "A FAILING TEST"
          my_helper
        end

        def my_helper
          tripwire
        end

        def tripwire
          assert false, "Instrumented failure"
        end
      TMETH
    else
      str = <<-TMETH
        def #{tname}
          puts "A FAILING TEST"
          assert false, "Instrumented failure"
        end
      TMETH
    end
    return str
  end

  def is_modern_test_unit?
    begin
      Test::Unit::TestCase::PASSTHROUGH_EXCEPTIONS
      return true
    rescue NameError
      return false
    end
  end

end
