Ian McQuillan

137 papers A 1B 4C 22Misc 1Journal 79Unranked 27
YearRankTypeTitle / Venue / Authors
2026 J jnl
Theor. Comput. Sci.
Noah Friesen, Oscar H. Ibarra, Jozef Jirásek, Ian McQuillan
2025 conf
CVPR Workshops
Nazifa Azam Khan, Mikolaj Cieslak, Mark G. Eramian, Ian McQuillan
2025 A conf
ECAI
Mohammad Mohammadzadeh Kalati, Farhad Maleki, Ian McQuillan
2025 J jnl
CoRR
Mohammad Mohammadzadeh Kalati, Farhad Maleki, Ian McQuillan
2025 J jnl
Nat. Comput.
Christopher Duffy, Sam Hillis, Umer Khan, Ian McQuillan, Sonja Linghui Shan
2025 J jnl
Int. J. Found. Comput. Sci.
Oscar H. Ibarra, Ian McQuillan
2025 C conf
DLT
Brennan Lockinger, Ian McQuillan
2025 J jnl
Nat. Comput.
Oscar H. Ibarra, Ian McQuillan
2025 J jnl
Theor. Comput. Sci.
Oscar H. Ibarra, Ian McQuillan
2025 J jnl
Inf. Comput.
Oscar H. Ibarra, Ian McQuillan
2025 C conf
DLT
Oscar H. Ibarra, Ian McQuillan
2025 conf
ISBRA (2)
C. Malcolm Todd, Yuan Tian, Nathaniel Osgood, Ian McQuillan, Lingling Jin
2025 J jnl
CoRR
Noah Friesen, Oscar H. Ibarra, Jozef Jirásek, Ian McQuillan
2024 J jnl
CoRR
Ali Lotfi, Ian McQuillan, Steven Rayan
2024 conf
ICCBB
Katie L. Ovens, Ebunoluwa Makinde, Samuel Horovatin, Elham Rezaei, Alan M. Rosenberg, Lingling Jin, Ian McQuillan, Anthony J. Kusalik, Farhad Maleki
2024 J jnl
CoRR
Nazifa Azam Khan, Mikolaj Cieslak, Ian McQuillan
2024 J jnl
CoRR
Ali Lotfi, Ian McQuillan
2024 J jnl
Comput. Sci. J. Moldova
Oscar H. Ibarra, Ian McQuillan
2024 conf
BIBM
C. Malcolm Todd, Lingling Jin, Ian McQuillan
2024 C conf
DLT
Oscar H. Ibarra, Ian McQuillan
2024 J jnl
CoRR
Oscar H. Ibarra, Ian McQuillan
2023 J jnl
Inf. Comput.
Oscar H. Ibarra, Ian McQuillan
2023 conf
ATVA (1)
Oscar H. Ibarra, Ian McQuillan
2023 J jnl
Inf. Comput.
Oscar H. Ibarra, Ian McQuillan
2023 J jnl
Soft Comput.
Jason Bernard, Ian McQuillan
2023 B conf
FoSSaCS
Pascal Baumann, Flavio D'Alessandro, Moses Ganardi, Oscar H. Ibarra, Ian McQuillan, Lia Schütze, Georg Zetzsche
2023 J jnl
CoRR
Pascal Baumann, Flavio D'Alessandro, Moses Ganardi, Oscar H. Ibarra, Ian McQuillan, Lia Schütze, Georg Zetzsche
2023 J jnl
Theory Comput. Syst.
Jozef Jirásek, Ian McQuillan
2022 J jnl
CoRR
Oscar H. Ibarra, Ian McQuillan
2022 J jnl
CoRR
Oscar H. Ibarra, Ian McQuillan
2022 C conf
DLT
Oscar H. Ibarra, Ian McQuillan
2022 J jnl
CoRR
Arturo Carpi, Flavio D'Alessandro, Oscar H. Ibarra, Ian McQuillan
2022 J jnl
CoRR
Oscar H. Ibarra, Ian McQuillan
2022 J jnl
CoRR
Oscar H. Ibarra, Jozef Jirásek, Ian McQuillan, Luca Prigioniero
2022 J jnl
CoRR
Oscar H. Ibarra, Ian McQuillan
2022 C conf
DLT
Jozef Jirásek, Ian McQuillan
2021 J jnl
Int. J. Found. Comput. Sci.
Oscar H. Ibarra, Ian McQuillan
2021 J jnl
BMC Bioinform.
Katie L. Ovens, Farhad Maleki, B. Frank Eames, Ian McQuillan
2021 J jnl
Inf. Comput.
Flavio D'Alessandro, Oscar H. Ibarra, Ian McQuillan
2021 J jnl
Nat. Comput.
Ian McQuillan, Shinnosuke Seki
2021 J jnl
Theor. Comput. Sci.
Arturo Carpi, Flavio D'Alessandro, Oscar H. Ibarra, Ian McQuillan
2021 J jnl
Int. J. Found. Comput. Sci.
Oscar H. Ibarra, Jozef Jirásek, Ian McQuillan, Luca Prigioniero
2021 J jnl
Swarm Evol. Comput.
Jason Bernard, Ian McQuillan
2020 conf
CVPR Workshops
Nazifa Azam Khan, Oliver A. S. Lyon, Mark G. Eramian, Ian McQuillan
2020 J jnl
CoRR
Nazifa Azam Khan, Oliver A. S. Lyon, Mark G. Eramian, Ian McQuillan
2020 J jnl
CoRR
Rifat Zahan, Ian McQuillan, Nathaniel D. Osgood
2020 B conf
ICTAI
Jason Bernard, Ian McQuillan
2020 J jnl
CoRR
Oscar H. Ibarra, Ian McQuillan
2020 J jnl
Int. J. Found. Comput. Sci.
Oscar H. Ibarra, Ian McQuillan
2020 C conf
DLT
Oscar H. Ibarra, Jozef Jirásek Jr., Ian McQuillan, Luca Prigioniero
2020 J jnl
CoRR
Jason Bernard, Ian McQuillan
2020 conf
BCB
Katie L. Ovens, B. Frank Eames, Ian McQuillan
2019 J jnl
CoRR
Mark Daley, Mark G. Eramian, Christopher Power, Ian McQuillan
2019 conf
BCB
Farhad Maleki, Katie L. Ovens, Ian McQuillan, Elham Rezaei, Alan M. Rosenberg, Anthony J. Kusalik
2019 J jnl
CoRR
Joey Eremondi, Oscar H. Ibarra, Ian McQuillan
2019 J jnl
J. Comput. Syst. Sci.
Joey Eremondi, Oscar H. Ibarra, Ian McQuillan
2019 J jnl
CoRR
Oscar H. Ibarra, Ian McQuillan, Bala Ravikumar
2019 J jnl
Theor. Comput. Sci.
Oscar H. Ibarra, Ian McQuillan, Bala Ravikumar
2019 J jnl
Theor. Comput. Sci.
Oscar H. Ibarra, Ian McQuillan
2019 J jnl
Inf. Comput.
Oscar H. Ibarra, Ian McQuillan
2019 J jnl
CoRR
Joey Eremondi, Oscar H. Ibarra, Ian McQuillan
2019 J jnl
CoRR
Oscar H. Ibarra, Ian McQuillan
2019 J jnl
Theor. Comput. Sci.
Oscar H. Ibarra, Ian McQuillan
2019 J jnl
CoRR
Jason Bernard, Ian McQuillan
2019 ed.
UCNC
Ian McQuillan, Shinnosuke Seki
2018 C conf
ALIFE
Jason Bernard, Ian McQuillan
2018 conf
UCNC
Ian McQuillan, Jason Bernard, Przemyslaw Prusinkiewicz
2018 conf
ICHI
Rifat Zahan, Ian McQuillan, Nathaniel Osgood
2018 J jnl
J. Autom. Lang. Comb.
Mark Daley, Mark G. Eramian, Christopher Power, Ian McQuillan
2018 C conf
DLT
Oscar H. Ibarra, Ian McQuillan
2018 B conf
ICTAI
Jason Bernard, Ian McQuillan
2018 ch.
Reversibility and Universality
Oscar H. Ibarra, Ian McQuillan
2018 conf
BIOMA
Jason Bernard, Ian McQuillan
2018 C conf
DLT
Oscar H. Ibarra, Ian McQuillan, Bala Ravikumar
2018 J jnl
Theor. Comput. Sci.
Oscar H. Ibarra, Ian McQuillan
2018 J jnl
Int. J. Found. Comput. Sci.
Joey Eremondi, Oscar H. Ibarra, Ian McQuillan
2018 J jnl
J. Autom. Lang. Comb.
Oscar H. Ibarra, Ian McQuillan
2018 J jnl
Inf. Comput.
Joey Eremondi, Oscar H. Ibarra, Ian McQuillan
2018 conf
BIBM
Lingling Jin, Ian McQuillan
2018 conf
BIBM
Farhad Maleki, Katie L. Ovens, Ian McQuillan, Anthony J. Kusalik
2018 C conf
CIAA
Oscar H. Ibarra, Ian McQuillan
2018 conf
DCFS
Oscar H. Ibarra, Ian McQuillan
2018 J jnl
Theor. Comput. Sci.
Oscar H. Ibarra, Ian McQuillan
2017 conf
AlCoB
T. M. Rezwanul Islam, Ian McQuillan
2017 J jnl
Inf. Comput.
Joey Eremondi, Oscar H. Ibarra, Ian McQuillan
2017 conf
UCNC
Nazifa Azam Khan, Ian McQuillan
2017 J jnl
CoRR
Jason Bernard, Ian McQuillan
2017 C conf
LATA
Flavio D'Alessandro, Oscar H. Ibarra, Ian McQuillan
2017 J jnl
CoRR
Oscar H. Ibarra, Ian McQuillan
2017 C conf
DLT
Oscar H. Ibarra, Ian McQuillan
2017 J jnl
CoRR
Oscar H. Ibarra, Ian McQuillan
2016 conf
BIBM
Lingling Jin, Ian McQuillan, Longhai Li
2016 J jnl
Soft Comput.
Lingling Jin, Ian McQuillan
2016 J jnl
CoRR
Joey Eremondi, Oscar H. Ibarra, Ian McQuillan
2016 C conf
CIAA
Oscar H. Ibarra, Ian McQuillan
2016 C conf
DLT
Oscar H. Ibarra, Ian McQuillan
2016 J jnl
CoRR
Flavio D'Alessandro, Oscar H. Ibarra, Ian McQuillan
2016 J jnl
CoRR
Joey Eremondi, Oscar H. Ibarra, Ian McQuillan
2016 J jnl
Theor. Comput. Sci.
Oscar H. Ibarra, Ian McQuillan
2015 C conf
TAMC
Joey Eremondi, Oscar H. Ibarra, Ian McQuillan
2015 C conf
LATA
Joey Eremondi, Oscar H. Ibarra, Ian McQuillan
2015 conf
DCFS
Joey Eremondi, Oscar H. Ibarra, Ian McQuillan
2015 C conf
DLT
Joey Eremondi, Oscar H. Ibarra, Ian McQuillan
2015 conf
UCNC
Md. Sowgat Ibne Mahmud, Ian McQuillan
2014 C conf
CIAA
Franziska Biegler, Ian McQuillan
2013 conf
TPNC
Lingling Jin, Ian McQuillan
2012 J jnl
Theor. Comput. Sci.
Franziska Biegler, Mark Daley, Ian McQuillan
2012 C conf
Developments in Language Theory
Markus Holzer, Sebastian Jakobi, Ian McQuillan
2012 J jnl
Int. J. Found. Comput. Sci.
Ian McQuillan, Giovanni Pighizzini
2011 conf
ICCABS
Jian Zhang, Ian McQuillan, Fang-Xiang Wu
2011 J jnl
Nat. Comput.
Mark Daley, Ian McQuillan, James M. McQuillan, Kalpana Mahalingam
2010 J jnl
Theor. Comput. Sci.
J. Mark Keil, Jing Liu, Ian McQuillan
2010 J jnl
Nat. Comput.
Mark Daley, Ian McQuillan
2010 J jnl
J. Autom. Lang. Comb.
Franziska Biegler, Mark Daley, Ian McQuillan
2010 ed.
DCFS
Ian McQuillan, Giovanni Pighizzini
2010 conf
BIBM Workshops
Jian Zhang, Ian McQuillan, Fang-Xiang Wu
2009 J jnl
Acta Cybern.
Helmut Jürgensen, Ian McQuillan
2009 conf
DCFS
Franziska Biegler, Mark Daley, Ian McQuillan
2009 J jnl
Theor. Comput. Sci.
Franziska Biegler, Mark Daley, Markus Holzer, Ian McQuillan
2008 conf
AFL
Helmut Jürgensen, Ian McQuillan
2008 C conf
IV
Christopher Power, Ian McQuillan, Helen Petrie, Peter Kennaugh, Mark Daley, Geoff Wozniak
2008 J jnl
J. Autom. Lang. Comb.
Mark Daley, Mark G. Eramian, Ian McQuillan
2007 J jnl
Theor. Comput. Sci.
Franziska Biegler, Ian McQuillan, Kai Salomaa
2006 conf
DCFS
Franziska Biegler, Ian McQuillan, Kai Salomaa
2006 Misc conf
COCOON
Ian McQuillan, Kai Salomaa, Mark Daley
2006 J jnl
Theory Comput. Syst.
Mark Daley, Ian McQuillan
2005 J jnl
Int. J. Found. Comput. Sci.
Mark Daley, Ian McQuillan
2005 B conf
DNA
Mark Daley, Ian McQuillan
2005 J jnl
Theor. Comput. Sci.
Mark Daley, Ian McQuillan
2005 J jnl
Theor. Comput. Sci.
Ian McQuillan
2004 J jnl
J. Autom. Lang. Comb.
Ian McQuillan
2004 J jnl
Theor. Comput. Sci.
Mark Daley, Lila Kari, Ian McQuillan
2004 C conf
CIAA
Mark Daley, Ian McQuillan
2003 C conf
CIAA
Mark Daley, Mark G. Eramian, Ian McQuillan
2003 conf
DCFS
Mark Daley, Ian McQuillan
2003 J jnl
J. Autom. Lang. Comb.
Mark Daley, Oscar H. Ibarra, Lila Kari, Ian McQuillan, Koji Nakano
2002 conf
DCFS
Ian McQuillan
tests/unit/test_apk_decompile_extractor.py
← Index tests/unit/test_apk_decompile_extractor.py python
"""
Unit tests for DecompileAPK extractor, export data, and ClickHouse schemas.

Tests the main DecompileAPK extractor class following the DecompileBinja test pattern.
All analysis is mocked — no JADX/apktool/Java installation required.
"""
import hashlib
import os
import tempfile
import zipfile

