Skip to content

Commit 007710d

Browse files
committed
Update specmatic to 2.27.0
- Add test container setup for contract-testing
1 parent c9d1abd commit 007710d

File tree

3 files changed

+160
-36
lines changed

3 files changed

+160
-36
lines changed

.github/workflows/main.yml

Lines changed: 60 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,45 +2,70 @@ name: CI with contracts run through command line
22

33
on:
44
push:
5-
branches: [ main ]
5+
branches: [main]
66

77
jobs:
88
build:
99
strategy:
1010
matrix:
11-
os: [ ubuntu-latest]
11+
os: [ubuntu-latest]
1212
runs-on: ${{ matrix.os }}
1313
steps:
14-
- uses: actions/checkout@v2
15-
with:
16-
path: main
17-
- name: Set up JRE 17
18-
uses: actions/setup-java@v3
19-
with:
20-
distribution: 'temurin'
21-
java-version: '17'
22-
java-package: 'jre'
23-
- name: Setup python
24-
uses: actions/setup-python@v4
25-
with:
26-
python-version: '3.11'
27-
cache: 'pip'
28-
- name: Run pip install
29-
working-directory: main
30-
run: pip install -r requirements.txt
31-
- name: Run contract as tests with Specmatic Python
32-
working-directory: main
33-
run: coverage run --branch -m pytest tests -v -s --junitxml contract-test-reports/TEST-junit-jupiter.xml
34-
- name: Publish contract test report
35-
uses: mikepenz/action-junit-report@v3
36-
if: always()
37-
with:
38-
report_paths: '**/contract-test-reports/TEST-*.xml'
39-
- name: Generate coverage report
40-
working-directory: main
41-
run: coverage html -d coverage-report
42-
- name: Upload coverage report
43-
uses: actions/upload-artifact@v4
44-
with:
45-
name: coverage-report
46-
path: main/coverage-report
14+
- uses: actions/checkout@v2
15+
16+
- name: Set up JRE 17
17+
uses: actions/setup-java@v3
18+
with:
19+
distribution: "temurin"
20+
java-version: "17"
21+
java-package: "jre"
22+
23+
- name: Setup python
24+
uses: actions/setup-python@v4
25+
with:
26+
python-version: "3.11"
27+
cache: "pip"
28+
29+
- name: Run pip install
30+
run: pip install -r requirements.txt
31+
32+
- name: Run contract as tests with Specmatic Python
33+
run: coverage run --branch -m pytest tests -v -s --junitxml contract-test-reports/TEST-junit-jupiter.xml
34+
35+
- name: Save Specmatic license
36+
if: false
37+
# if: runner.os == 'Linux'
38+
run: |
39+
mkdir -p ~/.specmatic
40+
echo "${{ secrets.SPECMATIC_LICENSE_KEY }}" > ~/.specmatic/specmatic-license.txt
41+
42+
- name: Run Specmatic Insights Build Reporter
43+
if: false
44+
# if: runner.os == 'Linux'
45+
run: |
46+
docker run \
47+
-v ${{ github.workspace }}:/workspace \
48+
-v ~/.specmatic:/root/.specmatic \
49+
-w /workspace \
50+
specmatic/specmatic-reporter \
51+
send-report \
52+
--metadata build_url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} \
53+
--branch-name ${{ github.ref_name }} \
54+
--repo-name ${{ github.event.repository.name }} \
55+
--repo-id ${{ github.repository_id }} \
56+
--repo-url ${{ github.event.repository.html_url }}
57+
58+
- name: Publish contract test report
59+
uses: mikepenz/action-junit-report@v3
60+
if: always()
61+
with:
62+
report_paths: "**/contract-test-reports/TEST-*.xml"
63+
64+
- name: Generate coverage report
65+
run: coverage html -d coverage-report
66+
67+
- name: Upload coverage report
68+
uses: actions/upload-artifact@v4
69+
with:
70+
name: coverage-report
71+
path: coverage-report

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
fastapi>=0.110
22
pytest>=7.4
33
python-dotenv>=1.0
4-
specmatic==2.26.1
4+
specmatic==2.27.0
55
redis==6.4.0
66
testcontainers==4.13.2
77
coverage==7.10.7
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import multiprocessing
2+
import os
3+
import sys
4+
import threading
5+
from pathlib import Path
6+
7+
import pytest
8+
import uvicorn
9+
from testcontainers.core.container import DockerContainer
10+
from testcontainers.core.wait_strategies import HttpWaitStrategy, LogMessageWaitStrategy
11+
12+
APPLICATION_HOST = "0.0.0.0"
13+
APPLICATION_PORT = 8000
14+
HTTP_STUB_PORT = 8080
15+
16+
17+
class UvicornServer(multiprocessing.Process):
18+
def __init__(self, config: uvicorn.Config):
19+
super().__init__()
20+
self.server = uvicorn.Server(config=config)
21+
self.config = config
22+
23+
def stop(self):
24+
self.terminate()
25+
26+
def run(self, *args, **kwargs):
27+
self.server.run()
28+
29+
30+
def stream_container_logs(container: DockerContainer, name=None):
31+
def _stream():
32+
for line in container.get_wrapped_container().logs(stream=True, follow=True):
33+
text = line.decode(errors="ignore").rstrip()
34+
prefix = f"[{name}] " if name else ""
35+
print(f"{prefix}{text}")
36+
37+
thread = threading.Thread(target=_stream, daemon=True)
38+
thread.start()
39+
return thread
40+
41+
42+
@pytest.fixture(scope="module")
43+
def api_service():
44+
config = uvicorn.Config("app.main:app", host=APPLICATION_HOST, port=APPLICATION_PORT, log_level="info")
45+
server = UvicornServer(config)
46+
server.start()
47+
yield server
48+
server.stop()
49+
50+
51+
@pytest.fixture(scope="module")
52+
def stub_container():
53+
examples_path = Path("test/contract/data").resolve()
54+
specmatic_yaml_path = Path("specmatic.yaml").resolve()
55+
build_reports_path = Path("build/reports/specmatic").resolve()
56+
container = (
57+
DockerContainer("specmatic/specmatic")
58+
.with_command(["virtualize", "--examples=examples", f"--port={HTTP_STUB_PORT}"])
59+
.with_bind_ports(HTTP_STUB_PORT, HTTP_STUB_PORT)
60+
.with_volume_mapping(examples_path, "/usr/src/app/examples", mode="ro")
61+
.with_volume_mapping(specmatic_yaml_path, "/usr/src/app/specmatic.yaml", mode="ro")
62+
.with_volume_mapping(build_reports_path, "/usr/src/app/build/reports/specmatic", mode="rw")
63+
.waiting_for(HttpWaitStrategy(HTTP_STUB_PORT, path="/actuator/health").with_method("GET").for_status_code(200))
64+
)
65+
container.start()
66+
stream_container_logs(container, name="specmatic-stub")
67+
yield container
68+
container.stop()
69+
70+
71+
@pytest.fixture(scope="module")
72+
def test_container():
73+
specmatic_yaml_path = Path("specmatic.yaml").resolve()
74+
build_reports_path = Path("build/reports/specmatic").resolve()
75+
container = (
76+
DockerContainer("specmatic/specmatic")
77+
.with_command(["test", "--host=host.docker.internal", f"--port={APPLICATION_PORT}"])
78+
.with_env("SPECMATIC_GENERATIVE_TESTS", "true")
79+
.with_volume_mapping(specmatic_yaml_path, "/usr/src/app/specmatic.yaml", mode="ro")
80+
.with_volume_mapping(build_reports_path, "/usr/src/app/build/reports/specmatic", mode="rw")
81+
.with_kwargs(extra_hosts={"host.docker.internal": "host-gateway"})
82+
.waiting_for(LogMessageWaitStrategy("Tests run:"))
83+
)
84+
container.start()
85+
stream_container_logs(container, name="specmatic-test")
86+
yield container
87+
container.stop()
88+
89+
90+
@pytest.mark.skipif(
91+
os.environ.get("CI") == "true" and not sys.platform.startswith("linux"),
92+
reason="Run only on Linux CI; all platforms allowed locally",
93+
)
94+
def test_contract(api_service, stub_container, test_container):
95+
stdout, stderr = test_container.get_logs()
96+
stdout = stdout.decode("utf-8")
97+
stderr = stderr.decode("utf-8")
98+
if stderr or "Failures: 0" not in stdout:
99+
raise AssertionError(f"Contract tests failed; container logs:\n{stdout}\n{stderr}") # noqa: EM102

0 commit comments

Comments
 (0)