Update project configuration and dependencies - Added .mypy_cache and .cursor to .gitignore. - Changed asyncio_default_fixture_loop_scope and asyncio_default_test_loop_scope to "session" in pyproject.toml. - Updated e2e testing dependencies in requirements-dev.txt, replacing fakeredis and mongomock with pytest-httpserver. - Updated requirements.txt to use sientia_do instead of a specific git commit. - Modified sonar-project.properties to remove a file from coverage exclusions. - Enhanced E2E test fixtures in e2e/conftest.py for better container management. - Cleaned up e2e test files related to CoreScouter and PIWebAPIScouter workflows.
125 lines
3.7 KiB
Python
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',
|
|
)
|