"""
Test finish out of an empty function (may be one-instruction long)
"""

import lldb
from lldbsuite.test.decorators import *
from lldbsuite.test.lldbtest import *
from lldbsuite.test import lldbutil


class FinishFromEmptyFunctionTestCase(TestBase):
    NO_DEBUG_INFO_TESTCASE = True

    @skipIf(compiler="clang", compiler_version=['<', '17.0'])
    def test_finish_from_empty_function(self):
        """Test that when stopped at a breakpoint located at the last instruction
        of a function, finish leaves it correctly."""
        self.build()
        target, _, thread, _ = lldbutil.run_to_source_breakpoint(
            self, "// Set breakpoint here", lldb.SBFileSpec("main.c")
        )
        # Find the address of the last instruction of 'done()' and set a breakpoint there.
        # Even though 'done()' is empty, it may contain prologue and epilogue code, so
        # simply setting a breakpoint at the function can place it before 'ret'.
        error = lldb.SBError()
        ret_bp_addr = lldb.SBAddress()
        while True:
            thread.StepInstruction(False, error)
            self.assertTrue(error.Success())
            frame = thread.GetSelectedFrame()
            if "done" in frame.GetFunctionName():
                ret_bp_addr = frame.GetPCAddress()
            elif ret_bp_addr.IsValid():
                # The entire function 'done()' has been stepped through, so 'ret_bp_addr'
                # now contains the address of its last instruction, i.e. 'ret'.
                break
        ret_bp = target.BreakpointCreateByAddress(ret_bp_addr.GetLoadAddress(target))
        self.assertTrue(ret_bp.IsValid())
        # Resume the execution and hit the new breakpoint.
        self.runCmd("cont")
        if self.TraceOn():
            self.runCmd("bt")

        return_statement_line = line_number("main.c", "return 0")
        safety_bp = target.BreakpointCreateByLocation(
            lldb.SBFileSpec("main.c"), return_statement_line
        )
        self.assertTrue(safety_bp.IsValid())

        thread.StepOut(error)
        self.assertTrue(error.Success())

        if self.TraceOn():
            self.runCmd("bt")

        # Compilers may generate source locations differently, so we expect to
        # either be:
        # * On the line of the second done() call, or -
        # * On the puts() line below.
        #
        # If we reach the return statement, we did not step out correctly.
        stepped_out_lines = [
            line_number("main.c", "leaving main"),
            line_number("main.c", "Second call to done"),
        ]

        self.assertIn(
            thread.GetSelectedFrame().line_entry.GetLine(),
            stepped_out_lines,
            "Step-out lost control of execution, ran too far",
        )
