Skip to content

Commit 87c2061

Browse files
authored
Merge pull request #24 from SciCatProject/integration-test
Add more integration helpers.
2 parents eecc1ad + 89d1d88 commit 87c2061

File tree

4 files changed

+113
-5
lines changed

4 files changed

+113
-5
lines changed

.github/workflows/integration.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,5 @@ jobs:
2727
- run: python -m pip install -e .
2828
- run: docker-compose version
2929
- run: docker-compose -f tests/docker-compose-file-writer.yml up -d
30-
- run: scicat_ingestor -c resources/config.sample.json --verbose
30+
- run: python tests/_scicat_ingestor.py -c resources/config.sample.json --verbose
3131
- run: docker-compose -f tests/docker-compose-file-writer.yml down

src/scicat_ingestor.py

+27-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
# SPDX-License-Identifier: BSD-3-Clause
22
# Copyright (c) 2024 ScicatProject contributors (https://github.com/ScicatProject)
33
import logging
4+
from collections.abc import Generator
5+
from contextlib import contextmanager
46

57
from scicat_configuration import build_main_arg_parser, build_scicat_config
6-
from scicat_kafka import build_consumer
8+
from scicat_kafka import build_consumer, wrdn_messages
79
from scicat_logging import build_logger
810

911

@@ -15,6 +17,22 @@ def quit(logger: logging.Logger, unexpected: bool = True) -> None:
1517
sys.exit(1 if unexpected else 0)
1618

1719

20+
@contextmanager
21+
def exit_at_exceptions(logger: logging.Logger) -> Generator[None, None, None]:
22+
"""Exit the program if an exception is raised."""
23+
try:
24+
yield
25+
except KeyboardInterrupt:
26+
logger.info("Received keyboard interrupt.")
27+
quit(logger, unexpected=False)
28+
except Exception as e:
29+
logger.error("An exception occurred: %s", e)
30+
quit(logger, unexpected=True)
31+
else:
32+
logger.error("Loop finished unexpectedly.")
33+
quit(logger, unexpected=True)
34+
35+
1836
def main() -> None:
1937
"""Main entry point of the app."""
2038
arg_parser = build_main_arg_parser()
@@ -26,6 +44,11 @@ def main() -> None:
2644
logger.info('Starting the Scicat Ingestor with the following configuration:')
2745
logger.info(config.to_dict())
2846

29-
# Kafka consumer
30-
if build_consumer(config.kafka_options, logger) is None:
31-
quit(logger)
47+
with exit_at_exceptions(logger):
48+
# Kafka consumer
49+
if (consumer := build_consumer(config.kafka_options, logger)) is None:
50+
raise RuntimeError("Failed to build the Kafka consumer")
51+
52+
# Receive messages
53+
for message in wrdn_messages(consumer, logger):
54+
logger.info("Processing message: %s", message)

src/scicat_kafka.py

+61
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
# SPDX-License-Identifier: BSD-3-Clause
22
# Copyright (c) 2024 ScicatProject contributors (https://github.com/ScicatProject)
33
import logging
4+
from collections.abc import Generator
45

56
from confluent_kafka import Consumer
7+
from streaming_data_types import deserialise_wrdn
8+
from streaming_data_types.finished_writing_wrdn import (
9+
FILE_IDENTIFIER as WRDN_FILE_IDENTIFIER,
10+
)
11+
from streaming_data_types.finished_writing_wrdn import WritingFinished
612

713
from scicat_configuration import kafkaOptions
814

@@ -66,3 +72,58 @@ def validate_consumer(consumer: Consumer, logger: logging.Logger) -> bool:
6672
else:
6773
logger.info("Kafka consumer successfully instantiated")
6874
return True
75+
76+
77+
def _validate_data_type(message_content: bytes, logger: logging.Logger) -> bool:
78+
logger.info("Data type: %s", (data_type := message_content[4:8]))
79+
if data_type == WRDN_FILE_IDENTIFIER:
80+
logger.info("WRDN message received.")
81+
return True
82+
else:
83+
logger.error("Unexpected data type: %s", data_type)
84+
return False
85+
86+
87+
def _filter_error_encountered(
88+
wrdn_content: WritingFinished, logger: logging.Logger
89+
) -> WritingFinished | None:
90+
"""Filter out messages with the ``error_encountered`` flag set to True."""
91+
if wrdn_content.error_encountered:
92+
logger.error(
93+
"``error_encountered`` flag True. "
94+
"Unable to deserialize message. Skipping the message."
95+
)
96+
return wrdn_content
97+
else:
98+
return None
99+
100+
101+
def _deserialise_wrdn(
102+
message_content: bytes, logger: logging.Logger
103+
) -> WritingFinished | None:
104+
if _validate_data_type(message_content, logger):
105+
logger.info("Deserialising WRDN message")
106+
wrdn_content: WritingFinished = deserialise_wrdn(message_content)
107+
logger.info("Deserialised WRDN message: %.5000s", wrdn_content)
108+
return _filter_error_encountered(wrdn_content, logger)
109+
110+
111+
def wrdn_messages(
112+
consumer: Consumer, logger: logging.Logger
113+
) -> Generator[WritingFinished | None, None, None]:
114+
"""Wait for a WRDN message and yield it.
115+
116+
Yield ``None`` if no message is received or an error is encountered.
117+
"""
118+
while True:
119+
# The decision to proceed or stop will be done by the caller.
120+
message = consumer.poll(timeout=1.0)
121+
if message is None:
122+
logger.info("Received no messages")
123+
yield None
124+
elif message.error():
125+
logger.error("Consumer error: %s", message.error())
126+
yield None
127+
else:
128+
logger.info("Received message.")
129+
yield _deserialise_wrdn(message.value(), logger)

tests/_scicat_ingestor.py

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Entry point for integration test.
2+
# All system arguments are passed to the ``scicat_ingestor``.
3+
4+
5+
if __name__ == "__main__":
6+
import signal
7+
import subprocess
8+
import sys
9+
from time import sleep
10+
11+
# Run the main function in a subprocess
12+
process = subprocess.Popen(
13+
[
14+
"scicat_ingestor",
15+
*(sys.argv[1:] or ["--verbose", "-c", "resources/config.sample.json"]),
16+
]
17+
)
18+
19+
# Send a SIGINT signal to the process after 5 seconds
20+
sleep(5)
21+
process.send_signal(signal.SIGINT)
22+
23+
# Kill the process after 5 more seconds
24+
sleep(5)

0 commit comments

Comments
 (0)