pytest-cov documentation recommends using explicit management of multiprocessing.Pool rather than the context manager. This is supposed to work better for collecting coverage data, particularly on Windows.
277 lines
8.0 KiB
Python
277 lines
8.0 KiB
Python
# © 2017 James R. Barlow: github.com/jbarlow83
|
|
#
|
|
# This file is part of OCRmyPDF.
|
|
#
|
|
# OCRmyPDF is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# OCRmyPDF is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import ast
|
|
import os
|
|
import platform
|
|
import sys
|
|
from pathlib import Path
|
|
from subprocess import PIPE, run
|
|
|
|
import pytest
|
|
|
|
from ocrmypdf import api, cli
|
|
|
|
pytest_plugins = ['helpers_namespace']
|
|
|
|
|
|
# pylint: disable=E1101
|
|
# pytest.helpers is dynamic so it confuses pylint
|
|
|
|
if sys.version_info < (3, 5):
|
|
print("Requires Python 3.5+")
|
|
sys.exit(1)
|
|
|
|
|
|
@pytest.helpers.register
|
|
def is_linux():
|
|
return platform.system() == 'Linux'
|
|
|
|
|
|
@pytest.helpers.register
|
|
def is_macos():
|
|
return platform.system() == 'Darwin'
|
|
|
|
|
|
@pytest.helpers.register
|
|
def running_in_docker():
|
|
# Docker creates a file named /.dockerenv (newer versions) or
|
|
# /.dockerinit (older) -- this is undocumented, not an offical test
|
|
return os.path.exists('/.dockerenv') or os.path.exists('/.dockerinit')
|
|
|
|
|
|
@pytest.helpers.register
|
|
def running_in_travis():
|
|
return os.environ.get('TRAVIS') == 'true'
|
|
|
|
|
|
@pytest.helpers.register
|
|
def have_unpaper():
|
|
try:
|
|
from ocrmypdf.exec import unpaper
|
|
|
|
unpaper.version()
|
|
except Exception:
|
|
return False
|
|
return True
|
|
|
|
|
|
TESTS_ROOT = os.path.abspath(os.path.dirname(__file__))
|
|
SPOOF_PATH = os.path.join(TESTS_ROOT, 'spoof')
|
|
PROJECT_ROOT = os.path.dirname(TESTS_ROOT)
|
|
OCRMYPDF = [sys.executable, '-m', 'ocrmypdf']
|
|
|
|
|
|
WINDOWS_SHIM_TEMPLATE = """
|
|
# This is a shim for Windows that has the same effect as a symlink to the target .py
|
|
# file
|
|
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)
|
|
"""
|
|
|
|
assert ast.parse(WINDOWS_SHIM_TEMPLATE.format(spoofer=repr(r"C:\\Temp\\file.py")))
|
|
|
|
|
|
@pytest.helpers.register
|
|
def spoof(tmp_path_factory, **kwargs):
|
|
"""Modify PATH to override subprocess executables
|
|
|
|
spoof(program1='replacement', ...)
|
|
|
|
Creates temporary directory with symlinks to targets.
|
|
|
|
"""
|
|
env = os.environ.copy()
|
|
slug = '-'.join(v.replace('.py', '') for v in sorted(kwargs.values()))
|
|
spoofer_base = tmp_path_factory.mktemp('spoofers')
|
|
tmpdir = Path(spoofer_base / slug)
|
|
tmpdir.mkdir(parents=True)
|
|
|
|
for replace_program, with_spoof in kwargs.items():
|
|
spoofer = Path(SPOOF_PATH) / with_spoof
|
|
if os.name != 'nt':
|
|
spoofer.chmod(0o755)
|
|
(tmpdir / replace_program).symlink_to(spoofer)
|
|
else:
|
|
py_file = WINDOWS_SHIM_TEMPLATE.format(
|
|
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
|
|
|
|
|
|
@pytest.fixture(scope='session')
|
|
def spoof_tesseract_noop(tmp_path_factory):
|
|
return spoof(tmp_path_factory, tesseract='tesseract_noop.py')
|
|
|
|
|
|
@pytest.fixture(scope='session')
|
|
def spoof_tesseract_cache(tmp_path_factory):
|
|
if running_in_docker():
|
|
return os.environ.copy()
|
|
return spoof(tmp_path_factory, tesseract="tesseract_cache.py")
|
|
|
|
|
|
@pytest.fixture
|
|
def resources():
|
|
return Path(TESTS_ROOT) / 'resources'
|
|
|
|
|
|
@pytest.fixture
|
|
def ocrmypdf_exec():
|
|
return OCRMYPDF
|
|
|
|
|
|
@pytest.fixture(scope="function")
|
|
def outdir(tmp_path):
|
|
return tmp_path
|
|
|
|
|
|
@pytest.fixture(scope="function")
|
|
def outpdf(tmp_path):
|
|
return tmp_path / 'out.pdf'
|
|
|
|
|
|
@pytest.fixture(scope="function")
|
|
def no_outpdf(tmp_path):
|
|
"""This just documents the fact that a test is not expected to produce
|
|
output. Unfortunately an assertion failure inside a test fixture produces
|
|
an error rather than a test failure, so no testing is done. It's up to
|
|
the test to confirm that no output file was created."""
|
|
return tmp_path / 'no_output.pdf'
|
|
|
|
|
|
@pytest.helpers.register
|
|
def check_ocrmypdf(input_file, output_file, *args, env=None):
|
|
"""Run ocrmypdf and confirmed that a valid file was created"""
|
|
|
|
options = cli.parser.parse_args(
|
|
[str(input_file), str(output_file)]
|
|
+ [str(arg) for arg in args if arg is not None]
|
|
)
|
|
api.check_options(options)
|
|
if env:
|
|
options.tesseract_env = env
|
|
options.tesseract_env['_OCRMYPDF_TEST_INFILE'] = os.fspath(input_file)
|
|
result = api.run_pipeline(options, api=True)
|
|
|
|
assert result == 0
|
|
assert os.path.exists(str(output_file)), "Output file not created"
|
|
assert os.stat(str(output_file)).st_size > 100, "PDF too small or empty"
|
|
|
|
return output_file
|
|
|
|
|
|
@pytest.helpers.register
|
|
def run_ocrmypdf_api(input_file, output_file, *args, env=None):
|
|
"""Run ocrmypdf via API and let caller deal with results
|
|
|
|
Does not currently have a way to manipulate the PATH except for Tesseract.
|
|
"""
|
|
|
|
options = cli.parser.parse_args(
|
|
[str(input_file), str(output_file)]
|
|
+ [str(arg) for arg in args if arg is not None]
|
|
)
|
|
api.check_options(options)
|
|
if env:
|
|
options.tesseract_env = env.copy()
|
|
options.tesseract_env['_OCRMYPDF_TEST_INFILE'] = os.fspath(input_file)
|
|
first_path = env.get('_OCRMYPDF_TEST_PATH', '').split(os.pathsep)[0]
|
|
if 'spoof' in first_path:
|
|
assert 'gs' not in first_path, "use run_ocrmypdf() for gs"
|
|
assert 'tesseract' in first_path
|
|
if options.tesseract_env:
|
|
assert all(isinstance(v, (str, bytes)) for v in options.tesseract_env.values())
|
|
|
|
return api.run_pipeline(options, api=False)
|
|
|
|
|
|
@pytest.helpers.register
|
|
def run_ocrmypdf(input_file, output_file, *args, env=None, universal_newlines=True):
|
|
"Run ocrmypdf and let caller deal with results"
|
|
|
|
if env is None:
|
|
env = os.environ.copy()
|
|
|
|
p_args = (
|
|
OCRMYPDF
|
|
+ [str(arg) for arg in args if arg is not None]
|
|
+ [str(input_file), str(output_file)]
|
|
)
|
|
|
|
# Tell subprocess where to find coverage.py configuration
|
|
# This has no effect except when coverage is running
|
|
# Details: https://coverage.readthedocs.io/en/coverage-5.0/subprocess.html
|
|
coverage_rc = Path(__file__).parent.parent / '.coveragerc'
|
|
assert coverage_rc.exists()
|
|
env['COVERAGE_PROCESS_START'] = os.fspath(coverage_rc)
|
|
|
|
p = run(
|
|
p_args, stdout=PIPE, stderr=PIPE, universal_newlines=universal_newlines, env=env
|
|
)
|
|
# print(p.stderr)
|
|
return p, p.stdout, p.stderr
|
|
|
|
|
|
@pytest.helpers.register
|
|
def first_page_dimensions(pdf):
|
|
from ocrmypdf import pdfinfo
|
|
|
|
info = pdfinfo.PdfInfo(pdf)
|
|
page0 = info[0]
|
|
return (page0.width_inches, page0.height_inches)
|
|
|
|
|
|
def pytest_addoption(parser):
|
|
parser.addoption(
|
|
"--runslow",
|
|
action="store_true",
|
|
default=False,
|
|
help=(
|
|
"run slow tests only useful for development (unlikely to be "
|
|
"useful for downstream packagers)"
|
|
),
|
|
)
|
|
|
|
|
|
def pytest_collection_modifyitems(config, items):
|
|
if config.getoption("--runslow"):
|
|
# --runslow given in cli: do not skip slow tests
|
|
return
|
|
skip_slow = pytest.mark.skip(reason="need --runslow option to run")
|
|
for item in items:
|
|
if "slow" in item.keywords:
|
|
item.add_marker(skip_slow)
|