import pytest
from unittest.mock import MagicMock, patch, PropertyMock
from dataclasses import asdict

pytestmark = [pytest.mark.unit, pytest.mark.apk, pytest.mark.decompile]


def _create_fake_apk_file():
    """Create a minimal APK file for testing and return its path."""
    tmpdir = tempfile.mkdtemp()
    apk_path = os.path.join(tmpdir, "test.apk")
    with zipfile.ZipFile(apk_path, "w") as zf:
        zf.writestr("AndroidManifest.xml", b"<manifest/>")
        zf.writestr("classes.dex", b"dex\n035\x00" + b"\x00" * 100)
    return apk_path


# ============================================================================
# DecompileAPK Extractor Tests
# ============================================================================

class TestDecompileAPK:
    """Tests for the DecompileAPK extractor class."""

    def test_tag(self):
        from redb.extractors.decompiler.DecompileAPK import DecompileAPK
        apk_path = _create_fake_apk_file()
        try:
            ext = DecompileAPK(apk_path, MagicMock())
            assert ext.tag() == "apk_decompiled"
        finally:
            os.unlink(apk_path)

    def test_get_clickhouse_table(self):
        from redb.extractors.decompiler.DecompileAPK import DecompileAPK
        apk_path = _create_fake_apk_file()
        try:
            ext = DecompileAPK(apk_path, MagicMock())
            assert ext.get_clickhouse_table() is None
        finally:
            os.unlink(apk_path)

    def test_timeout_default(self):
        from redb.extractors.decompiler.DecompileAPK import DecompileAPK
        apk_path = _create_fake_apk_file()
        try:
            ext = DecompileAPK(apk_path, MagicMock())
            assert ext.APK_DECOMPILE_TIMEOUT == 600
        finally:
            os.unlink(apk_path)

    def test_timeout_from_env(self):
        from redb.extractors.decompiler.DecompileAPK import DecompileAPK
        apk_path = _create_fake_apk_file()
        try:
            with patch.dict(os.environ, {"APK_DECOMPILE_TIMEOUT": "900"}):
                ext = DecompileAPK(apk_path, MagicMock())
                assert ext.APK_DECOMPILE_TIMEOUT == 900
        finally:
            os.unlink(apk_path)

    def test_timeout_invalid_env(self):
        from redb.extractors.decompiler.DecompileAPK import DecompileAPK
        apk_path = _create_fake_apk_file()
        try:
            with patch.dict(os.environ, {"APK_DECOMPILE_TIMEOUT": "not_a_number"}):
                ext = DecompileAPK(apk_path, MagicMock())
                assert ext.APK_DECOMPILE_TIMEOUT == 600
        finally:
            os.unlink(apk_path)

    def test_calculate_md5(self):
        from redb.extractors.decompiler.DecompileAPK import DecompileAPK
        apk_path = _create_fake_apk_file()
        try:
            ext = DecompileAPK(apk_path, MagicMock())
            result = ext.calculate_md5("test_string")
            assert len(result) == 32
            assert result == hashlib.md5(b"test_string").hexdigest()
        finally:
            os.unlink(apk_path)

    def test_context_manager(self):
        from redb.extractors.decompiler.DecompileAPK import DecompileAPK
        apk_path = _create_fake_apk_file()
        try:
            with DecompileAPK(apk_path, MagicMock()) as ext:
                assert ext is not None
        finally:
            os.unlink(apk_path)

    def test_cleanup_run(self):
        from redb.extractors.decompiler.DecompileAPK import DecompileAPK
        apk_path = _create_fake_apk_file()
        try:
            ext = DecompileAPK(apk_path, MagicMock())
            ext.analyzer = MagicMock()
            ext.cleanup_run()
            assert ext.analyzer is None
        finally:
            os.unlink(apk_path)


