#!/usr/bin/env python3
# Copyright 2021 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Tests for create_unwind_table.py.

This test suite contains tests for the custom unwind table creation for 32-bit
arm builds.
"""

import io
import struct

import unittest
import unittest.mock
import re

from create_unwind_table import (
    AddressCfi,
    AddressUnwind,
    FilterToNonTombstoneCfi,
    FunctionCfi,
    FunctionUnwind,
    EncodeAddressUnwind,
    EncodeAddressUnwinds,
    EncodedAddressUnwind,
    EncodeAsBytes,
    EncodeFunctionOffsetTable,
    EncodedFunctionUnwind,
    EncodeFunctionUnwinds,
    EncodeStackPointerUpdate,
    EncodePop,
    EncodePageTableAndFunctionTable,
    EncodeUnwindInfo,
    EncodeUnwindInstructionTable,
    GenerateUnwinds,
    GenerateUnwindTables,
    NullParser,
    ParseAddressCfi,
    PushOrSubSpParser,
    ReadFunctionCfi,
    REFUSE_TO_UNWIND,
    StoreSpParser,
    TRIVIAL_UNWIND,
    Uleb128Encode,
    UnwindInstructionsParser,
    UnwindType,
    VPushParser,
)


class _TestReadFunctionCfi(unittest.TestCase):
    def testFilterTombstone(self):
        input_lines = [
            'file name',
            'STACK CFI INIT 0 ',
            'STACK CFI 100 ',
            'STACK CFI INIT 1 ',
            'STACK CFI 200 ',
        ]

        f = io.StringIO(''.join(line + '\n' for line in input_lines))

        self.assertEqual(
            [
                'STACK CFI INIT 1 \n',
                'STACK CFI 200 \n',
            ],
            list(FilterToNonTombstoneCfi(f)),
        )

    def testReadFunctionCfiTombstoneFiltered(self):
        input_lines = [
            'STACK CFI INIT 0 50 .cfa: sp 0 + .ra: lr',  # Tombstone function.
            'STACK CFI 2 .cfa: sp 24 + .ra: .cfa - 4 + ^ r4: .cfa - 16 + ^ '
            'r5: .cfa - 12 + ^ r7: .cfa - 8 + ^',
            'STACK CFI INIT 15b6490 4 .cfa: sp 0 + .ra: lr',
        ]

        f = io.StringIO(''.join(line + '\n' for line in input_lines))

        self.assertEqual(
            [FunctionCfi(4, (AddressCfi(0x15B6490, '.cfa: sp 0 + .ra: lr'),))],
            list(ReadFunctionCfi(f)),
        )

    def testReadFunctionCfiSingleFunction(self):
        input_lines = [
            'STACK CFI INIT 15b6490 4 .cfa: sp 0 + .ra: lr',
            'STACK CFI 2 .cfa: sp 24 + .ra: .cfa - 4 + ^ r4: .cfa - 16 + ^ '
            'r5: .cfa - 12 + ^ r7: .cfa - 8 + ^',
        ]

        f = io.StringIO(''.join(line + '\n' for line in input_lines))

        self.assertEqual(
            [
                FunctionCfi(
                    4,
                    (
                        AddressCfi(0x15B6490, '.cfa: sp 0 + .ra: lr'),
                        AddressCfi(
                            0x2,
                            '.cfa: sp 24 + .ra: .cfa - 4 + ^ r4: .cfa - 16 + ^ '
                            'r5: .cfa - 12 + ^ r7: .cfa - 8 + ^',
                        ),
                    ),
                )
            ],
            list(ReadFunctionCfi(f)),
        )

    def testReadFunctionCfiMultipleFunctions(self):
        input_lines = [
            'STACK CFI INIT 15b6490 4 .cfa: sp 0 + .ra: lr',
            'STACK CFI 2 .cfa: sp 24 + .ra: .cfa - 4 + ^ r4: .cfa - 16 + ^ '
            'r5: .cfa - 12 + ^ r7: .cfa - 8 + ^',
            'STACK CFI INIT 15b655a 26 .cfa: sp 0 + .ra: lr',
            'STACK CFI 15b655c .cfa: sp 8 + .ra: .cfa - 4 + ^ r4: .cfa - 8 + ^',
        ]

        f = io.StringIO(''.join(line + '\n' for line in input_lines))

        self.assertEqual(
            [
                FunctionCfi(
                    0x4,
                    (
                        AddressCfi(0x15B6490, '.cfa: sp 0 + .ra: lr'),
                        AddressCfi(
                            0x2,
                            '.cfa: sp 24 + .ra: .cfa - 4 + ^ r4: .cfa - 16 + ^ '
                            'r5: .cfa - 12 + ^ r7: .cfa - 8 + ^',
                        ),
                    ),
                ),
                FunctionCfi(
                    0x26,
                    (
                        AddressCfi(0x15B655A, '.cfa: sp 0 + .ra: lr'),
                        AddressCfi(
                            0x15B655C,
                            '.cfa: sp 8 + .ra: .cfa - 4 + ^ r4: .cfa - 8 + ^',
                        ),
                    ),
                ),
            ],
            list(ReadFunctionCfi(f)),
        )


class _TestEncodeAsBytes(unittest.TestCase):
    def testOutOfBounds(self):
        self.assertRaises(ValueError, lambda: EncodeAsBytes(1024))
        self.assertRaises(ValueError, lambda: EncodeAsBytes(256))
        self.assertRaises(ValueError, lambda: EncodeAsBytes(-1))

    def testEncode(self):
        self.assertEqual(bytes([0]), EncodeAsBytes(0))
        self.assertEqual(bytes([255]), EncodeAsBytes(255))
        self.assertEqual(bytes([0, 1]), EncodeAsBytes(0, 1))


class _TestUleb128Encode(unittest.TestCase):
    def testNegativeValue(self):
        self.assertRaises(ValueError, lambda: Uleb128Encode(-1))

    def testSingleByte(self):
        self.assertEqual(bytes([0]), Uleb128Encode(0))
        self.assertEqual(bytes([1]), Uleb128Encode(1))
        self.assertEqual(bytes([127]), Uleb128Encode(127))

    def testMultiBytes(self):
        self.assertEqual(bytes([0b10000000, 0b1]), Uleb128Encode(128))
        self.assertEqual(
            bytes([0b10000000, 0b10000000, 0b1]), Uleb128Encode(128**2)
        )


class _TestEncodeStackPointerUpdate(unittest.TestCase):
    def testSingleByte(self):
        self.assertEqual(bytes([0b00000000 | 0]), EncodeStackPointerUpdate(4))
        self.assertEqual(bytes([0b01000000 | 0]), EncodeStackPointerUpdate(-4))

        self.assertEqual(
            bytes([0b00000000 | 0b00111111]), EncodeStackPointerUpdate(0x100)
        )
        self.assertEqual(
            bytes([0b01000000 | 0b00111111]), EncodeStackPointerUpdate(-0x100)
        )

        self.assertEqual(bytes([0b00000000 | 3]), EncodeStackPointerUpdate(16))
        self.assertEqual(bytes([0b01000000 | 3]), EncodeStackPointerUpdate(-16))

        self.assertEqual(bytes([0b00111111]), EncodeStackPointerUpdate(0x100))

        # 10110010 uleb128
        # vsp = vsp + 0x204 + (uleb128 << 2)
        self.assertEqual(
            bytes([0b10110010, 0b00000000]), EncodeStackPointerUpdate(0x204)
        )
        self.assertEqual(
            bytes([0b10110010, 0b00000001]), EncodeStackPointerUpdate(0x208)
        )

        # For vsp increments of 0x104-0x200, use 00xxxxxx twice.
        self.assertEqual(
            bytes([0b00111111, 0b00000000]), EncodeStackPointerUpdate(0x104)
        )
        self.assertEqual(
            bytes([0b00111111, 0b00111111]), EncodeStackPointerUpdate(0x200)
        )
        self.assertEqual(
            bytes([0b01111111, 0b01111111]), EncodeStackPointerUpdate(-0x200)
        )

        # Not multiple of 4.
        self.assertRaises(AssertionError, lambda: EncodeStackPointerUpdate(101))
        # offset=0 is meaningless.
        self.assertRaises(AssertionError, lambda: EncodeStackPointerUpdate(0))


class _TestEncodePop(unittest.TestCase):
    def testSingleRegister(self):
        # Should reject registers outside r4 ~ r15 range.
        for r in 0, 1, 2, 3, 16:
            self.assertRaises(AssertionError, lambda: EncodePop([r]))
        # Should use
        # 1000iiii iiiiiiii
        # Pop up to 12 integer registers under masks {r15-r12}, {r11-r4}.
        self.assertEqual(bytes([0b10000000, 0b00000001]), EncodePop([4]))
        self.assertEqual(bytes([0b10000000, 0b00001000]), EncodePop([7]))
        self.assertEqual(bytes([0b10000100, 0b00000000]), EncodePop([14]))
        self.assertEqual(bytes([0b10001000, 0b00000000]), EncodePop([15]))

    def testContinuousRegisters(self):
        # 10101nnn
        # Pop r4-r[4+nnn], r14.
        self.assertEqual(bytes([0b10101000]), EncodePop([4, 14]))
        self.assertEqual(bytes([0b10101001]), EncodePop([4, 5, 14]))
        self.assertEqual(
            bytes([0b10101111]), EncodePop([4, 5, 6, 7, 8, 9, 10, 11, 14])
        )

    def testDiscontinuousRegisters(self):
        # 1000iiii iiiiiiii
        # Pop up to 12 integer registers under masks {r15-r12}, {r11-r4}.
        self.assertEqual(bytes([0b10001000, 0b00000001]), EncodePop([4, 15]))
        self.assertEqual(bytes([0b10000100, 0b00011000]), EncodePop([7, 8, 14]))
        self.assertEqual(
            bytes([0b10000111, 0b11111111]),
            EncodePop([4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]),
        )
        self.assertEqual(
            bytes([0b10000100, 0b10111111]),
            EncodePop([4, 5, 6, 7, 8, 9, 11, 14]),
        )


class _TestEncodeAddressUnwind(unittest.TestCase):
    def testReturnToLr(self):
        self.assertEqual(
            bytes([0b10110000]),
            EncodeAddressUnwind(
                AddressUnwind(
                    address_offset=0,
                    unwind_type=UnwindType.RETURN_TO_LR,
                    sp_offset=0,
                    registers=tuple(),
                )
            ),
        )

    def testNoAction(self):
        self.assertEqual(
            bytes([]),
            EncodeAddressUnwind(
                AddressUnwind(
                    address_offset=0,
                    unwind_type=UnwindType.NO_ACTION,
                    sp_offset=0,
                    registers=tuple(),
                )
            ),
        )

    def testUpdateSpAndOrPopRegisters(self):
        self.assertEqual(
            bytes([0b0, 0b10101000]),
            EncodeAddressUnwind(
                AddressUnwind(
                    address_offset=0,
                    unwind_type=UnwindType.UPDATE_SP_AND_OR_POP_REGISTERS,
                    sp_offset=0x4,
                    registers=(4, 14),
                )
            ),
        )

        self.assertEqual(
            bytes([0b0]),
            EncodeAddressUnwind(
                AddressUnwind(
                    address_offset=0,
                    unwind_type=UnwindType.UPDATE_SP_AND_OR_POP_REGISTERS,
                    sp_offset=0x4,
                    registers=tuple(),
                )
            ),
        )

        self.assertEqual(
            bytes([0b10101000]),
            EncodeAddressUnwind(
                AddressUnwind(
                    address_offset=0,
                    unwind_type=UnwindType.UPDATE_SP_AND_OR_POP_REGISTERS,
                    sp_offset=0,
                    registers=(4, 14),
                )
            ),
        )

    def testRestoreSpFromRegisters(self):
        self.assertEqual(
            bytes([0b10010100, 0b0]),
            EncodeAddressUnwind(
                AddressUnwind(
                    address_offset=0,
                    unwind_type=UnwindType.RESTORE_SP_FROM_REGISTER,
                    sp_offset=0x4,
                    registers=(4,),
                )
            ),
        )

        self.assertEqual(
            bytes([0b10010100]),
            EncodeAddressUnwind(
                AddressUnwind(
                    address_offset=0,
                    unwind_type=UnwindType.RESTORE_SP_FROM_REGISTER,
                    sp_offset=0,
                    registers=(4,),
                )
            ),
        )

        self.assertRaises(
            AssertionError,
            lambda: EncodeAddressUnwind(
                AddressUnwind(
                    address_offset=0,
                    unwind_type=UnwindType.RESTORE_SP_FROM_REGISTER,
                    sp_offset=0x4,
                    registers=tuple(),
                )
            ),
        )


class _TestEncodeAddressUnwinds(unittest.TestCase):
    def testEncodeOrder(self):
        address_unwind1 = AddressUnwind(
            address_offset=0,
            unwind_type=UnwindType.RETURN_TO_LR,
            sp_offset=0,
            registers=tuple(),
        )
        address_unwind2 = AddressUnwind(
            address_offset=4,
            unwind_type=UnwindType.UPDATE_SP_AND_OR_POP_REGISTERS,
            sp_offset=0,
            registers=(4, 14),
        )

        def MockEncodeAddressUnwind(address_unwind):
            return {
                address_unwind1: bytes([1]),
                address_unwind2: bytes([2]),
            }[address_unwind]

        with unittest.mock.patch(
            "create_unwind_table.EncodeAddressUnwind",
            side_effect=MockEncodeAddressUnwind,
        ):
            encoded_unwinds = EncodeAddressUnwinds(
                (address_unwind1, address_unwind2)
            )
            self.assertEqual(
                (
                    EncodedAddressUnwind(4, bytes([2]) + bytes([1])),
                    EncodedAddressUnwind(0, bytes([1])),
                ),
                encoded_unwinds,
            )


PAGE_SIZE = 1 << 17


class _TestEncodeFunctionUnwinds(unittest.TestCase):
    @unittest.mock.patch('create_unwind_table.EncodeAddressUnwinds')
    def testEncodeOrder(self, MockEncodeAddressUnwinds):
        MockEncodeAddressUnwinds.return_value = EncodedAddressUnwind(0, b'\x00')

        self.assertEqual(
            [
                EncodedFunctionUnwind(
                    page_number=0,
                    page_offset=0,
                    address_unwinds=EncodedAddressUnwind(0, b'\x00'),
                ),
                EncodedFunctionUnwind(
                    page_number=0,
                    page_offset=100 >> 1,
                    address_unwinds=EncodedAddressUnwind(0, b'\x00'),
                ),
            ],
            list(
                EncodeFunctionUnwinds(
                    [
                        FunctionUnwind(
                            address=100,
                            size=PAGE_SIZE - 100,
                            address_unwinds=(),
                        ),
                        FunctionUnwind(address=0, size=100, address_unwinds=()),
                    ],
                    text_section_start_address=0,
                )
            ),
        )

    @unittest.mock.patch('create_unwind_table.EncodeAddressUnwinds')
    def testFillingGaps(self, MockEncodeAddressUnwinds):
        MockEncodeAddressUnwinds.return_value = EncodedAddressUnwind(0, b'\x00')

        self.assertEqual(
            [
                EncodedFunctionUnwind(
                    page_number=0,
                    page_offset=0,
                    address_unwinds=EncodedAddressUnwind(0, b'\x00'),
                ),
                EncodedFunctionUnwind(
                    page_number=0,
                    page_offset=50 >> 1,
                    address_unwinds=TRIVIAL_UNWIND,
                ),
                EncodedFunctionUnwind(
                    page_number=0,
                    page_offset=100 >> 1,
                    address_unwinds=EncodedAddressUnwind(0, b'\x00'),
                ),
            ],
            list(
                EncodeFunctionUnwinds(
                    [
                        FunctionUnwind(address=0, size=50, address_unwinds=()),
                        FunctionUnwind(
                            address=100,
                            size=PAGE_SIZE - 100,
                            address_unwinds=(),
                        ),
                    ],
                    text_section_start_address=0,
                )
            ),
        )

    @unittest.mock.patch('create_unwind_table.EncodeAddressUnwinds')
    def testFillingLastPage(self, MockEncodeAddressUnwinds):
        MockEncodeAddressUnwinds.return_value = EncodedAddressUnwind(0, b'\x00')

        self.assertEqual(
            [
                EncodedFunctionUnwind(
                    page_number=0,
                    page_offset=0,
                    address_unwinds=EncodedAddressUnwind(0, b'\x00'),
                ),
                EncodedFunctionUnwind(
                    page_number=0,
                    page_offset=100 >> 1,
                    address_unwinds=EncodedAddressUnwind(0, b'\x00'),
                ),
                EncodedFunctionUnwind(
                    page_number=0,
                    page_offset=200 >> 1,
                    address_unwinds=REFUSE_TO_UNWIND,
                ),
            ],
            list(
                EncodeFunctionUnwinds(
                    [
                        FunctionUnwind(
                            address=1100, size=100, address_unwinds=()
                        ),
                        FunctionUnwind(
                            address=1200, size=100, address_unwinds=()
                        ),
                    ],
                    text_section_start_address=1100,
                )
            ),
        )

    @unittest.mock.patch('create_unwind_table.EncodeAddressUnwinds')
    def testFillingFirstPage(self, MockEncodeAddressUnwinds):
        MockEncodeAddressUnwinds.return_value = EncodedAddressUnwind(0, b'\x00')

        self.assertEqual(
            [
                EncodedFunctionUnwind(
                    page_number=0,
                    page_offset=0,
                    address_unwinds=REFUSE_TO_UNWIND,
                ),
                EncodedFunctionUnwind(
                    page_number=0,
                    page_offset=100 >> 1,
                    address_unwinds=EncodedAddressUnwind(0, b'\x00'),
                ),
                EncodedFunctionUnwind(
                    page_number=0,
                    page_offset=200 >> 1,
                    address_unwinds=EncodedAddressUnwind(0, b'\x00'),
                ),
                EncodedFunctionUnwind(
                    page_number=0,
                    page_offset=300 >> 1,
                    address_unwinds=REFUSE_TO_UNWIND,
                ),
            ],
            list(
                EncodeFunctionUnwinds(
                    [
                        FunctionUnwind(
                            address=1100, size=100, address_unwinds=()
                        ),
                        FunctionUnwind(
                            address=1200, size=100, address_unwinds=()
                        ),
                    ],
                    text_section_start_address=1000,
                )
            ),
        )

    @unittest.mock.patch('create_unwind_table.EncodeAddressUnwinds')
    def testOverlappedFunctions(self, _):
        self.assertRaises(
            # Eval generator with `list`. Otherwise the code will not execute.
            AssertionError,
            lambda: list(
                EncodeFunctionUnwinds(
                    [
                        FunctionUnwind(address=0, size=100, address_unwinds=()),
                        FunctionUnwind(
                            address=50, size=100, address_unwinds=()
                        ),
                    ],
                    text_section_start_address=0,
                )
            ),
        )


class _TestNullParser(unittest.TestCase):
    def testCfaChange(self):
        parser = NullParser()
        match = parser.GetBreakpadInstructionsRegex().search(
            '.cfa: sp 0 + .ra: lr'
        )
        self.assertIsNotNone(match)

        address_unwind, new_cfa_sp_offset = parser.ParseFromMatch(
            address_offset=0, cfa_sp_offset=0, match=match
        )

        self.assertEqual(0, new_cfa_sp_offset)
        self.assertEqual(
            AddressUnwind(
                address_offset=0,
                unwind_type=UnwindType.RETURN_TO_LR,
                sp_offset=0,
                registers=(),
            ),
            address_unwind,
        )


class _TestPushOrSubSpParser(unittest.TestCase):
    def testCfaChange(self):
        parser = PushOrSubSpParser()
        match = parser.GetBreakpadInstructionsRegex().search('.cfa: sp 4 +')
        self.assertIsNotNone(match)

        address_unwind, new_cfa_sp_offset = parser.ParseFromMatch(
            address_offset=20, cfa_sp_offset=0, match=match
        )

        self.assertEqual(4, new_cfa_sp_offset)
        self.assertEqual(
            AddressUnwind(
                address_offset=20,
                unwind_type=UnwindType.UPDATE_SP_AND_OR_POP_REGISTERS,
                sp_offset=4,
                registers=(),
            ),
            address_unwind,
        )

    def testCfaAndRaChangePopOnly(self):
        parser = PushOrSubSpParser()
        match = parser.GetBreakpadInstructionsRegex().search(
            '.cfa: sp 4 + .ra: .cfa -4 + ^'
        )
        self.assertIsNotNone(match)

        address_unwind, new_cfa_sp_offset = parser.ParseFromMatch(
            address_offset=20, cfa_sp_offset=0, match=match
        )

        self.assertEqual(4, new_cfa_sp_offset)
        self.assertEqual(
            AddressUnwind(
                address_offset=20,
                unwind_type=UnwindType.UPDATE_SP_AND_OR_POP_REGISTERS,
                sp_offset=0,
                registers=(14,),
            ),
            address_unwind,
        )

    def testCfaAndRaChangePopAndSpUpdate(self):
        parser = PushOrSubSpParser()
        match = parser.GetBreakpadInstructionsRegex().search(
            '.cfa: sp 8 + .ra: .cfa -4 + ^'
        )
        self.assertIsNotNone(match)

        address_unwind, new_cfa_sp_offset = parser.ParseFromMatch(
            address_offset=20, cfa_sp_offset=0, match=match
        )

        self.assertEqual(8, new_cfa_sp_offset)
        self.assertEqual(
            AddressUnwind(
                address_offset=20,
                unwind_type=UnwindType.UPDATE_SP_AND_OR_POP_REGISTERS,
                sp_offset=4,
                registers=(14,),
            ),
            address_unwind,
        )

    def testCfaAndRaAndRegistersChangePopOnly(self):
        parser = PushOrSubSpParser()
        match = parser.GetBreakpadInstructionsRegex().search(
            '.cfa: sp 12 + .ra: .cfa -4 + ^ r4: .cfa -12 + ^ r7: .cfa -8 + ^'
        )
        self.assertIsNotNone(match)

        address_unwind, new_cfa_sp_offset = parser.ParseFromMatch(
            address_offset=20, cfa_sp_offset=0, match=match
        )

        self.assertEqual(12, new_cfa_sp_offset)
        self.assertEqual(
            AddressUnwind(
                address_offset=20,
                unwind_type=UnwindType.UPDATE_SP_AND_OR_POP_REGISTERS,
                sp_offset=0,
                registers=(4, 7, 14),
            ),
            address_unwind,
        )

    def testCfaAndRaAndRegistersChangePopAndSpUpdate(self):
        parser = PushOrSubSpParser()
        match = parser.GetBreakpadInstructionsRegex().search(
            '.cfa: sp 16 + .ra: .cfa -4 + ^ r4: .cfa -12 + ^ r7: .cfa -8 + ^'
        )
        self.assertIsNotNone(match)

        address_unwind, new_cfa_sp_offset = parser.ParseFromMatch(
            address_offset=20, cfa_sp_offset=0, match=match
        )

        self.assertEqual(16, new_cfa_sp_offset)
        self.assertEqual(
            AddressUnwind(
                address_offset=20,
                unwind_type=UnwindType.UPDATE_SP_AND_OR_POP_REGISTERS,
                sp_offset=4,
                registers=(4, 7, 14),
            ),
            address_unwind,
        )

    def testRegistersChange(self):
        parser = PushOrSubSpParser()
        match = parser.GetBreakpadInstructionsRegex().search(
            'r4: .cfa -8 + ^ r7: .cfa -4 + ^'
        )
        self.assertIsNotNone(match)

        address_unwind, new_cfa_sp_offset = parser.ParseFromMatch(
            address_offset=20, cfa_sp_offset=0, match=match
        )

        self.assertEqual(0, new_cfa_sp_offset)
        self.assertEqual(
            AddressUnwind(
                address_offset=20,
                unwind_type=UnwindType.UPDATE_SP_AND_OR_POP_REGISTERS,
                sp_offset=0,
                registers=(4, 7),
            ),
            address_unwind,
        )

    def testCfaAndRegistersChange(self):
        parser = PushOrSubSpParser()
        match = parser.GetBreakpadInstructionsRegex().search(
            '.cfa: sp 8 + r4: .cfa -8 + ^ r7: .cfa -4 + ^'
        )
        self.assertIsNotNone(match)

        address_unwind, new_cfa_sp_offset = parser.ParseFromMatch(
            address_offset=20, cfa_sp_offset=0, match=match
        )

        self.assertEqual(8, new_cfa_sp_offset)
        self.assertEqual(
            AddressUnwind(
                address_offset=20,
                unwind_type=UnwindType.UPDATE_SP_AND_OR_POP_REGISTERS,
                sp_offset=0,
                registers=(4, 7),
            ),
            address_unwind,
        )

    def testRegistersOrdering(self):
        parser = PushOrSubSpParser()
        match = parser.GetBreakpadInstructionsRegex().search(
            'r10: .cfa -8 + ^ r7: .cfa -4 + ^'
        )
        self.assertIsNotNone(match)

        address_unwind, new_cfa_sp_offset = parser.ParseFromMatch(
            address_offset=20, cfa_sp_offset=0, match=match
        )

        self.assertEqual(0, new_cfa_sp_offset)
        self.assertEqual(
            AddressUnwind(
                address_offset=20,
                unwind_type=UnwindType.UPDATE_SP_AND_OR_POP_REGISTERS,
                sp_offset=0,
                registers=(7, 10),
            ),
            address_unwind,
        )

    def testPoppingCallerSaveRegisters(self):
        """Regression test for pop unwinds that encode caller-save registers.

        Callee-save registers: r0 ~ r3.
        """
        parser = PushOrSubSpParser()
        match = parser.GetBreakpadInstructionsRegex().search(
            '.cfa: sp 16 + .ra: .cfa -4 + ^ '
            'r3: .cfa -16 + ^ r4: .cfa -12 + ^ r5: .cfa -8 + ^'
        )

        self.assertIsNotNone(match)

        address_unwind, new_cfa_sp_offset = parser.ParseFromMatch(
            address_offset=20, cfa_sp_offset=0, match=match
        )

        self.assertEqual(16, new_cfa_sp_offset)
        self.assertEqual(
            AddressUnwind(
                address_offset=20,
                unwind_type=UnwindType.UPDATE_SP_AND_OR_POP_REGISTERS,
                sp_offset=4,
                registers=(4, 5, 14),
            ),
            address_unwind,
        )


class _TestVPushParser(unittest.TestCase):
    def testCfaAndRegistersChange(self):
        parser = VPushParser()
        match = parser.GetBreakpadInstructionsRegex().search(
            '.cfa: sp 40 + unnamed_register264: .cfa -40 + ^ '
            'unnamed_register265: .cfa -32 + ^'
        )
        self.assertIsNotNone(match)

        address_unwind, new_cfa_sp_offset = parser.ParseFromMatch(
            address_offset=20, cfa_sp_offset=24, match=match
        )

        self.assertEqual(40, new_cfa_sp_offset)
        self.assertEqual(
            AddressUnwind(
                address_offset=20,
                unwind_type=UnwindType.UPDATE_SP_AND_OR_POP_REGISTERS,
                sp_offset=16,
                registers=(),
            ),
            address_unwind,
        )

    def testRegistersChange(self):
        parser = VPushParser()
        match = parser.GetBreakpadInstructionsRegex().search(
            'unnamed_register264: .cfa -40 + ^ unnamed_register265: .cfa -32 + ^'
        )
        self.assertIsNotNone(match)

        address_unwind, new_cfa_sp_offset = parser.ParseFromMatch(
            address_offset=20, cfa_sp_offset=24, match=match
        )

        self.assertEqual(24, new_cfa_sp_offset)
        self.assertEqual(
            AddressUnwind(
                address_offset=20,
                unwind_type=UnwindType.NO_ACTION,
                sp_offset=0,
                registers=(),
            ),
            address_unwind,
        )


class _TestStoreSpParser(unittest.TestCase):
    def testCfaAndRegistersChange(self):
        parser = StoreSpParser()
        match = parser.GetBreakpadInstructionsRegex().search('.cfa: r7 8 +')
        self.assertIsNotNone(match)

        address_unwind, new_cfa_sp_offset = parser.ParseFromMatch(
            address_offset=20, cfa_sp_offset=12, match=match
        )

        self.assertEqual(8, new_cfa_sp_offset)
        self.assertEqual(
            AddressUnwind(
                address_offset=20,
                unwind_type=UnwindType.RESTORE_SP_FROM_REGISTER,
                sp_offset=-4,
                registers=(7,),
            ),
            address_unwind,
        )


class _TestEncodeUnwindInstructionTable(unittest.TestCase):
    def testSingleEntry(self):
        table, offsets = EncodeUnwindInstructionTable([bytes([3])])

        self.assertEqual(bytes([3]), table)
        self.assertDictEqual(
            {
                bytes([3]): 0,
            },
            offsets,
        )

    def testMultipleEntries(self):
        self.maxDiff = None
        # Result should be sorted by score descending.
        table, offsets = EncodeUnwindInstructionTable(
            [
                bytes([1, 2, 3]),
                bytes([0, 3]),
                bytes([3]),
            ]
        )
        self.assertEqual(bytes([3, 0, 3, 1, 2, 3]), table)
        self.assertDictEqual(
            {
                bytes([1, 2, 3]): 3,  # score = 1 / 3 = 0.67
                bytes([0, 3]): 1,  # score = 1 / 2 = 0.5
                bytes([3]): 0,  # score = 1 / 1 = 1
            },
            offsets,
        )

        # When scores are same, sort by sequence descending.
        table, offsets = EncodeUnwindInstructionTable(
            [
                bytes([3]),
                bytes([0, 3]),
                bytes([0, 3]),
                bytes([1, 2, 3]),
                bytes([1, 2, 3]),
                bytes([1, 2, 3]),
            ]
        )
        self.assertEqual(bytes([3, 1, 2, 3, 0, 3]), table)
        self.assertDictEqual(
            {
                bytes([3]): 0,  # score = 1 / 1 = 1
                bytes([1, 2, 3]): 1,  # score = 3 / 3 = 1
                bytes([0, 3]): 4,  # score = 2 / 2 = 1
            },
            offsets,
        )


class _TestFunctionOffsetTable(unittest.TestCase):
    def testSingleEntry(self):
        self.maxDiff = None
        complete_instruction_sequence0 = bytes([3])
        complete_instruction_sequence1 = bytes([1, 3])

        sequence1 = (
            EncodedAddressUnwind(0x400, complete_instruction_sequence1),
            EncodedAddressUnwind(0x0, complete_instruction_sequence0),
        )

        address_unwind_sequences = [sequence1]

        table, offsets = EncodeFunctionOffsetTable(
            address_unwind_sequences,
            {
                complete_instruction_sequence0: 52,
                complete_instruction_sequence1: 50,
            },
        )

        self.assertEqual(
            bytes(
                [
                    # (0x200, 50)
                    128,
                    4,
                    50,
                    # (0, 52)
                    0,
                    52,
                ]
            ),
            table,
        )

        self.assertDictEqual(
            {
                sequence1: 0,
            },
            offsets,
        )

    def testMultipleEntry(self):
        self.maxDiff = None
        complete_instruction_sequence0 = bytes([3])
        complete_instruction_sequence1 = bytes([1, 3])
        complete_instruction_sequence2 = bytes([2, 3])

        sequence1 = (
            EncodedAddressUnwind(0x20, complete_instruction_sequence1),
            EncodedAddressUnwind(0x0, complete_instruction_sequence0),
        )
        sequence2 = (
            EncodedAddressUnwind(0x400, complete_instruction_sequence2),
            EncodedAddressUnwind(0x0, complete_instruction_sequence0),
        )
        address_unwind_sequences = [sequence1, sequence2]

        table, offsets = EncodeFunctionOffsetTable(
            address_unwind_sequences,
            {
                complete_instruction_sequence0: 52,
                complete_instruction_sequence1: 50,
                complete_instruction_sequence2: 80,
            },
        )

        self.assertEqual(
            bytes(
                [
                    # (0x10, 50)
                    0x10,
                    50,
                    # (0, 52)
                    0,
                    52,
                    # (0x200, 80)
                    128,
                    4,
                    80,
                    # (0, 52)
                    0,
                    52,
                ]
            ),
            table,
        )

        self.assertDictEqual(
            {
                sequence1: 0,
                sequence2: 4,
            },
            offsets,
        )

    def testDuplicatedEntry(self):
        self.maxDiff = None
        complete_instruction_sequence0 = bytes([3])
        complete_instruction_sequence1 = bytes([1, 3])
        complete_instruction_sequence2 = bytes([2, 3])

        sequence1 = (
            EncodedAddressUnwind(0x20, complete_instruction_sequence1),
            EncodedAddressUnwind(0x0, complete_instruction_sequence0),
        )
        sequence2 = (
            EncodedAddressUnwind(0x400, complete_instruction_sequence2),
            EncodedAddressUnwind(0x0, complete_instruction_sequence0),
        )
        sequence3 = sequence1

        address_unwind_sequences = [sequence1, sequence2, sequence3]

        table, offsets = EncodeFunctionOffsetTable(
            address_unwind_sequences,
            {
                complete_instruction_sequence0: 52,
                complete_instruction_sequence1: 50,
                complete_instruction_sequence2: 80,
            },
        )

        self.assertEqual(
            bytes(
                [
                    # (0x10, 50)
                    0x10,
                    50,
                    # (0, 52)
                    0,
                    52,
                    # (0x200, 80)
                    128,
                    4,
                    80,
                    # (0, 52)
                    0,
                    52,
                ]
            ),
            table,
        )

        self.assertDictEqual(
            {
                sequence1: 0,
                sequence2: 4,
            },
            offsets,
        )


class _TestEncodePageTableAndFunctionTable(unittest.TestCase):
    def testMultipleFunctionUnwinds(self):
        address_unwind_sequence0 = (
            EncodedAddressUnwind(0x10, bytes([0, 3])),
            EncodedAddressUnwind(0x0, bytes([3])),
        )
        address_unwind_sequence1 = (
            EncodedAddressUnwind(0x10, bytes([1, 3])),
            EncodedAddressUnwind(0x0, bytes([3])),
        )
        address_unwind_sequence2 = (
            EncodedAddressUnwind(0x200, bytes([2, 3])),
            EncodedAddressUnwind(0x0, bytes([3])),
        )

        function_unwinds = [
            EncodedFunctionUnwind(
                page_number=0,
                page_offset=0,
                address_unwinds=address_unwind_sequence0,
            ),
            EncodedFunctionUnwind(
                page_number=0,
                page_offset=0x8000,
                address_unwinds=address_unwind_sequence1,
            ),
            EncodedFunctionUnwind(
                page_number=1,
                page_offset=0x8000,
                address_unwinds=address_unwind_sequence2,
            ),
        ]

        function_offset_table_offsets = {
            address_unwind_sequence0: 0x100,
            address_unwind_sequence1: 0x200,
            address_unwind_sequence2: 0x300,
        }

        page_table, function_table = EncodePageTableAndFunctionTable(
            function_unwinds, function_offset_table_offsets
        )

        self.assertEqual(2 * 4, len(page_table))
        self.assertEqual((0, 2), struct.unpack('2I', page_table))

        self.assertEqual(6 * 2, len(function_table))
        self.assertEqual(
            (0, 0x100, 0x8000, 0x200, 0x8000, 0x300),
            struct.unpack('6H', function_table),
        )

    def testMultiPageFunction(self):
        address_unwind_sequence0 = (
            EncodedAddressUnwind(0x10, bytes([0, 3])),
            EncodedAddressUnwind(0x0, bytes([3])),
        )
        address_unwind_sequence1 = (
            EncodedAddressUnwind(0x10, bytes([1, 3])),
            EncodedAddressUnwind(0x0, bytes([3])),
        )
        address_unwind_sequence2 = (
            EncodedAddressUnwind(0x200, bytes([2, 3])),
            EncodedAddressUnwind(0x0, bytes([3])),
        )

        function_unwinds = [
            EncodedFunctionUnwind(
                page_number=0,
                page_offset=0,
                address_unwinds=address_unwind_sequence0,
            ),
            # Large function.
            EncodedFunctionUnwind(
                page_number=0,
                page_offset=0x8000,
                address_unwinds=address_unwind_sequence1,
            ),
            EncodedFunctionUnwind(
                page_number=4,
                page_offset=0x8000,
                address_unwinds=address_unwind_sequence2,
            ),
        ]

        function_offset_table_offsets = {
            address_unwind_sequence0: 0x100,
            address_unwind_sequence1: 0x200,
            address_unwind_sequence2: 0x300,
        }

        page_table, function_table = EncodePageTableAndFunctionTable(
            function_unwinds, function_offset_table_offsets
        )

        self.assertEqual(5 * 4, len(page_table))
        self.assertEqual((0, 2, 2, 2, 2), struct.unpack('5I', page_table))

        self.assertEqual(6 * 2, len(function_table))
        self.assertEqual(
            (0, 0x100, 0x8000, 0x200, 0x8000, 0x300),
            struct.unpack('6H', function_table),
        )


class MockReturnParser(UnwindInstructionsParser):
    def GetBreakpadInstructionsRegex(self):
        return re.compile(r'^RETURN$')

    def ParseFromMatch(self, address_offset, cfa_sp_offset, match):
        return AddressUnwind(address_offset, UnwindType.RETURN_TO_LR, 0, ()), 0


class MockEpilogueUnwindParser(UnwindInstructionsParser):
    def GetBreakpadInstructionsRegex(self):
        return re.compile(r'^EPILOGUE_UNWIND$')

    def ParseFromMatch(self, address_offset, cfa_sp_offset, match):
        return AddressUnwind(
            address_offset, UnwindType.UPDATE_SP_AND_OR_POP_REGISTERS, 0, ()
        ), -100


class MockWildcardParser(UnwindInstructionsParser):
    def GetBreakpadInstructionsRegex(self):
        return re.compile(r'.*')

    def ParseFromMatch(self, address_offset, cfa_sp_offset, match):
        return AddressUnwind(
            address_offset, UnwindType.UPDATE_SP_AND_OR_POP_REGISTERS, 0, ()
        ), -200


class _TestParseAddressCfi(unittest.TestCase):
    def testSuccessParse(self):
        address_unwind = AddressUnwind(
            address_offset=0x300,
            unwind_type=UnwindType.RETURN_TO_LR,
            sp_offset=0,
            registers=(),
        )

        self.assertEqual(
            (address_unwind, False, 0),
            ParseAddressCfi(
                AddressCfi(address=0x800, unwind_instructions='RETURN'),
                function_start_address=0x500,
                parsers=(MockReturnParser(),),
                prev_cfa_sp_offset=0,
            ),
        )

    def testUnhandledAddress(self):
        self.assertEqual(
            (None, False, 100),
            ParseAddressCfi(
                AddressCfi(address=0x800, unwind_instructions='UNKNOWN'),
                function_start_address=0x500,
                parsers=(MockReturnParser(),),
                prev_cfa_sp_offset=100,
            ),
        )

    def testEpilogueUnwind(self):
        self.assertEqual(
            (None, True, -100),
            ParseAddressCfi(
                AddressCfi(
                    address=0x800, unwind_instructions='EPILOGUE_UNWIND'
                ),
                function_start_address=0x500,
                parsers=(MockEpilogueUnwindParser(),),
                prev_cfa_sp_offset=100,
            ),
        )

    def testParsePrecedence(self):
        address_unwind = AddressUnwind(
            address_offset=0x300,
            unwind_type=UnwindType.RETURN_TO_LR,
            sp_offset=0,
            registers=(),
        )

        self.assertEqual(
            (address_unwind, False, 0),
            ParseAddressCfi(
                AddressCfi(address=0x800, unwind_instructions='RETURN'),
                function_start_address=0x500,
                parsers=(MockReturnParser(), MockWildcardParser()),
                prev_cfa_sp_offset=0,
            ),
        )


class _TestGenerateUnwinds(unittest.TestCase):
    def testSuccessUnwind(self):
        self.assertEqual(
            [
                FunctionUnwind(
                    address=0x100,
                    size=1024,
                    address_unwinds=(
                        AddressUnwind(
                            address_offset=0x0,
                            unwind_type=UnwindType.RETURN_TO_LR,
                            sp_offset=0,
                            registers=(),
                        ),
                        AddressUnwind(
                            address_offset=0x200,
                            unwind_type=UnwindType.RETURN_TO_LR,
                            sp_offset=0,
                            registers=(),
                        ),
                    ),
                )
            ],
            list(
                GenerateUnwinds(
                    [
                        FunctionCfi(
                            size=1024,
                            address_cfi=(
                                AddressCfi(
                                    address=0x100, unwind_instructions='RETURN'
                                ),
                                AddressCfi(
                                    address=0x300, unwind_instructions='RETURN'
                                ),
                            ),
                        )
                    ],
                    parsers=[MockReturnParser()],
                )
            ),
        )

    def testUnhandledAddress(self):
        self.assertEqual(
            [
                FunctionUnwind(
                    address=0x100,
                    size=1024,
                    address_unwinds=(
                        AddressUnwind(
                            address_offset=0x0,
                            unwind_type=UnwindType.RETURN_TO_LR,
                            sp_offset=0,
                            registers=(),
                        ),
                    ),
                )
            ],
            list(
                GenerateUnwinds(
                    [
                        FunctionCfi(
                            size=1024,
                            address_cfi=(
                                AddressCfi(
                                    address=0x100, unwind_instructions='RETURN'
                                ),
                                AddressCfi(
                                    address=0x300, unwind_instructions='UNKNOWN'
                                ),
                            ),
                        )
                    ],
                    parsers=[MockReturnParser()],
                )
            ),
        )

    def testEpilogueUnwind(self):
        self.assertEqual(
            [
                FunctionUnwind(
                    address=0x100,
                    size=1024,
                    address_unwinds=(
                        AddressUnwind(
                            address_offset=0x0,
                            unwind_type=UnwindType.RETURN_TO_LR,
                            sp_offset=0,
                            registers=(),
                        ),
                    ),
                )
            ],
            list(
                GenerateUnwinds(
                    [
                        FunctionCfi(
                            size=1024,
                            address_cfi=(
                                AddressCfi(
                                    address=0x100, unwind_instructions='RETURN'
                                ),
                                AddressCfi(
                                    address=0x300,
                                    unwind_instructions='EPILOGUE_UNWIND',
                                ),
                            ),
                        )
                    ],
                    parsers=[MockReturnParser(), MockEpilogueUnwindParser()],
                )
            ),
        )

    def testInvalidInitialUnwindInstructionAsserts(self):
        self.assertRaises(
            AssertionError,
            lambda: list(
                GenerateUnwinds(
                    [
                        FunctionCfi(
                            size=1024,
                            address_cfi=(
                                AddressCfi(
                                    address=0x100, unwind_instructions='UNKNOWN'
                                ),
                                AddressCfi(
                                    address=0x200, unwind_instructions='RETURN'
                                ),
                            ),
                        )
                    ],
                    parsers=[MockReturnParser()],
                )
            ),
        )


class _TestEncodeUnwindInfo(unittest.TestCase):
    def testEncodeTables(self):
        page_table = struct.pack('I', 0)
        function_table = struct.pack('4H', 1, 2, 3, 4)
        function_offset_table = bytes([1, 2])
        unwind_instruction_table = bytes([1, 2, 3])

        unwind_info = EncodeUnwindInfo(
            page_table,
            function_table,
            function_offset_table,
            unwind_instruction_table,
        )

        self.assertEqual(
            32
            + len(page_table)
            + len(function_table)
            + len(function_offset_table)
            + len(unwind_instruction_table),
            len(unwind_info),
        )
        # Header.
        self.assertEqual(
            (32, 1, 36, 2, 44, 2, 46, 3), struct.unpack('8I', unwind_info[:32])
        )
        # Body.
        self.assertEqual(
            page_table
            + function_table
            + function_offset_table
            + unwind_instruction_table,
            unwind_info[32:],
        )

    def testUnalignedTables(self):
        self.assertRaises(
            AssertionError, lambda: EncodeUnwindInfo(bytes([1]), b'', b'', b'')
        )
        self.assertRaises(
            AssertionError, lambda: EncodeUnwindInfo(b'', bytes([1]), b'', b'')
        )


class _TestGenerateUnwindTables(unittest.TestCase):
    def testGenerateUnwindTables(self):
        """This is an integration test that hooks everything together."""
        address_unwind_sequence0 = (
            EncodedAddressUnwind(0x20, bytes([0, 0xB0])),
            EncodedAddressUnwind(0x0, bytes([0xB0])),
        )
        address_unwind_sequence1 = (
            EncodedAddressUnwind(0x20, bytes([1, 0xB0])),
            EncodedAddressUnwind(0x0, bytes([0xB0])),
        )
        address_unwind_sequence2 = (
            EncodedAddressUnwind(0x200, bytes([2, 0xB0])),
            EncodedAddressUnwind(0x0, bytes([0xB0])),
        )

        (
            page_table,
            function_table,
            function_offset_table,
            unwind_instruction_table,
        ) = GenerateUnwindTables(
            [
                EncodedFunctionUnwind(
                    page_number=0, page_offset=0, address_unwinds=TRIVIAL_UNWIND
                ),
                EncodedFunctionUnwind(
                    page_number=0,
                    page_offset=0x1000,
                    address_unwinds=address_unwind_sequence0,
                ),
                EncodedFunctionUnwind(
                    page_number=1,
                    page_offset=0x2000,
                    address_unwinds=address_unwind_sequence1,
                ),
                EncodedFunctionUnwind(
                    page_number=3,
                    page_offset=0x1000,
                    address_unwinds=address_unwind_sequence2,
                ),
            ]
        )

        # Complete instruction sequences and their frequencies.
        # [0xb0]: 4
        # [0, 0xb0]: 1
        # [1, 0xb0]: 1
        # [2, 0xb0]: 1
        self.assertEqual(
            bytes([0xB0, 2, 0xB0, 1, 0xB0, 0, 0xB0]), unwind_instruction_table
        )

        self.assertEqual(
            bytes(
                [
                    # Trivial unwind.
                    0,
                    0,
                    # Address unwind sequence 0.
                    0x10,
                    5,
                    0,
                    0,
                    # Address unwind sequence 1.
                    0x10,
                    3,
                    0,
                    0,
                    # Address unwind sequence 2.
                    0x80,
                    2,
                    1,
                    0,
                    0,
                ]
            ),
            function_offset_table,
        )

        self.assertEqual(8 * 2, len(function_table))
        self.assertEqual(
            (0, 0, 0x1000, 2, 0x2000, 6, 0x1000, 10),
            struct.unpack('8H', function_table),
        )

        self.assertEqual(4 * 4, len(page_table))
        self.assertEqual((0, 2, 3, 3), struct.unpack('4I', page_table))
