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

Feature/add support and example messenger consumer using rust engine 380 #699

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
31bd379
Adding v3 and v4 message consumer tests
valkolovos May 16, 2024
c6dd880
created MessagePact, cleaned up the verify method to use interaction …
valkolovos May 16, 2024
2891e4a
removing write_message_file method as it is unecessary
valkolovos May 16, 2024
76e9ff2
lint cleanup
valkolovos May 22, 2024
1e62a37
lint cleanup
valkolovos May 22, 2024
d0daa48
revert change that shouldn't have been committed
valkolovos May 22, 2024
3d7dc66
moving message pact verification to interaction
valkolovos May 23, 2024
49f06ee
setting harder version on jsonpath_ng
valkolovos May 28, 2024
2e460f7
cleaning up ffi.py
valkolovos May 31, 2024
1645640
PR changes
valkolovos Jun 5, 2024
deec315
importing Self from typing_extensions for backwards compatibility
valkolovos Jun 5, 2024
9db0021
chore(examples): ensure docker compose is started
JP-Ellis Jun 6, 2024
9aef6b4
chore(examples): wait for servers to start
JP-Ellis Jun 6, 2024
dbcfe80
docs(examples): explain the purpose for fs class
JP-Ellis Jun 6, 2024
713e59d
chore(examples): remove redundant v3 in filename
JP-Ellis Jun 6, 2024
4070ecd
fix(examples): typing annotations
JP-Ellis Jun 6, 2024
cef8f21
chore(examples): silence deprecation warnings
JP-Ellis Jun 6, 2024
0f76c5f
refactor(v3): rename AsyncMessagePactResult to AsyncInteractionResult
JP-Ellis Jun 6, 2024
57d1a07
refactor(v3): merge Pact classes
JP-Ellis Jun 6, 2024
7bfa7b8
chore(v3): remove _pact_handle as it is never used
JP-Ellis Jun 6, 2024
07d413b
docs(v3): add some clarity about interaction parts
JP-Ellis Jun 6, 2024
a972986
docs(ffi): remove unnecessary safety message
JP-Ellis Jun 6, 2024
941f123
chore: use existing method
JP-Ellis Jun 6, 2024
7258455
chore(v3): minor refactor of with_contents
JP-Ellis Jun 6, 2024
dd4af8a
chore(v3): remove public reify
JP-Ellis Jun 6, 2024
8f0815d
chore(v3): use with_metadata_v2
JP-Ellis Jun 6, 2024
f79a20d
chore(v3): publicly export Pact and Verifier
JP-Ellis Jun 6, 2024
9326a90
chore(ffi): minor type/style fixes
JP-Ellis Jun 6, 2024
f777d0c
first pass at fixing tests
valkolovos Jun 11, 2024
ed26782
remove 'import pdb'
valkolovos Jun 11, 2024
4e13c37
chore(ffi): remove message handle
JP-Ellis Jun 11, 2024
1b14be1
chore(v3): add with_metadata
JP-Ellis Jun 12, 2024
73f82fc
style: use snake_case
JP-Ellis Jun 12, 2024
980565b
chore: remove with_content
JP-Ellis Jun 12, 2024
55b42ea
chore: remove interaction verify
JP-Ellis Jun 12, 2024
2d92f86
feat(ffi): use the new with_metadata
JP-Ellis Jun 12, 2024
e56f018
chore(ffi): add enum type alias
JP-Ellis Jun 16, 2024
aafeeda
chore: implement ffi
JP-Ellis Jun 16, 2024
466e107
feat(v3): implement interactions iterator
JP-Ellis Jun 16, 2024
ab3c02b
chore(v3): remove messages iterator
JP-Ellis Jun 16, 2024
5aaf1a7
feat(v3): add verify method for pact messages
JP-Ellis Jun 16, 2024
d64abb0
chore(v3): remove get_provider_states
JP-Ellis Jun 16, 2024
7ab24c8
fix(v3): various typing issues
JP-Ellis Jun 16, 2024
ee99af7
feat: optional freeing of memory
JP-Ellis Jun 18, 2024
d1c0d1b
chore(ffi): minor fixes and implementations
JP-Ellis Jun 18, 2024
5107a59
fix(ffi): ensure parent outline dependent objects
JP-Ellis Jun 18, 2024
d118431
feat(ffi): add `generate_contents` methods
JP-Ellis Jun 18, 2024
db2d343
feat(v3): add with_generators
JP-Ellis Jun 18, 2024
aeb2e95
chore(ffi): minor changes
JP-Ellis Jun 18, 2024
dccb863
feat(v3): add new exception types
JP-Ellis Jun 18, 2024
c3ddcd5
fix(v3): interactions iterator
JP-Ellis Jun 19, 2024
6f0eff6
chore(test): use named tuple more broadly
JP-Ellis Jun 19, 2024
cf6cfd7
refactor(test): v3 message consumer
JP-Ellis Jun 19, 2024
a9fa8f0
refactor(examples): v3 message consumer
JP-Ellis Jun 19, 2024
67f5322
fix(v3): typing issues
JP-Ellis Jun 19, 2024
654c904
feat(ffi): upgrade ffi library to v0.4.21
JP-Ellis Jun 19, 2024
999fb45
ci: add wheel target
JP-Ellis Jun 19, 2024
aed2e09
chore(v3): remove defunct test
JP-Ellis Jun 19, 2024
31b9ed4
refactor(tests): minor drying message consumer tests
JP-Ellis Jun 20, 2024
dd14a47
chore: ignore test outputs
JP-Ellis Jun 21, 2024
9e210f6
docs: fix discovery
JP-Ellis Jun 21, 2024
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
5 changes: 4 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,12 @@ jobs:
- os: ubuntu-20.04
archs: aarch64
build: musllinux
- os: macos-12
- os: macos-14
archs: arm64
build: ""
- os: windows-2019
archs: ARM64
build: ""

