Files
sientia-dataops-scouter_tem…/e2e/pi_web_api_test_server.py

125 lines
3.7 KiB
Python

"""
In-process HTTP server emulating PI Web API streamsets/recorded responses for E2E tests.
"""
from __future__ import annotations
import json
import time
from typing import Any, Literal
from pytest_httpserver import HTTPServer
from werkzeug import Request
from werkzeug.wrappers import Response
PIWebAPIMode = Literal['success', 'empty', 'error', 'timeout']
STREAMSETS_RECORDED_PATH = '/streamsets/recorded'
class PIWebAPITestServer:
"""
Thread-backed PI Web API stub using pytest-httpserver (real HTTP for pycurl clients).
"""
def __init__(self) -> None:
self._httpserver = HTTPServer(host='127.0.0.1', port=0)
self._mode: PIWebAPIMode = 'success'
self._rows: list[dict[str, Any]] = []
self._timeout_sleep_seconds = 60
self.requests: list[Request] = []
@property
def host(self) -> str:
return self._httpserver.host
@property
def port(self) -> int:
return self._httpserver.port
@property
def base_url(self) -> str:
return f'http://{self.host}:{self.port}'
def start(self) -> None:
"""Start the HTTP server and register the streamsets handler."""
self._httpserver.start()
self._register_handler()
def stop(self) -> None:
"""Stop the HTTP server."""
self._httpserver.stop()
def clear(self) -> None:
"""Clear recorded requests and reset mode to success with no rows."""
self.requests.clear()
self._mode = 'success'
self._rows = []
self._httpserver.clear()
self._register_handler()
def set_mode(
self,
mode: PIWebAPIMode,
rows: list[dict[str, Any]] | None = None,
*,
timeout_sleep_seconds: int = 60,
) -> None:
"""
Configure the next responses from the stub server.
Args:
- mode: Response mode (success, empty, error, timeout)
- rows: Optional list of row dicts with keys name, webid, timestamp, value
- timeout_sleep_seconds: Sleep duration for timeout mode (must exceed client timeout)
"""
self._mode = mode
if rows is not None:
self._rows = rows
self._timeout_sleep_seconds = timeout_sleep_seconds
self._register_handler()
def _register_handler(self) -> None:
self._httpserver.expect_request(
STREAMSETS_RECORDED_PATH,
method='GET',
).respond_with_handler(self._handle_streamsets_recorded)
def _handle_streamsets_recorded(self, request: Request):
self.requests.append(request)
if self._mode == 'timeout':
time.sleep(self._timeout_sleep_seconds)
return self._json_response({'Items': []}, status=200)
if self._mode == 'error':
return self._json_response({'error': 'internal'}, status=500)
if self._mode == 'empty' or not self._rows:
return self._json_response({'Items': []}, status=200)
items_by_name: dict[str, list[dict[str, Any]]] = {}
for row in self._rows:
name = row['name']
items_by_name.setdefault(name, []).append(
{
'Timestamp': row['timestamp'],
'Value': row['value'],
'Good': True,
'Questionable': False,
}
)
items = [
{'Name': name, 'Items': points}
for name, points in items_by_name.items()
]
return self._json_response({'Items': items}, status=200)
@staticmethod
def _json_response(payload: dict[str, Any], *, status: int) -> Response:
return Response(
json.dumps(payload),
status=status,
mimetype='application/json',
)