From 06a1f987d499f855d1576c3c67a4e49483c3e40d Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Thu, 28 Nov 2019 16:40:04 -0800 Subject: [PATCH] Use _OCRMYPDF_TEST_PATH for testing and .py stubs to simulate symlinks --- src/ocrmypdf/api.py | 1 + src/ocrmypdf/exec/__init__.py | 23 ++++++++++++++++++- src/ocrmypdf/exec/ghostscript.py | 5 +++-- src/ocrmypdf/exec/jbig2enc.py | 4 ++-- src/ocrmypdf/exec/qpdf.py | 4 ++-- src/ocrmypdf/exec/tesseract.py | 4 ++-- src/ocrmypdf/exec/unpaper.py | 5 ++--- tests/conftest.py | 37 ++++++++++++++++++++++++++----- tests/spoof/gs_feature_elision.py | 1 - tests/spoof/gs_pdfa_failure.py | 1 - tests/spoof/gs_raster_failure.py | 1 - tests/spoof/gs_render_failure.py | 1 - tests/spoof/tesseract_cache.py | 2 -- tests/test_main.py | 10 ++++----- 14 files changed, 70 insertions(+), 29 deletions(-) diff --git a/src/ocrmypdf/api.py b/src/ocrmypdf/api.py index a05edbc1..af47c969 100644 --- a/src/ocrmypdf/api.py +++ b/src/ocrmypdf/api.py @@ -18,6 +18,7 @@ import logging import os import sys +import warnings from enum import IntEnum from pathlib import Path diff --git a/src/ocrmypdf/exec/__init__.py b/src/ocrmypdf/exec/__init__.py index c1a18c9a..1f5656e7 100644 --- a/src/ocrmypdf/exec/__init__.py +++ b/src/ocrmypdf/exec/__init__.py @@ -21,14 +21,35 @@ import logging import os import re import sys +import shutil from collections.abc import Mapping -from subprocess import PIPE, STDOUT, CalledProcessError, run +from subprocess import PIPE, STDOUT, CalledProcessError, run as subprocess_run from ..exceptions import ExitCode, MissingDependencyError log = logging.Logger(__name__) +def _get_program(args, env=None): + program = args[0] + test_path = env.get('_OCRMYPDF_TEST_PATH', '') + if test_path: + program = shutil.which(program, path=test_path) + return program + + +def run(args, *, env=None, **kwargs): + if not env: + env = os.environ + program = _get_program(args, env) + if os.name == 'nt' and program.lower().endswith('.py'): + args = [sys.executable, program] + args[1:] + else: + args = [program] + args[1:] + log.debug(args) + return subprocess_run(args, env=env, **kwargs) + + def get_version(program, *, version_arg='--version', regex=r'(\d+(\.\d+)*)', env=None): "Get the version of the specified program" args_prog = [program, version_arg] diff --git a/src/ocrmypdf/exec/ghostscript.py b/src/ocrmypdf/exec/ghostscript.py index 434859ad..1d13122c 100644 --- a/src/ocrmypdf/exec/ghostscript.py +++ b/src/ocrmypdf/exec/ghostscript.py @@ -26,13 +26,13 @@ from functools import lru_cache from io import BytesIO from os import fspath from pathlib import Path -from subprocess import PIPE, run, CalledProcessError +from subprocess import PIPE, CalledProcessError from shutil import which from PIL import Image from ..exceptions import SubprocessOutputError, MissingDependencyError -from . import get_version +from . import get_version, run gslog = logging.getLogger() @@ -43,6 +43,7 @@ if os.name == 'nt': GS = which('gswin32c') if not GS: raise MissingDependencyError("Ghostscript (gswin64c or gswin32c)") + GS = Path(GS).stem @lru_cache(maxsize=1) diff --git a/src/ocrmypdf/exec/jbig2enc.py b/src/ocrmypdf/exec/jbig2enc.py index dff450c8..5218edbd 100644 --- a/src/ocrmypdf/exec/jbig2enc.py +++ b/src/ocrmypdf/exec/jbig2enc.py @@ -18,10 +18,10 @@ """Interface to jbig2 executable""" from functools import lru_cache -from subprocess import PIPE, run +from subprocess import PIPE from ..exceptions import MissingDependencyError -from . import get_version +from . import get_version, run @lru_cache(maxsize=1) diff --git a/src/ocrmypdf/exec/qpdf.py b/src/ocrmypdf/exec/qpdf.py index e96848c0..9be8692b 100644 --- a/src/ocrmypdf/exec/qpdf.py +++ b/src/ocrmypdf/exec/qpdf.py @@ -19,9 +19,9 @@ from functools import lru_cache from os import fspath -from subprocess import PIPE, STDOUT, CalledProcessError, run +from subprocess import PIPE, STDOUT, CalledProcessError -from . import get_version +from . import get_version, run @lru_cache(maxsize=1) diff --git a/src/ocrmypdf/exec/tesseract.py b/src/ocrmypdf/exec/tesseract.py index 4b0560d8..34bd8983 100644 --- a/src/ocrmypdf/exec/tesseract.py +++ b/src/ocrmypdf/exec/tesseract.py @@ -23,7 +23,7 @@ from collections import namedtuple from contextlib import suppress import logging from os import fspath -from subprocess import PIPE, STDOUT, CalledProcessError, TimeoutExpired, run +from subprocess import PIPE, STDOUT, CalledProcessError, TimeoutExpired from ..exceptions import ( MissingDependencyError, @@ -31,7 +31,7 @@ from ..exceptions import ( TesseractConfigError, ) from ..helpers import page_number, safe_symlink -from . import get_version +from . import get_version, run OrientationConfidence = namedtuple('OrientationConfidence', ('angle', 'confidence')) diff --git a/src/ocrmypdf/exec/unpaper.py b/src/ocrmypdf/exec/unpaper.py index 4515a33f..1143c0e9 100644 --- a/src/ocrmypdf/exec/unpaper.py +++ b/src/ocrmypdf/exec/unpaper.py @@ -22,7 +22,6 @@ import os import shlex -import subprocess from functools import lru_cache from subprocess import PIPE, STDOUT, CalledProcessError from tempfile import TemporaryDirectory @@ -30,7 +29,7 @@ from tempfile import TemporaryDirectory from PIL import Image from ..exceptions import MissingDependencyError, SubprocessOutputError -from . import get_version +from . import get_version, run as external_run @lru_cache(maxsize=1) @@ -77,7 +76,7 @@ def run(input_file, output_file, dpi, log, mode_args): # their unpaper arguments (whether intentionally or otherwise) args_unpaper.extend([input_pnm, output_pnm]) try: - proc = subprocess.run( + proc = external_run( args_unpaper, check=True, close_fds=True, diff --git a/tests/conftest.py b/tests/conftest.py index 3a2850a4..08b45fb2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -80,6 +80,19 @@ PROJECT_ROOT = os.path.dirname(TESTS_ROOT) OCRMYPDF = [sys.executable, '-m', 'ocrmypdf'] +PY_FILE_TEMPLATE = """ +import os +import subprocess +import sys + +args = [sys.executable, {spoofer}, *sys.argv[1:]] +p = subprocess.run(args, check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE) +sys.stdout.buffer.write(p.stdout) +sys.stderr.buffer.write(p.stderr) +sys.exit(p.returncode) +""" + + @pytest.helpers.register def spoof(tmp_path_factory, **kwargs): """Modify PATH to override subprocess executables @@ -97,12 +110,24 @@ def spoof(tmp_path_factory, **kwargs): for replace_program, with_spoof in kwargs.items(): spoofer = Path(SPOOF_PATH) / with_spoof - spoofer.chmod(0o755) - (tmpdir / replace_program).symlink_to(spoofer) - - env['_OCRMYPDF_SAVE_PATH'] = env['PATH'] - env['PATH'] = str(tmpdir) + ":" + env['PATH'] + if os.name != 'nt': + spoofer.chmod(0o755) + (tmpdir / replace_program).symlink_to(spoofer) + else: + py_file = PY_FILE_TEMPLATE.format( + python=sys.executable, spoofer=repr(os.fspath(spoofer.absolute())) + ) + if replace_program == 'gs': + programs = ['gswin64c', 'gswin32c'] + else: + programs = [replace_program] + for prog in programs: + (tmpdir / f'{prog}.py').write_text(py_file, encoding='utf-8') + env['_OCRMYPDF_TEST_PATH'] = str(tmpdir) + os.pathsep + env['PATH'] + if os.name == 'nt': + if '.py' not in env['PATHEXT'].lower(): + raise EnvironmentError("PATHEXT is not configured to support .py") return env @@ -178,7 +203,7 @@ def run_ocrmypdf_api(input_file, output_file, *args, env=None): ) api.check_options(options) if env: - options.tesseract_env = env + options.tesseract_env = env.copy() options.tesseract_env['_OCRMYPDF_TEST_INFILE'] = os.fspath(input_file) if options.tesseract_env: assert all(isinstance(v, (str, bytes)) for v in options.tesseract_env.values()) diff --git a/tests/spoof/gs_feature_elision.py b/tests/spoof/gs_feature_elision.py index 0ae46b46..f9856311 100755 --- a/tests/spoof/gs_feature_elision.py +++ b/tests/spoof/gs_feature_elision.py @@ -38,7 +38,6 @@ not permitted in PDF/A-2, overprint mode not set""" def main(): - os.environ['PATH'] = os.environ['_OCRMYPDF_SAVE_PATH'] if '--version' in sys.argv: print('9.20') print('SPOOFED: ' + os.path.basename(__file__)) diff --git a/tests/spoof/gs_pdfa_failure.py b/tests/spoof/gs_pdfa_failure.py index b8559192..6dd90e29 100755 --- a/tests/spoof/gs_pdfa_failure.py +++ b/tests/spoof/gs_pdfa_failure.py @@ -31,7 +31,6 @@ from gs import real_ghostscript def main(): - os.environ['PATH'] = os.environ['_OCRMYPDF_SAVE_PATH'] if '--version' in sys.argv: print('9.20') print('SPOOFED: ' + os.path.basename(__file__)) diff --git a/tests/spoof/gs_raster_failure.py b/tests/spoof/gs_raster_failure.py index f7269b3f..c07b881b 100755 --- a/tests/spoof/gs_raster_failure.py +++ b/tests/spoof/gs_raster_failure.py @@ -31,7 +31,6 @@ from gs import real_ghostscript def main(): - os.environ['PATH'] = os.environ['_OCRMYPDF_SAVE_PATH'] if '--version' in sys.argv: print('9.20') print('SPOOFED: ' + os.path.basename(__file__)) diff --git a/tests/spoof/gs_render_failure.py b/tests/spoof/gs_render_failure.py index 5bb6ce7c..a43833a8 100755 --- a/tests/spoof/gs_render_failure.py +++ b/tests/spoof/gs_render_failure.py @@ -30,7 +30,6 @@ from gs import real_ghostscript def main(): - os.environ['PATH'] = os.environ['_OCRMYPDF_SAVE_PATH'] if '--version' in sys.argv: print('9.20') print('SPOOFED: ' + os.path.basename(__file__)) diff --git a/tests/spoof/tesseract_cache.py b/tests/spoof/tesseract_cache.py index 528e5a1e..83c09535 100755 --- a/tests/spoof/tesseract_cache.py +++ b/tests/spoof/tesseract_cache.py @@ -59,8 +59,6 @@ import subprocess import sys from pathlib import Path -if '_OCRMYPDF_SAVE_PATH' in os.environ: - os.environ['PATH'] = os.environ['_OCRMYPDF_SAVE_PATH'] __version__ = subprocess.check_output( ['tesseract', '--version'], stderr=subprocess.STDOUT diff --git a/tests/test_main.py b/tests/test_main.py index 73f91e5e..c18a23f1 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -297,10 +297,10 @@ def test_force_ocr_on_pdf_with_no_images(spoof_tesseract_crash, resources, no_ou # As a correctness test, make sure that --force-ocr on a PDF with no # content still triggers tesseract. If tesseract crashes, then it was # called. - result = run_ocrmypdf_api( + p, _, _ = run_ocrmypdf( resources / 'blank.pdf', no_outpdf, '--force-ocr', env=spoof_tesseract_crash ) - assert result == ExitCode.child_process_error + assert p.returncode == ExitCode.child_process_error assert not os.path.exists(no_outpdf) @@ -389,7 +389,7 @@ def test_pagesegmode(renderer, spoof_tesseract_cache, resources, outpdf): @pytest.mark.parametrize('renderer', RENDERERS) def test_tesseract_crash(renderer, spoof_tesseract_crash, resources, no_outpdf, caplog): - result = run_ocrmypdf_api( + p, _, err = run_ocrmypdf( resources / 'ccitt.pdf', no_outpdf, '-v', @@ -398,9 +398,9 @@ def test_tesseract_crash(renderer, spoof_tesseract_crash, resources, no_outpdf, renderer, env=spoof_tesseract_crash, ) - assert result == ExitCode.child_process_error + assert p.returncode == ExitCode.child_process_error assert not os.path.exists(no_outpdf) - assert "SubprocessOutputError" in caplog.text + assert "SubprocessOutputError" in err def test_tesseract_crash_autorotate(spoof_tesseract_crash, resources, no_outpdf):