diff tests/conftest.py @ 3:2c135c81b16c

MERGE: upstream PyMuPDF 1.26.4 with MuPDF 1.26.7
author Franz Glasner <fzglas.hg@dom66.de>
date Mon, 15 Sep 2025 11:44:09 +0200
parents 1d09e1dec1d9
children a6bc019ac0b2
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/conftest.py	Mon Sep 15 11:44:09 2025 +0200
@@ -0,0 +1,143 @@
+import copy
+import os
+import platform
+import sys
+
+import pymupdf
+
+import pytest
+
+PYMUPDF_PYTEST_RESUME = os.environ.get('PYMUPDF_PYTEST_RESUME')
+
+@pytest.fixture(autouse=True)
+def wrap(request):
+    '''
+    Check that tests return with empty MuPDF warnings buffer. For example this
+    detects failure to call fz_close_output() before fz_drop_output(), which
+    (as of 2024-4-12) generates a warning from MuPDF.
+    
+    As of 2024-09-12 we also detect whether tests leave fds open; but for now
+    do not fail tests, because many tests need fixing.
+    '''
+    global PYMUPDF_PYTEST_RESUME
+    if PYMUPDF_PYTEST_RESUME:
+        # Skip all tests until we reach a matching name.
+        if PYMUPDF_PYTEST_RESUME == request.function.__name__:
+            print(f'### {PYMUPDF_PYTEST_RESUME=}: resuming at {request.function.__name__=}.')
+            PYMUPDF_PYTEST_RESUME = None
+        else:
+            print(f'### {PYMUPDF_PYTEST_RESUME=}: Skipping {request.function.__name__=}.')
+            return
+    
+    wt = pymupdf.TOOLS.mupdf_warnings()
+    assert not wt, f'{wt=}'
+    if platform.python_implementation() == 'GraalVM':
+        pymupdf.TOOLS.set_small_glyph_heights()
+    else:
+        assert not pymupdf.TOOLS.set_small_glyph_heights()
+    next_fd_before = os.open(__file__, os.O_RDONLY)
+    os.close(next_fd_before)
+    
+    if platform.system() == 'Linux' and platform.python_implementation() != 'GraalVM':
+        test_fds = True
+    else:
+        test_fds = False
+        
+    if test_fds:
+        # Gather detailed information about leaked fds.
+        def get_fds():
+            import subprocess
+            path = 'PyMuPDF-linx-fds'
+            path_l = 'PyMuPDF-linx-fds-l'
+            command = f'ls /proc/{os.getpid()}/fd > {path}'
+            command_l = f'ls -l /proc/{os.getpid()}/fd > {path_l}'
+            subprocess.run(command, shell=1)
+            subprocess.run(command_l, shell=1)
+            with open(path) as f:
+                ret = f.read()
+                ret = ret.replace('\n', ' ')
+            with open(path_l) as f:
+                ret_l = f.read()
+            return ret, ret_l
+        open_fds_before, open_fds_before_l = get_fds()
+    
+    pymupdf._log_items_clear()
+    pymupdf._log_items_active(True)
+    
+    JM_annot_id_stem = pymupdf.JM_annot_id_stem
+    
+    def get_members(a):
+        ret = dict()
+        for n in dir(a):
+            if not n.startswith('_'):
+                v = getattr(a, n)
+                ret[n] = v
+        return ret
+        
+    # Allow post-test checking that pymupdf._globals has not changed.
+    _globals_pre = get_members(pymupdf._globals)
+    
+    # Run the test.
+    rep = yield
+    
+    sys.stdout.flush()
+    
+    # Test has run; check it did not create any MuPDF warnings etc.
+    wt = pymupdf.TOOLS.mupdf_warnings()
+    if not hasattr(pymupdf, 'mupdf'):
+        print(f'Not checking mupdf_warnings on classic.')
+    else:
+        assert not wt, f'Warnings text not empty: {wt=}'
+    
+    assert not pymupdf.TOOLS.set_small_glyph_heights()
+    
+    _globals_post = get_members(pymupdf._globals)
+    if _globals_post != _globals_pre:
+        print(f'Test has changed pymupdf._globals from {_globals_pre=} to {_globals_post=}')
+        assert 0
+    
+    log_items = pymupdf._log_items()
+    assert not log_items, f'log() was called; {len(log_items)=}.'
+    
+    assert pymupdf.JM_annot_id_stem == JM_annot_id_stem, \
+            f'pymupdf.JM_annot_id_stem has changed from {JM_annot_id_stem!r} to {pymupdf.JM_annot_id_stem!r}'
+    
+    if test_fds:
+        # Show detailed information about leaked fds.
+        open_fds_after, open_fds_after_l = get_fds()
+        if open_fds_after != open_fds_before:
+            import textwrap
+            print(f'Test has changed process fds:')
+            print(f'    {open_fds_before=}')
+            print(f'     {open_fds_after=}')
+            print(f'open_fds_before_l:')
+            print(textwrap.indent(open_fds_before_l, '    '))
+            print(f'open_fds_after_l:')
+            print(textwrap.indent(open_fds_after_l, '    '))
+            #assert 0
+    
+    next_fd_after = os.open(__file__, os.O_RDONLY)
+    os.close(next_fd_after)
+    
+    if test_fds and next_fd_after != next_fd_before:
+        print(f'Test has leaked fds, {next_fd_before=} {next_fd_after=}.')
+        #assert 0, f'Test has leaked fds, {next_fd_before=} {next_fd_after=}. {args=} {kwargs=}.'
+    
+    if 0:
+        # This code can be useful to track down test failures caused by other
+        # tests modifying global state.
+        #
+        # We run a particular test menually after each test returns.
+        sys.path.insert(0, os.path.dirname(__file__))
+        try:
+            import test_tables
+        finally:
+            del sys.path[0]
+        print(f'### Calling test_tables.test_md_styles().')
+        try:
+            test_tables.test_md_styles()
+        except Exception as e:
+            print(f'### test_tables.test_md_styles() failed: {e}')
+            raise
+        else:
+            print(f'### test_tables.test_md_styles() passed.')