Jakob Axelsson

93 papers A* 1A 1B 8C 3Misc 1Journal 22Unranked 56
YearRankTypeTitle / Venue / Authors
2026 J jnl
Syst.
Joyce Martin, Jakob Axelsson, Jan Carlson
2026 J jnl
Future Gener. Comput. Syst.
Alessio Bucaioni, Jakob Axelsson, Moris Behnam, Enxhi Ferko
2026 C conf
MODELSWARD
Joyce Martin, Jakob Axelsson, Jan Carlson
2025 conf
SoSE
Terese Besker, Jakob Axelsson, Edgar Ramos, Valentin Tudor, Tomas Nordström
2025 conf
SoSE
Johan Cederbladh, Jakob Axelsson
2025 conf
SoSE
Joyce Martin, Johan Cederbladh, Jakob Axelsson
2025 J jnl
Syst.
Jakob Axelsson
2025 J jnl
ACM SIGSOFT Softw. Eng. Notes
Pablo Oliveira Antonino, Jakob Axelsson, Jason Jaskolka, Francesca Lonetti
2025 conf
SoSE
Zelalem Mihret Belay, Jakob Axelsson
2024 J jnl
Syst.
Zelalem Mihret Belay, Jakob Axelsson
2024 conf
SysCon
Joyce Martin, Jakob Axelsson, Jan Carlson, Jagadish Suryadevara
2024 conf
SESoS
Jakob Axelsson
2024 conf
SysCon
Zelalem Mihret, Jakob Axelsson, Eunkyoung Jee, Doo-Hwan Bae
2024 conf
SysCon
Marjorie Nawila Pettersson, Jakob Axelsson, Anna Johansson, Pontus Svenson
2024 conf
SOSE
Marjorie Nawila Pettersson, Jakob Axelsson, Anna Johansson, Pontus Svenson
2023 conf
SoSE
Jakob Axelsson, Peter Eriksson
2023 conf
SoSE
Terese Besker, Ulrik Franke, Jakob Axelsson
2023 conf
SoSE
Thomas Olsson, Jakob Axelsson
2023 conf
SoSE
Joyce Martin, Jakob Axelsson, Jan Carlson, Jagadish Suryadevara
2022 conf
SoSE
Pontus Svenson, Thomas Olsson, Jakob Axelsson
2022 conf
SoSE
Pontus Svenson, Jakob Axelsson
2022 conf
SoSE
Jakob Axelsson, Pontus Svenson
2022 conf
SoSE
Jakob Axelsson
2021 conf
SoSE
Pontus Svenson, Frida Reichenberg, Jakob Axelsson
2021 conf
SoSE
David Rylander, Jakob Axelsson
2021 conf
SoSE
Pontus Svenson, Jakob Axelsson
2021 J jnl
J. Softw. Evol. Process.
Claes Wohlin, Efi Papatheocharous, Jan Carlson, Kai Petersen, Emil Alégroth, Jakob Axelsson, Deepika Badampudi, Markus Borg, Antonio Cicchetti, Federico Ciccozzi, Thomas Olsson, Séverine Sentilles, Mikael Svahnberg, Krzysztof Wnuk, Tony Gorschek
2020 conf
SoSE
Jakob Axelsson
2020 conf
SoSE
Pontus Svenson, Jakob Axelsson
2020 conf
SoSE
Jakob Axelsson
2019 conf
SoSE
Jakob Axelsson
2019 J jnl
Syst. Eng.
Jakob Axelsson, Joakim Fröberg, Peter Eriksson
2019 conf
SoSE
Jakob Axelsson
2019 conf
SysCon
Jakob Axelsson
2019 J jnl
Inf. Softw. Technol.
Markus Borg, Panagiota Chatzipetrou, Krzysztof Wnuk, Emil Alégroth, Tony Gorschek, Efi Papatheocharous, Syed Muhammad Ali Shah, Jakob Axelsson
2018 conf
SoSE
Jakob Axelsson, Stina Nylander
2018 conf
SysCon
Jakob Axelsson
2018 J jnl
IEEE Trans. Software Eng.
Kai Petersen, Deepika Badampudi, Syed Muhammad Ali Shah, Krzysztof Wnuk, Tony Gorschek, Efi Papatheocharous, Jakob Axelsson, Séverine Sentilles, Ivica Crnkovic, Antonio Cicchetti
2018 conf
SoSE
Jakob Axelsson, Joakim Fröberg, Peter Eriksson
2018 conf
SysCon
Jakob Axelsson, Avenir Kobetski
2017 conf
SIES
Xinhai Zhang, Naveen Mohan, Martin Törngren, Jakob Axelsson, De-Jiu Chen
2017 J jnl
IEEE Trans. Intell. Transp. Syst.
Jakob Axelsson
2017 B conf
ENASE
Efi Papatheocharous, Kai Petersen, Jakob Axelsson, Claes Wohlin, Jan Carlson, Federico Ciccozzi, Séverine Sentilles, Antonio Cicchetti
2017 Misc conf
SAC
Avenir Kobetski, Jakob Axelsson
2017 conf
SysCon
Jakob Axelsson, Ulrik Franke, Jan Carlson, Séverine Sentilles, Antonio Cicchetti
2016 conf
SoSE
Sara Dersten, Peter Wallin, Joakim Fröberg, Jakob Axelsson
2016 J jnl
ACM SIGSOFT Softw. Eng. Notes
Jakob Axelsson, Efi Papatheocharous, Jaana Nyfjord, Martin Törngren
2016 J jnl
J. Syst. Softw.
Jakob Axelsson, Mats Skoglund
2015 conf
ECSA Workshops
Krzysztof Wnuk, Jakob Axelsson
2015 conf
SoSE
Jakob Axelsson
2015 conf
SysCon
Sara Dersten, Jakob Axelsson, Joakim Fröberg
2015 conf
ECSA Workshops
Jakob Axelsson
2015 conf
ICSOB
Efi Papatheocharous, Jesper Andersson, Jakob Axelsson
2015 J jnl
ERCIM News
Jakob Axelsson
2014 conf
ECSA Workshops
Jakob Axelsson, Avenir Kobetski
2014 J jnl
Inf. Softw. Technol.
Jakob Axelsson, Efi Papatheocharous, Jesper Andersson
2014 A* conf
DAC
Ze Ni, Avenir Kobetski, Jakob Axelsson
2014 B conf
Intelligent Vehicles Symposium
David Rylander, Jakob Axelsson, Peter Wallin
2014 conf
EUROMICRO-SEAA
Jakob Axelsson, Avenir Kobetski, Ze Ni, Shuzhou Zhang, Eilert Johansson
2014 J jnl
SIGBED Rev.
Shuzhou Zhang, Avenir Kobetski, Eilert Johansson, Jakob Axelsson, Huifeng Wang
2013 conf
SESoS
Efi Papatheocharous, Jakob Axelsson, Jesper Andersson
2013 J jnl
SIGBED Rev.
Jakob Axelsson, Avenir Kobetski
2013 J jnl
ERCIM News
Avenir Kobetski, Jakob Axelsson
2012 conf
CSER
Sara Dersten, Jakob Axelsson, Joakim Fröberg
2012 J jnl
ERCIM News
Jakob Axelsson
2012 J jnl
Inf. Softw. Technol.
Peter Wallin, Stig Larsson, Joakim Fröberg, Jakob Axelsson
2012 conf
VNC
David Rylander, Jakob Axelsson
2011 conf
ECBS
Håkan Gustavsson, Jakob Axelsson
2011 conf
EUROMICRO-SEAA
Sara Dersten, Jakob Axelsson, Joakim Fröberg
2011 B conf
ECSA
Jakob Axelsson
2010 conf
EUROMICRO-SEAA
Sara Dersten, Joakim Fröberg, Jakob Axelsson, Rikard Land
2010 conf
SHARK@ICSE
Ipek Ozkaya, Peter Wallin, Jakob Axelsson
2010 conf
ECSA Companion Volume
Jakob Axelsson
2009 conf
WICSA/ECSA
Jakob Axelsson
2009 conf
HICSS
Peter Wallin, Stefan Johnsson, Jakob Axelsson
2008 conf
ECBS
Peter Wallin, Jakob Axelsson
2008 B conf
SPLC
Håkan Gustavsson, Jakob Axelsson
2005 ch.
The Industrial Information Technology Handbook
Jakob Axelsson, Joakim Fröberg, Hans Hansson, Christer Norström, Kristian Sandström, Björn Villing
2005 B conf
RTCSA
Jakob Axelsson
2005 conf
ICCBSS
Ivica Crnkovic, Jakob Axelsson, Susanne Graf, Magnus Larsson, Rob C. van Ommering, Kurt C. Wallnau
2005 J jnl
ACM SIGSOFT Softw. Eng. Notes
Ulrik Eklund, Örjan Askerdal, Johan Granholm, Anders Alminger, Jakob Axelsson
2001 C conf
ISORC
Jakob Axelsson
2001 A conf
DATE
Jakob Axelsson
2001 conf
ECBS
Jakob Axelsson
2000 B conf
ICECCS
Jakob Axelsson
1999 B conf
ECRTS
Henrik Lönn, Jakob Axelsson
1999 C conf
ISORC
Jakob Axelsson
1997 conf
RTS
Jakob Axelsson
1997 J jnl
J. Syst. Archit.
Jakob Axelsson
1997 conf
CODES
Jakob Axelsson
1996 J jnl
J. Syst. Archit.
Jakob Axelsson
1995 conf
RTS
Jakob Axelsson
1993 B conf
ICTAI
Jakob Axelsson, Stefan Menth, Klaus Semmler
tests/unit/test_cfg_features.py
← Index tests/unit/test_cfg_features.py python
"""
Unit tests for cfg_features.py — all new CFG feature computations.

These tests use plain Python data structures (index-based adjacency lists)
and require no Binary Ninja dependency.
"""
import pytest

