"""Unit tests for the parse_segv general hook."""

import unittest

from problem_report import ProblemReport
from tests.helper import import_module_from_file
from tests.paths import get_data_directory

parse_segv = import_module_from_file(
    get_data_directory() / "general-hooks" / "parse_segv.py"
)

# Default global registers, maps, and disassembly for testing
REGS = """\
eax            0xffffffff -1
ecx            0xbfc6af40   -1077498048
edx            0x1  1
ebx            0x26eff4 2551796
esp            0xbfc6af24   0xbfc6af24
ebp            0xbfc6af28   0xbfc6af28
esi            0x826bb60    136756064
edi            0x8083480    134755456
eip            0x808354e    0x808354e <main+14>
eflags         0x200286 [ PF SF IF ID ]
cs             0x73 115
ss             0x7b 123
ds             0x7b 123
es             0x7b 123
fs             0x4  4
gs             0x33 51
"""
REGS64 = """\
rax            0xffffffffffffffff   -1
rbx            0x26eff4 2551796
rcx            0xffffffffffffffff   -1
rdx            0xffffffffff600180   -10485376
rsi            0x0  0
rdi            0x7fffffffe3b0   140737488348080
rbp            0x0  0x0
rsp            0x0000bfc6af24   0x0000bfc6af24
r8             0x0  0
r9             0x0  0
r10            0x7fffffffe140   140737488347456
r11            0x246    582
r12            0x7fffffffe400   140737488348160
r13            0x7fffffffe468   140737488348264
r14            0x1  1
r15            0x7fffffffe460   140737488348256
rip            0x7ffff790be10   0x7ffff790be10 <nanosleep+16>
eflags         0x246    [ PF ZF IF ]
cs             0x33 51
ss             0x2b 43
ds             0x0  0
es             0x0  0
fs             0x0  0
gs             0x0  0
fctrl          0x37f    895
fstat          0x0  0
ftag           0xffff   65535
fiseg          0x0  0
fioff          0x40303a 4206650
foseg          0x0  0
fooff          0x0  0
fop            0x5d8    1496
mxcsr          0x1f80   [ IM DM ZM OM UM PM ]
"""
REGS64VIRT = """\
rax            0x0                 0
rbx            0x0                 0
rcx            0x555555557e08      93824992247304
rdx            0x2a                42
rsi            0x7fffffffedb8      140737488350648
rdi            0x2a                42
rbp            0x7fffffffec90      0x7fffffffec90
rsp            0x7fffffffec90      0x7fffffffec90
r8             0x7ffff7fa6680      140737353770624
r9             0x7ffff7fa7fe0      140737353777120
r10            0x7fffffffe9f0      140737488349680
r11            0x202               514
r12            0x1                 1
r13            0x7ffff7ffd000      140737354125312
r14            0x7fffffffedc8      140737488350664
r15            0x555555557e08      93824992247304
rip            0x55555555513f      0x55555555513f <f+22>
eflags         0x10246             [ PF ZF IF RF ]
cs             0x33                51
ss             0x2b                43
ds             0x0                 0
es             0x0                 0
fs             0x0                 0
gs             0x0                 0
pl3_ssp        <unavailable>
fs_base        0x7ffff7d90740      140737351583552
gs_base        0x0                 0
"""
MAPS = """\
00110000-0026c000 r-xp 00000000 08:06 375131     /lib/tls/i686/cmov/libc-2.9.so
0026c000-0026d000 ---p 0015c000 08:06 375131     /lib/tls/i686/cmov/libc-2.9.so
0026d000-0026f000 r--p 0015c000 08:06 375131     /lib/tls/i686/cmov/libc-2.9.so
0026f000-00270000 rw-p 0015e000 08:06 375131     /lib/tls/i686/cmov/libc-2.9.so
00270000-00273000 rw-p 00000000 00:00 0
002c1000-002e5000 r-xp 00000000 08:06 375135     /lib/tls/i686/cmov/libm-2.9.so
002e5000-002e6000 r--p 00023000 08:06 375135     /lib/tls/i686/cmov/libm-2.9.so
002e6000-002e7000 rw-p 00024000 08:06 375135     /lib/tls/i686/cmov/libm-2.9.so
00318000-00334000 r-xp 00000000 08:06 977846     /lib/ld-2.9.so
00334000-00335000 r--p 0001b000 08:06 977846     /lib/ld-2.9.so
00335000-00336000 rw-p 0001c000 08:06 977846     /lib/ld-2.9.so
0056e000-005a1000 r-xp 00000000 08:06 65575      /lib/libncurses.so.5.7
005a1000-005a3000 r--p 00033000 08:06 65575      /lib/libncurses.so.5.7
005a3000-005a4000 rw-p 00035000 08:06 65575      /lib/libncurses.so.5.7
00b67000-00b68000 r-xp 00000000 00:00 0          [vdso]
00bb6000-00bcb000 r-xp 00000000 08:06 375202 \
    /lib/tls/i686/cmov/libpthread-2.9.so
00bcb000-00bcc000 r--p 00014000 08:06 375202 \
    /lib/tls/i686/cmov/libpthread-2.9.so
00bcc000-00bcd000 rw-p 00015000 08:06 375202 \
    /lib/tls/i686/cmov/libpthread-2.9.so
00bcd000-00bcf000 rw-p 00000000 00:00 0
00beb000-00bed000 r-xp 00000000 08:06 375134 \
    /lib/tls/i686/cmov/libdl-2.9.so
00bed000-00bee000 r--p 00001000 08:06 375134 \
    /lib/tls/i686/cmov/libdl-2.9.so
00bee000-00bef000 rw-p 00002000 08:06 375134 \
    /lib/tls/i686/cmov/libdl-2.9.so
00c56000-00c7a000 r-xp 00000000 08:06 1140420    /usr/lib/libexpat.so.1.5.2
00c7a000-00c7c000 r--p 00023000 08:06 1140420    /usr/lib/libexpat.so.1.5.2
00c7c000-00c7d000 rw-p 00025000 08:06 1140420    /usr/lib/libexpat.so.1.5.2
00dce000-00dfa000 r-xp 00000000 08:06 65612      /lib/libreadline.so.5.2
00dfa000-00dfb000 ---p 0002c000 08:06 65612      /lib/libreadline.so.5.2
00dfb000-00dfc000 r--p 0002c000 08:06 65612      /lib/libreadline.so.5.2
00dfc000-00dff000 rw-p 0002d000 08:06 65612      /lib/libreadline.so.5.2
00dff000-00e00000 rw-p 00000000 00:00 0
08048000-0831c000 r-xp 00000000 08:06 1140349    /usr/bin/gdb
0831c000-0831d000 r--p 002d3000 08:06 1140349    /usr/bin/gdb
0831d000-08325000 rw-p 002d4000 08:06 1140349    /usr/bin/gdb
08325000-0833f000 rw-p 00000000 00:00 0
b8077000-b807a000 rw-p 00000000 00:00 0
b8096000-b8098000 rw-p 00000000 00:00 0
bfc57000-bfc6c000 rw-p 00000000 00:00 0          [stack]
"""
DISASM = """\
0x08083540 <main+0>:    lea    0x4(%esp),%ecx
0x08083544 <main+4>:    and    $0xfffffff0,%esp
0x08083547 <main+7>:    pushl  -0x4(%ecx)
0x0808354a <main+10>:   push   %ebp
0x0808354b <main+11>:   mov    %esp,%ebp
0x0808354d <main+13>:   push   %ecx
0x0808354e <main+14>:   sub    $0x14,%esp
0x08083551 <main+17>:   mov    (%ecx),%eax
0x08083553 <main+19>:   mov    0x4(%ecx),%edx
0x08083556 <main+22>:   lea    -0x14(%ebp),%ecx
0x08083559 <main+25>:   movl   $0x0,-0xc(%ebp)
0x08083560 <main+32>:   movl   $0x826bc68,-0x8(%ebp)
0x08083567 <main+39>:   mov    %eax,-0x14(%ebp)
0x0808356a <main+42>:   mov    %edx,-0x10(%ebp)
0x0808356d <main+45>:   mov    %ecx,(%esp)
0x08083570 <main+48>:   call   0x8083580 <gdb_main>
0x08083575 <main+53>:   add    $0x14,%esp
0x08083578 <main+56>:   pop    %ecx
0x08083579 <main+57>:   pop    %ebp
0x0808357a <main+58>:   lea    -0x4(%ecx),%esp
0x0808357d <main+61>:   ret
"""
DISASM64 = """\
=> 0x55555555513f <f+22>:	mov    %edx,(%rax)
   0x555555555141 <f+24>:	mov    -0x14(%rbp),%eax
   0x555555555144 <f+27>:	add    $0x1,%eax
   0x555555555147 <f+30>:	pop    %rbp
   0x555555555148 <f+31>:	ret
   0x555555555149 <main>:	push   %rbp
   0x55555555514a <main+1>:	mov    %rsp,%rbp
   0x55555555514d <main+4>:	mov    $0x2a,%edi
   0x555555555152 <main+9>:	call   0x555555555129 <f>
   0x555555555157 <main+14>:	pop    %rbp
   0x555555555158 <main+15>:	ret
   0x555555555159:	add    %al,(%rax)
   0x55555555515b:	add    %cl,-0x7d(%rax)
   0x55555555515e <_fini+2>:	in     (%dx),%al
   0x55555555515f <_fini+3>:	or     %cl,-0x7d(%rax)
   0x555555555162 <_fini+6>:	(bad)
"""


