Skip to content

Commit

Permalink
testsuite: work on tmp data scoping
Browse files Browse the repository at this point in the history
  • Loading branch information
mara004 committed Dec 15, 2023
1 parent 5b3941b commit cbc9d60
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 66 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ name: Publish Python distributions to PyPI
# release:
# types: [published]

# XXX temporary trigger no-op to avoid syntax errors
on:
- workflow_dispatch

jobs:
build-n-publish:
name: Build and publish Python distributions to PyPI
Expand Down
62 changes: 39 additions & 23 deletions tests/ctypesgentest.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import os
import sys
import glob
import json
import atexit
import types
import subprocess
import shutil
from itertools import product
import tempfile
from pathlib import Path

import ctypesgen.__main__
Expand All @@ -15,6 +14,22 @@

TEST_DIR = Path(__file__).resolve().parent
COMMON_DIR = TEST_DIR/"common"
TMP_DIR = TEST_DIR/"tmp"
COUNTER = 0
CLEANUP_OK = bool(int(os.environ.get("CLEANUP_OK", "1")))


def init_tmpdir():
if TMP_DIR.exists():
shutil.rmtree(TMP_DIR)
TMP_DIR.mkdir()

def cleanup_tmpdir():
if CLEANUP_OK:
shutil.rmtree(TMP_DIR)

init_tmpdir()
atexit.register(cleanup_tmpdir)


def ctypesgen_main(args):
Expand All @@ -32,14 +47,22 @@ def module_from_code(name, python_code):

def generate(header_str, args=[], lang="py"):

tmp_in = TEST_DIR/"tmp_in.h"
tmp_in.write_text(header_str)
tmp_out = TEST_DIR/"tmp_out.py"
ctypesgen_main(["-i", tmp_in, "-o", tmp_out, "--output-language", lang, *args])
content = tmp_out.read_text()
# use custom tempfiles scoping so we may retain data for inspection
# also note that python stdlib tempfiles don't play well with windows

global COUNTER
COUNTER += 1

tmp_in.unlink()
tmp_out.unlink()
tmp_in = TMP_DIR/f"in_header_{COUNTER:02d}.h"
tmp_in.write_text(header_str)
try:
tmp_out = TMP_DIR/f"out_bindings_{COUNTER:02d}.py"
ctypesgen_main(["-i", tmp_in, "-o", tmp_out, "--output-language", lang, *args])
content = tmp_out.read_text()
finally:
if CLEANUP_OK:
tmp_in.unlink()
tmp_out.unlink()

if lang.startswith("py"):
return module_from_code("tmp_module", content)
Expand All @@ -49,12 +72,6 @@ def generate(header_str, args=[], lang="py"):
assert False


def cleanup(filepattern="temp.*"):
fnames = glob.glob(filepattern)
for fname in fnames:
os.unlink(fname)


def set_logging_level(log_level):
messages.log.setLevel(log_level)

Expand Down Expand Up @@ -109,9 +126,9 @@ def _replace_anon_tag(self, json, tag, new_tag):
elif key == "tag" and isinstance(value, str):
if value == tag:
json[key] = new_tag
elif key == "src" and isinstance(value, list):
if value and "temp.h" in value[0]:
value[0] = "/some-path/temp.h"
elif sys.platform == "win32" and key == "src" and isinstance(value, list) and value:
# for whatever reason, on windows ctypesgen's json output contains double slashes in paths, whereas the expectation contains only single slashes, so normalize the thing
value[0] = value[0].replace("\\\\", "\\")
else:
self._replace_anon_tag(value, tag, new_tag)

Expand Down Expand Up @@ -168,11 +185,9 @@ def _create_common_files():
void bar(struct mystruct *m) { }
"""

try:
COMMON_DIR.mkdir()
except FileExistsError:
if COMMON_DIR.exists():
shutil.rmtree(COMMON_DIR)
COMMON_DIR.mkdir()
COMMON_DIR.mkdir()

for (name, source) in names.items():
with (COMMON_DIR/name).open("w") as f:
Expand Down Expand Up @@ -202,4 +217,5 @@ def cleanup_common():
# Attention: currently not working on MS Windows.
# cleanup_common() tries to delete "common.dll" while it is still loaded
# by ctypes. See unittest for further details.
shutil.rmtree(COMMON_DIR)
if CLEANUP_OK:
shutil.rmtree(COMMON_DIR)
64 changes: 21 additions & 43 deletions tests/testsuite.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""Simple test suite using unittest.
By clach04 (Chris Clark).
Originally written by clach04 (Chris Clark).
Calling:
Expand All @@ -21,27 +21,29 @@
Aims to test for regressions. Where possible use stdlib to
avoid the need to compile C code.
Note, you may set CLEANUP_OK=0 to retain generated data.
This can be useful for inspection.
"""

import sys
import os
import ctypes
import json as JSON
import math
import atexit
import unittest
import logging
from subprocess import Popen, PIPE
from tempfile import NamedTemporaryFile

from tests.ctypesgentest import (
cleanup,
cleanup_common,
ctypesgen_version,
generate,
generate_common,
JsonHelper,
set_logging_level,
TEST_DIR,
TMP_DIR,
CLEANUP_OK,
)


Expand Down Expand Up @@ -115,7 +117,6 @@ def setUpClass(cls):
@classmethod
def tearDownClass(cls):
del cls.module
cleanup()