from redb.extractors.decompiler.bninja.analysis.cfg_features import (
    bfs_order,
    bfs_max_depth,
    count_back_edges,
    compute_topology_hash,
    compute_md_index_topdown,
    compute_md_index_bottomup,
    compute_prime_product,
    build_block_features,
    compute_cfg_feature_tlsh,
    compute_wl_minhash,
    pack_adjacency,
    LLIL_OP_CATEGORIES,
    CAT_ARITHMETIC,
    CAT_LOGIC,
    CAT_CALL,
    CAT_MEMORY,
    NUM_WL_MINHASH_PERMS,
)


# ===================================================================
# Helper: common graph topologies
# ===================================================================

def _linear_chain(n):
    """0 -> 1 -> 2 -> ... -> (n-1)"""
    return [[i + 1] if i < n - 1 else [] for i in range(n)]


def _diamond():
    """
    0 -> 1, 0 -> 2, 1 -> 3, 2 -> 3
    (classic if/else diamond)
    """
    return [[1, 2], [3], [3], []]


def _predecessors_from_successors(successors, n):
    preds = [[] for _ in range(n)]
    for src, targets in enumerate(successors):
        for tgt in targets:
            preds[tgt].append(src)
    return preds


# ===================================================================
# TestBfsOrder
# ===================================================================

