# This file is a part of Julia. License is MIT: https://julialang.org/license

using Test

@testset "Basic exception stack handling" begin
    # Exiting the catch block normally pops the exception
    try
        error("A")
    catch
        @test length(current_exceptions()) == 1
    end
    @test length(current_exceptions()) == 0
    # Exiting via a finally block does not pop the exception
    try
        try
            error("A")
        finally
            @test length(current_exceptions()) == 1
        end
    catch
        @test length(current_exceptions()) == 1
    end
    # The combined try-catch-finally form obeys the same rules as above
    try
        error("A")
    catch
        @test length(current_exceptions()) == 1
    finally
        @test length(current_exceptions()) == 0
    end
    @test length(current_exceptions()) == 0
    # Errors are pushed onto the stack according to catch block nesting
    try
        error("RootCause")
    catch
        @test length(current_exceptions()) == 1
        try
            error("B")
        catch
            stack = current_exceptions()
            @test length(stack) == 2
            @test stack[1].exception.msg == "RootCause"
            @test stack[2].exception.msg == "B"
        end
        # Stack pops correctly
        stack = current_exceptions()
        @test length(stack) == 1
        @test stack[1].exception.msg == "RootCause"
    end
end

@testset "Exception stack lowering special cases" begin
    # try block in value position
    val = try
        error("A")
    catch
        @test length(current_exceptions()) == 1
        1
    end
    @test val == 1
    function test_exc_stack_tailpos()
        # try block in tail position
        try
            error("A")
        catch
            length(current_exceptions())
        end
    end
    @test test_exc_stack_tailpos() == 1
    @test length(current_exceptions()) == 0
end

@testset "Exception stacks - early exit from try or catch" begin
    # Exiting a catch block early with normal control flow — break, continue,
    # return, goto — will result in popping of the exception stack.
    function test_exc_stack_catch_return()
        try
            error("A")
        catch
            @test length(current_exceptions()) == 1
            return
        end
    end
    test_exc_stack_catch_return()

    for i=1:1
        try
            error("A")
        catch
            @test length(current_exceptions()) == 1
            break
        end
    end
    # Also test try-break-finally forms here. See #31766
    for i=1:1
        try
            error("A")
        catch
            @test length(current_exceptions()) == 1
            break
        finally
            @test length(current_exceptions()) == 0
        end
    end
    @test length(current_exceptions()) == 0

    for i=1:1
        try
            error("A")
        catch
            @test length(current_exceptions()) == 1
            continue
        end
    end
    for i=1:1
        try
            error("A")
        catch
            @test length(current_exceptions()) == 1
            continue
        finally
            @test length(current_exceptions()) == 0
        end
    end
    @test length(current_exceptions()) == 0

    try
        error("A")
    catch
        @test length(current_exceptions()) == 1
        @goto outofcatch
    end
    @label outofcatch
    try
        error("A")
    catch
        @test length(current_exceptions()) == 1
        @goto outofcatch2
    finally
        @test length(current_exceptions()) == 0
    end
    @label outofcatch2
    @test length(current_exceptions()) == 0

    # Exiting from a try block in various ways should not affect the exception
    # stack state.
    try
        error("ExceptionInOuterTry")
    catch
        @test length(current_exceptions()) == 1
        function test_exc_stack_try_return()
            try
                return
            catch
            end
        end
        test_exc_stack_try_return()
        for i=1:1
            try
                break
            catch
            end
        end
        for i=1:1
            try
                continue
            catch
            end
        end
        try
            @goto outoftry
        catch
        end
        @label outoftry
        @test length(current_exceptions()) == 1
        @test current_exceptions()[1].exception == ErrorException("ExceptionInOuterTry")
    end
end

@testset "Finally handling with exception stacks" begin
    # The lowering of finally is quite subtle when combined with break or
    # return because each finally block may be entered via multiple code paths
    # (eg, different occurrences of return), and these code paths must diverge
    # again once the finally block has completed. To complicate matters
    # further, the return code path must thread through every nested finally
    # block before actually returning, all the while preserving the information
    # about which variable to return.

    # Issue #34579
    (()-> begin
        try
            throw("err")
        catch
            # Explicit return => exception should be popped before finally block
            return
        finally
            @test length(Base.current_exceptions()) == 0
        end
    end)()
    @test length(Base.current_exceptions()) == 0

    while true
        try
            error("err1")
        catch
            try
                # Break target is outside catch block, but finally is inside =>
                # exception should not be popped inside finally block
                break
            finally
                @test length(Base.current_exceptions()) == 1
            end
        end
    end
    @test length(Base.current_exceptions()) == 0

    # Nested finally handling with `return`: each finally block should observe
    # only the active exceptions as according to its nesting depth.
    (() -> begin
        try
            try
                error("err1")
            catch
                try
                    try
                        error("err2")
                    catch
                        # This return needs to thread control flow through
                        # multiple finally blocks in the linearized IR.
                        return
                    end
                finally
                    # At this point err2 is dealt with
                    @test length(Base.current_exceptions()) == 1
                    @test Base.current_exceptions()[1].exception == ErrorException("err1")
                end
            end
        finally
            # At this point err1 is dealt with
            @test length(Base.current_exceptions()) == 0
        end
    end)()
    @test length(Base.current_exceptions()) == 0

    (()-> begin
        while true
            try
                error("foo")
            finally
                break
            end
        end
        @test length(Base.current_exceptions()) == 0
    end)()
    @test length(Base.current_exceptions()) == 0
