Files
framebridge/source/tests/test_screen_rpc.py
T

128 lines
6.5 KiB
Python

import base64
import hashlib
import json
import os
from pathlib import Path
import tempfile
import unittest
from unittest.mock import patch
from framebridge_expanded.real_screen import screen_rpc as rpc
class ScreenRpcTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.directory = Path(self.temp.name)
self.counter = 0
self.binding = {'backend': 'visible-tmux', 'pane_id': '%1', 'node': 'TEST', 'uid': os.getuid()}
self.actions = []
self.patches = [
patch.object(rpc, 'snapshot_binding', return_value=self.binding),
patch.object(rpc, 'capture_screen', side_effect=self.capture),
patch.object(rpc, 'bind_screen', side_effect=lambda m: {**m, 'binding': self.binding}),
patch.object(rpc, 'read_chunk', side_effect=self.chunk),
patch.object(rpc, 'act', side_effect=self.action),
patch.object(rpc.time, 'sleep'),
]
for p in self.patches:
p.start()
self.addCleanup(p.stop)
def capture(self, directory):
self.counter += 1
capture_id = f'{self.counter:032x}'
data = b'actual-pixel-test-fixture-' + capture_id.encode()
path = directory / (capture_id + '.png')
path.write_bytes(data)
return {'capture_id': capture_id, 'source': 'linux-framebuffer', 'screen_type': 'tty',
'node': 'TEST', 'uid': os.getuid(), 'width': 1920, 'height': 1080,
'path': str(path), 'bytes': len(data), 'sha256': hashlib.sha256(data).hexdigest(),
'active_vt': 'tty1', 'synthetic': False, 'pixel_source_verified': True}
def chunk(self, directory, capture_id, index, **kwargs):
data = (directory / (capture_id + '.png')).read_bytes()
digest = hashlib.sha256(data).hexdigest()
if digest != kwargs['expected_sha256']:
raise rpc.CaptureError('CAPTURE_SHA256_MISMATCH')
return {'capture_id': capture_id, 'index': index, 'bytes': len(data), 'sha256': digest,
'total_chunks': 1, 'encoding': 'base64', 'data': base64.b64encode(data).decode(), 'more': False}
def action(self, manifest, kind, **kwargs):
self.actions.append((manifest['capture_id'], kind, kwargs))
return {'performed': True}
def observe(self, **kwargs):
return rpc.dispatch({'op': 'observe', **kwargs}, self.directory)['result']
def test_observation_uses_capture_pixels_not_terminal_text(self):
m = self.observe(session_id='hidden_terminal')
self.assertTrue(m['full_screen'] and m['authentic_pixels'])
self.assertFalse(m['requested_session_visible'])
self.assertNotIn('path', m)
self.assertTrue(m['control_available'])
self.assertEqual(m['width'], 1920)
def test_hidden_terminal_id_cannot_receive_input_for_visible_image(self):
m = self.observe()
with self.assertRaisesRegex(rpc.ScreenRpcError, 'SESSION_NOT_VISIBLE'):
rpc.dispatch({'op': 'key', 'capture_id': m['capture_id'], 'session_id': 'hidden_terminal', 'key': 'ENTER'}, self.directory)
self.assertEqual(self.actions, [])
def test_input_returns_new_observation_and_cannot_replay_old_capture(self):
m = self.observe()
after = rpc.dispatch({'op': 'text', 'capture_id': m['capture_id'], 'session_id': m['session_id'], 'text': 'visible marker'}, self.directory)['result']
self.assertNotEqual(after['capture_id'], m['capture_id'])
self.assertEqual(len(self.actions), 1)
with self.assertRaisesRegex(rpc.ScreenRpcError, 'CAPTURE_ALREADY_USED'):
rpc.dispatch({'op': 'key', 'capture_id': m['capture_id'], 'key': 'ENTER'}, self.directory)
self.assertEqual(len(self.actions), 1)
def test_unknown_send_result_does_not_replay_input(self):
m = self.observe()
with patch.object(rpc, 'act', side_effect=rpc.ControlError('SCREEN_BINDING_CHANGED')):
with self.assertRaisesRegex(rpc.ScreenRpcError, 'SCREEN_BINDING_CHANGED'):
rpc.dispatch({'op': 'key', 'capture_id': m['capture_id'], 'key': 'ENTER'}, self.directory)
with self.assertRaisesRegex(rpc.ScreenRpcError, 'CAPTURE_ALREADY_USED'):
rpc.dispatch({'op': 'key', 'capture_id': m['capture_id'], 'key': 'ENTER'}, self.directory)
def test_target_change_during_capture_returns_no_bound_image(self):
with patch.object(rpc, 'bind_screen', side_effect=lambda m: {**m, 'binding': {**self.binding, 'pane_id': '%0'}}):
with self.assertRaisesRegex(rpc.ScreenRpcError, 'VISIBLE_TARGET_CHANGED_DURING_CAPTURE'):
self.observe()
self.assertEqual(list(self.directory.glob('*.bound.json')), [])
def test_sent_input_is_reported_if_following_capture_fails(self):
m = self.observe()
with patch.object(rpc, 'capture_screen', side_effect=rpc.CaptureError('ACTIVE_VT_CHANGED')):
with self.assertRaises(rpc.ScreenRpcError) as caught:
rpc.dispatch({'op': 'key', 'capture_id': m['capture_id'], 'key': 'ENTER'}, self.directory)
self.assertEqual(caught.exception.code, 'INPUT_SENT_OBSERVE_FAILED')
self.assertTrue(caught.exception.details['performed'])
self.assertEqual(len(self.actions), 1)
def test_view_can_succeed_without_safe_input_target(self):
with patch.object(rpc, 'snapshot_binding', side_effect=rpc.ControlError('SCREEN_VISIBLE_TMUX_NOT_FOUND')):
m = self.observe()
self.assertFalse(m['control_available'])
with self.assertRaisesRegex(rpc.ScreenRpcError, 'VISIBLE_INPUT_UNAVAILABLE'):
rpc.dispatch({'op': 'key', 'capture_id': m['capture_id'], 'key': 'ENTER'}, self.directory)
self.assertEqual(self.actions, [])
def test_changed_png_cannot_drive_input(self):
m = self.observe()
(self.directory / (m['capture_id'] + '.png')).write_bytes(b'different pixels')
with self.assertRaisesRegex(rpc.CaptureError, 'CAPTURE_SHA256_MISMATCH'):
rpc.dispatch({'op': 'key', 'capture_id': m['capture_id'], 'key': 'ENTER'}, self.directory)
self.assertEqual(self.actions, [])
def test_read_requires_expected_image_hash(self):
m = self.observe()
with self.assertRaisesRegex(rpc.ScreenRpcError, 'CAPTURE_SHA256_MISMATCH'):
rpc.dispatch({'op': 'read_chunk', 'capture_id': m['capture_id'], 'index': 0, 'expected_sha256': '0' * 64}, self.directory)
if __name__ == '__main__':
unittest.main()