class TestBfsOrder:
    def test_empty_graph(self):
        assert bfs_order([], 0) == []

    def test_single_node(self):
        assert bfs_order([[]], 1) == [0]

    def test_linear_chain(self):
        succs = _linear_chain(4)
        assert bfs_order(succs, 4) == [0, 1, 2, 3]

    def test_diamond(self):
        succs = _diamond()
        order = bfs_order(succs, 4)
        assert order[0] == 0
        assert order[-1] == 3
        assert set(order) == {0, 1, 2, 3}

    def test_unreachable_nodes(self):
        # 0 -> 1, node 2 is unreachable
        succs = [[1], [], []]
        order = bfs_order(succs, 3)
        assert order[:2] == [0, 1]
        assert 2 in order  # unreachable appended

    def test_all_nodes_visited(self):
        succs = _diamond()
        order = bfs_order(succs, 4)
        assert len(order) == 4


# ===================================================================
# TestBfsMaxDepth
# ===================================================================

class TestBfsMaxDepth:
    def test_empty_graph(self):
        assert bfs_max_depth([], 0) == 0

    def test_single_block(self):
        assert bfs_max_depth([[]], 1) == 0

    def test_linear_chain(self):
        succs = _linear_chain(5)
        assert bfs_max_depth(succs, 5) == 4

    def test_diamond(self):
        succs = _diamond()
        assert bfs_max_depth(succs, 4) == 2

    def test_wide_graph(self):
        # 0 -> 1, 0 -> 2, 0 -> 3 (all at depth 1)
        succs = [[1, 2, 3], [], [], []]
        assert bfs_max_depth(succs, 4) == 1