steps:
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
src/pact/bin
src/pact/data

# Test outputs
examples/tests/pacts

# Version is determined from the VCS
src/pact/__version__.py

Expand Down
4 changes: 3 additions & 1 deletion examples/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ services:
broker:
image: pactfoundation/pact-broker:latest-multi
depends_on:
- postgres
postgres:
condition: service_healthy
ports:
- "9292:9292"
restart: always
Expand All @@ -41,3 +42,4 @@ services:
interval: 1s
timeout: 2s
retries: 5
start_period: 30s
17 changes: 16 additions & 1 deletion examples/src/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,22 @@


class Filesystem:
"""Filesystem interface."""
"""
Filesystem interface.

In practice, the handler would process messages and perform some actions on
other systems, whether that be a database, a filesystem, or some other
service. This capability would typically be offered by some library;
however, when running tests, we typically wish to avoid actually interacting
with this external service.

In order to avoid side effects while testing, the test setup should mock out
the calls to the external service.

This class provides a simple dummy filesystem interface (which evidently
would fail if actually used), and serves to demonstrate how to mock out
external services when testing.
"""

def __init__(self) -> None:
"""Initialize the filesystem connection."""
Expand Down
2 changes: 2 additions & 0 deletions examples/tests/test_01_provider_fastapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

from __future__ import annotations

import time
from multiprocessing import Process
from typing import Any, Dict, Generator, Union
from unittest.mock import MagicMock
Expand Down Expand Up @@ -93,6 +94,7 @@ def verifier() -> Generator[Verifier, Any, None]:
provider_base_url=str(PROVIDER_URL),
)
proc.start()
time.sleep(2)
yield verifier
proc.kill()

Expand Down
2 changes: 2 additions & 0 deletions examples/tests/test_01_provider_flask.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

from __future__ import annotations

import time
from multiprocessing import Process
from typing import Any, Dict, Generator, Union
from unittest.mock import MagicMock
Expand Down Expand Up @@ -81,6 +82,7 @@ def verifier() -> Generator[Verifier, Any, None]:
provider_base_url=str(PROVIDER_URL),
)
proc.start()
time.sleep(2)
yield verifier
proc.kill()

Expand Down
Empty file added examples/tests/v3/__init__.py
Empty file.
170 changes: 170 additions & 0 deletions examples/tests/v3/test_01_message_consumer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
"""
Consumer test of example message handler using the v3 API.

This test will create a pact between the message handler
and the message provider.
"""

from __future__ import annotations

import json
import logging
from pathlib import Path
from typing import (
TYPE_CHECKING,
Any,
Dict,
Generator,
)
from unittest.mock import MagicMock

import pytest

from examples.src.message import Handler
from pact.v3.pact import Pact

if TYPE_CHECKING:
from collections.abc import Callable


log = logging.getLogger(__name__)


@pytest.fixture(scope="module")
def pact() -> Generator[Pact, None, None]:
"""
Set up Message Pact Consumer.

This fixtures sets up the Message Pact consumer and the pact it has with a
provider. The consumer defines the expected messages it will receive from
the provider, and the Python test suite verifies that the correct actions
are taken.

The verify method takes a function as an argument. This function
will be called with one or two arguments - the value of `with_body` and
the contents of `with_metadata` if provided.

If the function under test does not take those parameters, you can create
a wrapper function to convert the pact parameters into the values
expected by your function.


For each interaction, the consumer defines the following:

```python
(
pact = Pact("consumer name", "provider name")
processed_messages: list[MessagePact.MessagePactResult] = pact \
.with_specification("V3")
.upon_receiving("a request", "Async") \
.given("a request to write test.txt") \
.with_body(msg) \
.with_metadata({"Content-Type": "application/json"})
.verify(pact_handler)
)

```
"""
pact_dir = Path(Path(__file__).parent.parent / "pacts")
pact = Pact("v3_message_consumer", "v3_message_provider")
log.info("Creating Message Pact with V3 specification")
yield pact.with_specification("V3")
pact.write_file(pact_dir, overwrite=True)