# ============================================================================
# Export Data Tests
# ============================================================================

class TestPrepareExportData:
    """Tests for DecompileAPK.prepare_export_data() ClickHouse schemas."""

    def _make_extractor_with_results(self):
        from redb.extractors.decompiler.DecompileAPK import DecompileAPK
        apk_path = _create_fake_apk_file()
        ext = DecompileAPK(apk_path, MagicMock())
        ext.analysis_results = {
            "sha256": "a" * 64,
            "sha1": "b" * 40,
            "md5": "c" * 32,
            "decompiled_content": [
                {
                    "decompiled_method_hash": "d" * 64,
                    "decompiled_method": "public void foo() {}",
                    "decompiled_method_type": "USER",
                    "decompiled_has_string_encryption": False,
                    "decompiled_has_reflection_calls": False,
                    "decompiled_excessive_goto_count": False,
                }
            ],
            "decompiled_refs": [
                {
                    "decompiled_method_hash": "d" * 64,
                    "smali_method_hash": "e" * 64,
                    "decompiled_class_name": "com.example.Foo",
                    "decompiled_method_name": "foo",
                    "decompiled_method_signature": "()V",
                    "decompiled_method_prototype": "void foo()",
                    "functions_caller": ["bar"],
                    "functions_call": ["baz"],
                }
            ],
            "smali_content": [
                {
                    "smali_method_hash": "e" * 64,
                    "smali_method": "invoke-virtual {p0}, Lfoo;->bar()V\nreturn-void",
                    "smali_method_type": "USER",
                    "smali_instructions_count": 10,
                    "smali_register_count": 3,
                    "smali_has_string_encryption": False,
                    "smali_has_reflection_calls": False,
                    "smali_excessive_goto_count": False,
                    "smali_flattened_score": 0.0,
                    "smali_mba_score": 0.0,
                }
            ],
            "smali_refs": [
                {
                    "smali_method_hash": "e" * 64,
                    "decompiled_method_hash": "d" * 64,
                    "smali_class_name": "com.example.Foo",
                    "smali_method_name": "foo",
                    "smali_method_signature": "()V",
                    "ssdeep_smali": None,
                    "tlsh_smali": None,
                }
            ],
            "similarity_metrics": [
                {
                    "smali_method_hash": "e" * 64,
                    "cyclomatic_complexity": 2,
                    "ssdeep_smali": None,
                    "tlsh_smali": None,
                    "ssdeep_smali_normalized": None,
                    "tlsh_smali_normalized": None,
                    "minhash": [],
                }
            ],
            "cfg": [
                {
                    "smali_method_hash": "e" * 64,
                    "cfg_topology_hash": b'\x01' * 16,
                    "block_count": 3,
                    "edge_count": 3,
                    "cfg_instructions_count": 10,
                    "call_count": 1,
                    "cyclomatic_complexity": 2,
                    "loop_count": 0,
                    "max_depth": 2,
                    "max_fan_out": 2,
                    "md_index_topdown": 12345,
                    "md_index_bottomup": 67890,
                    "prime_product_smali": 999,
                    "cfg_feature_tlsh": None,
                    "wl_minhash": [0] * 128,
                    "bb_features": [[5, 1, 0, 1, 1, 0, 0, 2]] * 3,
                    "cfg_adjacency": [0x00000001, 0x00000002],
                }
            ],
            "strings": [
                {
                    "string": "https://evil.example.com/payload",
                    "string_encoding": "UTF8",
                    "string_offset": 0,
                    "string_length": 31,
                    "string_entropy": 3.95,
                }
            ],
            "analysis_errors": [
                {
                    "class_name": "com.example.Foo",
                    "method_name": "bar",
                    "error_location": "jadx",
                    "error_message": "timeout",
                    "error_type": "TimeoutError",
                }
            ],
        }
        return ext, apk_path

    def test_returns_none_when_no_results(self):
        from redb.extractors.decompiler.DecompileAPK import DecompileAPK
        apk_path = _create_fake_apk_file()
        try:
            ext = DecompileAPK(apk_path, MagicMock())
            assert ext.prepare_export_data("ClickHouseExporter") is None
        finally:
            os.unlink(apk_path)

    def test_multi_table_flag(self):
        ext, apk_path = self._make_extractor_with_results()
        try:
            export = ext.prepare_export_data("ClickHouseExporter")
            assert export["multi_table"] is True
        finally:
            os.unlink(apk_path)

    def test_all_table_keys_present(self):
        ext, apk_path = self._make_extractor_with_results()
        try:
            export = ext.prepare_export_data("ClickHouseExporter")
            assert "decompiled_content" in export
            assert "decompiled_refs" in export
            assert "smali_content" in export
            assert "smali_refs" in export
            assert "method_similarity_metrics" in export
            assert "strings_raw" in export
            assert "analysis_errors" in export
        finally:
            os.unlink(apk_path)

    def test_decompiled_content_schema(self):
        ext, apk_path = self._make_extractor_with_results()
        try:
            export = ext.prepare_export_data("ClickHouseExporter")
            dc = export["decompiled_content"]
            assert dc["table"] == "code_apk_decompiled_methods_content"
            assert len(dc["column_names"]) == 7
            assert len(dc["column_type_names"]) == 7
            assert "decompiled_method_hash" in dc["column_names"]
            assert "decompiled_method" in dc["column_names"]
            assert "analysis_date" in dc["column_names"]
            assert len(dc["data"]) == 1
            assert len(dc["data"][0]) == 7
        finally:
            os.unlink(apk_path)

    def test_decompiled_refs_schema(self):
        ext, apk_path = self._make_extractor_with_results()
        try:
            export = ext.prepare_export_data("ClickHouseExporter")
            dr = export["decompiled_refs"]
            assert dr["table"] == "code_apk_decompiled_methods_references"
            assert len(dr["column_names"]) == 10
            assert len(dr["column_type_names"]) == 10
            assert "sha256" in dr["column_names"]
            assert "sha1" not in dr["column_names"]
            assert "md5" not in dr["column_names"]
            assert "functions_caller" in dr["column_names"]
            assert "functions_call" in dr["column_names"]
        finally:
            os.unlink(apk_path)

    def test_smali_content_schema(self):
        ext, apk_path = self._make_extractor_with_results()
        try:
            export = ext.prepare_export_data("ClickHouseExporter")
            sc = export["smali_content"]
            assert sc["table"] == "code_apk_smali_methods_content"
            assert len(sc["column_names"]) == 11
            assert len(sc["column_type_names"]) == 11
            assert "smali_instructions_count" in sc["column_names"]
            assert "smali_register_count" in sc["column_names"]
            assert "smali_flattened_score" in sc["column_names"]
            assert "smali_mba_score" in sc["column_names"]
        finally:
            os.unlink(apk_path)

    def test_smali_refs_schema(self):
        ext, apk_path = self._make_extractor_with_results()
        try:
            export = ext.prepare_export_data("ClickHouseExporter")
            sr = export["smali_refs"]
            assert sr["table"] == "code_apk_smali_methods_references"
            assert len(sr["column_names"]) == 7
            assert len(sr["column_type_names"]) == 7
            assert "sha1" not in sr["column_names"]
            assert "md5" not in sr["column_names"]
            assert "ssdeep_smali" not in sr["column_names"]
            assert "tlsh_smali" not in sr["column_names"]
        finally:
            os.unlink(apk_path)

    def test_similarity_metrics_schema(self):
        ext, apk_path = self._make_extractor_with_results()
        try:
            export = ext.prepare_export_data("ClickHouseExporter")
            sm = export["method_similarity_metrics"]
            assert sm["table"] == "code_apk_method_similarity_metrics"
            assert len(sm["column_names"]) == 7
            assert "minhash" in sm["column_names"]
            assert "ssdeep_smali_normalized" in sm["column_names"]
            assert "tlsh_smali_normalized" in sm["column_names"]
            # cyclomatic_complexity moved to cfg table
            assert "cyclomatic_complexity" not in sm["column_names"]
            assert "block_count" not in sm["column_names"]
            assert "loop_count" not in sm["column_names"]
        finally:
            os.unlink(apk_path)

    def test_analysis_errors_schema(self):
        ext, apk_path = self._make_extractor_with_results()
        try:
            export = ext.prepare_export_data("ClickHouseExporter")
            ae = export["analysis_errors"]
            assert ae["table"] == "code_apk_analysis_errors"
            assert len(ae["column_names"]) == 9
            assert "error_hash" in ae["column_names"]
            assert "status" in ae["column_names"]
        finally:
            os.unlink(apk_path)

    def test_sha256_propagated_to_refs(self):
        ext, apk_path = self._make_extractor_with_results()
        try:
            export = ext.prepare_export_data("ClickHouseExporter")
            # Check that sha256 from analysis_results appears in ref rows
            dr_row = export["decompiled_refs"]["data"][0]
            assert dr_row[0] == "a" * 64  # sha256
            # sha1 and md5 no longer included in refs
            assert "sha1" not in export["decompiled_refs"]["column_names"]
            assert "md5" not in export["decompiled_refs"]["column_names"]
        finally:
            os.unlink(apk_path)

    def test_empty_tables_not_included(self):
        from redb.extractors.decompiler.DecompileAPK import DecompileAPK
        apk_path = _create_fake_apk_file()
        try:
            ext = DecompileAPK(apk_path, MagicMock())
            ext.analysis_results = {
                "sha256": "a" * 64,
                "sha1": "b" * 40,
                "md5": "c" * 32,
                "decompiled_content": [],
                "decompiled_refs": [],
                "smali_content": [],
                "smali_refs": [],
                "similarity_metrics": [],
                "cfg": [],
                "strings": [],
                "analysis_errors": [],
            }
            export = ext.prepare_export_data("ClickHouseExporter")
            assert export == {"multi_table": True}
        finally:
            os.unlink(apk_path)

    def test_non_clickhouse_exporter_returns_none(self):
        ext, apk_path = self._make_extractor_with_results()
        try:
            result = ext.prepare_export_data("ElasticsearchExporter")
            assert result is None
        finally:
            os.unlink(apk_path)