# ===================================================================
# TestCountBackEdges
# ===================================================================

class TestCountBackEdges:
    def test_empty_graph(self):
        assert count_back_edges([], 0) == 0

    def test_no_loops(self):
        succs = _linear_chain(3)
        assert count_back_edges(succs, 3) == 0

    def test_single_loop(self):
        # 0 -> 1 -> 2 -> 0 (one back edge: 2->0)
        succs = [[1], [2], [0]]
        assert count_back_edges(succs, 3) == 1

    def test_nested_loops(self):
        # 0 -> 1 -> 2 -> 1 (inner), 2 -> 3 -> 0 (outer)
        succs = [[1], [2], [1, 3], [0]]
        assert count_back_edges(succs, 4) == 2

    def test_self_loop(self):
        # 0 -> 0 (self-loop)
        succs = [[0]]
        assert count_back_edges(succs, 1) == 1

    def test_diamond_no_loops(self):
        succs = _diamond()
        assert count_back_edges(succs, 4) == 0

    def test_single_node_no_loop(self):
        succs = [[]]
        assert count_back_edges(succs, 1) == 0


# ===================================================================
# TestTopologyHash
# ===================================================================

class TestTopologyHash:
    def test_same_graph_same_hash(self):
        succs = _diamond()
        bfs = bfs_order(succs, 4)
        h1 = compute_topology_hash(succs, bfs, 4)
        h2 = compute_topology_hash(succs, bfs, 4)
        assert h1 == h2

    def test_different_graphs_different_hash(self):
        succs1 = _linear_chain(3)
        bfs1 = bfs_order(succs1, 3)
        h1 = compute_topology_hash(succs1, bfs1, 3)

        succs2 = _diamond()
        bfs2 = bfs_order(succs2, 4)
        h2 = compute_topology_hash(succs2, bfs2, 4)

        assert h1 != h2

    def test_returns_16_bytes(self):
        succs = _diamond()
        bfs = bfs_order(succs, 4)
        h = compute_topology_hash(succs, bfs, 4)
        assert isinstance(h, bytes)
        assert len(h) == 16

    def test_isomorphic_graphs_same_hash(self):
        # Graph A: 0->1, 0->2, 1->3, 2->3 (diamond with successors [1,2])
        succs_a = [[1, 2], [3], [3], []]
        # Graph B: same structure but successors listed as [2,1]
        # BFS from 0 will visit them in different order, but after remapping
        # the canonical form should be identical for isomorphic graphs
        succs_b = [[2, 1], [3], [3], []]

        bfs_a = bfs_order(succs_a, 4)
        bfs_b = bfs_order(succs_b, 4)

        h_a = compute_topology_hash(succs_a, bfs_a, 4)
        h_b = compute_topology_hash(succs_b, bfs_b, 4)
        assert h_a == h_b

    def test_empty_graph(self):
        h = compute_topology_hash([], [], 0)
        assert h == b'\x00' * 16

    def test_single_node(self):
        succs = [[]]
        bfs = bfs_order(succs, 1)
        h = compute_topology_hash(succs, bfs, 1)
        assert isinstance(h, bytes)
        assert len(h) == 16


