Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/ffi provider #245

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ __pycache__/
*$py.class

# C extensions
*.so
# *.so

# Distribution / packaging
.Python
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,8 @@ Please read [CONTRIBUTING.md](https://github.com/pact-foundation/pact-python/blo
To setup a development environment:

1. If you want to run tests for all Python versions, install 2.7, 3.3, 3.4, 3.5, and 3.6 from source or using a tool like [pyenv]
2. Its recommended to create a Python [virtualenv] for the project
2. Its recommended to create a Python [virtualenv] for the project.
3. We are now using FFI bindings. For mac you might want to read these [setup FFI](https://cffi.readthedocs.io/en/latest/installation.html)

The setup the environment, run tests, and package the application, run:
`make release`
Expand Down
12 changes: 11 additions & 1 deletion docker/py37.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,17 @@ COPY requirements_dev.txt .
RUN apk update
RUN apk upgrade

RUN apk add gcc py-pip python-dev libffi-dev openssl-dev gcc libc-dev bash make
RUN apk add gcc py-pip python-dev libffi-dev openssl-dev gcc libc-dev bash cmake make libc6-compat
RUN ln -s /lib/libc.musl-x86_64.so.1 /lib/ld-linux-x86-64.so.2
ENV MUSL_LOCALE_DEPS musl-dev gettext-dev libintl

RUN apk add --no-cache \
$MUSL_LOCALE_DEPS \
&& wget https://gitlab.com/rilian-la-te/musl-locales/-/archive/master/musl-locales-master.zip \
&& unzip musl-locales-master.zip \
&& cd musl-locales-master \
&& cmake -DLOCALE_PROFILE=OFF -D CMAKE_INSTALL_PREFIX:PATH=/usr . && make && make install \
&& cd .. && rm -r musl-locales-master

RUN python -m pip install psutil
RUN pip install -r requirements_dev.txt
Expand Down
Binary file added libs/libpact_ffi-linux-x86_64.so
Binary file not shown.
Binary file added libs/libpact_ffi-osx-x86_64.dylib
Binary file not shown.
Binary file added musl-locales-master.zip
Binary file not shown.
12 changes: 6 additions & 6 deletions pact/cli/verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,12 +188,12 @@ def main(pacts, base_url, pact_url, pact_urls, states_url, states_setup_url,
options = dict(filter(lambda item: item[1] != '', options.items()))
options = dict(filter(lambda item: is_empty_list(item), options.items()))

success, logs = VerifyWrapper().call_verify(*all_pact_urls,
provider=provider,
provider_base_url=base_url,
enable_pending=enable_pending,
include_wip_pacts_since=include_wip_pacts_since,
**options)
success, logs = VerifyWrapper().verify(*all_pact_urls,
provider=provider,
provider_base_url=base_url,
enable_pending=enable_pending,
include_wip_pacts_since=include_wip_pacts_since,
**options)
sys.exit(success)


Expand Down
58 changes: 58 additions & 0 deletions pact/ffi/ffi_verifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""Wrapper to pact reference dynamic libraries using FFI."""
from pact.pact_exception import PactException
from cffi import FFI
import platform


class FFIVerify(object):
"""A Pact Verifier Wrapper."""

def version(self):
"""Publish version info."""
ffi = FFI()
ffi.cdef("""
char *pactffi_version(void);
""")
lib = self._load_ffi_library(ffi)
result = lib.pactffi_version()
return ffi.string(result).decode('utf-8')

def verify(self, *pacts, provider_base_url, provider, enable_pending=False,
include_wip_pacts_since=None, **kwargs):
"""Call verify method."""
self._validate_input(pacts, **kwargs)

ffi = FFI()
ffi.cdef("""
char *pactffi_verify(void);
""")
lib = self._load_ffi_library(ffi)
result = lib.pactffi_version()
return ffi.string(result).decode('utf-8')

def _load_ffi_library(self, ffi):
"""Load the right library."""
target_platform = platform.platform().lower()

if 'darwin' in target_platform or 'macos' in target_platform:
libname = "libs/libpact_ffi-osx-x86_64.dylib"
elif 'linux' in target_platform:
libname = "libs/libpact_ffi-linux-x86_64.so"
elif 'windows' in target_platform:
libname = "libs/libpact_ffi-osx-x86_64.dylib"
else:
msg = ('Unfortunately, {} is not a supported platform. Only Linux,'
' Windows, and OSX are currently supported.').format(
platform.platform())
raise Exception(msg)

return ffi.dlopen(libname)

def _validate_input(self, pacts, **kwargs):
if len(pacts) == 0 and not self._broker_present(**kwargs):
raise PactException('Pact urls or Pact broker required')

def _broker_present(self, **kwargs):
if kwargs.get('broker_url') is None:
return False
return True
21 changes: 21 additions & 0 deletions pact/pact_exception.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Custom Pact Exception."""

class PactException(Exception):
"""PactException when input isn't valid.

Args:
Exception ([type]): [description]

Raises:
KeyError: [description]
Exception: [description]

Returns:
[type]: [description]

"""

def __init__(self, *args, **kwargs):
"""Create wrapper."""
super().__init__(*args, **kwargs)
self.message = args[0]
26 changes: 15 additions & 11 deletions pact/verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ def __str__(self):
"""
return 'Verifier for {} with url {}'.format(self.provider, self.provider_base_url)

def version(self):
"""Return version info."""
return VerifyWrapper().version()

def validate_publish(self, **kwargs):
"""Validate publish has a version."""
if ((kwargs.get('publish') is not None) and (kwargs.get('publish_version') is None)):
Expand All @@ -52,12 +56,12 @@ def verify_pacts(self, *pacts, enable_pending=False, include_wip_pacts_since=Non
# rerun_command() # env =

options = self.extract_params(**kwargs)
success, logs = VerifyWrapper().call_verify(*pacts,
provider=self.provider,
provider_base_url=self.provider_base_url,
enable_pending=enable_pending,
include_wip_pacts_since=include_wip_pacts_since,
**options)
success, logs = VerifyWrapper().verify(*pacts,
provider=self.provider,
provider_base_url=self.provider_base_url,
enable_pending=enable_pending,
include_wip_pacts_since=include_wip_pacts_since,
**options)

return success, logs

Expand Down Expand Up @@ -86,11 +90,11 @@ def verify_with_broker(self, enable_pending=False, include_wip_pacts_since=None,
}
options.update(self.extract_params(**kwargs))

success, logs = VerifyWrapper().call_verify(provider=self.provider,
provider_base_url=self.provider_base_url,
enable_pending=enable_pending,
include_wip_pacts_since=include_wip_pacts_since,
**options)
success, logs = VerifyWrapper().verify(provider=self.provider,
provider_base_url=self.provider_base_url,
enable_pending=enable_pending,
include_wip_pacts_since=include_wip_pacts_since,
**options)
return success, logs

def extract_params(self, **kwargs):
Expand Down
35 changes: 5 additions & 30 deletions pact/verify_wrapper.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Wrapper to verify previously created pacts."""

from pact.constants import VERIFIER_PATH
from pact.pact_exception import PactException
import sys
import os
import platform
Expand Down Expand Up @@ -101,26 +102,6 @@ def rerun_command():
env['PACT_INTERACTION_RERUN_COMMAND'] = command
return env

class PactException(Exception):
"""PactException when input isn't valid.

Args:
Exception ([type]): [description]

Raises:
KeyError: [description]
Exception: [description]

Returns:
[type]: [description]

"""

def __init__(self, *args, **kwargs):
"""Create wrapper."""
super().__init__(*args, **kwargs)
self.message = args[0]

class VerifyWrapper(object):
"""A Pact Verifier Wrapper."""

Expand All @@ -133,7 +114,7 @@ def _validate_input(self, pacts, **kwargs):
if len(pacts) == 0 and not self._broker_present(**kwargs):
raise PactException('Pact urls or Pact broker required')

def call_verify(
def verify(
self, *pacts, provider_base_url, provider, enable_pending=False,
include_wip_pacts_since=None, **kwargs
):
Expand Down Expand Up @@ -196,12 +177,6 @@ def call_verify(

return result.returncode, logs

def publish_results(self, provider_app_version, command):
"""Publish results to broker."""
if not provider_app_version:
# todo implement
raise Exception('todo')

command.extend(["--provider-app-version",
provider_app_version,
"--publish-verification-results"])
def version(self):
"""Publish version info."""
return '0.0.0'
8 changes: 6 additions & 2 deletions requirements_dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@ Click>=2.0.0
coverage==5.4
Flask==1.1.1
configparser==3.5.0
fastapi==0.67.0
flake8==3.8.3
mock==3.0.5
psutil==5.7.0
pycodestyle==2.6.0
pydocstyle==4.0.1
tox==3.14.0
tox==3.24.1
pytest==5.4.1
pytest-cov==2.11.1
requests==2.26
tox-travis==0.8
urllib3>=1.26.5
wheel==0.24.0
six>=1.9.0
fastapi==0.67.0
uvicorn==0.14.0
cffi==1.14.6
77 changes: 71 additions & 6 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@
import tarfile

from zipfile import ZipFile
import shutil
import gzip

from setuptools import setup
from setuptools.command.develop import develop
from setuptools.command.install import install
from urllib.request import urlopen


IS_64 = sys.maxsize > 2 ** 32
PACT_STANDALONE_VERSION = '1.88.51'

PACT_FFI_VERSION = '0.0.0'

here = os.path.abspath(os.path.dirname(__file__))

Expand All @@ -40,6 +43,7 @@ def run(self):
os.mkdir(bin_path)

install_ruby_app(bin_path)
install_rust_app(bin_path)


class PactPythonInstallCommand(install):
Expand All @@ -56,6 +60,71 @@ def run(self):
bin_path = os.path.join(self.install_lib, 'pact', 'bin')
os.mkdir(bin_path)
install_ruby_app(bin_path)
install_rust_app(bin_path)


def install_rust_app(bin_path):
"""
Download the relevant rust binaries and install it for use.

:param bin_path: The path where binaries should be installed.
"""
target_platform = platform.platform().lower()

if 'darwin' in target_platform or 'macos' in target_platform:
suffix = 'libpact_ffi-osx-x86_64'
elif 'linux' in target_platform:
suffix = 'libpact_ffi-linux-x86_64'
elif 'windows' in target_platform:
suffix = 'pact_ffi-windows-x86_64'
else:
msg = ('Unfortunately, {} is not a supported platform. Only Linux,'
' Windows, and OSX are currently supported.').format(
platform.platform())
raise Exception(msg)

if 'windows' in platform.platform().lower():
fetch_lib(bin_path, suffix, 'dll')
fetch_lib(bin_path, suffix, 'dll.lib')
fetch_lib(bin_path, suffix, 'lib')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you are using the DLL, you don't need the static lib (.lib). But there is no harm in downloading it, apart from it being the biggest one.


else:
fetch_lib(bin_path, suffix, 'a')
if 'darwin' in target_platform or 'macos' in target_platform:
fetch_lib(bin_path, suffix, 'dylib')

elif 'linux' in target_platform:
fetch_lib(bin_path, suffix, 'so')


def fetch_lib(bin_path, suffix, type):
"""
Fetch rust binaries to the bin_path.

:param bin_path: The path where binaries should be installed.
:param suffix: The suffix filenamne unique to this platform (e.g. libpact_ffi-osx-x86_64).
"param type: The type of library (e.g. so|a|dll|dylib)
Raises:
RuntimeError: [description]

"""
dest = os.path.join(bin_path, f'{suffix}.{type}')
zipped = os.path.join(bin_path, f'{suffix}.{type}.gz')
uri = (
f"https://github.com/pact-foundation/pact-reference/releases"
f"/download/libpact_ffi-v{PACT_FFI_VERSION}/{suffix}.{type}.gz")

resp = urlopen(uri)
with open(zipped, 'wb') as f:
if resp.code == 200:
f.write(resp.read())
else:
raise RuntimeError(
'Received HTTP {} when downloading {}'.format(
resp.code, resp.url))

with gzip.open(zipped) as g, open(dest, 'wb') as f_out:
shutil.copyfileobj(g, f_out)


def install_ruby_app(bin_path):
Expand All @@ -82,11 +151,6 @@ def install_ruby_app(bin_path):
platform.platform())
raise Exception(msg)

if sys.version_info.major == 2:
from urllib import urlopen
else:
from urllib.request import urlopen

path = os.path.join(bin_path, suffix)
resp = urlopen(uri.format(version=PACT_STANDALONE_VERSION, suffix=suffix))
with open(path, 'wb') as f:
Expand All @@ -113,6 +177,7 @@ def read(filename):


dependencies = [
'cffi==1.14.6',
'click>=2.0.0',
'psutil>=2.0.0',
'requests>=2.5.0',
Expand Down
Loading
Loading