diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ec9a76b46..fe1c559cf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -58,36 +58,6 @@ jobs: path: ./wheelhouse/*.whl if-no-files-found: error - build-x86: - name: Build wheels on ${{ matrix.os }} (x86, 32-bit) - - if: github.event_name == 'push' || ! github.event.pull_request.draft - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - include: - - os: windows-latest - archs: x86 - - steps: - - uses: actions/checkout@v4 - with: - # Fetch all tags - fetch-depth: 0 - - - name: Create wheels - uses: pypa/cibuildwheel@v2.15.0 - env: - CIBW_ARCHS: ${{ matrix.archs }} - - - name: Upload wheels - uses: actions/upload-artifact@v3 - with: - name: wheels - path: ./wheelhouse/*.whl - if-no-files-found: error - build-arm64: name: Build wheels on ${{ matrix.os }} (arm64) @@ -102,10 +72,8 @@ jobs: include: - os: ubuntu-latest archs: aarch64 - build: "*manylinux*" - os: macos-latest archs: arm64 - build: "*" steps: - uses: actions/checkout@v4 @@ -123,7 +91,6 @@ jobs: uses: pypa/cibuildwheel@v2.15.0 env: CIBW_ARCHS: ${{ matrix.archs }} - CIBW_BUILD: ${{ matrix.build }} - name: Upload wheels uses: actions/upload-artifact@v3 @@ -139,7 +106,6 @@ jobs: needs: - build-x86_64 - - build-x86 - build-arm64 steps: diff --git a/hatch_build.py b/hatch_build.py index 630fab65e..01b9d122e 100644 --- a/hatch_build.py +++ b/hatch_build.py @@ -1,34 +1,57 @@ -"""Hatchling build hook for Pact binary download.""" +""" +Hatchling build hook for binary downloads. + +Pact Python is built on top of the Ruby Pact binaries and the Rust Pact library. +This build script downloads the binaries and library for the current platform +and installs them in the `pact` directory under `/bin` and `/lib`. + +The version of the binaries and library can be controlled with the +`PACT_BIN_VERSION` and `PACT_LIB_VERSION` environment variables. If these are +not set, a pinned version will be used instead. +""" from __future__ import annotations +import gzip import os import shutil -import typing +import tarfile +import tempfile +import warnings +import zipfile from pathlib import Path from typing import Any, Dict +import cffi +import requests from hatchling.builders.hooks.plugin.interface import BuildHookInterface from packaging.tags import sys_tags ROOT_DIR = Path(__file__).parent.resolve() -PACT_VERSION = "2.0.7" -PACT_URL = "https://github.com/pact-foundation/pact-ruby-standalone/releases/download/v{version}/pact-{version}-{os}-{machine}.{ext}" -PACT_DISTRIBUTIONS: list[tuple[str, str, str]] = [ - ("linux", "arm64", "tar.gz"), - ("linux", "x86_64", "tar.gz"), - ("osx", "arm64", "tar.gz"), - ("osx", "x86_64", "tar.gz"), - ("windows", "x86", "zip"), - ("windows", "x86_64", "zip"), -] - - -class PactBuildHook(BuildHookInterface): + +PACT_BIN_VERSION = os.getenv("PACT_BIN_VERSION", "2.0.7") +PACT_BIN_URL = "https://github.com/pact-foundation/pact-ruby-standalone/releases/download/v{version}/pact-{version}-{os}-{machine}.{ext}" + +PACT_LIB_VERSION = os.getenv("PACT_LIB_VERSION", "0.4.9") +PACT_LIB_URL = "https://github.com/pact-foundation/pact-reference/releases/download/libpact_ffi-v{version}/{prefix}pact_ffi-{os}-{machine}.{ext}" + + +class PactBuildHook(BuildHookInterface[Any]): """Custom hook to download Pact binaries.""" PLUGIN_NAME = "custom" + def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: ANN401 + """ + Initialize the build hook. + + For this hook, we additionally define the lib extension based on the + current platform. + """ + super().__init__(*args, **kwargs) + self.tmpdir = Path(tempfile.TemporaryDirectory().name) + self.tmpdir.mkdir(parents=True, exist_ok=True) + def clean(self, versions: list[str]) -> None: # noqa: ARG002 """Clean up any files created by the build hook.""" for subdir in ["bin", "lib", "data"]: @@ -43,10 +66,10 @@ def initialize( build_data["infer_tag"] = True build_data["pure_python"] = False - pact_version = os.getenv("PACT_VERSION", PACT_VERSION) - self.install_pact_binaries(pact_version) + self.pact_bin_install(PACT_BIN_VERSION) + self.pact_lib_install(PACT_LIB_VERSION) - def install_pact_binaries(self, version: str) -> None: # noqa: PLR0912 + def pact_bin_install(self, version: str) -> None: """ Install the Pact standalone binaries. @@ -54,10 +77,30 @@ def install_pact_binaries(self, version: str) -> None: # noqa: PLR0912 the current operating system is determined automatically. Args: - version: The Pact version to install. Defaults to the value in - `PACT_VERSION`. + version: The Pact version to install. + """ + url = self._pact_bin_url(version) + if url: + artifact = self._download(url) + self._pact_bin_extract(artifact) + + def _pact_bin_url(self, version: str) -> str | None: # noqa: PLR0911 + """ + Generate the download URL for the Pact binaries. + + Generate the download URL for the Pact binaries based on the current + platform and specified version. This function mainly contains a lot of + matching logic to determine the correct URL to use, due to the + inconsistencies in naming conventions between ecosystems. + + Args: + version: The upstream Pact version. + + Returns: + The URL to download the Pact binaries from, or None if the current + platform is not supported. """ - platform = typing.cast(str, next(sys_tags()).platform) + platform = next(sys_tags()).platform if platform.startswith("macosx"): os = "osx" @@ -67,81 +110,311 @@ def install_pact_binaries(self, version: str) -> None: # noqa: PLR0912 machine = "x86_64" else: msg = f"Unknown macOS machine {platform}" - raise ValueError(msg) - url = PACT_URL.format(version=version, os=os, machine=machine, ext="tar.gz") + warnings.warn(msg, RuntimeWarning, stacklevel=2) + return None + return PACT_BIN_URL.format( + version=version, + os=os, + machine=machine, + ext="tar.gz", + ) - elif platform.startswith("win"): + if platform.startswith("win"): os = "windows" if platform.endswith("amd64"): machine = "x86_64" elif platform.endswith(("x86", "win32")): machine = "x86" + else: + msg = f"Unknown Windows machine {platform}" + warnings.warn(msg, RuntimeWarning, stacklevel=2) + return None + return PACT_BIN_URL.format( + version=version, + os=os, + machine=machine, + ext="zip", + ) + + if "linux" in platform and "musl" not in platform: + os = "linux" + if platform.endswith("x86_64"): + machine = "x86_64" + elif platform.endswith("aarch64"): + machine = "arm64" + else: + msg = f"Unknown Linux machine {platform}" + warnings.warn(msg, RuntimeWarning, stacklevel=2) + return None + return PACT_BIN_URL.format( + version=version, + os=os, + machine=machine, + ext="tar.gz", + ) + + msg = f"Unknown platform {platform}" + warnings.warn(msg, RuntimeWarning, stacklevel=2) + return None + + def _pact_bin_extract(self, artifact: Path) -> None: + """ + Extract the Pact binaries. + + The upstream distributables contain a lot of files which are not needed + for this library. This function ensures that only the files in + `pact/bin` are extracted to avoid unnecessary bloat. + + Args: + artifact: The path to the downloaded artifact. + """ + if str(artifact).endswith(".zip"): + with zipfile.ZipFile(artifact) as f: + f.extractall(ROOT_DIR) + + if str(artifact).endswith(".tar.gz"): + with tarfile.open(artifact) as f: + f.extractall(ROOT_DIR) + + # Cleanup the extract `README.md` + (ROOT_DIR / "pact" / "README.md").unlink() + + def pact_lib_install(self, version: str) -> None: + """ + Install the Pact library binary. + + The library is installed in `pact/lib`, and the relevant version for + the current operating system is determined automatically. + + Args: + version: The Pact version to install. + """ + url = self._pact_lib_url(version) + artifact = self._download(url) + self._pact_lib_extract(artifact) + includes = self._pact_lib_header(url) + self._pact_lib_cffi(includes) + + def _pact_lib_url(self, version: str) -> str: # noqa: C901, PLR0912 + """ + Generate the download URL for the Pact library. + + Generate the download URL for the Pact library based on the current + platform and specified version. This function mainly contains a lot of + matching logic to determine the correct URL to use, due to the + inconsistencies in naming conventions between ecosystems. + + Args: + version: The upstream Pact version. + + Returns: + The URL to download the Pact library from. + + Raises: + ValueError: + If the current platform is not supported. + """ + platform = next(sys_tags()).platform + + if platform.startswith("macosx"): + os = "osx" + if platform.endswith("arm64"): + machine = "aarch64-apple-darwin" + elif platform.endswith("x86_64"): + machine = "x86_64" + else: + msg = f"Unknown macOS machine {platform}" + raise ValueError(msg) + return PACT_LIB_URL.format( + prefix="lib", + version=version, + os=os, + machine=machine, + ext="a.gz", + ) + + if platform.startswith("win"): + os = "windows" + + if platform.endswith("amd64"): + machine = "x86_64" else: msg = f"Unknown Windows machine {platform}" raise ValueError(msg) + return PACT_LIB_URL.format( + prefix="", + version=version, + os=os, + machine=machine, + ext="lib.gz", + ) - url = PACT_URL.format(version=version, os=os, machine=machine, ext="zip") + if "linux" in platform and "musl" in platform: + os = "linux" + if platform.endswith("x86_64"): + machine = "x86_64-musl" + else: + msg = f"Unknown MUSL Linux machine {platform}" + raise ValueError(msg) + return PACT_LIB_URL.format( + prefix="lib", + version=version, + os=os, + machine=machine, + ext="a.gz", + ) - elif "linux" in platform: + if "linux" in platform: os = "linux" if platform.endswith("x86_64"): machine = "x86_64" elif platform.endswith("aarch64"): - machine = "arm64" + machine = "aarch64" else: msg = f"Unknown Linux machine {platform}" raise ValueError(msg) - url = PACT_URL.format(version=version, os=os, machine=machine, ext="tar.gz") + return PACT_LIB_URL.format( + prefix="lib", + version=version, + os=os, + machine=machine, + ext="a.gz", + ) - else: - msg = f"Unknown platform {platform}" + msg = f"Unknown platform {platform}" + raise ValueError(msg) + + def _pact_lib_extract(self, artifact: Path) -> None: + """ + Extract the Pact library. + + Extract the Pact library from the downloaded artifact and place it in + `pact/lib`. + + Args: + artifact: The URL to download the Pact binaries from. + """ + if not str(artifact).endswith(".gz"): + msg = f"Unknown artifact type {artifact}" raise ValueError(msg) - self.download_and_extract_pact(url) + with gzip.open(artifact, "rb") as f_in, ( + self.tmpdir / (artifact.name.split("-")[0] + artifact.suffixes[0]) + ).open("wb") as f_out: + shutil.copyfileobj(f_in, f_out) - def download_and_extract_pact(self, url: str) -> None: + def _pact_lib_header(self, url: str) -> list[str]: """ - Download and extract the Pact binaries. + Download the Pact library header. - If the download artifact is already present, it will be used instead of - downloading it again. + Download the Pact library header from GitHub and place it in + `pact/include`. This uses the same URL as for the artifact, replacing + the final segment with `pact.h`. + + This also processes the header to strip out elements which are not + supported by CFFI (i.e., any line starting with `#`). The list of + `#include` statements is returned for use in the CFFI bindings. Args: - url: The URL to download the Pact binaries from. + url: The URL pointing to the Pact library artifact. + """ + url = url.rsplit("/", 1)[0] + "/pact.h" + artifact = self._download(url) + includes: list[str] = [] + with artifact.open("r", encoding="utf-8") as f_in, ( + self.tmpdir / "pact.h" + ).open("w", encoding="utf-8") as f_out: + for line in f_in: + sline = line.strip() + if sline.startswith("#include"): + includes.append(sline) + continue + if sline.startswith("#"): + continue + + f_out.write(line) + return includes + + def _pact_lib_cffi(self, includes: list[str]) -> None: + """ + Build the CFFI bindings for the Pact library. + + This will build the CFFI bindings for the Pact library and place them in + `pact/lib`. + + A list of additional `#include` statements can be passed to this + function, which will be included in the generated bindings. + + Args: + includes: + A list of additional `#include` statements to include in the + generated bindings. + """ + if os.name == "nt": + extra_libs = [ + "advapi32", + "bcrypt", + "crypt32", + "iphlpapi", + "ncrypt", + "netapi32", + "ntdll", + "ole32", + "oleaut32", + "pdh", + "powrprof", + "psapi", + "secur32", + "shell32", + "user32", + "userenv", + "ws2_32", + ] + else: + extra_libs = [] + + ffibuilder = cffi.FFI() + with (self.tmpdir / "pact.h").open( + "r", + encoding="utf-8", + ) as f: + ffibuilder.cdef(f.read()) + ffibuilder.set_source( + "_ffi", + "\n".join([*includes, '#include "pact.h"']), + libraries=["pact_ffi", *extra_libs], + library_dirs=[str(self.tmpdir)], + ) + output = ffibuilder.compile(verbose=True, tmpdir=str(self.tmpdir)) + shutil.copy(output, ROOT_DIR / "pact" / "v3") + + def _download(self, url: str) -> Path: + """ + Download the target URL. + + This will download the target URL to the `pact/data` directory. If the + download artifact is already present, its path will be returned. + + Args: + url: The URL to download + + Return: + The path to the downloaded artifact. """ filename = url.split("/")[-1] artifact = ROOT_DIR / "pact" / "data" / filename artifact.parent.mkdir(parents=True, exist_ok=True) - if not filename.endswith((".zip", ".tar.gz")): - msg = f"Unknown artifact type {filename}" - raise ValueError(msg) - if not artifact.exists(): - import requests - response = requests.get(url, timeout=30) - response.raise_for_status() + try: + response.raise_for_status() + except requests.HTTPError as e: + msg = f"Failed to download from {url}." + raise RuntimeError(msg) from e with artifact.open("wb") as f: f.write(response.content) - if filename.endswith(".zip"): - import zipfile - - with zipfile.ZipFile(artifact) as f: - f.extractall(ROOT_DIR) - if filename.endswith(".tar.gz"): - import tarfile - - with tarfile.open(artifact) as f: - f.extractall(ROOT_DIR) - - # Move the README that is extracted from the Ruby standalone binaries to - # the `data` subdirectory. - if (ROOT_DIR / "pact" / "README.md").exists(): - shutil.move( - ROOT_DIR / "pact" / "README.md", - ROOT_DIR / "pact" / "data" / "README.md", - ) + return artifact diff --git a/pact/constants.py b/pact/constants.py index 225d0a528..f29805940 100644 --- a/pact/constants.py +++ b/pact/constants.py @@ -1,38 +1,60 @@ -"""Constant values for the pact-python package.""" +""" +Constant values for the pact-python package. + +This will default to the bundled Pact binaries bundled with the package, but +should these be unavailable or the environment variable `PACT_USE_SYSTEM_BINS` is +set to `TRUE` or `YES`, the system Pact binaries will be used instead. +""" import os +import shutil +import warnings from pathlib import Path - -def broker_client_exe() -> str: - """Get the appropriate executable name for this platform.""" - if os.name == "nt": - return "pact-broker.bat" - return "pact-broker" - - -def message_exe() -> str: - """Get the appropriate executable name for this platform.""" - if os.name == "nt": - return "pact-message.bat" - return "pact-message" - - -def mock_service_exe() -> str: - """Get the appropriate executable name for this platform.""" - if os.name == "nt": - return "pact-mock-service.bat" - return "pact-mock-service" - - -def provider_verifier_exe() -> str: - """Get the appropriate provider executable name for this platform.""" - if os.name == "nt": - return "pact-provider-verifier.bat" - return "pact-provider-verifier" - - -ROOT_DIR = Path(__file__).parent.resolve() -BROKER_CLIENT_PATH = ROOT_DIR / "bin" / broker_client_exe() -MESSAGE_PATH = ROOT_DIR / "bin" / message_exe() -MOCK_SERVICE_PATH = ROOT_DIR / "bin" / mock_service_exe() -VERIFIER_PATH = ROOT_DIR / "bin" / provider_verifier_exe() +__all__ = [ + "BROKER_CLIENT_PATH", + "MESSAGE_PATH", + "MOCK_SERVICE_PATH", + "VERIFIER_PATH", +] + + +_USE_SYSTEM_BINS = os.getenv("PACT_USE_SYSTEM_BINS", "").upper() in ("TRUE", "YES") +_BIN_DIR = Path(__file__).parent.resolve() / "bin" + + +def _find_executable(executable: str) -> str: + """ + Find the path to an executable. + + This inspects the environment variable `PACT_USE_SYSTEM_BINS` to determine + whether to use the bundled Pact binaries or the system ones. Note that if + the local executables are not found, this will fall back to the system + executables (if found). + + Args: + executable: + The name of the executable to find without the extension. Python + will automatically append the correct extension for the current + platform. + + Returns: + The absolute path to the executable. + + Warns: + RuntimeWarning: + If the executable cannot be found in the system path. + """ + if _USE_SYSTEM_BINS: + bin_path = shutil.which(executable) + else: + bin_path = shutil.which(executable, path=_BIN_DIR) or shutil.which(executable) + if bin_path is None: + msg = f"Unable to find {executable} binary executable." + warnings.warn(msg, RuntimeWarning, stacklevel=2) + return bin_path or "" + + +BROKER_CLIENT_PATH = _find_executable("pact-broker") +MESSAGE_PATH = _find_executable("pact-message") +MOCK_SERVICE_PATH = _find_executable("pact-mock-service") +VERIFIER_PATH = _find_executable("pact-provider-verifier") diff --git a/pact/v3/__init__.py b/pact/v3/__init__.py new file mode 100644 index 000000000..443bf9164 --- /dev/null +++ b/pact/v3/__init__.py @@ -0,0 +1,21 @@ +""" +Pact Python V3. + +The next major release of Pact Python will make use of the Pact reference +library written in Rust. This will allow us to support all of the features of +Pact, and bring the Python library in line with the other Pact libraries. + +The migration will happen in stages, and this module will be used to provide +access to the new functionality without breaking existing code. The stages will +be as follows: + +- **Stage 1**: The new library is exposed within `pact.v3` and can be used + alongside the existing library. During this stage, no guarantees are made + about the stability of the `pact.v3` module. +- **Stage 2**: The library within `pact.v3` is considered stable, and we begin + the process of deprecating the existing library by raising deprecation + warnings when it is used. A detailed migration guide will be provided. +- **Stage 3**: The `pact.v3` module is renamed to `pact`, and the existing + library is moved to the `pact.v2` scope. The `pact.v2` module will be + considered deprecated, and will be removed in a future release. +""" diff --git a/pact/v3/ffi.py b/pact/v3/ffi.py new file mode 100644 index 000000000..3ff7bbfa7 --- /dev/null +++ b/pact/v3/ffi.py @@ -0,0 +1,18 @@ +""" +Python bindings for the Pact FFI. + +This module provides a Python interface to the Pact FFI. It is a thin wrapper +around the C API, and is intended to be used by the Pact Python client library +to provide a Pythonic interface to Pact. + +This module is not intended to be used directly by Pact users. Pact users +should use the Pact Python client library instead. No guarantees are made +about the stability of this module's API. +""" + +from ._ffi import ffi, lib + + +def version() -> str: + """Return the version of the Pact FFI library.""" + return ffi.string(lib.pactffi_version()).decode("utf-8") diff --git a/pyproject.toml b/pyproject.toml index 2fff1c825..c059fc2d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,7 +70,7 @@ dev = [ ################################################################################ [build-system] -requires = ["hatchling", "packaging", "requests"] +requires = ["hatchling", "packaging", "requests", "cffi"] build-backend = "hatchling.build" [tool.hatch.version] @@ -78,17 +78,11 @@ path = "pact/__version__.py" [tool.hatch.build] include = ["pact/**/*.py", "*.md", "LICENSE"] -artifacts = ["pact/bin/*", "pact/data/*"] - -[tool.hatch.build.targets.sdist] -# Ignore binaries in the source distribution, but include the data files -# so that they can be installed from the source distribution. -exclude = ["pact/bin/*"] [tool.hatch.build.targets.wheel] # Ignore the data files in the wheel as their contents are already included # in the package. -exclude = ["pact/data/*"] +artifacts = ["pact/bin/*", "pact/lib/*"] [tool.hatch.build.targets.wheel.hooks.custom] @@ -100,7 +94,7 @@ exclude = ["pact/data/*"] # workflow. [tool.hatch.envs.default] features = ["dev"] -extra-dependencies = ["hatchling", "packaging", "requests"] +extra-dependencies = ["hatchling", "packaging", "requests", "cffi"] [tool.hatch.envs.default.scripts] lint = ["black --check --diff {args:.}", "ruff {args:.}", "mypy {args:.}"] diff --git a/tests/ruff.toml b/tests/ruff.toml new file mode 100644 index 000000000..7d4b21b00 --- /dev/null +++ b/tests/ruff.toml @@ -0,0 +1,6 @@ +extend = "../pyproject.toml" +ignore = [ + "D103", # Require docstrings on public functions + "S101", # Disable assert + "PLR2004", # Forbid magic numbers +] diff --git a/tests/test_constants.py b/tests/test_constants.py index 38bc48b90..384ae2dc6 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -1,67 +1,51 @@ -from unittest import TestCase +"""Test the values in pact.constants.""" -from mock import patch +import os -from pact import constants as constants +def test_broker_client() -> None: + """Test the value of BROKER_CLIENT_PATH on POSIX.""" + import pact.constants -class BrokerClientExeTestCase(TestCase): - def setUp(self): - super(BrokerClientExeTestCase, self).setUp() - self.addCleanup(patch.stopall) - self.mock_os = patch.object(constants, 'os', autospec=True).start() + if os.name == "nt": + # As the Windows filesystem is case insensitive, we must normalize it. + assert pact.constants.BROKER_CLIENT_PATH.lower().endswith("pact-broker.bat") + else: + assert pact.constants.BROKER_CLIENT_PATH.endswith("pact-broker") - def test_other(self): - self.mock_os.name = 'posix' - self.assertEqual(constants.broker_client_exe(), 'pact-broker') - def test_windows(self): - self.mock_os.name = 'nt' - self.assertEqual(constants.broker_client_exe(), 'pact-broker.bat') +def test_message() -> None: + """Test the value of MESSAGE_PATH on POSIX.""" + import pact.constants + if os.name == "nt": + # As the Windows filesystem is case insensitive, we must normalize it. + assert pact.constants.MESSAGE_PATH.lower().endswith("pact-message.bat") + else: + assert pact.constants.MESSAGE_PATH.endswith("pact-message") -class MockServiceExeTestCase(TestCase): - def setUp(self): - super(MockServiceExeTestCase, self).setUp() - self.addCleanup(patch.stopall) - self.mock_os = patch.object(constants, 'os', autospec=True).start() - def test_other(self): - self.mock_os.name = 'posix' - self.assertEqual(constants.mock_service_exe(), 'pact-mock-service') +def test_mock_service() -> None: + """Test the value of MOCK_SERVICE_PATH on POSIX.""" + import pact.constants - def test_windows(self): - self.mock_os.name = 'nt' - self.assertEqual(constants.mock_service_exe(), 'pact-mock-service.bat') + if os.name == "nt": + # As the Windows filesystem is case insensitive, we must normalize it. + assert pact.constants.MOCK_SERVICE_PATH.lower().endswith( + "pact-mock-service.bat", + ) + else: + assert pact.constants.MOCK_SERVICE_PATH.endswith("pact-mock-service") -class MessageExeTestCase(TestCase): - def setUp(self): - super(MessageExeTestCase, self).setUp() - self.addCleanup(patch.stopall) - self.mock_os = patch.object(constants, 'os', autospec=True).start() +def test_verifier() -> None: + """Test the value of VERIFIER_PATH on POSIX.""" + import pact.constants - def test_other(self): - self.mock_os.name = 'posix' - self.assertEqual(constants.message_exe(), 'pact-message') - - def test_windows(self): - self.mock_os.name = 'nt' - self.assertEqual(constants.message_exe(), 'pact-message.bat') - - -class ProviderVerifierExeTestCase(TestCase): - def setUp(self): - super(ProviderVerifierExeTestCase, self).setUp() - self.addCleanup(patch.stopall) - self.mock_os = patch.object(constants, 'os', autospec=True).start() - - def test_other(self): - self.mock_os.name = 'posix' - self.assertEqual( - constants.provider_verifier_exe(), 'pact-provider-verifier') - - def test_windows(self): - self.mock_os.name = 'nt' - self.assertEqual( - constants.provider_verifier_exe(), 'pact-provider-verifier.bat') + if os.name == "nt": + # As the Windows filesystem is case insensitive, we must normalize it. + assert pact.constants.VERIFIER_PATH.lower().endswith( + "pact-provider-verifier.bat", + ) + else: + assert pact.constants.VERIFIER_PATH.endswith("pact-provider-verifier") diff --git a/tests/test_ffi.py b/tests/test_ffi.py new file mode 100644 index 000000000..be3aff537 --- /dev/null +++ b/tests/test_ffi.py @@ -0,0 +1,15 @@ +""" +Tests of the FFI module. + +These tests are intended to ensure that the FFI module is working correctly. +They are not intended to test the Pact API itself, as that is handled by the +client library. +""" + +from pact.v3 import ffi + + +def test_version() -> None: + assert isinstance(ffi.version(), str) + assert len(ffi.version()) > 0 + assert ffi.version().count(".") == 2