end

@testset "Deep exception stacks" begin
    # Generate deep exception stack with recursive handlers.
    #
    # (Note that if you let this overflow the program stack (not the exception
    # stack) julia will crash. See #28577.)
    function test_exc_stack_deep(n)
        n != 1 || error("RootCause")
        try
            test_exc_stack_deep(n-1)
        catch
            error("n==$n")
        end
    end
    @test try
        test_exc_stack_deep(100)
    catch
        @test current_exceptions()[1].exception == ErrorException("RootCause")
        length(current_exceptions())
    end == 100
    @test length(current_exceptions()) == 0
end

@testset "Exception stacks and Tasks" begin
    # Task switching should not affect exception state. See #12485.
    try
        error("A")
    catch
        t = @task try
            error("B")
        catch exc
            exc
        end
        yield(t)
        @test t.state === :done
        @test t.result == ErrorException("B")
        # Task exception state is preserved around task switches
        @test length(current_exceptions()) == 1
        @test current_exceptions()[1].exception == ErrorException("A")
    end
    @test length(current_exceptions()) == 0
    # test rethrow() rethrows correct state
    bt = []
    try
        try
            error("A")
        catch
            bt = catch_backtrace()
            t = @task try
                error("B")
            catch exc
                exc
            end
            yield(t)
            @test t.state === :done
            @test t.result == ErrorException("B")
            @test bt == catch_backtrace()
            rethrow()
        end
    catch exc
        @test exc == ErrorException("A")
        @test bt == catch_backtrace()
    end
    @test length(current_exceptions()) == 0
    # test rethrow with argument
    bt = []
    try
        try
            error("A")
        catch
            t = @task try
                error("B")
            catch exc
                exc
            end
            yield(t)
            @test t.state === :done
            @test t.result == ErrorException("B")
            bt = catch_backtrace()
            rethrow(ErrorException("C"))
        end
    catch exc
        @test exc == ErrorException("C")
        @test bt == catch_backtrace()
    end
    @test length(current_exceptions()) == 0
    # Exception stacks on other tasks
    t = @task try
        error("A")
    catch
        error("B")
    end
    yield(t)
    @test t.state === :failed
    @test t.result == ErrorException("B")
    @test current_exceptions(t, backtrace=false) == [
        (exception=ErrorException("A"),backtrace=nothing),
        (exception=ErrorException("B"),backtrace=nothing)
    ]
    # Exception stacks for tasks which never get the chance to start
    t = @task nothing
    @test (try
        @async Base.throwto(t, ErrorException("expected"))
        wait(t)
    catch e
        e
    end).task.exception == ErrorException("expected")
    @test length(current_exceptions(t)) == 1
    @test length(current_exceptions(t)[1].backtrace) > 0 # backtrace is nonempty
    # Exception stacks should not be accessed on concurrently running tasks
    t = @task ()->nothing
    @test_throws ErrorException("Inspecting the exception stack of a task which might "*
                                "be running concurrently isn't allowed.") current_exceptions(t)
end

@testset "rethrow" begin
    @test try
        rethrow()
    catch exc
        exc
    end == ErrorException("rethrow() not allowed outside a catch block")
    @test try
        rethrow(ErrorException("A"))
    catch exc
        exc
    end == ErrorException("rethrow(exc) not allowed outside a catch block")
end

# issue #36527
function f36527()
    caught = false
    🏡 = Core.eval(Main, :(module asdf36527 end))
    try
        Core.eval(🏡, :(include_string($🏡, "@assert z36527 == 10")))
    catch ex
        GC.gc()
        catch_backtrace()
        caught = true
    end
    return caught
end

@test f36527()

# accessing an undefined var in tail position in a catch block
function undef_var_in_catch()
    try
        error("first error")
    catch
        __probably_n0t_defined__
    end
end
@test length(try
    undef_var_in_catch()
    []
catch
    current_exceptions()
end) == 2
