Skip to content
Draft
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
903371b
feat: added support for message provider (#251)
pulphix Jul 31, 2021
0089937
fix: issue originating from snyk with requests and urllib (#252)
elliottmurray Jul 31, 2021
da49cd7
chore: Releasing version 1.4.0
elliottmurray Aug 7, 2021
34f67a7
feat: added rust linux/osx support
elliottmurray Jul 17, 2021
afd0931
feat: fix to pytest request signature@
elliottmurray Jul 17, 2021
6c95248
feat: first ffi interface
elliottmurray Jul 19, 2021
de93a1f
feat: added so file
elliottmurray Jul 19, 2021
be6c971
fix: so now a relative path so picked up by tests (temp fix) Also som…
elliottmurray Jul 19, 2021
4b239e4
chore: move dyn libs to libs folder@
elliottmurray Jul 24, 2021
e7d686a
feat: some work on preparing verify call
elliottmurray Jul 24, 2021
050ee0e
feat: refactoring to prepare verify
elliottmurray Jul 24, 2021
7dc8864
fix: make uvicorn versions over 0.14 (#255)
elliottmurray Aug 17, 2021
e370786
chore: Releasing version 1.4.1
elliottmurray Aug 17, 2021
7d18a2f
docs(readme): trivial 'hello world' changes :)
mikegeeves Aug 18, 2021
214c47d
docs(readme): update python versions mentioned to those supported (go…
mikegeeves Aug 18, 2021
a84f856
build(flake8): exclude venv and tox dirs from flake8 linting
mikegeeves Aug 18, 2021
2d30706
build(ffi): use libs from pact dir instead of libs. Bump to 0.0.1. Mi…
mikegeeves Aug 18, 2021
c6ded0a
feat(ffi verifier): very basic call of verify, with no args
mikegeeves Aug 20, 2021
e9c1bb0
feat(ffi verifier): verifier test, looking at log output
mikegeeves Aug 21, 2021
358748f
feat(ffi verifier): verifier test, looking at log output
mikegeeves Aug 21, 2021
070f905
feat(ffi verifier): set log level for output
mikegeeves Aug 21, 2021
cf0bfee
feat(ffi verifier): help and version tests
mikegeeves Aug 21, 2021
f2230b6
chore: Bundle Ruby standalones into dist artifact. (#256)
taj-p Aug 22, 2021
43fedd1
chore: Releasing version 1.4.2
elliottmurray Aug 22, 2021
3471c12
feat(ffi verifier cli): use FFI to determine the args for the CLI wra…
mikegeeves Aug 22, 2021
11228ed
feat(ffi verifier): add possible values e.g. to loglevel
mikegeeves Aug 23, 2021
69233a6
feat(ffi verify): add in default value
mikegeeves Aug 23, 2021
3486281
feat(ffi verifier cli): handle both flags and options, along with mul…
mikegeeves Aug 26, 2021
283dcef
feat(ffi verifier cli): construct args string from inputs. Call pactf…
mikegeeves Aug 27, 2021
411ad66
feat(ffi verifier): add a first (currently broken) test
mikegeeves Aug 27, 2021
bd809ff
feat(ffi verifier cli): call verifier from CLI
mikegeeves Aug 27, 2021
08f0dc0
feat: added support for message provider using pact broker (#257)
pulphix Sep 5, 2021
216229e
chore: Releasing version 1.4.3
elliottmurray Sep 5, 2021
d773d85
Add support for ENVs, tidy, docs
mikegeeves Sep 8, 2021
a2ce9f4
feat(ffi verifier cli): correct return type of cli_args
mikegeeves Sep 8, 2021
65d99d3
feat(ffi verifier): switch to using the log buffer instead of interme…
mikegeeves Sep 8, 2021
ccb2641
feat(ffi verifier cli): adding in real test using httpserver
mikegeeves Sep 8, 2021
273e1f3
feat(ffi verifier cli): both a successful and an unsuccessful verific…
mikegeeves Sep 8, 2021
55f4e55
feat(ffi verifier): add same success and fail tests to the verifier d…
mikegeeves Sep 8, 2021
8701015
feat(ffi verifier): minor tidy
mikegeeves Sep 8, 2021
087a655
feat(ffi verifier): generate Verifier args class by various methods
mikegeeves Sep 8, 2021
f3601fd
Merge branch 'master' into feat/ffi-provider-mike
mikegeeves Sep 13, 2021
741cf99
feat(ffi verifier): tidy, comments
mikegeeves Sep 13, 2021
5703160
Merge branch 'feat/ffi-provider-mike' of https://github.com/mikegeeve…
mikegeeves Sep 13, 2021
b18128a
refactor(setup.py): minor tidy
mikegeeves Sep 13, 2021
5ebf947
feat(ffi verifier): tidying up
mikegeeves Sep 20, 2021
53819eb
feat(ffi verifier cli): tidy usage etc, add info about ENVs being used
mikegeeves Sep 21, 2021
3882837
test(pact): fixing tests, failing after allowRedirect was added elsew…
mikegeeves Sep 21, 2021
3c82f1f
refactor(setup.py): black
mikegeeves Sep 21, 2021
778ef4c
feat(ffi): use the same approach to download the FFI libs as the Ruby…
mikegeeves Sep 21, 2021
f91e9b1
Merge branch 'feat/ffi-provider' into feat/ffi-provider-mike
mikegeeves Sep 21, 2021
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
1 change: 1 addition & 0 deletions .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
ignore = E226,E302,E41,W503
max-line-length = 160
max-complexity = 10
exclude = venv, .tox
42 changes: 25 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,19 @@ Note: As of Version 1.0 deprecates support for python 2.7 to allow us to incorpo
# How to use pact-python

## Installation

```
pip install pact-python
```

## Getting started

<!-- Absolute link for rendering page in docs.pact.io -->
A guide follows but if you go to the [e2e examples](https://github.com/pact-foundation/pact-python/tree/master/examples/e2e). This has a consumer, provider and pact-broker set of tests.

## Writing a Pact

Creating a complete contract is a two step process:
Creating a complete contract is a two-step process:

1. Create a test on the consumer side that declares the expectations it has of the provider
2. Create a provider state that allows the contract to pass when replayed against the provider
Expand Down Expand Up @@ -82,7 +84,6 @@ class GetUserInfoContract(unittest.TestCase):
result = user('UserA')

self.assertEqual(result, expected)

```

This does a few important things:
Expand Down Expand Up @@ -164,11 +165,13 @@ The mock service offers you several important features when building your contra
- Finally, it will record your contracts as a JSON file that you can store in your repository or publish to a Pact broker.

## Expecting Variable Content

The above test works great if that user information is always static, but what happens if
the user has a last updated field that is set to the current time every time the object is
modified? To handle variable data and make your tests more robust, there are 3 helpful matchers:

### Term(matcher, generate)

Asserts the value should match the given regular expression. You could use this
to expect a timestamp with a particular format in the request or response where
you know you need a particular format, but are unconcerned about the exact date:
Expand All @@ -194,6 +197,7 @@ provider, the regex will be used to search the response from the real provider s
and the test will be considered successful if the regex finds a match in the response.

### Like(matcher)

Asserts the element's type matches the matcher. For example:

```python
Expand All @@ -202,6 +206,7 @@ Like(123) # Matches if the value is an integer
Like('hello world') # Matches if the value is a string
Like(3.14) # Matches if the value is a float
```

The argument supplied to `Like` will be what the mock service responds with.

When a dictionary is used as an argument for Like, all the child objects (and their child objects etc.) will be matched according to their types, unless you use a more specific matcher like a Term.
Expand Down Expand Up @@ -262,18 +267,18 @@ Format().ip_address # Matches if the value is a ip address

We've created a number of them for you to save you the time:

| matcher | description |
|-----------------|-------------------------------------------------------------------------------------------------|
| `identifier` | Match an ID (e.g. 42) |
| `integer` | Match all numbers that are integers (both ints and longs) |
| `decimal` | Match all real numbers (floating point and decimal) |
| `hexadecimal` | Match all hexadecimal encoded strings |
| `date` | Match string containing basic ISO8601 dates (e.g. 2016-01-01) |
| `timestamp` | Match a string containing an RFC3339 formatted timestapm (e.g. Mon, 31 Oct 2016 15:21:41 -0400) |
| `time` | Match string containing times in ISO date format (e.g. T22:44:30.652Z) |
| `ip_address` | Match string containing IP4 formatted address |
| matcher | description |
|----------------|-------------------------------------------------------------------------------------------------|
| `identifier` | Match an ID (e.g. 42) |
| `integer` | Match all numbers that are integers (both ints and longs) |
| `decimal` | Match all real numbers (floating point and decimal) |
| `hexadecimal` | Match all hexadecimal encoded strings |
| `date` | Match string containing basic ISO8601 dates (e.g. 2016-01-01) |
| `timestamp` | Match a string containing an RFC3339 formatted timestamp (e.g. Mon, 31 Oct 2016 15:21:41 -0400) |
| `time` | Match string containing times in ISO date format (e.g. T22:44:30.652Z) |
| `ip_address` | Match string containing IP4 formatted address |
| `ipv6_address` | Match string containing IP6 formatted address |
| `uuid` | Match strings containing UUIDs |
| `uuid` | Match strings containing UUIDs |

These can be used to replace other matchers

Expand Down Expand Up @@ -399,18 +404,20 @@ The provider application version. Required for publishing verification results.
Publish verification results to the broker.

### Python API

You can use the Verifier class. This has all the same parameters as the cli tool but allows you to write native python code and the test framework of your choice.

```python
verifier = Verifier(provider='UserService',
provider_base_url=PACT_URL)

output, logs = verifier.verify_pacts('./userserviceclient-userservice.json')

```

You can see more details in the [e2e examples](https://github.com/pact-foundation/pact-python/tree/master/examples/e2e/tests/provider/test_provider.py).

### Provider States

In many cases, your contracts will need very specific data to exist on the provider
to pass successfully. If you are fetching a user profile, that user needs to exist,
if querying a list of records, one or more records needs to exist. To support
Expand All @@ -429,19 +436,20 @@ on the provider application or a separate one. Some strategies for managing stat
For more information about provider states, refer to the [Pact documentation] on [Provider States].

# Development

<!-- Absolute link for rendering page in docs.pact.io -->
Please read [CONTRIBUTING.md](https://github.com/pact-foundation/pact-python/blob/master/CONTRIBUTING.md)

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]
Copy link
Owner Author

Choose a reason for hiding this comment

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

Matching up with the list in tox.ini

2. Its recommended to create a Python [virtualenv] for the project.
1. If you want to run tests for all Python versions, install 3.6, 3.7, 3.8, and 3.9 from source or using a tool like [pyenv]
2. It's 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`

If you are just interested in packaging pact-python so you can install it using pip:
If you are just interested in packaging pact-python you can install it using pip:

`make package`

Expand Down
Binary file removed libs/libpact_ffi-linux-x86_64.so
Binary file not shown.
Binary file removed libs/libpact_ffi-osx-x86_64.dylib
Binary file not shown.
46 changes: 0 additions & 46 deletions pact/ffi/ffi_verifier.py

This file was deleted.

58 changes: 58 additions & 0 deletions pact/ffi/pact_ffi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""Wrapper to pact reference dynamic libraries using FFI."""

import platform

from cffi import FFI


class PactFFI(object):
"""This interfaces with the Rust crate `pact_ffi`_, which exposes the Pact
API via a C Foreign Function Interface. In the case of python, the library
is then accessed using `CFFI`_.
This class will implement the shared library loading, along with a wrapper
for the functions provided by the base crate. For each of the Rust modules
exposed, a corresponding python class will extend this base class, and
provide the wrapper for the functions the module provides.
.. _pact_ffi:
https://docs.rs/pact_ffi/0.0.1/pact_ffi/index.html
.. _CFFI:
https://cffi.readthedocs.io/en/latest/
"""

def version(self) -> str:
"""Get the current library version.
:return: pact_ffi library version, for example "0.0.1"
"""
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 _load_ffi_library(self, ffi):
"""Load the appropriate library for the current platform."""
target_platform = platform.platform().lower()

if target_platform in ['darwin', 'macos']:
libname = "pact/bin/libpact_ffi-osx-x86_64.dylib"
elif 'linux' in target_platform:
libname = "pact/bin/libpact_ffi-linux-x86_64.so"
elif 'windows' in target_platform:
libname = "pact/bin/pact_ffi-windows-x86_64.dll"
Copy link
Owner Author

Choose a reason for hiding this comment

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

This had the osx lib, think this is correct but no Windows to check it actually works

else:
msg = (
f'Unfortunately, {platform.platform()} is not a supported '
f'platform. Only Linux, Windows, and OSX are currently '
f'supported.'
)
raise Exception(msg)

return ffi.dlopen(libname)
79 changes: 79 additions & 0 deletions pact/ffi/verifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""Wrapper to pact reference dynamic libraries using FFI."""
import os
import tempfile
from enum import Enum, unique
from typing import NamedTuple, List

from cffi import FFI

from pact.ffi.pact_ffi import PactFFI


@unique
class VerifyStatus(Enum):
SUCCESS = 0 # Operation succeeded
VERIFIER_FAILED = 1 # The verification process failed, see output for errors
NULL_POINTER = 2 # A null pointer was received
PANIC = 3 # The method panicked
INVALID_ARGS = 4 # Invalid arguments were provided to the verification process


@unique
class LogToBufferStatus(Enum):
SUCCESS = 0 # Operation succeeded
CANT_SET_LOGGER = -1 # Can't set the logger
NO_LOGGER = -2 # No logger has been initialized
SPECIFIER_NOT_UTF8 = -3 # The sink specifier was not UTF-8 encoded
UNKNOWN_SINK_TYPE = -4 # The sink type specified is not a known type
MISSING_FILE_PATH = -5 # No file path was specified in the sink specification
CANT_OPEN_SINK_TO_FILE = -6 # Opening a sink to the given file failed
CANT_CONSTRUCT_SINK = -7 # Can't construct sink


class VerifyResult(NamedTuple):
return_code: VerifyStatus
logs: List[str]


class Verifier(PactFFI):
"""A Pact Verifier Wrapper.

This interfaces with the Rust FFI crate pact_ffi, specifically the
`verifier`_ module.

.. _verifier:
https://docs.rs/pact_ffi/0.0.1/pact_ffi/verifier/index.html
"""

def verify(self, args=None) -> VerifyResult:
"""Call verify method."""
ffi = FFI()
ffi.cdef(
"""
int pactffi_verify(char *);
int pactffi_log_to_file(char *);
"""
)
lib = self._load_ffi_library(ffi)

if args:
c_args = ffi.new('char[]', bytes(args, 'utf-8'))
else:
c_args = ffi.NULL

# This fails if called a second time after the library has been loaded
Copy link
Owner Author

Choose a reason for hiding this comment

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

TODO: pull this out before merging, I wanted to try and setup to initialise the library for each call so it is completely independent, but wasn't able to get that to work

# ..but still seems to work
# result = lib.pactffi_log_to_buffer()
# assert LogToBufferStatus(result) == LogToBufferStatus.SUCCESS
# Additionally, when reading from the buffer it seems to come back empty
# when running normally. Storing to a temporary file, at least for now.
with tempfile.TemporaryDirectory() as td:
output = os.path.join(td, 'output')
output_c = ffi.new('char[]', bytes(output, 'utf-8'))
lib.pactffi_log_to_file(output_c)

result = lib.pactffi_verify(c_args)

lines = open(output).readlines()

return VerifyResult(result, lines)
9 changes: 5 additions & 4 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

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

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

Expand All @@ -31,7 +31,7 @@ class PactPythonDevelopCommand(develop):
Custom develop mode installer for pact-python.

When the package is installed using `python setup.py develop` or
`pip install -e` it will download and unpack the appropriate Pact
`pip install -e .` it will download and unpack the appropriate Pact
mock service and provider verifier.
"""

Expand Down Expand Up @@ -59,6 +59,7 @@ def run(self):
install.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)

Expand Down Expand Up @@ -102,8 +103,8 @@ 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)
:param suffix: The suffix filename 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]

Expand Down
4 changes: 0 additions & 4 deletions tests/ffi/test_ffi_verifier.py

This file was deleted.

17 changes: 17 additions & 0 deletions tests/ffi/test_verifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from unittest import TestCase

from pact.ffi.verifier import Verifier, VerifyStatus


class VerifierTestCase(TestCase):
def test_version(self):
assert Verifier().version() == "0.0.1"

def test_verify_no_args(self):
result = Verifier().verify(args=None)
self.assertEqual(VerifyStatus(result.return_code), VerifyStatus.NULL_POINTER)

def test_verify_invalid_args(self):
result = Verifier().verify(args="Your argument is invalid")
assert VerifyStatus(result.return_code) == VerifyStatus.INVALID_ARGS
self.assertIn('UnknownArgument', '\n'.join(result.logs))