class TestHookParseSegv(unittest.TestCase):
    """Test Segfault Parser."""

    def test_add_info_without_segv_reason(self) -> None:
        """Test add_info() with no SegvReason in result."""
        report = ProblemReport()
        report["Signal"] = "11"
        report["Architecture"] = "amd64"
        report["Disassembly"] = DISASM
        report["ProcMaps"] = MAPS
        report["Registers"] = REGS64

        parse_segv.add_info(report)

        self.assertEqual(
            report.get("SegvAnalysis"),
            "Segfault happened at: 0x08083540 <main+0>:    lea    0x4(%esp),%ecx\n"
            "PC (0x08083540) ok\n"
            "insn (lea) does not access VMA\n"
            "SP (0xbfc6af24) ok\n"
            "Reason could not be automatically determined.",
        )
        self.assertNotIn("SegvReason", report)

    def test_invalid_00_registers(self):
        """Require valid registers."""
        regs = "a 0x10\nb !!!\n"
        self.assertRaises(ValueError, parse_segv.ParseSegv, regs, "", "")
        try:
            segv = parse_segv.ParseSegv(regs, "", "")
        except ValueError as error:
            self.assertIn("invalid literal for int()", str(error))

        regs = "a 0x10"
        disasm = "0x08083540 <main+0>:    lea    0x4(%esp),%ecx\n"
        segv = parse_segv.ParseSegv(regs, disasm, "")
        self.assertEqual(segv.regs["a"], 0x10, segv)

        segv.regs = None
        self.assertRaises(ValueError, segv.parse_disassembly, "")

    def test_invalid_01_disassembly(self):
        # TODO: Split into separate test cases
        # pylint: disable=too-many-statements
        """Require valid disassembly."""
        regs = "a 0x10"

        disasm = ""
        self.assertRaises(ValueError, parse_segv.ParseSegv, regs, disasm, "")

        disasm = "Dump ..."
        self.assertRaises(ValueError, parse_segv.ParseSegv, regs, disasm, "")

        disasm = "Dump ...\nmonkey"
        self.assertRaises(ValueError, parse_segv.ParseSegv, regs, disasm, "")

        disasm = "monkey"
        self.assertRaises(ValueError, parse_segv.ParseSegv, regs, disasm, "")

        disasm = "0x1111111111: Cannot access memory at address 0x1111111111\n"
        segv = parse_segv.ParseSegv(regs, disasm, "")
        self.assertEqual(segv.pc, 0x1111111111, segv.pc)
        self.assertIsNone(segv.insn, segv.insn)
        self.assertIsNone(segv.src, segv.src)
        self.assertIsNone(segv.dest, segv.dest)

        disasm = "0x2111111111: \n"
        segv = parse_segv.ParseSegv(regs, disasm, "")
        self.assertEqual(segv.pc, 0x2111111111, segv.pc)
        self.assertIsNone(segv.insn, segv.insn)
        self.assertIsNone(segv.src, segv.src)
        self.assertIsNone(segv.dest, segv.dest)

        disasm = "0x8069ff0 <fopen@plt+132220>: cmpb   $0x0,(%eax,%ebx,1)\n"
        segv = parse_segv.ParseSegv(regs, disasm, "")
        self.assertEqual(segv.pc, 0x8069FF0, segv.pc)
        self.assertEqual(segv.insn, "cmpb", segv.insn)
        self.assertEqual(segv.src, "$0x0", segv.src)
        self.assertEqual(segv.dest, "(%eax,%ebx,1)", segv.dest)

        disasm = "0xb765bb48 <_XSend+440>:  call   *0x40(%edi)\n"
        segv = parse_segv.ParseSegv(regs, disasm, "")
        self.assertEqual(segv.pc, 0xB765BB48, segv.pc)
        self.assertEqual(segv.insn, "call", segv.insn)
        self.assertEqual(segv.src, "*0x40(%edi)", segv.src)
        self.assertIsNone(segv.dest, segv.dest)

        disasm = "0xb7aae5a0:   call   0xb7a805af <_Unwind_Find_FDE@plt+111>\n"
        segv = parse_segv.ParseSegv(regs, disasm, "")
        self.assertEqual(segv.pc, 0xB7AAE5A0, segv.pc)
        self.assertEqual(segv.insn, "call", segv.insn)
        self.assertEqual(segv.src, "0xb7a805af", segv.src)
        self.assertIsNone(segv.dest, segv.dest)

        disasm = "0x09083540:    mov    0x4(%esp),%es:%ecx\n"
        segv = parse_segv.ParseSegv(regs, disasm, "")
        self.assertEqual(segv.pc, 0x09083540, segv.pc)
        self.assertEqual(segv.insn, "mov", segv.insn)
        self.assertEqual(segv.src, "0x4(%esp)", segv.src)
        self.assertEqual(segv.dest, "%es:%ecx", segv.dest)

        disasm = "0x08083540 <main+0>:    lea    0x4(%esp),%ecx\n"
        segv = parse_segv.ParseSegv(regs, disasm, "")
        self.assertEqual(segv.pc, 0x08083540, segv.pc)
        self.assertEqual(segv.insn, "lea", segv.insn)
        self.assertEqual(segv.src, "0x4(%esp)", segv.src)
        self.assertEqual(segv.dest, "%ecx", segv.dest)

        disasm = (
            "0x404127 <exo_mount_hal_device_mount+167>:\n"
            "    repz cmpsb %es:(%rdi),%ds:(%rsi)\n"
        )
        segv = parse_segv.ParseSegv(regs, disasm, "")
        self.assertEqual(segv.pc, 0x0404127, segv.pc)
        self.assertEqual(segv.insn, "repz cmpsb", segv.insn)
        self.assertEqual(segv.src, "%es:(%rdi)", segv.src)
        self.assertEqual(segv.dest, "%ds:(%rsi)", segv.dest)

        disasm = "0xb031765a <hufftab16+570>: add    0x3430433,%eax"
        segv = parse_segv.ParseSegv(regs, disasm, "")
        self.assertEqual(segv.pc, 0xB031765A, segv.pc)
        self.assertEqual(segv.insn, "add", segv.insn)
        self.assertEqual(segv.src, "0x3430433", segv.src)
        self.assertEqual(segv.dest, "%eax", segv.dest)

        disasm = "Dump ...\n0x08083540 <main+0>:    lea    0x4(%esp),%ecx\n"
        segv = parse_segv.ParseSegv(regs, disasm, "")
        self.assertEqual(segv.pc, 0x08083540, segv.pc)
        self.assertEqual(segv.insn, "lea", segv.insn)
        self.assertEqual(segv.src, "0x4(%esp)", segv.src)
        self.assertEqual(segv.dest, "%ecx", segv.dest)

        disasm = "0x08083550 <main+0>:    nop\n"
        segv = parse_segv.ParseSegv(regs, disasm, "")
        self.assertEqual(segv.pc, 0x08083550, segv.pc)
        self.assertEqual(segv.insn, "nop", segv.insn)
        self.assertIsNone(segv.src, segv.src)
        self.assertIsNone(segv.dest, segv.dest)

        regs = "esp 0x444"
        disasm = "0x08083560 <main+0>:    push %ecx\n"
        segv = parse_segv.ParseSegv(regs, disasm, "")
        self.assertEqual(segv.pc, 0x08083560, segv.pc)
        self.assertEqual(segv.insn, "push", segv.insn)
        self.assertEqual(segv.src, "%ecx", segv.src)
        self.assertEqual(segv.dest, "(%esp)", segv.dest)

        # GDB 7.1
        regs = "esp 0x444"
        disasm = "=> 0x08083560 <main+0>:    push %ecx\n"
        segv = parse_segv.ParseSegv(regs, disasm, "")
        self.assertEqual(segv.pc, 0x08083560, segv.pc)
        self.assertEqual(segv.insn, "push", segv.insn)
        self.assertEqual(segv.src, "%ecx", segv.src)
        self.assertEqual(segv.dest, "(%esp)", segv.dest)

    def test_unavailable_register(self) -> None:
        """Test pl3_ssp register value being unavailable."""
        segv = parse_segv.ParseSegv(REGS64VIRT, DISASM64, "")
        self.assertNotIn("pl3_ssp", segv.regs)
        self.assertLessEqual(
            {"rax", "rbx", "rcx", "rdx", "rsi", "rdi", "rbp", "rsp"}, set(segv.regs)
        )
        self.assertLessEqual({f"r{i}" for i in range(8, 16)}, set(segv.regs))

    def test_ioport_operation(self):
        """I/O port violations."""
        regs = "rax            0x3  3"
        disasm = (
            "0x4087f1 <snd_pcm_hw_params_set_channels_near@plt+19345>:\n"
            "    out    %al,$0xb3\n"
        )
        maps = (
            "00400000-00412000 r-xp 00000000 08:04 10371157                   "
            "        /usr/sbin/pommed\n"
            "00611000-00614000 rw-p 00011000 08:04 10371157                   "
            "        /usr/sbin/pommed\n"
            "00614000-00635000 rw-p 00614000 00:00 0                          "
            "        [heap]\n"
        )
        segv = parse_segv.ParseSegv(regs, disasm, maps)
        self.assertEqual(segv.pc, 0x4087F1, segv.pc)
        self.assertEqual(segv.insn, "out", segv.insn)
        self.assertEqual(segv.src, "%al", segv.src)
        self.assertEqual(segv.dest, "$0xb3", segv.dest)

        understood, reason, details = segv.report()
        self.assertTrue(understood, details)
        self.assertTrue("disallowed I/O port operation on port 3" in reason, reason)

    def test_invalid_02_maps(self):
        """Require valid maps."""
        regs = "a 0x10"
        disasm = "Dump ...\n0x08083540 <main+0>:    lea    0x4(%esp),%ecx\n"

        maps = "asdlkfjaadf"
        self.assertRaises(ValueError, parse_segv.ParseSegv, regs, disasm, maps)

        maps = (
            "005a3000-005a4000 rw-p 00035000 08:06 65575  "
            "    /lib/libncurses.so.5.7\n"
            "00b67000-00b68000 r-xp 00000000 00:00 0          [vdso]\n"
            "00c67000-00c68000 r--p 00000000 00:00 0 "
        )
        segv = parse_segv.ParseSegv(regs, disasm, maps)
        self.assertEqual(segv.maps[0]["start"], 0x005A3000, segv)
        self.assertEqual(segv.maps[0]["end"], 0x005A4000, segv)
        self.assertEqual(segv.maps[0]["perms"], "rw-p", segv)
        self.assertEqual(segv.maps[0]["name"], "/lib/libncurses.so.5.7", segv)

        self.assertEqual(segv.maps[1]["start"], 0x00B67000, segv)
        self.assertEqual(segv.maps[1]["end"], 0x00B68000, segv)
        self.assertEqual(segv.maps[1]["perms"], "r-xp", segv)
        self.assertEqual(segv.maps[1]["name"], "[vdso]", segv)

        self.assertEqual(segv.maps[2]["start"], 0x00C67000, segv)
        self.assertEqual(segv.maps[2]["end"], 0x00C68000, segv)
        self.assertEqual(segv.maps[2]["perms"], "r--p", segv)
        self.assertIsNone(segv.maps[2]["name"], segv)

    def test_debug(self):
        """Debug mode works."""
        regs = "a 0x10"
        disasm = "Dump ...\n0x08083540 <main+0>:    lea    0x4(%esp),%ecx\n"
        maps = (
            "005a3000-005a4000 rw-p 00035000 08:06 65575  "
            "    /lib/libncurses.so.5.7\n"
            "00b67000-00b68000 r-xp 00000000 00:00 0          [vdso]\n"
            "00c67000-00c68000 r--p 00000000 00:00 0 "
        )
        with self.assertLogs(level="DEBUG"):
            segv = parse_segv.ParseSegv(regs, disasm, maps)
        self.assertTrue(segv is not None, segv)

    def test_register_values(self):
        """Sub-register parsing."""
        disasm = """0x08083540 <main+0>:    mov    $1,%ecx"""
        segv = parse_segv.ParseSegv(REGS64, disasm, "")

        val = segv.register_value("%rdx")
        self.assertEqual(val, 0xFFFFFFFFFF600180, hex(val))
        val = segv.register_value("%edx")
        self.assertEqual(val, 0xFF600180, hex(val))
        val = segv.register_value("%dx")
        self.assertEqual(val, 0x0180, hex(val))
        val = segv.register_value("%dl")
        self.assertEqual(val, 0x80, hex(val))

    def test_segv_unknown(self):
        """Handle unknown segfaults."""
        disasm = """0x08083540 <main+0>:    mov    $1,%ecx"""
        segv = parse_segv.ParseSegv(REGS, disasm, MAPS)
        understood, _, details = segv.report()
        self.assertFalse(understood, details)

        # Verify calculations
        self.assertEqual(segv.calculate_arg("(%ecx)"), 0xBFC6AF40, segv.regs["ecx"])
        self.assertEqual(segv.calculate_arg("0x10(%ecx)"), 0xBFC6AF50, segv.regs["ecx"])
        self.assertEqual(
            segv.calculate_arg("-0x20(%ecx)"), 0xBFC6AF20, segv.regs["ecx"]
        )
        self.assertEqual(segv.calculate_arg("%fs:(%ecx)"), 0xBFC6AF44, segv.regs["ecx"])
        self.assertEqual(segv.calculate_arg("0x3404403"), 0x3404403, "0x3404403")
        self.assertEqual(segv.calculate_arg("*0x40(%edi)"), 0x80834C0, segv.regs["edi"])
        self.assertEqual(
            segv.calculate_arg("(%edx,%ebx,1)"), 0x26EFF5, segv.regs["ebx"]
        )
        self.assertEqual(
            segv.calculate_arg("(%eax,%ebx,1)"), 0x26EFF3, segv.regs["ebx"]
        )
        self.assertEqual(
            segv.calculate_arg("0x10(,%ebx,1)"), 0x26F004, segv.regs["ebx"]
        )

        # Again, but 64bit
        disasm = """0x08083540 <main+0>:    mov    $1,%rcx"""
        segv = parse_segv.ParseSegv(REGS64, disasm, MAPS)
        understood, _, details = segv.report()
        self.assertFalse(understood, details)

        self.assertEqual(
            segv.calculate_arg("(%rax,%rbx,1)"), 0x26EFF3, segv.regs["rbx"]
        )

    def test_segv_pc_missing(self):
        """Handle PC in missing VMA."""
        disasm = """0x00083540 <main+0>:    lea    0x4(%esp),%ecx"""
        segv = parse_segv.ParseSegv(REGS, disasm, MAPS)
        understood, reason, details = segv.report()
        self.assertTrue(understood, details)
        self.assertIn("PC (0x00083540) not located in a known VMA region", details)
        self.assertIn("executing unknown VMA", reason)

        disasm = """0x00083544:"""
        segv = parse_segv.ParseSegv(REGS, disasm, MAPS)
        understood, reason, details = segv.report()
        self.assertTrue(understood, details)
        self.assertIn("PC (0x00083544) not located in a known VMA region", details)
        self.assertIn("executing unknown VMA", reason)

    def test_segv_pc_null(self):
        """Handle PC in NULL VMA."""
        disasm = """0x00000540 <main+0>:    lea    0x4(%esp),%ecx"""
        segv = parse_segv.ParseSegv(REGS, disasm, MAPS)
        understood, reason, details = segv.report()
        self.assertTrue(understood, details)
        self.assertIn("PC (0x00000540) not located in a known VMA region", details)
        self.assertIn("executing NULL VMA", reason)

    def test_segv_pc_nx_writable(self):
        """Handle PC in writable NX VMA."""
        disasm = """0x005a3000 <main+0>:    lea    0x4(%esp),%ecx"""
        segv = parse_segv.ParseSegv(REGS, disasm, MAPS)
        understood, reason, details = segv.report()
        self.assertTrue(understood, details)
        self.assertIn("PC (0x005a3000) in non-executable VMA region:", details)
        self.assertIn("executing writable VMA /lib/libncurses.so.5.7", reason)

    def test_segv_pc_nx_unwritable(self):
        """Handle PC in non-writable NX VMA."""
        disasm = """0x00dfb000 <main+0>:    lea    0x4(%esp),%ecx"""
        segv = parse_segv.ParseSegv(REGS, disasm, MAPS)
        understood, reason, details = segv.report()
        self.assertTrue(understood, details)
        self.assertIn("PC (0x00dfb000) in non-executable VMA region:", details)
        self.assertIn("executing non-writable VMA /lib/libreadline.so.5.2", reason)

    def test_segv_src_missing(self):
        """Handle source in missing VMA."""
        reg = f"{REGS}ecx            0x0006af24   0xbfc6af24"
        disasm = "0x08083547 <main+7>:    pushl  -0x4(%ecx)"

        # Valid crash
        segv = parse_segv.ParseSegv(reg, disasm, MAPS)
        understood, reason, details = segv.report()
        self.assertTrue(understood, details)
        self.assertIn(
            'source "-0x4(%ecx)" (0x0006af20) not located in a known VMA region',
            details,
        )
        self.assertIn("reading unknown VMA", reason)

        # Valid crash
        disasm = "0x08083547 <main+7>:    callq  *%ecx"
        segv = parse_segv.ParseSegv(reg, disasm, MAPS)
        understood, reason, details = segv.report()
        self.assertTrue(understood, details)
        self.assertIn(
            'source "*%ecx" (0x0006af24) not located in a known VMA region', details
        )
        self.assertIn("reading unknown VMA", reason)

    def test_segv_src_null(self):
        """Handle source in NULL VMA."""
        reg = f"{REGS}ecx            0x00000024   0xbfc6af24"
        disasm = "0x08083547 <main+7>:    pushl  -0x4(%ecx)"

        segv = parse_segv.ParseSegv(reg, disasm, MAPS)
        understood, reason, details = segv.report()
        self.assertTrue(understood, details)
        self.assertIn(
            'source "-0x4(%ecx)" (0x00000020) not located in a known VMA region',
            details,
        )
        self.assertIn("reading NULL VMA", reason)

    def test_segv_src_not_readable(self):
        """Handle source not in readable VMA."""
        reg = f"{REGS}ecx            0x0026c080   0xbfc6af24"
        disasm = "0x08083547 <main+7>:    pushl  -0x4(%ecx)"
        segv = parse_segv.ParseSegv(reg, disasm, MAPS)
        understood, reason, details = segv.report()
        self.assertTrue(understood, details)
        self.assertIn(
            'source "-0x4(%ecx)" (0x0026c07c) in non-readable VMA region:', details
        )
        self.assertIn("reading VMA /lib/tls/i686/cmov/libc-2.9.so", reason)
        self.assertNotIn("Stack memory exhausted", details)
        self.assertNotIn("Stack pointer not within stack segment", details)

    def test_segv_dest_missing(self):
        """Handle destination in missing VMA."""
        reg = f"{REGS}esp            0x0006af24   0xbfc6af24"
        disasm = "0x08083547 <main+7>:    pushl  -0x4(%ecx)"

        segv = parse_segv.ParseSegv(reg, disasm, MAPS)
        understood, reason, details = segv.report()
        self.assertTrue(understood, details)
        self.assertIn(
            'destination "(%esp)" (0x0006af24) not located in a known VMA region',
            details,
        )
        self.assertIn("writing unknown VMA", reason)

    def test_segv_dest_null(self):
        """Handle destination in NULL VMA."""
        reg = f"{REGS}esp            0x00000024   0xbfc6af24"
        disasm = "0x08083547 <main+7>:    pushl  -0x4(%ecx)"

        segv = parse_segv.ParseSegv(reg, disasm, MAPS)
        understood, reason, details = segv.report()
        self.assertTrue(understood, details)
        self.assertIn(
            'destination "(%esp)" (0x00000024) not located in a known VMA region',
            details,
        )
        self.assertIn("writing NULL VMA", reason)

    def test_segv_dest_not_writable(self):
        """Handle destination not in writable VMA."""
        reg = f"{REGS}esp            0x08048080   0xbfc6af24"
        disasm = "0x08083547 <main+7>:    pushl  -0x4(%ecx)"
        segv = parse_segv.ParseSegv(reg, disasm, MAPS)
        understood, reason, details = segv.report()
        self.assertTrue(understood, details)
        self.assertIn(
            'destination "(%esp)" (0x08048080) in non-writable VMA region:', details
        )
        self.assertIn("writing VMA /usr/bin/gdb", reason)

    def test_segv_crackful_disasm(self):
        """Reject insane disassemblies."""
        disasm = "0x08083547 <main+7>:    pushl  -0x4(blah)"
        segv = parse_segv.ParseSegv(REGS, disasm, MAPS)
        self.assertRaises(ValueError, segv.report)

        disasm = "0x08083547 <main+7>:    pushl  -04(%ecx)"
        segv = parse_segv.ParseSegv(REGS, disasm, MAPS)
        self.assertRaises(ValueError, segv.report)

    def test_segv_stack_failure(self):
        """Handle walking off the stack."""
        # Triggered via "push"
        reg = f"{REGS}esp            0xbfc56ff0   0xbfc56ff0"
        disasm = "0x08083547 <main+7>:    push  %eax"
        segv = parse_segv.ParseSegv(reg, disasm, MAPS)
        understood, _, details = segv.report()
        self.assertTrue(understood, details)
        self.assertIn(
            'destination "(%esp)" (0xbfc56ff0) not located'
            " in a known VMA region (needed writable region)!",
            details,
        )

        # Triggered via "call"
        reg = f"{REGS}esp            0xbfc56fff   0xbfc56fff"
        disasm = "0x08083547 <main+7>:    callq  0x08083540"
        segv = parse_segv.ParseSegv(reg, disasm, MAPS)
        understood, _, details = segv.report()
        self.assertTrue(understood, details)
        self.assertIn(
            'destination "(%esp)" (0xbfc56fff) not located'
            " in a known VMA region (needed writable region)!",
            details,
        )
        self.assertIn("Stack memory exhausted", details)

        # Triggered via unknown reason
        reg = f"{REGS}esp            0xdfc56000   0xdfc56000"
        disasm = """0x08083540 <main+0>:    mov    $1,%rcx"""
        segv = parse_segv.ParseSegv(reg, disasm, MAPS)
        understood, _, details = segv.report()
        self.assertTrue(understood, details)
        self.assertIn(
            "SP (0xdfc56000) not located in a known VMA region"
            " (needed readable region)!",
            details,
        )
        self.assertIn("Stack pointer not within stack segment", details)

    def test_segv_stack_kernel_segfault(self):
        """Handle unknown segfaults in kernel."""
        # Crash in valid code path
        disasm = """0x0056e010: ret"""
        segv = parse_segv.ParseSegv(REGS, disasm, MAPS)
        understood, _, details = segv.report()
        self.assertFalse(understood, details)
        self.assertIn("Reason could not be automatically determined.", details)
        self.assertNotIn("(Unhandled exception in kernel code?)", details)

        # Crash from kernel code path
        disasm = """0x00b67422 <__kernel_vsyscall+2>: ret"""
        segv = parse_segv.ParseSegv(REGS, disasm, MAPS)
        understood, _, details = segv.report()
        self.assertFalse(understood, details)
        self.assertIn(
            "Reason could not be automatically determined."
            " (Unhandled exception in kernel code?)",
            details,
        )
