about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorTheofilos Augoustis <theofilos.augoustis@gmail.com>2025-09-10 10:29:10 +0000
committerTheofilos Augoustis <theofilos.augoustis@gmail.com>2025-09-10 10:29:10 +0000
commitb90c46e2777ffec89c1fdbd8ded150fcc9ed4084 (patch)
tree60348e73e4b3a08a89d374684561c6ed5654d82d
parentaafb69e4d91b79fa5a8a3c31dd1004edb66af0bd (diff)
downloadfocaccia-b90c46e2777ffec89c1fdbd8ded150fcc9ed4084.tar.gz
focaccia-b90c46e2777ffec89c1fdbd8ded150fcc9ed4084.zip
Add support for running tests with flake check
-rw-r--r--flake.nix22
-rw-r--r--pyproject.toml6
-rw-r--r--tests/test_snapshot.py120
-rw-r--r--tests/test_sparse_memory.py55
-rw-r--r--uv.lock44
5 files changed, 164 insertions, 83 deletions
diff --git a/flake.nix b/flake.nix
index 8150a57..4141100 100644
--- a/flake.nix
+++ b/flake.nix
@@ -302,6 +302,28 @@
 				shellHook = uvShellHook;
 			};
 		};
+
+		checks = {
+			focaccia-tests = pkgs.stdenv.mkDerivation {
+				name = "focaccia-tests";
+				src = ./.;
+
+				doCheck = true;
+				dontBuild = true;
+
+				nativeCheckInputs = [ packages.dev pythonDevEnv ];
+
+				checkPhase = ''
+					set -euo pipefail
+					export REPO_ROOT="$PWD"
+					${packages.dev}/bin/python -m 'pytest' -q tests
+					touch $out
+				'';
+
+				env = uvEnv;
+				shellHook = uvShellHook;
+			};
+		};
 	});
 }
 