def test_getenv_returns_string(self):
""" Test string return """
Expand Down Expand Up @@ -156,6 +157,7 @@ def test_getenv_returns_null(self):
self.assertEqual(result, None)


# NOTE currently under testing
# This test is currently not working on MS Windows. The reason is the call of
# cleanup_common(), which tries to delete "common.dll" while it is still loaded
# by ctypes.
Expand All @@ -166,18 +168,12 @@ def test_getenv_returns_null(self):
# means the dll is loaded to memory.
# On Linux/macOS this is no problem, as .so libaries can be overwritten/deleted
# on file system while still loaded to memory.
@unittest.skipIf(
sys.platform == "win32",
"Currently not working on Windows. See code comment for details."
)
class CommonHeaderTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
generate_common()

@classmethod
def tearDownClass(cls):
cleanup_common()
# try to work around the problem described above
atexit.register(cleanup_common)

# NOTE `common` is a meta-module hosted by the test class, and {a,b}{shared,unshared} are the actual python files in question

Expand Down Expand Up @@ -227,7 +223,6 @@ def setUpClass(cls):
@classmethod
def tearDownClass(cls):
del cls.module
cleanup()

def test_stdbool_type(self):
"""Test if bool is parsed correctly"""
Expand Down Expand Up @@ -261,7 +256,6 @@ def setUpClass(cls):
@classmethod
def tearDownClass(cls):
del cls.module
cleanup()

def test_int_types(self):
"""Test if different integer types are parsed correctly"""
Expand Down Expand Up @@ -316,7 +310,6 @@ def _json(self, name):
@classmethod
def tearDownClass(cls):
del cls.module, cls.json
cleanup()

def test_macro_constant_int(self):
"""Tests from simple_macros.py"""
Expand Down Expand Up @@ -638,7 +631,6 @@ def setUpClass(cls):
@classmethod
def tearDownClass(cls):
del StructuresTest.module
cleanup()

def test_struct_json(self):
json_ans = [
Expand Down Expand Up @@ -2053,7 +2045,6 @@ def setUpClass(cls):
@classmethod
def tearDownClass(cls):
del cls.module
cleanup()

def test_sin(self):
"""Based on math_functions.py"""
Expand Down Expand Up @@ -2103,7 +2094,6 @@ def setUpClass(cls):
@classmethod
def tearDownClass(cls):
del cls.module, cls.json
cleanup()

def test_enum(self):
self.assertEqual(EnumTest.module.TEST_1, 0)
Expand Down Expand Up @@ -2215,7 +2205,6 @@ def setUpClass(cls):
@classmethod
def tearDownClass(cls):
del cls.json
cleanup()

def test_function_prototypes_json(self):
json_ans = [
Expand Down Expand Up @@ -2368,7 +2357,6 @@ def setUpClass(cls):
@classmethod
def tearDownClass(cls):
del cls.module
cleanup()

def test_longdouble_type(self):
"""Test if long double is parsed correctly"""
Expand Down Expand Up @@ -2442,7 +2430,6 @@ def test_unchecked_prototype(self):
@classmethod
def tearDownClass(cls):
del cls.module
cleanup()


class ConstantsTest(unittest.TestCase):
Expand Down Expand Up @@ -2482,7 +2469,6 @@ def setUpClass(cls):
@classmethod
def tearDownClass(cls):
del ConstantsTest.module
cleanup()

def test_integer_constants(self):
"""Test if integer constants are parsed correctly"""
Expand Down Expand Up @@ -2534,7 +2520,6 @@ def setUpClass(cls):
@classmethod
def tearDownClass(cls):
del NULLTest.module
cleanup()

def test_null_type(self):
"""Test if NULL is parsed correctly"""
Expand Down Expand Up @@ -2572,7 +2557,6 @@ def setUpClass(cls):
def tearDownClass(cls):
del cls.module
os.remove(cls.mac_roman_file)
cleanup()

def test_macroman_encoding_source(self):
module = MacromanEncodeTest.module
Expand All @@ -2590,23 +2574,17 @@ def setUpClass(cls):

def test_type_error_catch(self):
with self.assertRaises(ctypes.ArgumentError):
self.module.printf(123)
# in case this slipped through as binary data, you would see chr(33) = '!' at the end
self.module.printf(33)

def test_call(self):
tmp = TEST_DIR/"tmp_testdata.txt"
tmp = TMP_DIR/f"out_{type(self).__name__}.txt"
tmp.touch()
c_file = self.module.fopen(str(tmp).encode(), b"w")
self.module.fprintf(c_file, b"Test variadic function: %s %d", b"Hello", 123)
self.module.fclose(c_file)
assert tmp.read_bytes() == b"Test variadic function: Hello 123"
tmp.unlink()


def main():
set_logging_level(logging.CRITICAL) # do not log anything
unittest.main()
return 0


if __name__ == "__main__":
sys.exit(main())
try:
c_file = self.module.fopen(str(tmp).encode(), b"w")
self.module.fprintf(c_file, b"Test variadic function: %s %d", b"Hello", 123)
self.module.fclose(c_file)
assert tmp.read_bytes() == b"Test variadic function: Hello 123"
finally:
if CLEANUP_OK:
tmp.unlink()

0 comments on commit cbc9d60

Please sign in to comment.