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


@skipIfNoIntelPT
class TestTraceStartStopMultipleThreads(TraceIntelPTTestCaseBase):
    @skipIf(oslist=no_match(["linux"]), archs=no_match(["i386", "x86_64"]))
    @testSBAPIAndCommands
    def testStartMultipleLiveThreads(self):
        self.build()
        exe = self.getBuildArtifact("a.out")

        self.dbg.CreateTarget(exe)

        self.expect("b main")
        self.expect("b 6")
        self.expect("b 11")

        self.expect("r")
        self.traceStartProcess()

        self.expect("continue")
        self.expect("thread trace dump instructions", substrs=["main.cpp:9"])

        # We'll see here the second thread
        self.expect("continue")
        self.expect("thread trace dump instructions", substrs=["main.cpp:4"])

        self.traceStopProcess()

    @skipIf(oslist=no_match(["linux"]), archs=no_match(["i386", "x86_64"]))
    @testSBAPIAndCommands
    def testStartMultipleLiveThreadsWithStops(self):
        self.build()
        exe = self.getBuildArtifact("a.out")
        self.dbg.CreateTarget(exe)

        self.expect("b main")
        self.expect("b 6")
        self.expect("b 11")

        self.expect("r")

        self.traceStartProcess()

        # We'll see here the first thread
        self.expect("continue")

        # We are in thread 2
        self.expect("thread trace dump instructions", substrs=["main.cpp:9"])
        self.expect("thread trace dump instructions 2", substrs=["main.cpp:9"])

        # We stop tracing all
        self.expect("thread trace stop all")

        # The trace is still in memory
        self.expect("thread trace dump instructions 2", substrs=["main.cpp:9"])

        # We'll stop at the next breakpoint in thread 3, thread 2 and 3 will be alive, but only 3 traced.
        self.expect("continue")
        self.expect("thread trace dump instructions", substrs=["main.cpp:4"])
        self.expect("thread trace dump instructions 3", substrs=["main.cpp:4"])
        self.expect(
            "thread trace dump instructions 1", substrs=["not traced"], error=True
        )
        self.expect(
            "thread trace dump instructions 2", substrs=["not traced"], error=True
        )

        self.traceStopProcess()

    @skipIf(oslist=no_match(["linux"]), archs=no_match(["i386", "x86_64"]))
    def testStartMultipleLiveThreadsWithThreadStartAll(self):
        self.build()
        exe = self.getBuildArtifact("a.out")
        target = self.dbg.CreateTarget(exe)

        self.expect("b main")
        self.expect("b 6")
        self.expect("b 11")

        self.expect("r")

        self.expect("continue")
        # We are in thread 2
        self.expect("thread trace start all")
        # Now we have instructions in thread's 2 trace
        self.expect("n")

        self.expect("thread trace dump instructions 2", substrs=["main.cpp:11"])

        # We stop tracing all
        self.runCmd("thread trace stop all")

        # The trace is still in memory
        self.expect("thread trace dump instructions 2", substrs=["main.cpp:11"])

        # We'll stop at the next breakpoint in thread 3, and nothing should be traced
        self.expect("continue")
        self.expect(
            "thread trace dump instructions 3", substrs=["not traced"], error=True
        )
        self.expect(
            "thread trace dump instructions 1", substrs=["not traced"], error=True
        )
        self.expect(
            "thread trace dump instructions 2", substrs=["not traced"], error=True
        )

    @skipIf(oslist=no_match(["linux"]), archs=no_match(["i386", "x86_64"]))
    @testSBAPIAndCommands
    def testStartMultipleLiveThreadsWithSmallTotalLimit(self):
        self.build()
        exe = self.getBuildArtifact("a.out")

        self.dbg.CreateTarget(exe)

        self.expect("b main")
        self.expect("r")

        # trace the entire process with enough total size for 1 thread trace
        self.traceStartProcess(processBufferSizeLimit=5000)

        # we get the stop event when trace 2 appears and can't be traced
        self.expect("c", substrs=["Thread", "can't be traced"])
        # we get the stop event when trace 3 appears and can't be traced
        self.expect("c", substrs=["Thread", "can't be traced"])

        self.traceStopProcess()

    @skipIf(oslist=no_match(["linux"]), archs=no_match(["i386", "x86_64"]))
    @testSBAPIAndCommands
    def testStartPerCpuSession(self):
        self.skipIfPerCpuTracingIsNotSupported()

        self.build()
        exe = self.getBuildArtifact("a.out")
        self.dbg.CreateTarget(exe)

        self.expect("b main")
        self.expect("r")

        # We should fail if we hit the total buffer limit. Useful if the number
        # of cpus is huge.
        self.traceStartProcess(
            error="True",
            processBufferSizeLimit=100,
            perCpuTracing=True,
            substrs=[
                "The process can't be traced because the process trace size "
                "limit has been reached. Consider retracing with a higher limit."
            ],
        )

        self.traceStartProcess(perCpuTracing=True)
        self.traceStopProcess()

        self.traceStartProcess(perCpuTracing=True)
        # We can't support multiple per-cpu tracing sessions.
        self.traceStartProcess(
            error=True,
            perCpuTracing=True,
            substrs=["Process currently traced. Stop process tracing first"],
        )

        # We can't support tracing per thread is per cpu is enabled.
        self.traceStartThread(
            error="True", substrs=["Thread with tid ", "is currently traced"]
        )

        # We can't stop individual thread when per cpu is enabled.
        self.traceStopThread(
            error="True",
            substrs=[
                "Can't stop tracing an individual thread when per-cpu process tracing is enabled"
            ],
        )

        # We move forward a little bit to collect some data
        self.expect("b 19")
        self.expect("c")

        # We will assert that the trace state will contain valid context switch and intel pt trace buffer entries.
        # Besides that, we need to get tsc-to-nanos conversion information.

        # We first parse the json response from the custom packet
        self.runCmd(
            """process plugin packet send 'jLLDBTraceGetState:{"type":"intel-pt"}]'"""
        )
        response_header = "response: "
        output = None
        for line in self.res.GetOutput().splitlines():
            if line.find(response_header) != -1:
                response = line[
                    line.find(response_header) + len(response_header) :
                ].strip()
                output = json.loads(response)

        self.assertIsNotNone(output)
        self.assertIn("cpus", output)
        self.assertIn("tscPerfZeroConversion", output)
        found_non_empty_context_switch = False

        for cpu in output["cpus"]:
            context_switch_size = None
            ipt_trace_size = None
            for binary_data in cpu["binaryData"]:
                if binary_data["kind"] == "iptTrace":
                    ipt_trace_size = binary_data["size"]
                elif binary_data["kind"] == "perfContextSwitchTrace":
                    context_switch_size = binary_data["size"]
            self.assertIsNotNone(context_switch_size)
            self.assertIsNotNone(ipt_trace_size)
            if context_switch_size > 0:
                found_non_empty_context_switch = True

        # We must have captured the context switch of when the target resumed
        self.assertTrue(found_non_empty_context_switch)

        self.expect("thread trace dump instructions")

        self.traceStopProcess()