# ===================================================================
# TestMdIndex
# ===================================================================

class TestMdIndex:
    def test_single_block_topdown(self):
        succs = [[]]
        preds = [[]]
        bfs = [0]
        result = compute_md_index_topdown(succs, preds, bfs)
        assert isinstance(result, int)
        assert result > 0

    def test_single_block_bottomup(self):
        succs = [[]]
        preds = [[]]
        result = compute_md_index_bottomup(succs, preds, 1)
        assert isinstance(result, int)
        assert result > 0

    def test_linear_chain_topdown_vs_bottomup(self):
        succs = _linear_chain(4)
        preds = _predecessors_from_successors(succs, 4)
        bfs = bfs_order(succs, 4)
        td = compute_md_index_topdown(succs, preds, bfs)
        bu = compute_md_index_bottomup(succs, preds, 4)
        # Top-down and bottom-up should be different for a linear chain
        # (entry has in_deg=0, exit has out_deg=0, so the sequences differ)
        assert td != bu

    def test_deterministic(self):
        succs = _diamond()
        preds = _predecessors_from_successors(succs, 4)
        bfs = bfs_order(succs, 4)
        td1 = compute_md_index_topdown(succs, preds, bfs)
        td2 = compute_md_index_topdown(succs, preds, bfs)
        assert td1 == td2

    def test_different_graphs_different_index(self):
        succs1 = _linear_chain(3)
        preds1 = _predecessors_from_successors(succs1, 3)
        bfs1 = bfs_order(succs1, 3)
        td1 = compute_md_index_topdown(succs1, preds1, bfs1)

        succs2 = _diamond()
        preds2 = _predecessors_from_successors(succs2, 4)
        bfs2 = bfs_order(succs2, 4)
        td2 = compute_md_index_topdown(succs2, preds2, bfs2)

        assert td1 != td2

    def test_topdown_empty(self):
        assert compute_md_index_topdown([], [], []) == 0

    def test_bottomup_empty(self):
        assert compute_md_index_bottomup([], [], 0) == 0


# ===================================================================
# TestPrimeProduct
# ===================================================================

class TestPrimeProduct:
    def test_empty(self):
        assert compute_prime_product([]) == 0

    def test_known_sequence(self):
        # Use actual LLIL enum values from conftest_binja_stubs:
        # LLIL_NOP=0 -> prime 1, LLIL_LOAD=4 -> prime 5
        from redb.extractors.decompiler.bninja.analysis.cfg_features import LLIL_OP_PRIMES
        nop_val = 0   # LLIL_NOP
        load_val = 4  # LLIL_LOAD
        expected = LLIL_OP_PRIMES.get(nop_val, 1) * LLIL_OP_PRIMES.get(load_val, 1)
        result = compute_prime_product([nop_val, load_val])
        assert result == expected

    def test_order_independence(self):
        # LLIL_LOAD=4, LLIL_STORE=5, LLIL_ADD=13
        ops_a = [4, 5, 13]
        ops_b = [13, 4, 5]
        assert compute_prime_product(ops_a) == compute_prime_product(ops_b)

    def test_unknown_ops_map_to_1(self):
        # Unknown ops get prime 1, so they don't change the product
        result_known = compute_prime_product([4])  # LLIL_LOAD -> 5
        result_with_unknown = compute_prime_product([4, 9999])  # LOAD * unknown(1)
        assert result_known == result_with_unknown

    def test_mod_2_64(self):
        # Product should be mod 2^64
        result = compute_prime_product([4] * 1000)  # LLIL_LOAD
        assert 0 <= result < 2**64

    def test_single_op(self):
        # LLIL_STORE=5 -> prime 7
        assert compute_prime_product([5]) == 7


