require 'test/unit'

# Unit and regression tests for backtraces.
# Note: The tests follow MRI 1.8.6 behavior.
# Behavior of MRI 1.9 is different:
# http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-core/15589
class TestBacktraces < Test::Unit::TestCase
  def setup
    @offset = nil
  end

  # Convenience method to obtain the exception,
  # and to print the stack trace, if needed.
  def get_exception(verbose=false)
    begin
      @get_exception_yield_line = __LINE__ + 1
      yield
    rescue Exception => ex
      puts ex.backtrace.join("\n") if verbose
      ex
    end
  end

  # Main verification method that performs actual checks
  # on the stacktraces.
  def check(expectations, exception)
    backtrace = []
    expectations.strip.split("\n").each { |line|
      line.strip!

      # if line starts with +nnn, we prepend
      # the current file and offset
      md = line.match(/^\+(\d+)(:.*)/)
      if (md)
        flunk("@offset is not defined in the test case") unless @offset
        line = "#{__FILE__}:#{$1.to_i + @offset}#{$2}"
      end

      backtrace << line.strip
    }
    backtrace.each_with_index { |expected, idx|
      assert_equal(expected, exception.backtrace[idx])
    }
  end

  def test_simple_exception
    @offset = __LINE__
    raise RuntimeError.new("Test")
  rescue Exception => ex
    expectation = "+1:in `test_simple_exception'"
    check(expectation, ex)
  end

  import org.jruby.test.TestHelper

  def test_java_backtrace
    TestHelper.throwTestHelperException
  rescue NativeException => ex
    backtrace = ex.backtrace.join("\r\n")

    if (!backtrace.include?("throwTestHelperException"))
      flunk("test_java_backtrace not in backtrace")
    end
  end
    
  def test_simple_exception_recursive
    @offset = __LINE__
    def meth(n)
      raise RuntimeError.new("Test") if n == 10
      n += 1
      meth(n)
    end
    meth(0)
  rescue Exception => ex
    expectation = %q{
      +2:in `meth'
      +4:in `meth'
      +6:in `test_simple_exception_recursive'
    }
    check(expectation, ex)
  end

  def test_native_exception_recursive
    @offset = __LINE__
    def meth(n)
      raise "hello".sub(/l/, 5) if n == 10
      n += 1
      meth(n)
    end
    meth(0)
  rescue Exception => ex
    expectation = %q{
      +2:in `sub'
      +2:in `meth'
      +4:in `meth'
      +6:in `test_native_exception_recursive'
    }
    check(expectation, ex)
  end

  def test_exception_from_block
    @offset = __LINE__
    def foo
      yield
    end
    def bar
      yield
    end
    foo { bar { raise TypeError.new("HEH") } }
  rescue Exception => ex
    expectation = %q{
      +7:in `test_exception_from_block'
      +5:in `bar'
      +7:in `test_exception_from_block'
      +2:in `foo'
      +7:in `test_exception_from_block'
    }
    check(expectation, ex)
  end
  
  def test_exception_from_for
    array = [1,2,3,4,5]
    @offset = __LINE__
    for element in array
      raise RuntimeError
    end
  rescue Exception => ex
    expectation = %q{
      +2:in `test_exception_from_for'
      +1:in `each'
      +1:in `test_exception_from_for'
    }
    check(expectation, ex)
  end

  def test_exception_from_proc
    p = Proc.new {
      @offset = __LINE__
      raise StandardError.new
    }
    p.call
  rescue Exception => ex
    expectation = %q{
      +1:in `test_exception_from_proc'
      +3:in `call'
      +3:in `test_exception_from_proc'
    }
    check(expectation, ex)
  end

  def test_exception_from_lambda
    l = lambda {
      @offset = __LINE__
      raise StandardError.new
    }
    l.call
  rescue Exception => ex
    expectation = %q{
      +1:in `test_exception_from_lambda'
      +3:in `call'
      +3:in `test_exception_from_lambda'
    }
    check(expectation, ex)
  end

  # TODO: currently fails
  def XXXtest_exception_from_array_plus
    @offset = __LINE__
    [1,2,3] + 5
  rescue Exception => ex
    expectation = %q{
      +1:in `+'
      +1:in `test_exception_from_array_plus'
    }
    check(expectation, ex)
  end
  
  # JRUBY-2138
  # # TODO: currently fails
  def XXXtest_exception_from_string_plus
    @offset = __LINE__
    "hello" + nil
  rescue Exception => ex
    expectation = %q{
      +1:in `+'
      +1:in `test_exception_from_string_plus'
    }
    check(expectation, ex)
  end

  def test_exception_from_string_sub
    @offset = __LINE__
    "hello".sub(/l/, 5)
  rescue Exception => ex
    expectation = %q{
      +1:in `sub'
      +1:in `test_exception_from_string_sub'
    }
    check(expectation, ex)
  end

  # TODO: currently fails
  def XXXtest_zero_devision_exception
    @offset = __LINE__
    1/0
  rescue Exception => ex
    expectation = %q{
      +1:in `/'
      +1:in `test_zero_devision_exception'
    }
    check(expectation, ex)
  end

  # TODO: currently fails
  def XXXtest_exeption_from_object_send
    @offset = __LINE__
    "hello".__send__(:sub, /l/, 5)
  rescue Exception => ex
    expectation = %q{
      +1:in `sub'
      +1:in `__send__'
      +1:in `test_exeption_from_object_send'
    }
    check(expectation, ex)
  end

  # TODO: currently fails
  def XXXtest_arity_exception
    @offset = __LINE__
    "hello".sub
  rescue Exception => ex
    expectation = "+1:in `sub'"
    check(expectation, ex)
  end

  # TODO: currently fails
  def XXXtest_exception_from_eval
    ex = get_exception {
      @offset = __LINE__
      eval("raise RuntimeError.new")
    }
    expectation = %Q{
      +1:in `test_exception_from_eval'
      #{__FILE__}:#{@get_exception_yield_line}:in `eval'
      +1:in `test_exception_from_eval'
      #{__FILE__}:#{@get_exception_yield_line}:in `get_exception'
    }
    check(expectation, ex)
  end

  # TODO: currently fails
  def XXXtest_exception_from_block_inside_eval
    ex = get_exception {
      @offset = __LINE__
      eval("def foo; yield; end; foo { raise RuntimeError.new }")
    }
    expectation = %Q{
      +1:in `test_exception_from_block_inside_eval'
      (eval):1:in `foo'
      (eval):1:in `test_exception_from_block_inside_eval'
      #{__FILE__}:#{@get_exception_yield_line}:in `eval'
      +1:in `test_exception_from_block_inside_eval'
      #{__FILE__}:#{@get_exception_yield_line}:in `get_exception'
    }
    check(expectation, ex)
  end

  # JRUBY-2695
  def test_exception_from_thread_with_abort_on_exception_true
    require 'stringio'
    $stderr = StringIO.new

    Thread.abort_on_exception = true
    ex = get_exception {
      @offset = __LINE__
      t = Thread.new do
        raise RuntimeError.new "DUMMY_MSG"
      end
      sleep 3
      t.join
    }

    assert_match(
      /test_backtraces.rb:#{@offset + 2}.*DUMMY_MSG.*RuntimeError/,
      $stderr.string
    )

    assert_equal(SystemExit, ex.class)

    # This check is not fully MRI-compatible (MRI reports more frames),
    # but at list this is something.
    expectation = %Q{
      +2:in `test_exception_from_thread_with_abort_on_exception_true'
    }
    check(expectation, ex)
  ensure
    Thread.abort_on_exception = false
    $stderr = STDERR
  end
end