@pytest.fixture()
def handler() -> Handler:
"""
Fixture for the Handler.

This fixture mocks the filesystem calls in the handler, so that we can
verify that the handler is calling the filesystem correctly.
"""
handler = Handler()
handler.fs = MagicMock()
handler.fs.write.return_value = None
handler.fs.read.return_value = "Hello world!"
return handler


@pytest.fixture()
def verifier(
handler: Handler,
) -> Generator[Callable[[str | bytes | None, Dict[str, Any]], None], Any, None]:
"""
Verifier function for the Pact.

This function is passed to the `verify` method of the Pact object. It is
responsible for taking in the messages (along with the context/metadata)
and ensuring that the consumer is able to process the message correctly.

In our case, we deserialize the message and pass it to the (pre-mocked)
handler for processing. We then verify that the underlying filesystem
calls were made as expected.
"""
assert isinstance(handler.fs, MagicMock), "Handler filesystem not mocked"

def _verifier(msg: str | bytes | None, context: Dict[str, Any]) -> None:
assert msg is not None, "Message is None"
data = json.loads(msg)
log.info(
"Processing message: ",
extra={"input": msg, "processed_message": data, "context": context},
)
handler.process(data)

yield _verifier

assert handler.fs.mock_calls, "Handler did not call the filesystem"


def test_async_message_handler_write(
pact: Pact,
handler: Handler,
verifier: Callable[[str | bytes | None, Dict[str, Any]], None],
) -> None:
"""
Create a pact between the message handler and the message provider.
"""
assert isinstance(handler.fs, MagicMock), "Handler filesystem not mocked"

(
pact.upon_receiving("a write request", "Async")
.given("a request to write test.txt")
.with_body(
json.dumps({
"action": "WRITE",
"path": "my_file.txt",
"contents": "Hello, world!",
})
)
)
pact.verify(verifier, "Async")

handler.fs.write.assert_called_once_with("my_file.txt", "Hello, world!")


def test_async_message_handler_read(
pact: Pact,
handler: Handler,
verifier: Callable[[str | bytes | None, Dict[str, Any]], None],
) -> None:
"""
Create a pact between the message handler and the message provider.
"""
assert isinstance(handler.fs, MagicMock), "Handler filesystem not mocked"

(
pact.upon_receiving("a read request", "Async")
.given("a request to read test.txt")
.with_body(
json.dumps({
"action": "READ",
"path": "my_file.txt",
"contents": "Hello, world!",
})
)
)
pact.verify(verifier, "Async")

handler.fs.read.assert_called_once_with("my_file.txt")
6 changes: 4 additions & 2 deletions hatch_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@

# Latest version available at:
# https://github.com/pact-foundation/pact-reference/releases
PACT_LIB_VERSION = os.getenv("PACT_LIB_VERSION", "0.4.19")
PACT_LIB_VERSION = os.getenv("PACT_LIB_VERSION", "0.4.21")
PACT_LIB_URL = "https://github.com/pact-foundation/pact-reference/releases/download/libpact_ffi-v{version}/{prefix}pact_ffi-{os}-{machine}.{ext}"


Expand Down Expand Up @@ -256,7 +256,7 @@ def _pact_lib_url(self, version: str) -> str: # noqa: C901, PLR0912
if platform.startswith("macosx"):
os = "macos"
if platform.endswith("arm64"):
machine = "aarch64-apple-darwin"
machine = "aarch64"
elif platform.endswith("x86_64"):
machine = "x86_64"
else:
Expand All @@ -274,6 +274,8 @@ def _pact_lib_url(self, version: str) -> str: # noqa: C901, PLR0912

if platform.endswith("amd64"):
machine = "x86_64"
elif platform.endswith(("arm64", "aarch64")):
machine = "aarch64"
else:
raise UnsupportedPlatformError(platform)
return PACT_LIB_URL.format(
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,10 @@ addopts = [
"--cov-report=xml",
]
filterwarnings = [
"ignore::DeprecationWarning:examples",
"ignore::DeprecationWarning:pact",
"ignore::DeprecationWarning:tests",
"ignore::PendingDeprecationWarning:examples",
"ignore::PendingDeprecationWarning:pact",
"ignore::PendingDeprecationWarning:tests",
]
Expand Down
6 changes: 4 additions & 2 deletions src/pact/v3/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,14 @@

import warnings

from pact.v3.pact import Pact # noqa: F401
from pact.v3.verifier import Verifier # noqa: F401
from pact.v3.pact import Pact
from pact.v3.verifier import Verifier

warnings.warn(
"The `pact.v3` module is not yet stable. Use at your own risk, and expect "
"breaking changes in future releases.",
stacklevel=2,
category=ImportWarning,
)

__all__ = ["Pact", "Verifier"]
Loading
Loading