# ===================================================================
# TestBuildBlockFeatures
# ===================================================================

class TestBuildBlockFeatures:
    def test_empty_llil(self):
        succs = [[1], []]
        features = build_block_features([[], []], succs, 2)
        assert len(features) == 2
        # All zeros except successor_count
        assert features[0] == [0, 0, 0, 0, 0, 0, 0, 1]  # 1 successor
        assert features[1] == [0, 0, 0, 0, 0, 0, 0, 0]  # 0 successors

    def test_correct_categorization(self):
        # Set up categories for testing
        import redb.extractors.decompiler.bninja.analysis.cfg_features as cf
        old_cats = cf.LLIL_OP_CATEGORIES.copy()
        cf.LLIL_OP_CATEGORIES.update({
            100: CAT_ARITHMETIC,
            101: CAT_ARITHMETIC,
            200: CAT_LOGIC,
            300: CAT_CALL,
            400: CAT_MEMORY,
        })
        try:
            block_ops = [[100, 101, 200, 300, 400]]
            succs = [[]]
            features = build_block_features(block_ops, succs, 1)
            assert features[0][0] == 5   # instr_count
            assert features[0][1] == 2   # arithmetic
            assert features[0][2] == 1   # logic
            assert features[0][4] == 1   # call
            assert features[0][6] == 1   # memory
        finally:
            cf.LLIL_OP_CATEGORIES.clear()
            cf.LLIL_OP_CATEGORIES.update(old_cats)

    def test_cap_at_65535(self):
        # More than 65535 ops in one block
        huge_ops = [0] * 70000  # NOP x 70000
        succs = [[]]
        features = build_block_features([huge_ops], succs, 1)
        assert features[0][0] == 65535  # capped

    def test_missing_block_ops(self):
        # block_llil_ops shorter than n
        succs = [[1], [2], []]
        features = build_block_features([[1, 2]], succs, 3)
        assert len(features) == 3
        # Block 1 and 2 get empty ops since block_llil_ops only has 1 entry
        assert features[1] == [0, 0, 0, 0, 0, 0, 0, 1]
        assert features[2] == [0, 0, 0, 0, 0, 0, 0, 0]


# ===================================================================
# TestCfgFeatureTlsh
# ===================================================================

class TestCfgFeatureTlsh:
    def test_too_few_blocks_returns_none(self):
        # 5 blocks = 5 * 9 bytes = 45 < 50
        bb_features = [[10, 1, 0, 2, 0, 1, 1, 2]] * 5
        bfs = list(range(5))
        result = compute_cfg_feature_tlsh(bb_features, bfs)
        assert result is None

    def test_uniform_data_returns_none(self):
        # 7 identical blocks — TLSH returns TNULL for low-entropy input
        bb_features = [[10, 1, 0, 2, 0, 1, 1, 2]] * 7
        bfs = list(range(7))
        result = compute_cfg_feature_tlsh(bb_features, bfs)
        assert result is None

    def test_varied_data_returns_string(self):
        # 20 blocks with varied features — enough entropy for TLSH
        bb_features = [
            [i * 7 + 3, (i * 13) % 50, (i * 17) % 30, (i * 23) % 40,
             (i * 11) % 20, (i * 7) % 25, (i * 19) % 35, (i * 3) % 10]
            for i in range(20)
        ]
        bfs = list(range(20))
        result = compute_cfg_feature_tlsh(bb_features, bfs)
        assert isinstance(result, str)
        assert len(result) > 0
        assert result.startswith("T1")


# ===================================================================
# TestWlMinhash
# ===================================================================

