Skip to content

Commit 24d3a20

Browse files
authored
Add unittests and CI (#7)
Previously, we did not have any testing. This introduces unittests and configures them to run as CI workflow. Fix #6
1 parent fcaffed commit 24d3a20

File tree

8 files changed

+395
-2
lines changed

8 files changed

+395
-2
lines changed

.github/workflows/ci.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: test
2+
3+
on:
4+
push:
5+
branches:
6+
- '**'
7+
8+
jobs:
9+
build:
10+
runs-on: ${{ matrix.os }}
11+
strategy:
12+
matrix:
13+
os:
14+
- ubuntu-latest
15+
- windows-latest
16+
17+
steps:
18+
- name: Checkout code
19+
uses: actions/checkout@v4
20+
21+
- name: Set up Python
22+
uses: actions/setup-python@v4
23+
with:
24+
python-version: "3.10"
25+
architecture: x64
26+
27+
- name: Install Python dependencies
28+
run: pip install -r requirements.txt
29+
30+
- name: Run Python Tests
31+
run: python -m unittest discover

config.ini.default

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[SERVICE]
22
endpoint=http://127.0.0.1
33
port=8000
4-
equivalence_table_file=./data/equivalence_table.json
4+
equivalence_table_file=./resources/equivalence_table.json
55

66
[RESOLVER]
77
endpoint=http://semantic_id_resolver
File renamed without changes.

semantic_matcher/service.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ def get_matches(
112112
definition=request_body.definition
113113
)
114114
url = f"{remote_matching_service}/get_matches"
115-
new_matches_response = requests.get(url, json=remote_matching_request.dict())
115+
new_matches_response = requests.get(url, json=remote_matching_request.model_dump_json())
116116
match_response = service_model.MatchesList.model_validate_json(new_matches_response.text)
117117
additional_remote_matches.extend(match_response.matches)
118118
# Finally, put all matches together and return

test/__init__.py

Whitespace-only changes.

test/test_semantic_matcher.py

Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
import os
2+
import configparser
3+
import multiprocessing
4+
5+
import requests
6+
import unittest
7+
8+
from fastapi import FastAPI
9+
import uvicorn
10+
11+
from semantic_matcher import model
12+
from semantic_matcher.model import SemanticMatch
13+
from semantic_matcher.service import SemanticMatchingService
14+
15+
from contextlib import contextmanager
16+
import signal
17+
import time
18+
19+
import json as js
20+
21+
22+
def run_server():
23+
# Load test configuration
24+
config = configparser.ConfigParser()
25+
config.read([
26+
os.path.abspath(os.path.join(os.path.dirname(__file__), "../test_resources/config.ini")),
27+
])
28+
29+
# Read in equivalence table
30+
EQUIVALENCES = model.EquivalenceTable.from_file(
31+
filename=os.path.abspath(os.path.join(
32+
os.path.dirname(__file__),
33+
"..",
34+
config["SERVICE"]["equivalence_table_file"]
35+
))
36+
)
37+
38+
# Initialise SemanticMatchingService
39+
semantic_matching_service = SemanticMatchingService(
40+
endpoint=config["SERVICE"]["endpoint"],
41+
equivalences=EQUIVALENCES
42+
)
43+
44+
# Mock resolver
45+
def mock_get_matcher(self, semantic_id):
46+
return "http://remote-service:8000"
47+
48+
SemanticMatchingService._get_matcher_from_semantic_id = mock_get_matcher
49+
50+
# Mock remote service
51+
original_requests_get = requests.get
52+
53+
class SimpleResponse:
54+
def __init__(self, content: str, status_code: int = 200):
55+
self.text = content
56+
self.status_code = status_code
57+
58+
def mock_requests_get(url, json):
59+
if url == "http://remote-service:8000/get_matches":
60+
match_one = SemanticMatch(
61+
base_semantic_id="s-heppner.com/semanticID/three",
62+
match_semantic_id="remote-service.com/semanticID/tres",
63+
score=1.0,
64+
meta_information={"matchSource": "Defined by Moritz Sommer",
65+
"path": ["remote-service.com/semanticID/trois"]}
66+
)
67+
matches_data = {
68+
"matches": [match_one.model_dump()]
69+
}
70+
matches_json = js.dumps(matches_data)
71+
return SimpleResponse(content=matches_json)
72+
else:
73+
return original_requests_get(url, json=json)
74+
75+
requests.get = mock_requests_get
76+
77+
# Run server
78+
app = FastAPI()
79+
app.include_router(semantic_matching_service.router)
80+
uvicorn.run(app, host=config["SERVICE"]["ENDPOINT"], port=int(config["SERVICE"]["PORT"]), log_level="error")
81+
82+
83+
@contextmanager
84+
def run_server_context():
85+
server_process = multiprocessing.Process(target=run_server)
86+
server_process.start()
87+
try:
88+
time.sleep(2) # Wait for the server to start
89+
yield
90+
finally:
91+
server_process.terminate()
92+
server_process.join(timeout=5)
93+
if server_process.is_alive():
94+
os.kill(server_process.pid, signal.SIGKILL)
95+
server_process.join()
96+
97+
98+
class TestSemanticMatchingService(unittest.TestCase):
99+
100+
def test_get_all_matches(self):
101+
with run_server_context():
102+
response = requests.get("http://localhost:8000/all_matches")
103+
expected_matches = {
104+
's-heppner.com/semanticID/one': [
105+
{
106+
'base_semantic_id': 's-heppner.com/semanticID/one',
107+
'match_semantic_id': 's-heppner.com/semanticID/1',
108+
'score': 1.0,
109+
'meta_information': {'matchSource': 'Defined by Sebastian Heppner'}
110+
},
111+
{
112+
'base_semantic_id': 's-heppner.com/semanticID/one',
113+
'match_semantic_id': 's-heppner.com/semanticID/two',
114+
'score': 0.8,
115+
'meta_information': {'matchSource': 'Defined by Sebastian Heppner'}
116+
}
117+
],
118+
's-heppner.com/semanticID/two': [
119+
{
120+
'base_semantic_id': 's-heppner.com/semanticID/two',
121+
'match_semantic_id': 's-heppner.com/semanticID/2',
122+
'score': 1.0,
123+
'meta_information': {'matchSource': 'Defined by Sebastian Heppner'}
124+
}
125+
],
126+
's-heppner.com/semanticID/three': [
127+
{
128+
'base_semantic_id': 's-heppner.com/semanticID/three',
129+
'match_semantic_id': 'remote-service.com/semanticID/trois',
130+
'score': 1.0,
131+
'meta_information': {'matchSource': 'Defined by Moritz Sommer'}
132+
}
133+
]
134+
}
135+
actual_matches = response.json()
136+
self.assertEqual(expected_matches, actual_matches)
137+
138+
def test_post_matches(self):
139+
with run_server_context():
140+
new_match = {
141+
"base_semantic_id": "s-heppner.com/semanticID/new",
142+
"match_semantic_id": "s-heppner.com/semanticID/3",
143+
"score": 0.95,
144+
"meta_information": {"matchSource": "Defined by UnitTest"}
145+
}
146+
matches_list = {
147+
"matches": [new_match]
148+
}
149+
requests.post(
150+
"http://localhost:8000/post_matches",
151+
json=matches_list
152+
)
153+
response = requests.get("http://localhost:8000/all_matches")
154+
actual_matches = response.json()
155+
self.assertIn("s-heppner.com/semanticID/new", actual_matches)
156+
self.assertEqual(
157+
actual_matches["s-heppner.com/semanticID/new"][0]["match_semantic_id"],
158+
"s-heppner.com/semanticID/3"
159+
)
160+
161+
self.assertEqual(
162+
actual_matches["s-heppner.com/semanticID/new"][0]["score"],
163+
0.95
164+
)
165+
166+
self.assertEqual(
167+
actual_matches["s-heppner.com/semanticID/new"][0]["meta_information"]["matchSource"],
168+
"Defined by UnitTest"
169+
)
170+
171+
def test_get_matches_local_only(self):
172+
with run_server_context():
173+
match_request = {
174+
"semantic_id": "s-heppner.com/semanticID/one",
175+
"score_limit": 0.5,
176+
"local_only": True
177+
}
178+
response = requests.get("http://localhost:8000/get_matches", json=match_request)
179+
expected_matches = {
180+
"matches": [
181+
{
182+
"base_semantic_id": "s-heppner.com/semanticID/one",
183+
"match_semantic_id": "s-heppner.com/semanticID/1",
184+
"score": 1.0,
185+
"meta_information": {"matchSource": "Defined by Sebastian Heppner"}
186+
},
187+
{
188+
"base_semantic_id": "s-heppner.com/semanticID/one",
189+
"match_semantic_id": "s-heppner.com/semanticID/two",
190+
"score": 0.8,
191+
"meta_information": {"matchSource": "Defined by Sebastian Heppner"}
192+
},
193+
{
194+
"base_semantic_id": "s-heppner.com/semanticID/one",
195+
"match_semantic_id": "s-heppner.com/semanticID/2",
196+
"score": 0.8,
197+
"meta_information": {"matchSource": "Defined by Sebastian Heppner",
198+
"path": ["s-heppner.com/semanticID/two"]}
199+
}
200+
]
201+
}
202+
actual_matches = response.json()
203+
self.assertEqual(expected_matches, actual_matches)
204+
205+
def test_get_matches_local_and_remote(self):
206+
with run_server_context():
207+
match_request = {
208+
"semantic_id": "s-heppner.com/semanticID/three",
209+
"score_limit": 0.7,
210+
"local_only": False
211+
}
212+
response = requests.get("http://localhost:8000/get_matches", json=match_request)
213+
expected_matches = {
214+
"matches": [
215+
{
216+
"base_semantic_id": "s-heppner.com/semanticID/three",
217+
"match_semantic_id": "remote-service.com/semanticID/trois",
218+
"score": 1.0,
219+
"meta_information": {"matchSource": "Defined by Moritz Sommer"}
220+
},
221+
{
222+
"base_semantic_id": "s-heppner.com/semanticID/three",
223+
"match_semantic_id": "remote-service.com/semanticID/tres",
224+
"score": 1.0,
225+
"meta_information": {"matchSource": "Defined by Moritz Sommer",
226+
"path": ["remote-service.com/semanticID/trois"]}
227+
},
228+
]
229+
}
230+
actual_matches = response.json()
231+
self.assertEqual(expected_matches, actual_matches)
232+
233+
def test_get_matches_no_matches(self):
234+
with run_server_context():
235+
match_request = {
236+
"semantic_id": "s-heppner.com/semanticID/unknown",
237+
"score_limit": 0.5,
238+
"local_only": True
239+
}
240+
response = requests.get("http://localhost:8000/get_matches", json=match_request)
241+
expected_matches = {"matches": []}
242+
actual_matches = response.json()
243+
self.assertEqual(expected_matches, actual_matches)
244+
245+
def test_get_matches_with_low_score_limit(self):
246+
with run_server_context():
247+
match_request = {
248+
"semantic_id": "s-heppner.com/semanticID/one",
249+
"score_limit": 0.9,
250+
"local_only": True
251+
}
252+
response = requests.get("http://localhost:8000/get_matches", json=match_request)
253+
expected_matches = {
254+
"matches": [
255+
{
256+
"base_semantic_id": "s-heppner.com/semanticID/one",
257+
"match_semantic_id": "s-heppner.com/semanticID/1",
258+
"score": 1.0,
259+
"meta_information": {"matchSource": "Defined by Sebastian Heppner"}
260+
}
261+
]
262+
}
263+
actual_matches = response.json()
264+
self.assertEqual(expected_matches, actual_matches)
265+
266+
def test_get_matches_with_nlp_parameters(self):
267+
with run_server_context():
268+
match_request = {
269+
"semantic_id": "s-heppner.com/semanticID/one",
270+
"score_limit": 0.5,
271+
"local_only": True,
272+
"name": "Example Name",
273+
"definition": "Example Definition"
274+
}
275+
response = requests.get("http://localhost:8000/get_matches", json=match_request)
276+
expected_matches = {
277+
"matches": [
278+
{
279+
"base_semantic_id": "s-heppner.com/semanticID/one",
280+
"match_semantic_id": "s-heppner.com/semanticID/1",
281+
"score": 1.0,
282+
"meta_information": {"matchSource": "Defined by Sebastian Heppner"}
283+
},
284+
{
285+
"base_semantic_id": "s-heppner.com/semanticID/one",
286+
"match_semantic_id": "s-heppner.com/semanticID/two",
287+
"score": 0.8,
288+
"meta_information": {"matchSource": "Defined by Sebastian Heppner"}
289+
},
290+
{
291+
"base_semantic_id": "s-heppner.com/semanticID/one",
292+
"match_semantic_id": "s-heppner.com/semanticID/2",
293+
"score": 0.8,
294+
"meta_information": {"matchSource": "Defined by Sebastian Heppner",
295+
"path": ["s-heppner.com/semanticID/two"]}
296+
}
297+
]
298+
}
299+
actual_matches = response.json()
300+
self.assertEqual(expected_matches, actual_matches)
301+
302+
def test_remove_all_matches(self):
303+
with run_server_context():
304+
requests.post("http://localhost:8000/clear")
305+
response = requests.get("http://localhost:8000/all_matches")
306+
expected_matches = {}
307+
actual_matches = response.json()
308+
self.assertEqual(expected_matches, actual_matches)
309+
310+
311+
if __name__ == '__main__':
312+
unittest.main()

test_resources/config.ini

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[SERVICE]
2+
endpoint=127.0.0.1
3+
port=8000
4+
equivalence_table_file=./test_resources/equivalence_table.json
5+
6+
[RESOLVER]
7+
endpoint=http://semantic_id_resolver
8+
port=8125

0 commit comments

Comments
 (0)