164 lines
6.3 KiB
Python
164 lines
6.3 KiB
Python
import json
|
|
import unittest
|
|
|
|
from pi_dictate_service.audio import AudioError, pcm_s16le_to_floats
|
|
from pi_dictate_service.protocol import (
|
|
MAX_ERROR_DETAIL_BYTES,
|
|
MAX_FRAME_BYTES,
|
|
MAX_ID_BYTES,
|
|
MAX_TRANSCRIPT_BYTES,
|
|
PROTOCOL_VERSION,
|
|
FrameDecoder,
|
|
ProtocolError,
|
|
encode_frame,
|
|
parse_request,
|
|
)
|
|
|
|
|
|
class FrameDecoderTests(unittest.TestCase):
|
|
def test_decodes_fragmented_and_multiple_frames(self) -> None:
|
|
decoder = FrameDecoder()
|
|
|
|
self.assertEqual(decoder.feed(f'{{"version":{PROTOCOL_VERSION}'.encode()), [])
|
|
frames = decoder.feed(
|
|
b',"type":"ping","requestId":"one"}\n'
|
|
+ f'{{"version":{PROTOCOL_VERSION},"type":"ping","requestId":"two"}}\n'.encode()
|
|
)
|
|
|
|
self.assertEqual([frame["requestId"] for frame in frames], ["one", "two"])
|
|
|
|
def test_rejects_malformed_json_and_oversized_frames_as_fatal(self) -> None:
|
|
with self.assertRaisesRegex(ProtocolError, "malformed JSON") as malformed:
|
|
FrameDecoder().feed(b'{nope}\n')
|
|
self.assertTrue(malformed.exception.close_connection)
|
|
self.assertEqual(malformed.exception.code, "malformed_json")
|
|
|
|
with self.assertRaises(ProtocolError) as oversized:
|
|
FrameDecoder().feed(b"x" * (MAX_FRAME_BYTES + 1))
|
|
self.assertTrue(oversized.exception.close_connection)
|
|
self.assertEqual(oversized.exception.code, "frame_too_large")
|
|
|
|
|
|
class RequestValidationTests(unittest.TestCase):
|
|
def test_requires_exact_fields_bounded_ids_and_supported_version(self) -> None:
|
|
request = parse_request(
|
|
{"version": PROTOCOL_VERSION, "type": "start", "requestId": "r1", "generationId": "g1"}
|
|
)
|
|
self.assertEqual(request.generation_id, "g1")
|
|
|
|
with self.assertRaises(ProtocolError) as unknown:
|
|
parse_request(
|
|
{
|
|
"version": PROTOCOL_VERSION,
|
|
"type": "start",
|
|
"requestId": "r1",
|
|
"generationId": "g1",
|
|
"path": "/tmp/audio",
|
|
}
|
|
)
|
|
self.assertEqual(unknown.exception.code, "invalid_request")
|
|
self.assertFalse(unknown.exception.close_connection)
|
|
|
|
with self.assertRaises(ProtocolError) as long_id:
|
|
parse_request(
|
|
{
|
|
"version": PROTOCOL_VERSION,
|
|
"type": "ping",
|
|
"requestId": "é" * ((MAX_ID_BYTES // 2) + 1),
|
|
}
|
|
)
|
|
self.assertEqual(long_id.exception.code, "invalid_request")
|
|
|
|
with self.assertRaises(ProtocolError) as unsupported:
|
|
parse_request({"version": PROTOCOL_VERSION + 1, "type": "hello", "requestId": "r2"})
|
|
self.assertEqual(unsupported.exception.code, "unsupported_version")
|
|
self.assertTrue(unsupported.exception.close_connection)
|
|
|
|
def test_rejects_unhashable_types_invalid_unicode_and_huge_integers(self) -> None:
|
|
for request_type in ([], {}):
|
|
with self.subTest(request_type=request_type):
|
|
with self.assertRaises(ProtocolError) as failed:
|
|
parse_request(
|
|
{"version": PROTOCOL_VERSION, "type": request_type, "requestId": "request"}
|
|
)
|
|
self.assertEqual(failed.exception.code, "invalid_request")
|
|
|
|
with self.assertRaises(ProtocolError) as surrogate:
|
|
parse_request({"version": PROTOCOL_VERSION, "type": "ping", "requestId": "\ud800"})
|
|
self.assertEqual(surrogate.exception.request_id, "")
|
|
|
|
huge_integer = b'{"version":' + (b"9" * 5000) + b',"type":"hello","requestId":"r"}\n'
|
|
with self.assertRaises(ProtocolError) as huge:
|
|
FrameDecoder().feed(huge_integer)
|
|
self.assertEqual(huge.exception.code, "malformed_json")
|
|
self.assertTrue(huge.exception.close_connection)
|
|
|
|
def test_error_echoes_only_valid_bounded_ids(self) -> None:
|
|
oversized = "x" * (MAX_ID_BYTES + 1)
|
|
with self.assertRaises(ProtocolError) as unsupported:
|
|
parse_request(
|
|
{"version": PROTOCOL_VERSION + 1, "type": "hello", "requestId": oversized}
|
|
)
|
|
self.assertEqual(unsupported.exception.request_id, "")
|
|
|
|
encoded = encode_frame(
|
|
{
|
|
"version": PROTOCOL_VERSION,
|
|
"type": "error",
|
|
"requestId": oversized,
|
|
"generationId": "\ud800",
|
|
"code": "invalid_request",
|
|
"detail": "bad request",
|
|
}
|
|
)
|
|
payload = json.loads(encoded)
|
|
self.assertEqual(payload["requestId"], "")
|
|
self.assertNotIn("generationId", payload)
|
|
|
|
def test_encoded_transcripts_and_error_details_are_bounded(self) -> None:
|
|
with self.assertRaises(ProtocolError) as transcript:
|
|
encode_frame(
|
|
{
|
|
"version": PROTOCOL_VERSION,
|
|
"type": "result",
|
|
"generationId": "g1",
|
|
"transcript": "é" * ((MAX_TRANSCRIPT_BYTES // 2) + 1),
|
|
}
|
|
)
|
|
self.assertEqual(transcript.exception.code, "transcript_too_large")
|
|
|
|
encoded = encode_frame(
|
|
{
|
|
"version": PROTOCOL_VERSION,
|
|
"type": "error",
|
|
"requestId": "r1",
|
|
"code": "failed",
|
|
"detail": "é" * MAX_ERROR_DETAIL_BYTES,
|
|
}
|
|
)
|
|
payload = json.loads(encoded)
|
|
self.assertLessEqual(len(payload["detail"].encode("utf-8")), MAX_ERROR_DETAIL_BYTES)
|
|
self.assertLessEqual(len(encoded) - 1, MAX_FRAME_BYTES)
|
|
|
|
|
|
class AudioConversionTests(unittest.TestCase):
|
|
def test_converts_little_endian_signed_16_bit_pcm(self) -> None:
|
|
pcm = b"\x00\x80\x00\xc0\x00\x00\x00\x40\xff\x7f"
|
|
|
|
samples = pcm_s16le_to_floats(pcm)
|
|
self.assertEqual(samples.itemsize, 4)
|
|
self.assertEqual(
|
|
list(samples),
|
|
[-1.0, -0.5, 0.0, 0.5, 32767 / 32768.0],
|
|
)
|
|
|
|
def test_rejects_odd_or_overlong_pcm(self) -> None:
|
|
with self.assertRaisesRegex(AudioError, "even"):
|
|
pcm_s16le_to_floats(b"\x00")
|
|
with self.assertRaisesRegex(AudioError, "duration"):
|
|
pcm_s16le_to_floats(b"\x00\x00" * 5, max_bytes=8)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|