class TestWlMinhash:
    def test_empty_function(self):
        result = compute_wl_minhash([], [], [], 0)
        assert result == [255] * NUM_WL_MINHASH_PERMS

    def test_returns_128_elements(self):
        succs = _diamond()
        preds = _predecessors_from_successors(succs, 4)
        bb_feats = [[5, 1, 0, 2, 0, 1, 1, 2]] * 4
        result = compute_wl_minhash(succs, preds, bb_feats, 4)
        assert len(result) == 128

    def test_all_uint8(self):
        succs = _linear_chain(3)
        preds = _predecessors_from_successors(succs, 3)
        bb_feats = [[3, 1, 0, 1, 0, 0, 1, 1]] * 3
        result = compute_wl_minhash(succs, preds, bb_feats, 3)
        assert all(0 <= v <= 255 for v in result)

    def test_identical_graphs_same_signature(self):
        succs = _diamond()
        preds = _predecessors_from_successors(succs, 4)
        bb_feats = [[5, 1, 0, 2, 0, 1, 1, 2]] * 4
        sig1 = compute_wl_minhash(succs, preds, bb_feats, 4)
        sig2 = compute_wl_minhash(succs, preds, bb_feats, 4)
        assert sig1 == sig2

    def test_different_graphs_different_signatures(self):
        # Graph 1: linear chain
        succs1 = _linear_chain(4)
        preds1 = _predecessors_from_successors(succs1, 4)
        bb_feats1 = [[5, 1, 0, 2, 0, 1, 1, i] for i in range(4)]
        sig1 = compute_wl_minhash(succs1, preds1, bb_feats1, 4)

        # Graph 2: diamond
        succs2 = _diamond()
        preds2 = _predecessors_from_successors(succs2, 4)
        bb_feats2 = [[10, 3, 2, 1, 0, 0, 0, i] for i in range(4)]
        sig2 = compute_wl_minhash(succs2, preds2, bb_feats2, 4)

        assert sig1 != sig2

    def test_single_node(self):
        succs = [[]]
        preds = [[]]
        bb_feats = [[1, 0, 0, 0, 0, 0, 0, 0]]
        result = compute_wl_minhash(succs, preds, bb_feats, 1)
        assert len(result) == 128


# ===================================================================
# TestPackAdjacency
# ===================================================================

class TestPackAdjacency:
    def test_empty(self):
        assert pack_adjacency([]) == []

    def test_single_edge(self):
        succs = [[1], []]
        edges = pack_adjacency(succs)
        assert len(edges) == 1
        assert edges[0] == (0 << 16) | 1

    def test_correct_packing(self):
        succs = _diamond()
        edges = pack_adjacency(succs)
        assert len(edges) == 4
        # 0->1, 0->2, 1->3, 2->3
        expected = {
            (0 << 16) | 1,
            (0 << 16) | 2,
            (1 << 16) | 3,
            (2 << 16) | 3,
        }
        assert set(edges) == expected

    def test_roundtrip(self):
        """Unpack edges and verify source/target pairs."""
        succs = [[1, 2], [3], [3], []]
        edges = pack_adjacency(succs)
        unpacked = [(e >> 16, e & 0xFFFF) for e in edges]
        expected = [(0, 1), (0, 2), (1, 3), (2, 3)]
        assert sorted(unpacked) == sorted(expected)

    def test_large_index_filtered(self):
        # Create a successor list where index >= 65536
        succs = [[] for _ in range(65537)]
        succs[0] = [65536]  # target is exactly 65536 — should be filtered
        edges = pack_adjacency(succs)
        assert len(edges) == 0

    def test_max_valid_index(self):
        # Index 65535 is the maximum valid
        succs = [[] for _ in range(65536)]
        succs[0] = [65535]
        edges = pack_adjacency(succs)
        assert len(edges) == 1
        assert edges[0] == (0 << 16) | 65535