# ============================================================================
# Dataclass Tests
# ============================================================================

class TestAPKCodeAnalysisDataclasses:
    """Tests for the new APK code analysis dataclasses."""

    def test_decompiled_method_content(self):
        from redb.models.dataclasses import APKDecompiledMethodContent
        dc = APKDecompiledMethodContent(
            decompiled_method_hash="a" * 64,
            decompiled_method="public void foo() {}",
            decompiled_method_type="USER",
        )
        assert dc.decompiled_has_string_encryption is False
        assert dc.decompiled_has_reflection_calls is False
        assert dc.decompiled_excessive_goto_count is False

    def test_decompiled_method_content_populated(self):
        from redb.models.dataclasses import APKDecompiledMethodContent
        dc = APKDecompiledMethodContent(
            decompiled_method_hash="a" * 64,
            decompiled_method="public void foo() {}",
            decompiled_method_type="USER",
            decompiled_has_string_encryption=True,
            decompiled_has_reflection_calls=True,
            decompiled_excessive_goto_count=True,
        )
        assert dc.decompiled_has_string_encryption is True
        d = asdict(dc)
        assert d["decompiled_method_hash"] == "a" * 64

    def test_decompiled_method_reference(self):
        from redb.models.dataclasses import APKDecompiledMethodReference
        dr = APKDecompiledMethodReference(
            sha256="a" * 64,
            sha1="b" * 40,
            md5="c" * 32,
            decompiled_method_hash="d" * 64,
        )
        assert dr.functions_caller == []
        assert dr.functions_call == []
        assert dr.smali_method_hash is None

    def test_smali_method_content(self):
        from redb.models.dataclasses import APKSmaliMethodContent
        sc = APKSmaliMethodContent(
            smali_method_hash="e" * 64,
            smali_method="invoke-virtual {p0}, Lfoo;->bar()V",
            smali_method_type="USER",
            smali_instructions_count=5,
            smali_register_count=3,
        )
        assert sc.smali_instructions_count == 5
        assert sc.smali_register_count == 3

    def test_smali_method_reference(self):
        from redb.models.dataclasses import APKSmaliMethodReference
        sr = APKSmaliMethodReference(
            sha256="a" * 64,
            sha1="b" * 40,
            md5="c" * 32,
            smali_method_hash="e" * 64,
        )
        assert sr.decompiled_method_hash is None
        assert sr.ssdeep_smali is None
        assert sr.tlsh_smali is None

    def test_method_similarity_metrics(self):
        from redb.models.dataclasses import APKMethodSimilarityMetrics
        ms = APKMethodSimilarityMetrics(smali_method_hash="e" * 64)
        assert ms.cyclomatic_complexity is None
        assert ms.ssdeep_smali_normalized is None
        assert ms.tlsh_smali_normalized is None
        assert ms.minhash == []

    def test_code_analysis_error(self):
        from redb.models.dataclasses import APKCodeAnalysisError
        err = APKCodeAnalysisError(
            sha256="a" * 64,
            error_location="jadx",
            error_message="timeout",
        )
        assert err.class_name is None
        assert err.method_name is None
        assert err.error_type is None


# ============================================================================
# Tag Enum Tests
# ============================================================================

class TestAPKDecompiledTag:
    """Tests for the APK_DECOMPILED tag enum."""

    def test_tag_exists(self):
        from redb.extractors.enum import Tag
        assert hasattr(Tag, "APK_DECOMPILED")

    def test_tag_value(self):
        from redb.extractors.enum import Tag
        assert Tag.APK_DECOMPILED.value == "apk_decompiled"


# ============================================================================
# Worker Integration Tests
# ============================================================================

class TestAPKWorkerDecompileIntegration:
    """Tests that DecompileAPK is registered in workers.py."""

    def test_module_registered(self):
        """Verify DecompileAPK is discoverable via the extractor registry."""
        from redb.extractor_registry import get_extractor_class
        cls = get_extractor_class("DecompileAPK")
        assert cls is not None, "DecompileAPK not found in extractor registry"
        assert cls.__name__ == "DecompileAPK"

    def test_worker_apk_dispatch_includes_decompile(self):
        import inspect
        import redb.workers as workers
        source = inspect.getsource(workers.process_binary_file)
        assert "DecompileAPK" in source