diff --git a/pyproject.toml b/pyproject.toml
index 4e323df..9a80ed2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -21,6 +21,7 @@ dependencies = [
 dev = [
 	"ruff",
 	"black",
+	"pytest",
 	"pyright",
 ]
 
@@ -70,3 +71,8 @@ reportMissingTypeStubs = false
 useLibraryCodeForTypes = true
 executionEnvironments = [ {root = "src"} ]
 
+[tool.pytest.ini_options]
+testpaths = ["tests"]
+python_files = ["test_*.py"]
+python_functions = ["test_*"]
+
diff --git a/tests/test_snapshot.py b/tests/test_snapshot.py
index ddad410..789caf5 100644
--- a/tests/test_snapshot.py
+++ b/tests/test_snapshot.py
@@ -1,74 +1,78 @@
 import unittest
+import pytest
 
 from focaccia.arch import x86
 from focaccia.snapshot import ProgramState, RegisterAccessError
 
-class TestProgramState(unittest.TestCase):
-    def setUp(self):
-        self.arch = x86.ArchX86()
+@pytest.fixture
+def arch():
+    return x86.ArchX86()
 
-    def test_register_access_empty_state(self):
-        state = ProgramState(self.arch)
-        for reg in x86.regnames:
-            self.assertRaises(RegisterAccessError, state.read_register, reg)
+@pytest.fixture
+def state(arch):
+    return ProgramState(arch)
 
-    def test_register_read_write(self):
-        state = ProgramState(self.arch)
-        for reg in x86.regnames:
-            state.set_register(reg, 0x42)
-        for reg in x86.regnames:
-            val = state.read_register(reg)
-            self.assertEqual(val, 0x42)
+@pytest.mark.parametrize("reg", x86.regnames)
+def test_register_access_empty_state(state, reg):
+    with pytest.raises(RegisterAccessError):
+        state.read_register(reg)
 
-    def test_register_aliases_empty_state(self):
-        state = ProgramState(self.arch)
-        for reg in self.arch.all_regnames:
-            self.assertRaises(RegisterAccessError, state.read_register, reg)
+def test_register_read_write(arch):
+    state = ProgramState(arch)
+    for reg in x86.regnames:
+        state.set_register(reg, 0x42)
+    for reg in x86.regnames:
+        val = state.read_register(reg)
+        assert val == 0x42
 
-    def test_register_aliases_read_write(self):
-        state = ProgramState(self.arch)
-        for reg in ['EAX', 'EBX', 'ECX', 'EDX']:
-            state.set_register(reg, 0xa0ff0)
+def test_register_aliases_empty_state(arch):
+    state = ProgramState(arch)
+    for reg in arch.all_regnames:
+        with pytest.raises(RegisterAccessError): 
+            state.read_register(reg)
 
-        for reg in ['AH', 'BH', 'CH', 'DH']:
-            self.assertEqual(state.read_register(reg), 0xf, reg)
-        for reg in ['AL', 'BL', 'CL', 'DL']:
-            self.assertEqual(state.read_register(reg), 0xf0, reg)
-        for reg in ['AX', 'BX', 'CX', 'DX']:
-            self.assertEqual(state.read_register(reg), 0x0ff0, reg)
-        for reg in ['EAX', 'EBX', 'ECX', 'EDX',
-                    'RAX', 'RBX', 'RCX', 'RDX']:
-            self.assertEqual(state.read_register(reg), 0xa0ff0, reg)
+def test_register_aliases_read_write(arch):
+    state = ProgramState(arch)
+    for reg in ['EAX', 'EBX', 'ECX', 'EDX']:
+        state.set_register(reg, 0xa0ff0)
 
-    def test_flag_aliases(self):
-        flags = ['CF', 'PF', 'AF', 'ZF', 'SF', 'TF', 'IF', 'DF', 'OF',
-                 'IOPL', 'NT', 'RF', 'VM', 'AC', 'VIF', 'VIP', 'ID']
-        state = ProgramState(self.arch)
+    for reg in ['AH', 'BH', 'CH', 'DH']:
+        assert state.read_register(reg) == 0xf, reg
+    for reg in ['AL', 'BL', 'CL', 'DL']:
+        assert state.read_register(reg) == 0xf0, reg
+    for reg in ['AX', 'BX', 'CX', 'DX']:
+        assert state.read_register(reg) == 0x0ff0, reg
+    for reg in ['EAX', 'EBX', 'ECX', 'EDX',
+                'RAX', 'RBX', 'RCX', 'RDX']:
+        assert state.read_register(reg) == 0xa0ff0, reg
 
-        state.set_register('RFLAGS', 0)
-        for flag in flags:
-            self.assertEqual(state.read_register(flag), 0)
+def test_flag_aliases(arch):
+    flags = ['CF', 'PF', 'AF', 'ZF', 'SF', 'TF', 'IF', 'DF', 'OF',
+             'IOPL', 'NT', 'RF', 'VM', 'AC', 'VIF', 'VIP', 'ID']
+    state = ProgramState(arch)
 
-        state.set_register('RFLAGS',
-                           x86.compose_rflags({'ZF': 1, 'PF': 1, 'OF': 0}))
-        self.assertEqual(state.read_register('ZF'), 1, self.arch.get_reg_accessor('ZF'))
-        self.assertEqual(state.read_register('PF'), 1)
-        self.assertEqual(state.read_register('OF'), 0)
-        self.assertEqual(state.read_register('AF'), 0)
-        self.assertEqual(state.read_register('ID'), 0)
-        self.assertEqual(state.read_register('SF'), 0)
+    state.set_register('RFLAGS', 0)
+    for flag in flags:
+        assert state.read_register(flag) == 0
 
-        for flag in flags:
-            state.set_register(flag, 1)
-        for flag in flags:
-            self.assertEqual(state.read_register(flag), 1)
+    state.set_register('RFLAGS',
+                       x86.compose_rflags({'ZF': 1, 'PF': 1, 'OF': 0}))
+    assert state.read_register('ZF') == 1, arch.get_reg_accessor('ZF')
+    assert state.read_register('PF') == 1
+    assert state.read_register('OF') == 0
+    assert state.read_register('AF') == 0
+    assert state.read_register('ID') == 0
+    assert state.read_register('SF') == 0
 
-        state.set_register('OF', 1)
-        state.set_register('AF', 1)
-        state.set_register('SF', 1)
-        self.assertEqual(state.read_register('OF'), 1)
-        self.assertEqual(state.read_register('AF'), 1)
-        self.assertEqual(state.read_register('SF'), 1)
+    for flag in flags:
+        state.set_register(flag, 1)
+    for flag in flags:
+        assert state.read_register(flag) == 1
+
+    state.set_register('OF', 1)
+    state.set_register('AF', 1)
+    state.set_register('SF', 1)
+    assert state.read_register('OF') == 1
+    assert state.read_register('AF') == 1
+    assert state.read_register('SF') == 1
 
-if __name__ == '__main__':
-    unittest.main()
diff --git a/tests/test_sparse_memory.py b/tests/test_sparse_memory.py
index 4fd9cba..7dda8f0 100644
--- a/tests/test_sparse_memory.py
+++ b/tests/test_sparse_memory.py
@@ -1,33 +1,38 @@
-import unittest
+import pytest
 
 from focaccia.snapshot import SparseMemory, MemoryAccessError
 
-class TestSparseMemory(unittest.TestCase):
-    def test_oob_read(self):
-        mem = SparseMemory()
-        for addr in range(mem.page_size):
-            self.assertRaises(MemoryAccessError, mem.read, addr, 1)
-            self.assertRaises(MemoryAccessError, mem.read, addr, 30)
-            self.assertRaises(MemoryAccessError, mem.read, addr + 0x10, 30)
-            self.assertRaises(MemoryAccessError, mem.read, addr, mem.page_size)
-            self.assertRaises(MemoryAccessError, mem.read, addr, mem.page_size - 1)
-            self.assertRaises(MemoryAccessError, mem.read, addr, mem.page_size + 1)
+@pytest.fixture
+def mem():
+    return SparseMemory()
 
-    def test_basic_read_write(self):
-        mem = SparseMemory()
+def test_oob_read(mem):
+    for addr in range(mem.page_size):
+        with pytest.raises(MemoryAccessError):
+            mem.read(addr, 1)
+        with pytest.raises(MemoryAccessError): 
+            mem.read(addr, 30)
+        with pytest.raises(MemoryAccessError): 
+            mem.read(addr + 0x10, 30)
+        with pytest.raises(MemoryAccessError): 
+            mem.read(addr, mem.page_size)
+        with pytest.raises(MemoryAccessError): 
+            mem.read(addr, mem.page_size - 1)
+        with pytest.raises(MemoryAccessError): 
+            mem.read(addr, mem.page_size + 1)
 
-        data = b'a' * mem.page_size * 2
-        mem.write(0x300, data)
-        self.assertEqual(mem.read(0x300, len(data)), data)
-        self.assertEqual(mem.read(0x300, 1), b'a')
-        self.assertEqual(mem.read(0x400, 1), b'a')
-        self.assertEqual(mem.read(0x299 + mem.page_size * 2, 1), b'a')
-        self.assertEqual(mem.read(0x321, 12), b'aaaaaaaaaaaa')
+def test_basic_read_write(mem):
+    data = b'a' * mem.page_size * 2
+    mem.write(0x300, data)
+    assert mem.read(0x300, len(data)) == data
+    assert mem.read(0x300, 1) == b'a'
+    assert mem.read(0x400, 1) == b'a'
+    assert mem.read(0x299 + mem.page_size * 2, 1) == b'a'
+    assert mem.read(0x321, 12) == b'aaaaaaaaaaaa'
 
-        mem.write(0x321, b'Hello World!')
-        self.assertEqual(mem.read(0x321, 12), b'Hello World!')
+    mem.write(0x321, b'Hello World!')
+    assert mem.read(0x321, 12) == b'Hello World!'
 
-        self.assertRaises(MemoryAccessError, mem.read, 0x300, mem.page_size * 3)
+    with pytest.raises(MemoryAccessError): 
+        mem.read(0x300, mem.page_size * 3)
 
-if __name__ == '__main__':
-    unittest.main()
diff --git a/uv.lock b/uv.lock
index 63245a9..e518c51 100644
--- a/uv.lock
+++ b/uv.lock
@@ -58,6 +58,7 @@ dependencies = [
 dev = [
     { name = "black", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
     { name = "pyright", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
+    { name = "pytest", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
     { name = "ruff", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
 ]
 
@@ -67,6 +68,7 @@ requires-dist = [
     { name = "cpuid", git = "https://github.com/taugoust/cpuid.py.git?rev=master" },
     { name = "miasm", directory = "miasm" },
     { name = "pyright", marker = "extra == 'dev'" },
+    { name = "pytest", marker = "extra == 'dev'" },
     { name = "ruff", marker = "extra == 'dev'" },
     { name = "setuptools" },
 ]
@@ -82,6 +84,15 @@ wheels = [
 ]
 
 [[package]]
+name = "iniconfig"
+version = "2.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
+]
+
+[[package]]
 name = "miasm"
 version = "0.1.4"
 source = { directory = "miasm" }
@@ -144,6 +155,24 @@ wheels = [
 ]
 
 [[package]]
+name = "pluggy"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
+]
+
+[[package]]
+name = "pygments"
+version = "2.19.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
+]
+
+[[package]]
 name = "pyparsing"
 version = "3.2.3"
 source = { registry = "https://pypi.org/simple" }
@@ -166,6 +195,21 @@ wheels = [
 ]
 
 [[package]]
+name = "pytest"
+version = "8.4.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "iniconfig", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
+    { name = "packaging", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
+    { name = "pluggy", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
+    { name = "pygments", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" },
+]
+
+[[package]]
 name = "ruff"
 version = "0.12.10"
 source = { registry = "https://pypi.org/simple" }