graphics.py 48 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214
  1. #!/usr/bin/env python
  2. # License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
  3. import os
  4. import random
  5. import tempfile
  6. import time
  7. import unittest
  8. import zlib
  9. from contextlib import suppress
  10. from dataclasses import dataclass
  11. from io import BytesIO
  12. from kitty.fast_data_types import base64_decode, base64_encode, has_avx2, has_sse4_2, load_png_data, shm_unlink, shm_write, test_xor64
  13. from . import BaseTest, parse_bytes
  14. try:
  15. from PIL import Image
  16. except ImportError:
  17. Image = None
  18. def send_command(screen, cmd, payload=b''):
  19. cmd = '\033_G' + cmd
  20. if payload:
  21. if isinstance(payload, str):
  22. payload = payload.encode('utf-8')
  23. payload = base64_encode(payload).decode('ascii')
  24. cmd += ';' + payload
  25. cmd += '\033\\'
  26. c = screen.callbacks
  27. c.clear()
  28. parse_bytes(screen, cmd.encode('ascii'))
  29. return c.wtcbuf
  30. def parse_response(res):
  31. if not res:
  32. return
  33. return res.decode('ascii').partition(';')[2].partition('\033')[0]
  34. def parse_response_with_ids(res):
  35. if not res:
  36. return
  37. a, b = res.decode('ascii').split(';', 1)
  38. code = b.partition('\033')[0].split(':', 1)[0]
  39. a = a.split('G', 1)[1]
  40. return code, a
  41. @dataclass(frozen=True)
  42. class Response:
  43. code: str = 'OK'
  44. msg: str = ''
  45. image_id: int = 0
  46. image_number: int = 0
  47. frame_number: int = 0
  48. def parse_full_response(res):
  49. if not res:
  50. return
  51. a, b = res.decode('ascii').split(';', 1)
  52. code = b.partition('\033')[0].split(':', 1)
  53. if len(code) == 1:
  54. code = code[0]
  55. msg = ''
  56. else:
  57. code, msg = code
  58. a = a.split('G', 1)[1]
  59. ans = {'code': code, 'msg': msg}
  60. for x in a.split(','):
  61. k, _, v = x.partition('=')
  62. ans[{'i': 'image_id', 'I': 'image_number', 'r': 'frame_number'}[k]] = int(v)
  63. return Response(**ans)
  64. all_bytes = bytes(bytearray(range(256)))
  65. def byte_block(sz):
  66. d, m = divmod(sz, len(all_bytes))
  67. return (all_bytes * d) + all_bytes[:m]
  68. def load_helpers(self):
  69. s = self.create_screen()
  70. g = s.grman
  71. def pl(payload, **kw):
  72. kw.setdefault('i', 1)
  73. cmd = ','.join(f'{k}={v}' for k, v in kw.items())
  74. res = send_command(s, cmd, payload)
  75. return parse_response(res)
  76. def sl(payload, **kw):
  77. if isinstance(payload, str):
  78. payload = payload.encode('utf-8')
  79. data = kw.pop('expecting_data', payload)
  80. cid = kw.setdefault('i', 1)
  81. self.ae('OK', pl(payload, **kw))
  82. img = g.image_for_client_id(cid)
  83. self.assertIsNotNone(img, f'No image with id {cid} found')
  84. self.ae(img['client_id'], cid)
  85. self.ae(img['data'], data)
  86. if 's' in kw:
  87. self.ae((kw['s'], kw['v']), (img['width'], img['height']))
  88. self.ae(img['is_4byte_aligned'], kw.get('f') != 24)
  89. return img
  90. return s, g, pl, sl
  91. def put_helpers(self, cw, ch, cols=10, lines=5):
  92. iid = 0
  93. def create_screen():
  94. s = self.create_screen(cols, lines, cell_width=cw, cell_height=ch)
  95. return s, 2 / s.columns, 2 / s.lines
  96. def put_cmd(
  97. z=0, num_cols=0, num_lines=0, x_off=0, y_off=0, width=0, height=0, cell_x_off=0,
  98. cell_y_off=0, placement_id=0, cursor_movement=0, unicode_placeholder=0, parent_id=0,
  99. parent_placement_id=0, offset_from_parent_x=0, offset_from_parent_y=0,
  100. ):
  101. return (
  102. f'z={z},c={num_cols},r={num_lines},x={x_off},y={y_off},w={width},h={height},'
  103. f'X={cell_x_off},Y={cell_y_off},p={placement_id},C={cursor_movement},'
  104. f'U={unicode_placeholder},P={parent_id},Q={parent_placement_id},'
  105. f'H={offset_from_parent_x},V={offset_from_parent_y}'
  106. )
  107. def put_image(screen, w, h, **kw):
  108. nonlocal iid
  109. iid += 1
  110. imgid = kw.pop('id', None) or iid
  111. no_id = kw.pop('no_id', False)
  112. if no_id:
  113. cmd = 'a=T,f=24,s=%d,v=%d,%s' % (w, h, put_cmd(**kw))
  114. else:
  115. cmd = 'a=T,f=24,i=%d,s=%d,v=%d,%s' % (imgid, w, h, put_cmd(**kw))
  116. data = b'x' * w * h * 3
  117. res = send_command(screen, cmd, data)
  118. return imgid, parse_response(res)
  119. def put_ref(screen, **kw):
  120. imgid = kw.pop('id', None) or iid
  121. cmd = 'a=p,i=%d,%s' % (imgid, put_cmd(**kw))
  122. return imgid, parse_response_with_ids(send_command(screen, cmd))
  123. def layers(screen, scrolled_by=0, xstart=-1, ystart=1):
  124. return screen.grman.update_layers(scrolled_by, xstart, ystart, dx, dy, screen.columns, screen.lines, cw, ch)
  125. def rect_eq(r, left, top, right, bottom):
  126. for side in 'left top right bottom'.split():
  127. a, b = r[side], locals()[side]
  128. if abs(a - b) > 0.0001:
  129. self.ae(a, b, 'the %s side is not equal' % side)
  130. s, dx, dy = create_screen()
  131. return s, dx, dy, put_image, put_ref, layers, rect_eq
  132. def make_send_command(screen):
  133. def li(payload='abcdefghijkl'*3, s=4, v=3, f=24, a='f', i=1, **kw):
  134. if s:
  135. kw['s'] = s
  136. if v:
  137. kw['v'] = v
  138. if f:
  139. kw['f'] = f
  140. if i:
  141. kw['i'] = i
  142. kw['a'] = a
  143. cmd = ','.join(f'{k}={v}' for k, v in kw.items())
  144. res = send_command(screen, cmd, payload)
  145. return parse_full_response(res)
  146. return li
  147. class TestGraphics(BaseTest):
  148. def test_xor_data(self):
  149. base_data = b'\x01' * 64
  150. key = b'\x02' * 64
  151. sizes = []
  152. if has_sse4_2:
  153. sizes.append(2)
  154. if has_avx2:
  155. sizes.append(3)
  156. sizes.append(0)
  157. def t(key, data, align_offset=0):
  158. expected = test_xor64(key, data, 1, 0)
  159. for which_function in sizes:
  160. actual = test_xor64(key, data, which_function, align_offset)
  161. self.ae(expected, actual, f'{align_offset=} {len(data)=}')
  162. t(key, b'')
  163. for base in (b'abc', base_data):
  164. for extra in range(len(base_data)):
  165. for align_offset in range(64):
  166. data = base + base_data[:extra]
  167. t(key, data, align_offset)
  168. def test_disk_cache(self):
  169. s = self.create_screen()
  170. dc = s.grman.disk_cache
  171. dc.small_hole_threshold = 0
  172. data = {}
  173. def key_as_bytes(key):
  174. if isinstance(key, int):
  175. key = str(key)
  176. if isinstance(key, str):
  177. key = key.encode('utf-8')
  178. return bytes(key)
  179. def add(key, val):
  180. bkey = key_as_bytes(key)
  181. data[key] = key_as_bytes(val)
  182. dc.add(bkey, data[key])
  183. def remove(key):
  184. bkey = key_as_bytes(key)
  185. data.pop(key, None)
  186. return dc.remove(bkey)
  187. def check_data():
  188. for key, val in data.items():
  189. self.ae(dc.get(key_as_bytes(key)), val)
  190. def reset(small_hole_threshold=0):
  191. nonlocal dc, data, s
  192. s = self.create_screen()
  193. dc = s.grman.disk_cache
  194. dc.small_hole_threshold = small_hole_threshold
  195. data = {}
  196. for i in range(25):
  197. self.assertIsNone(add(i, f'{i}' * i))
  198. self.assertEqual(dc.total_size, sum(map(len, data.values())))
  199. self.assertTrue(dc.wait_for_write())
  200. check_data()
  201. sz = dc.size_on_disk()
  202. self.assertEqual(sz, sum(map(len, data.values())))
  203. for x in (2, 4, 6, 8):
  204. remove(x)
  205. check_data()
  206. self.assertRaises(KeyError, dc.get, key_as_bytes(x))
  207. self.assertEqual(sz, dc.size_on_disk())
  208. self.assertEqual(sz, dc.size_on_disk())
  209. for x in ('xy', 'C'*4, 'B'*6, 'A'*8):
  210. add(x, x)
  211. self.assertTrue(dc.wait_for_write())
  212. self.assertEqual(sz, dc.size_on_disk())
  213. check_data()
  214. check_data()
  215. dc.clear()
  216. st = time.monotonic()
  217. while dc.size_on_disk() and time.monotonic() - st < 2:
  218. time.sleep(0.001)
  219. self.assertEqual(dc.size_on_disk(), 0)
  220. data.clear()
  221. for i in range(25):
  222. self.assertIsNone(add(i, f'{i}' * i))
  223. dc.wait_for_write()
  224. check_data()
  225. before = dc.size_on_disk()
  226. while dc.total_size > before // 3:
  227. key = random.choice(tuple(data))
  228. self.assertTrue(remove(key))
  229. check_data()
  230. add('trigger defrag', 'XXX')
  231. dc.wait_for_write()
  232. self.assertLess(dc.size_on_disk(), before)
  233. check_data()
  234. dc.clear()
  235. st = time.monotonic()
  236. while dc.size_on_disk() and time.monotonic() - st < 20:
  237. time.sleep(0.01)
  238. self.assertEqual(dc.size_on_disk(), 0)
  239. for frame in range(32):
  240. add(f'1:{frame}', f'{frame:02d}' * 8)
  241. dc.wait_for_write()
  242. self.assertEqual(dc.size_on_disk(), 32 * 16)
  243. self.assertEqual(dc.num_cached_in_ram(), 0)
  244. num_in_ram = 0
  245. for frame in range(32):
  246. dc.get(key_as_bytes(f'1:{frame}'))
  247. self.assertEqual(dc.num_cached_in_ram(), num_in_ram)
  248. for frame in range(32):
  249. dc.get(key_as_bytes(f'1:{frame}'), True)
  250. num_in_ram += 1
  251. self.assertEqual(dc.num_cached_in_ram(), num_in_ram)
  252. def clear_predicate(key):
  253. return key.startswith(b'1:')
  254. dc.remove_from_ram(clear_predicate)
  255. self.assertEqual(dc.num_cached_in_ram(), 0)
  256. reset(512)
  257. self.assertIsNone(add(1, '1' * 1024))
  258. self.assertIsNone(add(2, '2' * 1024))
  259. dc.wait_for_write()
  260. sz = dc.size_on_disk()
  261. remove(1)
  262. self.ae(sz, dc.size_on_disk())
  263. self.assertIsNone(add(3, '3' * 800))
  264. dc.wait_for_write()
  265. self.ae(sz, dc.size_on_disk())
  266. self.assertIsNone(add(4, '4' * 100))
  267. sz += 100
  268. dc.wait_for_write()
  269. self.ae(sz, dc.size_on_disk())
  270. check_data()
  271. remove(4)
  272. self.assertIsNone(add(5, '5' * 10))
  273. sz += 10
  274. dc.wait_for_write()
  275. self.ae(sz, dc.size_on_disk())
  276. def test_suppressing_gr_command_responses(self):
  277. s, g, pl, sl = load_helpers(self)
  278. self.ae(pl('abcd', s=10, v=10, q=1), 'ENODATA:Insufficient image data: 4 < 400')
  279. self.ae(pl('abcd', s=10, v=10, q=2), None)
  280. self.assertIsNone(pl('abcd', s=1, v=1, a='q', q=1))
  281. # Test chunked load
  282. self.assertIsNone(pl('abcd', s=2, v=2, m=1, q=1))
  283. self.assertIsNone(pl('efgh', m=1))
  284. self.assertIsNone(pl('ijkl', m=1))
  285. self.assertIsNone(pl('mnop', m=0))
  286. # errors
  287. self.assertIsNone(pl('abcd', s=2, v=2, m=1, q=1))
  288. self.ae(pl('mnop', m=0), 'ENODATA:Insufficient image data: 8 < 16')
  289. self.assertIsNone(pl('abcd', s=2, v=2, m=1, q=2))
  290. self.assertIsNone(pl('mnop', m=0))
  291. # frames
  292. s = self.create_screen()
  293. li = make_send_command(s)
  294. self.assertEqual(li().code, 'ENOENT')
  295. self.assertIsNone(li(q=2))
  296. self.assertIsNone(li(a='t', q=1))
  297. self.assertIsNone(li(payload='2' * 12, z=77, m=1, q=1))
  298. self.assertIsNone(li(payload='2' * 12, m=1))
  299. self.assertIsNone(li(payload='2' * 12))
  300. self.assertIsNone(li(payload='2' * 12, z=77, m=1, q=1))
  301. self.ae(li(payload='2' * 12).code, 'ENODATA')
  302. self.assertIsNone(li(payload='2' * 12, z=77, m=1, q=2))
  303. self.assertIsNone(li(payload='2' * 12))
  304. def test_load_images(self):
  305. s, g, pl, sl = load_helpers(self)
  306. self.assertEqual(g.disk_cache.total_size, 0)
  307. # Test load query
  308. self.ae(pl('abcd', s=1, v=1, a='q'), 'OK')
  309. self.ae(g.image_count, 0)
  310. # Test simple load
  311. for f in 32, 24:
  312. p = 'abc' + ('d' if f == 32 else '')
  313. img = sl(p, s=1, v=1, f=f)
  314. self.ae(bool(img['is_4byte_aligned']), f == 32)
  315. # Test chunked load
  316. self.assertIsNone(pl('abcd', s=2, v=2, m=1))
  317. self.assertIsNone(pl('efgh', m=1))
  318. self.assertIsNone(pl('ijkl', m=1))
  319. self.ae(pl('mnop', m=0), 'OK')
  320. img = g.image_for_client_id(1)
  321. self.ae(img['data'], b'abcdefghijklmnop')
  322. random_data = byte_block(32 * 1024)
  323. sl(
  324. random_data,
  325. s=1024,
  326. v=8,
  327. expecting_data=random_data
  328. )
  329. # Test compression
  330. compressed_random_data = zlib.compress(random_data)
  331. sl(
  332. compressed_random_data,
  333. s=1024,
  334. v=8,
  335. o='z',
  336. expecting_data=random_data
  337. )
  338. # Test chunked + compressed
  339. b = len(compressed_random_data) // 2
  340. self.assertIsNone(pl(compressed_random_data[:b], s=1024, v=8, o='z', m=1))
  341. self.ae(pl(compressed_random_data[b:], m=0), 'OK')
  342. img = g.image_for_client_id(1)
  343. self.ae(img['data'], random_data)
  344. # Test loading from file
  345. def load_temp(prefix='tty-graphics-protocol-'):
  346. f = tempfile.NamedTemporaryFile(prefix=prefix)
  347. f.write(random_data), f.flush()
  348. sl(f.name, s=1024, v=8, t='f', expecting_data=random_data)
  349. self.assertTrue(os.path.exists(f.name))
  350. f.seek(0), f.truncate(), f.write(compressed_random_data), f.flush()
  351. sl(f.name, s=1024, v=8, t='t', o='z', expecting_data=random_data)
  352. return f
  353. f = load_temp()
  354. self.assertFalse(os.path.exists(f.name), f'Temp file at {f.name} was not deleted')
  355. with suppress(FileNotFoundError):
  356. f.close()
  357. f = load_temp('')
  358. self.assertTrue(os.path.exists(f.name), f'Temp file at {f.name} was deleted')
  359. f.close()
  360. # Test loading from POSIX SHM
  361. name = '/kitty-test-shm'
  362. shm_write(name, random_data)
  363. sl(name, s=1024, v=8, t='s', expecting_data=random_data)
  364. self.assertRaises(
  365. FileNotFoundError, shm_unlink, name
  366. ) # check that file was deleted
  367. s.reset()
  368. self.assertEqual(g.disk_cache.total_size, 0)
  369. @unittest.skipIf(Image is None, 'PIL not available, skipping PNG tests')
  370. def test_load_png(self):
  371. s, g, pl, sl = load_helpers(self)
  372. w, h = 5, 3
  373. rgba_data = byte_block(w * h * 4)
  374. img = Image.frombytes('RGBA', (w, h), rgba_data)
  375. rgb_data = img.convert('RGB').convert('RGBA').tobytes()
  376. self.assertEqual(g.disk_cache.total_size, 0)
  377. def png(mode='RGBA'):
  378. buf = BytesIO()
  379. i = img
  380. if mode != i.mode:
  381. i = img.convert(mode)
  382. i.save(buf, 'PNG')
  383. return buf.getvalue()
  384. for mode in 'RGBA RGB'.split():
  385. data = png(mode)
  386. sl(data, f=100, expecting_data=rgb_data if mode == 'RGB' else rgba_data)
  387. for m in 'LP':
  388. img = img.convert(m)
  389. rgba_data = img.convert('RGBA').tobytes()
  390. data = png(m)
  391. sl(data, f=100, expecting_data=rgba_data)
  392. self.ae(pl(b'a' * 20, f=100, S=20).partition(':')[0], 'EBADPNG')
  393. s.reset()
  394. self.assertEqual(g.disk_cache.total_size, 0)
  395. def test_load_png_simple(self):
  396. # 1x1 transparent PNG
  397. png_data = base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P+/HgAFhAJ/wlseKgAAAABJRU5ErkJggg==')
  398. expected = b'\x00\xff\xff\x7f'
  399. self.ae(load_png_data(png_data), (expected, 1, 1))
  400. s, g, pl, sl = load_helpers(self)
  401. sl(png_data, f=100, expecting_data=expected)
  402. # test error handling for loading bad png data
  403. self.assertRaisesRegex(ValueError, '[EBADPNG]', load_png_data, b'dsfsdfsfsfd')
  404. def test_gr_operations_with_numbers(self):
  405. s = self.create_screen()
  406. g = s.grman
  407. self.assertEqual(g.disk_cache.total_size, 0)
  408. def li(payload, **kw):
  409. cmd = ','.join(f'{k}={v}' for k, v in kw.items())
  410. res = send_command(s, cmd, payload)
  411. return parse_response_with_ids(res)
  412. code, ids = li('abc', s=1, v=1, f=24, I=1, i=3)
  413. self.ae(code, 'EINVAL')
  414. code, ids = li('abc', s=1, v=1, f=24, I=1)
  415. self.ae((code, ids), ('OK', 'i=1,I=1'))
  416. img = g.image_for_client_number(1)
  417. self.ae(img['client_number'], 1)
  418. self.ae(img['client_id'], 1)
  419. code, ids = li('abc', s=1, v=1, f=24, I=1)
  420. self.ae((code, ids), ('OK', 'i=2,I=1'))
  421. img = g.image_for_client_number(1)
  422. self.ae(img['client_number'], 1)
  423. self.ae(img['client_id'], 2)
  424. code, ids = li('abc', s=1, v=1, f=24, I=1)
  425. self.ae((code, ids), ('OK', 'i=3,I=1'))
  426. code, ids = li('abc', s=1, v=1, f=24, i=5)
  427. self.ae((code, ids), ('OK', 'i=5'))
  428. code, ids = li('abc', s=1, v=1, f=24, I=3)
  429. self.ae((code, ids), ('OK', 'i=4,I=3'))
  430. # Test chunked load with number
  431. self.assertIsNone(li('abcd', s=2, v=2, m=1, I=93))
  432. self.assertIsNone(li('efgh', m=1))
  433. self.assertIsNone(li('ijkx', m=1))
  434. self.ae(li('mnop', m=0), ('OK', 'i=6,I=93'))
  435. img = g.image_for_client_number(93)
  436. self.ae(img['data'], b'abcdefghijkxmnop')
  437. self.ae(img['client_id'], 6)
  438. # test put with number
  439. def put(**kw):
  440. cmd = ','.join(f'{k}={v}' for k, v in kw.items())
  441. cmd = 'a=p,' + cmd
  442. return parse_response_with_ids(send_command(s, cmd))
  443. code, idstr = put(c=2, r=2, I=93)
  444. self.ae((code, idstr), ('OK', 'i=6,I=93'))
  445. code, idstr = put(c=2, r=2, I=94)
  446. self.ae(code, 'ENOENT')
  447. # test delete with number
  448. def delete(ac='N', **kw):
  449. cmd = 'a=d'
  450. if ac:
  451. cmd += f',d={ac}'
  452. if kw:
  453. cmd += ',' + ','.join(f'{k}={v}' for k, v in kw.items())
  454. send_command(s, cmd)
  455. count = s.grman.image_count
  456. put(i=1), put(i=2), put(i=3), put(i=4), put(i=5)
  457. delete(I=94)
  458. self.ae(s.grman.image_count, count)
  459. delete(I=93)
  460. self.ae(s.grman.image_count, count - 1)
  461. delete(I=1)
  462. self.ae(s.grman.image_count, count - 2)
  463. s.reset()
  464. self.assertEqual(g.disk_cache.total_size, 0)
  465. def test_image_put(self):
  466. cw, ch = 10, 20
  467. s, dx, dy, put_image, put_ref, layers, rect_eq = put_helpers(self, cw, ch)
  468. self.ae(put_image(s, cw, ch)[1], 'OK')
  469. l0 = layers(s)
  470. self.ae(len(l0), 1)
  471. rect_eq(l0[0]['src_rect'], 0, 0, 1, 1)
  472. rect_eq(l0[0]['dest_rect'], -1, 1, -1 + dx, 1 - dy)
  473. self.ae(l0[0]['group_count'], 1)
  474. self.ae(s.cursor.x, 1), self.ae(s.cursor.y, 0)
  475. src_width, src_height = 3, 5
  476. iid, (code, idstr) = put_ref(s, num_cols=s.columns, num_lines=1, x_off=2, y_off=1, width=src_width, height=src_height,
  477. cell_x_off=3, cell_y_off=1, z=-1, placement_id=17)
  478. self.ae(idstr, f'i={iid},p=17')
  479. l2 = layers(s)
  480. self.ae(len(l2), 2)
  481. self.ae(l2[1], l0[0])
  482. rect_eq(l2[0]['src_rect'], 2 / 10, 1 / 20, (2 + 3) / 10, (1 + 5)/20)
  483. self.ae(l2[0]['group_count'], 2)
  484. left, top = -1 + dx + 3 * dx / cw, 1 - 1 * dy / ch
  485. right = -1 + (1 + s.columns) * dx
  486. bottom = 1 - dy
  487. rect_eq(l2[0]['dest_rect'], left, top, right, bottom)
  488. self.ae(s.cursor.x, 0), self.ae(s.cursor.y, 1)
  489. self.ae(put_image(s, 10, 20, cursor_movement=1)[1], 'OK')
  490. self.ae(s.cursor.x, 0), self.ae(s.cursor.y, 1)
  491. s.reset()
  492. self.assertEqual(s.grman.disk_cache.total_size, 0)
  493. self.ae(put_image(s, 2*cw, 2*ch, num_cols=3)[1], 'OK')
  494. self.ae((s.cursor.x, s.cursor.y), (3, 2))
  495. rect_eq(layers(s)[0]['dest_rect'], -1, 1, -1 + 3 * dx, 1 - 3*dy)
  496. def test_image_layer_grouping(self):
  497. cw, ch = 10, 20
  498. s, dx, dy, put_image, put_ref, layers, rect_eq = put_helpers(self, cw, ch)
  499. def group_counts():
  500. return tuple(x['group_count'] for x in layers(s))
  501. self.ae(put_image(s, 10, 20, id=1)[1], 'OK')
  502. self.ae(group_counts(), (1,))
  503. put_ref(s, id=1, num_cols=2, num_lines=1, placement_id=2)
  504. put_ref(s, id=1, num_cols=2, num_lines=1, placement_id=3, z=-2)
  505. put_ref(s, id=1, num_cols=2, num_lines=1, placement_id=4, z=-2)
  506. self.ae(group_counts(), (4, 3, 2, 1))
  507. self.ae(put_image(s, 8, 16, id=2, z=-1)[1], 'OK')
  508. self.ae(group_counts(), (2, 1, 1, 2, 1))
  509. def test_image_parents(self):
  510. cw, ch = 10, 20
  511. iw, ih = 10, 20
  512. s, dx, dy, put_image, put_ref, layers, rect_eq = put_helpers(self, cw, ch)
  513. def positions():
  514. ans = {}
  515. def x(x):
  516. return round(((x + 1)/2) * s.columns)
  517. def y(y):
  518. return int(((-y + 1)/2) * s.lines)
  519. for i in layers(s):
  520. d = i['dest_rect']
  521. ans[(i['image_id'], i['ref_id'])] = {'x': x(d['left']), 'y': y(d['top'])}
  522. return ans
  523. def p(x, y=0):
  524. return {'x':x, 'y': y}
  525. self.ae(put_image(s, iw, ih, id=1)[1], 'OK')
  526. self.ae(put_ref(s, id=1, placement_id=1), (1, ('OK', 'i=1,p=1')))
  527. pos = {(1, 1): p(0), (1, 2): p(1)}
  528. self.ae(positions(), pos)
  529. # check that adding a reference to a non-existent parent fails
  530. self.ae(put_ref(s, id=1, placement_id=33, parent_id=1, parent_placement_id=2), (1, ('ENOPARENT', 'i=1,p=33')))
  531. self.ae(put_ref(s, id=1, placement_id=33, parent_id=33), (1, ('ENOPARENT', 'i=1,p=33')))
  532. # check that we cannot add a reference that is its own parent
  533. self.ae(put_ref(s, id=1, placement_id=1, parent_id=1, parent_placement_id=1), (1, ('EINVAL', 'i=1,p=1')))
  534. self.ae(put_image(s, iw, ih, id=2)[1], 'OK')
  535. pos[(2,1)] = p(2)
  536. self.ae(positions(), pos)
  537. # Add two children to the first placement of img2
  538. before = s.cursor.x, s.cursor.y
  539. self.ae(put_ref(s, id=1, placement_id=2, parent_id=2, offset_from_parent_y=3), (1, ('OK', 'i=1,p=2')))
  540. self.ae(before, (s.cursor.x, s.cursor.y), 'Cursor must not move for child image')
  541. pos[(1,3)] = p(2, 3)
  542. self.ae(positions(), pos)
  543. self.ae(put_ref(s, id=2, placement_id=3, parent_id=2, offset_from_parent_y=4), (2, ('OK', 'i=2,p=3')))
  544. pos[(2,2)] = p(2, 4)
  545. self.ae(positions(), pos)
  546. # Add a grand child to the second child of img2
  547. self.ae(put_ref(s, id=2, placement_id=4, parent_id=2, parent_placement_id=3, offset_from_parent_x=-1), (2, ('OK', 'i=2,p=4')))
  548. pos[(2,3)] = p(pos[(2,2)]['x']-1, pos[(2,2)]['y'])
  549. self.ae(positions(), pos)
  550. # Check that creating a cycle is prevented
  551. self.ae(put_ref(s, id=2, placement_id=3, parent_id=2, parent_placement_id=4), (2, ('ECYCLE', 'i=2,p=3')))
  552. self.ae(positions(), pos)
  553. # Check that depth is limited
  554. for i in range(5, 12):
  555. q = put_ref(s, id=2, placement_id=i, parent_id=2, parent_placement_id=i-1, offset_from_parent_x=-1)[1][0]
  556. if q == 'ETOODEEP':
  557. break
  558. self.ae(q, 'OK')
  559. else:
  560. self.assertTrue(False, 'Failed to limit reference chain depth')
  561. # Check that deleting a parent removes all descendants
  562. send_command(s, 'a=d,d=i,i=2,p=3')
  563. pos.pop((2,3)), pos.pop((2,2))
  564. self.ae(positions(), pos)
  565. # Check that deleting a parent deletes all descendants and also removes
  566. # images with no remaining placements
  567. self.ae(put_ref(s, id=2, placement_id=3, parent_id=2, offset_from_parent_y=4), (2, ('OK', 'i=2,p=3')))
  568. pos[(2,11)] = p(2, 4)
  569. self.ae(positions(), pos)
  570. self.ae(put_image(s, iw, ih, id=3, placement_id=97, parent_id=2, parent_placement_id=3)[1], 'OK')
  571. pos[(3,1)] = p(2, 4)
  572. self.ae(positions(), pos)
  573. send_command(s, 'a=d,d=i,i=2')
  574. pos.pop((3,1)), pos.pop((2,11)), pos.pop((2,1)), pos.pop((1,3))
  575. self.ae(positions(), pos)
  576. # Check that virtual placements that try to be relative are rejected
  577. self.ae(put_ref(s, id=1, placement_id=11, parent_id=1, unicode_placeholder=1), (1, ('EINVAL', 'i=1,p=11')))
  578. # Check creation of children of a unicode placeholder based image
  579. s, dx, dy, put_image, put_ref, layers, rect_eq = put_helpers(self, cw, ch)
  580. put_image(s, 20, 20, num_cols=4, num_lines=2, unicode_placeholder=1, id=42)
  581. s.update_only_line_graphics_data()
  582. self.assertFalse(positions()) # the reference is virtual
  583. self.ae(put_ref(s, id=42, placement_id=11, parent_id=42, offset_from_parent_y=2, offset_from_parent_x=1), (42, ('OK', 'i=42,p=11')))
  584. self.assertFalse(positions()) # the reference is virtual without any cell images so the child is invisible
  585. s.apply_sgr("38;5;42")
  586. # These two characters will become one 2x1 ref.
  587. s.cursor.x = s.cursor.y = 1
  588. s.draw("\U0010EEEE\u0305\u0305\U0010EEEE\u0305\u030D")
  589. s.cursor.x = s.cursor.y = 0
  590. s.draw("\U0010EEEE\u0305\u0305\U0010EEEE\u0305\u030D")
  591. s.update_only_line_graphics_data()
  592. pos = {(1, 2): p(1, 2), (1, 3): p(0), (1, 4): p(1)}
  593. self.ae(positions(), pos)
  594. s.cursor.x = s.cursor.y = 0
  595. s.erase_in_display(0, False)
  596. s.update_only_line_graphics_data()
  597. self.assertFalse(positions()) # the reference is virtual without any cell images so the child is invisible
  598. s.cursor.x = s.cursor.y = 2
  599. s.draw("\U0010EEEE\u0305\u0305\U0010EEEE\u0305\u030D")
  600. s.update_only_line_graphics_data()
  601. self.ae(positions(), {(1, 5): {'x': 2, 'y': 2}, (1, 2): {'x': 3, 'y': 4}})
  602. def test_unicode_placeholders(self):
  603. # This test tests basic image placement using using unicode placeholders
  604. cw, ch = 10, 20
  605. s, dx, dy, put_image, put_ref, layers, rect_eq = put_helpers(self, cw, ch)
  606. # Upload two images.
  607. put_image(s, 20, 20, num_cols=4, num_lines=2, unicode_placeholder=1, id=42)
  608. put_image(s, 10, 20, num_cols=4, num_lines=2, unicode_placeholder=1, id=(42<<16) + (43<<8) + 44)
  609. # The references are virtual, so no visible refs yet.
  610. s.update_only_line_graphics_data()
  611. refs = layers(s)
  612. self.ae(len(refs), 0)
  613. # A reminder of row/column diacritics meaning (assuming 0-based):
  614. # \u0305 -> 0
  615. # \u030D -> 1
  616. # \u030E -> 2
  617. # \u0310 -> 3
  618. # Now print the placeholders for the first image.
  619. # Encode the id as an 8-bit color.
  620. s.apply_sgr("38;5;42")
  621. # These two characters will become one 2x1 ref.
  622. s.draw("\U0010EEEE\u0305\u0305\U0010EEEE\u0305\u030D")
  623. # These two characters will be two separate refs (not contiguous).
  624. s.draw("\U0010EEEE\u0305\u0305\U0010EEEE\u0305\u030E")
  625. s.cursor_back(4)
  626. s.update_only_line_graphics_data()
  627. refs = layers(s)
  628. self.ae(len(refs), 3)
  629. self.ae(refs[0]['src_rect'], {'left': 0.0, 'top': 0.0, 'right': 0.5, 'bottom': 0.5})
  630. self.ae(refs[1]['src_rect'], {'left': 0.0, 'top': 0.0, 'right': 0.25, 'bottom': 0.5})
  631. self.ae(refs[2]['src_rect'], {'left': 0.5, 'top': 0.0, 'right': 0.75, 'bottom': 0.5})
  632. # Erase the line.
  633. s.erase_in_line(2)
  634. # There must be 0 refs after the line is erased.
  635. s.update_only_line_graphics_data()
  636. refs = layers(s)
  637. self.ae(len(refs), 0)
  638. # Now test encoding IDs with the 24-bit color.
  639. # The first image, 1x1
  640. s.apply_sgr("38;2;0;0;42")
  641. s.draw("\U0010EEEE\u0305\u0305")
  642. # The second image, 2x1
  643. s.apply_sgr("38;2;42;43;44")
  644. s.draw("\U0010EEEE\u0305\u030D\U0010EEEE\u0305\u030E")
  645. s.cursor_back(2)
  646. s.update_only_line_graphics_data()
  647. refs = layers(s)
  648. self.ae(len(refs), 2)
  649. self.ae(refs[0]['src_rect'], {'left': 0.0, 'top': 0.0, 'right': 0.25, 'bottom': 0.5})
  650. # The second ref spans the whole widths of the second image because it's
  651. # fit to height and centered in a 4x2 box (specified in put_image).
  652. self.ae(refs[1]['src_rect'], {'left': 0.0, 'top': 0.0, 'right': 1.0, 'bottom': 0.5})
  653. # Erase the line.
  654. s.erase_in_line(2)
  655. # Now test implicit column numbers.
  656. # We will mix implicit and explicit column/row specifications, but they
  657. # will be combine into just two references.
  658. s.apply_sgr("38;5;42")
  659. # full row 0 of the first image
  660. s.draw("\U0010EEEE\u0305\u0305\U0010EEEE\u0305\U0010EEEE\U0010EEEE\u0305")
  661. # full row 1 of the first image
  662. s.draw("\U0010EEEE\u030D\U0010EEEE\U0010EEEE\U0010EEEE\u030D\u0310")
  663. s.cursor_back(8)
  664. s.update_only_line_graphics_data()
  665. refs = layers(s)
  666. self.ae(len(refs), 2)
  667. self.ae(refs[0]['src_rect'], {'left': 0.0, 'top': 0.0, 'right': 1.0, 'bottom': 0.5})
  668. self.ae(refs[1]['src_rect'], {'left': 0.0, 'top': 0.5, 'right': 1.0, 'bottom': 1.0})
  669. # Now reset the screen, the images should be erased.
  670. s.reset()
  671. refs = layers(s)
  672. self.ae(len(refs), 0)
  673. def test_unicode_placeholders_3rd_combining_char(self):
  674. # This test tests that we can use the 3rd diacritic for the most
  675. # significant byte
  676. cw, ch = 10, 20
  677. s, dx, dy, put_image, put_ref, layers, rect_eq = put_helpers(self, cw, ch)
  678. # Upload two images.
  679. put_image(s, 20, 20, num_cols=4, num_lines=2, unicode_placeholder=1, id=42)
  680. put_image(s, 20, 10, num_cols=4, num_lines=1, unicode_placeholder=1, id=(42 << 24) + 43)
  681. # This one will have id=43, which does not exist.
  682. s.apply_sgr("38;2;0;0;43")
  683. s.draw("\U0010EEEE\u0305\U0010EEEE\U0010EEEE\U0010EEEE")
  684. s.cursor_back(4)
  685. s.update_only_line_graphics_data()
  686. refs = layers(s)
  687. self.ae(len(refs), 0)
  688. s.erase_in_line(2)
  689. # This one will have id=42. We explicitly specify that the most
  690. # significant byte is 0 (third \u305). Specifying the zero byte like
  691. # this is not necessary but is correct.
  692. s.apply_sgr("38;2;0;0;42")
  693. s.draw("\U0010EEEE\u0305\u0305\u0305\U0010EEEE\u0305\u030D\u0305")
  694. # This is the second image.
  695. # \u059C -> 42
  696. s.apply_sgr("38;2;0;0;43")
  697. s.draw("\U0010EEEE\u0305\u0305\u059C\U0010EEEE\u0305\u030D\u059C")
  698. # Check that we can continue by using implicit row/column specification.
  699. s.draw("\U0010EEEE\u0305\U0010EEEE")
  700. s.cursor_back(6)
  701. s.update_only_line_graphics_data()
  702. refs = layers(s)
  703. self.ae(len(refs), 2)
  704. self.ae(refs[0]['src_rect'], {'left': 0.0, 'top': 0.0, 'right': 0.5, 'bottom': 0.5})
  705. self.ae(refs[1]['src_rect'], {'left': 0.0, 'top': 0.0, 'right': 1.0, 'bottom': 1.0})
  706. s.erase_in_line(2)
  707. # Now test the 8-bit color mode. Using the third diacritic, we can
  708. # specify 16 bits: the most significant byte and the least significant
  709. # byte.
  710. s.apply_sgr("38;5;42")
  711. s.draw("\U0010EEEE\u0305\u0305\u0305\U0010EEEE")
  712. s.apply_sgr("38;5;43")
  713. s.draw("\U0010EEEE\u0305\u0305\u059C\U0010EEEE\U0010EEEE\u0305\U0010EEEE")
  714. s.cursor_back(6)
  715. s.update_only_line_graphics_data()
  716. refs = layers(s)
  717. self.ae(len(refs), 2)
  718. self.ae(refs[0]['src_rect'], {'left': 0.0, 'top': 0.0, 'right': 0.5, 'bottom': 0.5})
  719. self.ae(refs[1]['src_rect'], {'left': 0.0, 'top': 0.0, 'right': 1.0, 'bottom': 1.0})
  720. def test_unicode_placeholders_multiple_placements(self):
  721. # Here we test placement specification via underline color.
  722. cw, ch = 10, 20
  723. s, dx, dy, put_image, put_ref, layers, rect_eq = put_helpers(self, cw, ch)
  724. put_image(s, 20, 20, num_cols=1, num_lines=1, placement_id=1, unicode_placeholder=1, id=42)
  725. put_ref(s, id=42, num_cols=2, num_lines=1, placement_id=22, unicode_placeholder=1)
  726. put_ref(s, id=42, num_cols=4, num_lines=2, placement_id=44, unicode_placeholder=1)
  727. # The references are virtual, so no visible refs yet.
  728. s.update_only_line_graphics_data()
  729. refs = layers(s)
  730. self.ae(len(refs), 0)
  731. # Draw the first row of each placement.
  732. s.apply_sgr("38;5;42")
  733. s.apply_sgr("58;5;1")
  734. s.draw("\U0010EEEE\u0305")
  735. s.apply_sgr("58;5;22")
  736. s.draw("\U0010EEEE\u0305\U0010EEEE\u0305")
  737. s.apply_sgr("58;5;44")
  738. s.draw("\U0010EEEE\u0305\U0010EEEE\u0305\U0010EEEE\u0305\U0010EEEE\u0305")
  739. s.update_only_line_graphics_data()
  740. refs = layers(s)
  741. self.ae(len(refs), 3)
  742. self.ae(refs[0]['src_rect'], {'left': 0.0, 'top': 0.0, 'right': 1.0, 'bottom': 1.5})
  743. self.ae(refs[1]['src_rect'], {'left': 0.0, 'top': 0.0, 'right': 1.0, 'bottom': 1.0})
  744. self.ae(refs[2]['src_rect'], {'left': 0.0, 'top': 0.0, 'right': 1.0, 'bottom': 0.5})
  745. def test_unicode_placeholders_scroll(self):
  746. # Here we test scrolling of a region. We'll draw an image spanning 8
  747. # rows and then scroll only the middle part of this image. Each
  748. # reference corresponds to one row.
  749. cw, ch = 5, 10
  750. s, dx, dy, put_image, put_ref, layers, rect_eq = put_helpers(self, cw, ch, lines=8)
  751. put_image(s, 5, 80, num_cols=1, num_lines=8, unicode_placeholder=1, id=42)
  752. s.apply_sgr("38;5;42")
  753. s.cursor_position(1, 0)
  754. s.draw("\U0010EEEE\u0305\n")
  755. s.cursor_position(2, 0)
  756. s.draw("\U0010EEEE\u030D\n")
  757. s.cursor_position(3, 0)
  758. s.draw("\U0010EEEE\u030E\n")
  759. s.cursor_position(4, 0)
  760. s.draw("\U0010EEEE\u0310\n")
  761. s.cursor_position(5, 0)
  762. s.draw("\U0010EEEE\u0312\n")
  763. s.cursor_position(6, 0)
  764. s.draw("\U0010EEEE\u033D\n")
  765. s.cursor_position(7, 0)
  766. s.draw("\U0010EEEE\u033E\n")
  767. s.cursor_position(8, 0)
  768. s.draw("\U0010EEEE\u033F")
  769. # Each line will contain a part of the image.
  770. s.update_only_line_graphics_data()
  771. refs = layers(s)
  772. refs = sorted(refs, key=lambda r: r['src_rect']['top'])
  773. self.ae(len(refs), 8)
  774. for i in range(8):
  775. self.ae(refs[i]['src_rect'], {'left': 0.0, 'top': 0.125*i, 'right': 1.0, 'bottom': 0.125*(i + 1)})
  776. self.ae(refs[i]['dest_rect']['top'], 1 - 0.25*i)
  777. # Now set margins to lines 3 and 6.
  778. s.set_margins(3, 6) # 1-based indexing
  779. # Scroll two lines down (i.e. move lines 3..6 up).
  780. # Lines 3 and 4 will be erased.
  781. s.cursor_position(6, 0)
  782. s.index()
  783. s.index()
  784. s.update_only_line_graphics_data()
  785. refs = layers(s)
  786. refs = sorted(refs, key=lambda r: r['src_rect']['top'])
  787. self.ae(len(refs), 6)
  788. # Lines 1 and 2 are outside of the region, not scrolled.
  789. self.ae(refs[0]['src_rect'], {'left': 0.0, 'top': 0.0, 'right': 1.0, 'bottom': 0.125})
  790. self.ae(refs[0]['dest_rect']['top'], 1.0)
  791. self.ae(refs[1]['src_rect'], {'left': 0.0, 'top': 0.125*1, 'right': 1.0, 'bottom': 0.125*2})
  792. self.ae(refs[1]['dest_rect']['top'], 1.0 - 0.25*1)
  793. # Lines 3 and 4 are erased.
  794. # Lines 5 and 6 are now higher.
  795. self.ae(refs[2]['src_rect'], {'left': 0.0, 'top': 0.125*4, 'right': 1.0, 'bottom': 0.125*5})
  796. self.ae(refs[2]['dest_rect']['top'], 1.0 - 0.25*2)
  797. self.ae(refs[3]['src_rect'], {'left': 0.0, 'top': 0.125*5, 'right': 1.0, 'bottom': 0.125*6})
  798. self.ae(refs[3]['dest_rect']['top'], 1.0 - 0.25*3)
  799. # Lines 7 and 8 are outside of the region.
  800. self.ae(refs[4]['src_rect'], {'left': 0.0, 'top': 0.125*6, 'right': 1.0, 'bottom': 0.125*7})
  801. self.ae(refs[4]['dest_rect']['top'], 1.0 - 0.25*6)
  802. self.ae(refs[5]['src_rect'], {'left': 0.0, 'top': 0.125*7, 'right': 1.0, 'bottom': 0.125*8})
  803. self.ae(refs[5]['dest_rect']['top'], 1.0 - 0.25*7)
  804. # Now scroll three lines up (i.e. move lines 5..6 down).
  805. # Line 6 will be erased.
  806. s.cursor_position(3, 0)
  807. s.reverse_index()
  808. s.reverse_index()
  809. s.reverse_index()
  810. s.update_only_line_graphics_data()
  811. refs = layers(s)
  812. refs = sorted(refs, key=lambda r: r['src_rect']['top'])
  813. self.ae(len(refs), 5)
  814. # Lines 1 and 2 are outside of the region, not scrolled.
  815. self.ae(refs[0]['src_rect'], {'left': 0.0, 'top': 0.0, 'right': 1.0, 'bottom': 0.125})
  816. self.ae(refs[0]['dest_rect']['top'], 1.0)
  817. self.ae(refs[1]['src_rect'], {'left': 0.0, 'top': 0.125*1, 'right': 1.0, 'bottom': 0.125*2})
  818. self.ae(refs[1]['dest_rect']['top'], 1.0 - 0.25*1)
  819. # Lines 3, 4 and 6 are erased.
  820. # Line 5 is now lower.
  821. self.ae(refs[2]['src_rect'], {'left': 0.0, 'top': 0.125*4, 'right': 1.0, 'bottom': 0.125*5})
  822. self.ae(refs[2]['dest_rect']['top'], 1.0 - 0.25*5)
  823. # Lines 7 and 8 are outside of the region.
  824. self.ae(refs[3]['src_rect'], {'left': 0.0, 'top': 0.125*6, 'right': 1.0, 'bottom': 0.125*7})
  825. self.ae(refs[3]['dest_rect']['top'], 1.0 - 0.25*6)
  826. self.ae(refs[4]['src_rect'], {'left': 0.0, 'top': 0.125*7, 'right': 1.0, 'bottom': 0.125*8})
  827. self.ae(refs[4]['dest_rect']['top'], 1.0 - 0.25*7)
  828. def test_gr_scroll(self):
  829. cw, ch = 10, 20
  830. s, dx, dy, put_image, put_ref, layers, rect_eq = put_helpers(self, cw, ch)
  831. put_image(s, 10, 20, no_id=True) # a one cell image at (0, 0)
  832. self.ae(len(layers(s)), 1)
  833. for i in range(s.lines):
  834. s.index()
  835. self.ae(len(layers(s)), 0), self.ae(s.grman.image_count, 1)
  836. for i in range(s.historybuf.ynum - 1):
  837. s.index()
  838. self.ae(len(layers(s)), 0), self.ae(s.grman.image_count, 1)
  839. s.index()
  840. self.ae(s.grman.image_count, 0)
  841. # Now test with margins
  842. s.reset()
  843. # Test images outside page area untouched
  844. put_image(s, cw, ch) # a one cell image at (0, 0)
  845. for i in range(s.lines - 1):
  846. s.index()
  847. put_image(s, cw, ch) # a one cell image at (0, bottom)
  848. s.set_margins(2, 4) # 1-based indexing
  849. self.ae(s.grman.image_count, 2)
  850. for i in range(s.lines + s.historybuf.ynum):
  851. s.index()
  852. self.ae(s.grman.image_count, 2)
  853. for i in range(s.lines): # ensure cursor is at top margin
  854. s.reverse_index()
  855. # Test clipped scrolling during index
  856. put_image(s, cw, 2*ch, z=-1, no_id=True) # 1x2 cell image
  857. self.ae(s.grman.image_count, 3)
  858. self.ae(layers(s)[0]['src_rect'], {'left': 0.0, 'top': 0.0, 'right': 1.0, 'bottom': 1.0})
  859. s.index(), s.index()
  860. l0 = layers(s)
  861. self.ae(len(l0), 3)
  862. self.ae(layers(s)[0]['src_rect'], {'left': 0.0, 'top': 0.5, 'right': 1.0, 'bottom': 1.0})
  863. s.index()
  864. self.ae(s.grman.image_count, 2)
  865. # Test clipped scrolling during reverse_index
  866. for i in range(s.lines):
  867. s.reverse_index()
  868. put_image(s, cw, 2*ch, z=-1, no_id=True) # 1x2 cell image
  869. self.ae(s.grman.image_count, 3)
  870. self.ae(layers(s)[0]['src_rect'], {'left': 0.0, 'top': 0.0, 'right': 1.0, 'bottom': 1.0})
  871. while s.cursor.y != 1:
  872. s.reverse_index()
  873. s.reverse_index(), s.reverse_index()
  874. self.ae(layers(s)[0]['src_rect'], {'left': 0.0, 'top': 0.0, 'right': 1.0, 'bottom': 0.5})
  875. s.reverse_index()
  876. self.ae(s.grman.image_count, 2)
  877. s.reset()
  878. self.assertEqual(s.grman.disk_cache.total_size, 0)
  879. def test_gr_reset(self):
  880. cw, ch = 10, 20
  881. s, dx, dy, put_image, put_ref, layers, rect_eq = put_helpers(self, cw, ch)
  882. put_image(s, cw, ch) # a one cell image at (0, 0)
  883. self.ae(len(layers(s)), 1)
  884. s.reset()
  885. self.ae(s.grman.image_count, 0)
  886. put_image(s, cw, ch) # a one cell image at (0, 0)
  887. self.ae(s.grman.image_count, 1)
  888. for i in range(s.lines):
  889. s.index()
  890. s.reset()
  891. self.ae(s.grman.image_count, 1)
  892. def test_gr_delete(self):
  893. cw, ch = 10, 20
  894. s, dx, dy, put_image, put_ref, layers, rect_eq = put_helpers(self, cw, ch)
  895. def delete(ac=None, **kw):
  896. cmd = 'a=d'
  897. if ac:
  898. cmd += f',d={ac}'
  899. if kw:
  900. cmd += ',' + ','.join(f'{k}={v}' for k, v in kw.items())
  901. send_command(s, cmd)
  902. put_image(s, cw, ch)
  903. delete()
  904. self.ae(s.grman.image_count, 1)
  905. self.ae(len(layers(s)), 0)
  906. self.ae(s.grman.image_count, 1)
  907. delete('A')
  908. self.ae(s.grman.image_count, 0)
  909. self.assertEqual(s.grman.disk_cache.total_size, 0)
  910. iid = put_image(s, cw, ch)[0]
  911. delete('I', i=iid, p=7)
  912. self.ae(s.grman.image_count, 1)
  913. delete('I', i=iid)
  914. self.ae(s.grman.image_count, 0)
  915. self.assertEqual(s.grman.disk_cache.total_size, 0)
  916. iid = put_image(s, cw, ch, placement_id=9)[0]
  917. delete('I', i=iid, p=9)
  918. self.ae(s.grman.image_count, 0)
  919. self.assertEqual(s.grman.disk_cache.total_size, 0)
  920. s.reset()
  921. put_image(s, cw, ch)
  922. put_image(s, cw, ch)
  923. delete('C')
  924. self.ae(s.grman.image_count, 2)
  925. s.cursor_position(1, 1)
  926. delete('C')
  927. self.ae(s.grman.image_count, 1)
  928. delete('P', x=2, y=1)
  929. self.ae(s.grman.image_count, 0)
  930. self.assertEqual(s.grman.disk_cache.total_size, 0)
  931. put_image(s, cw, ch, z=9)
  932. delete('Z', z=9)
  933. self.ae(s.grman.image_count, 0)
  934. put_image(s, cw, ch, id=1)
  935. put_image(s, cw, ch, id=2)
  936. put_image(s, cw, ch, id=3)
  937. delete('R', y=2)
  938. self.ae(s.grman.image_count, 1)
  939. delete('R', x=3, y=3)
  940. self.ae(s.grman.image_count, 0)
  941. self.assertEqual(s.grman.disk_cache.total_size, 0)
  942. # test put + delete + put
  943. iid = 999999
  944. self.ae(put_image(s, cw, ch, id=iid), (iid, 'OK'))
  945. self.ae(put_ref(s, id=iid), (iid, ('OK', f'i={iid}')))
  946. delete('i', i=iid)
  947. self.ae(s.grman.image_count, 1)
  948. self.ae(put_ref(s, id=iid), (iid, ('OK', f'i={iid}')))
  949. delete('I', i=iid)
  950. self.ae(put_ref(s, id=iid), (iid, ('ENOENT', f'i={iid}')))
  951. self.ae(s.grman.image_count, 0)
  952. self.assertEqual(s.grman.disk_cache.total_size, 0)
  953. def test_animation_frame_loading(self):
  954. s = self.create_screen()
  955. g = s.grman
  956. li = make_send_command(s)
  957. def t(code='OK', image_id=1, frame_number=2, **kw):
  958. res = li(**kw)
  959. if code is not None:
  960. self.assertEqual(code, res.code, f'{code} != {res.code}: {res.msg}')
  961. if image_id is not None:
  962. self.assertEqual(image_id, res.image_id)
  963. if frame_number is not None:
  964. self.assertEqual(frame_number, res.frame_number)
  965. # test error on send frame for non-existent image
  966. self.assertEqual(li().code, 'ENOENT')
  967. # create image
  968. self.assertEqual(li(a='t').code, 'OK')
  969. self.assertEqual(g.disk_cache.total_size, 36)
  970. # simple new frame (width=4, height=3)
  971. self.assertIsNone(li(payload='2' * 12, z=77, m=1))
  972. self.assertIsNone(li(payload='2' * 12, z=77, m=1))
  973. t(payload='2' * 12, z=77)
  974. img = g.image_for_client_id(1)
  975. self.assertEqual(img['extra_frames'], ({'gap': 77, 'id': 2, 'data': b'2' * 36},))
  976. # test editing a frame
  977. t(payload='3' * 36, r=2)
  978. img = g.image_for_client_id(1)
  979. self.assertEqual(img['extra_frames'], ({'gap': 77, 'id': 2, 'data': b'3' * 36},))
  980. # test editing part of a frame
  981. t(payload='4' * 12, r=2, s=2, v=2)
  982. img = g.image_for_client_id(1)
  983. def expand(*rows):
  984. ans = []
  985. for r in rows:
  986. ans.append(''.join(x * 3 for x in str(r)))
  987. return ''.join(ans).encode('ascii')
  988. self.assertEqual(img['extra_frames'], ({'gap': 77, 'id': 2, 'data': expand(4433, 4433, 3333)},))
  989. t(payload='5' * 12, r=2, s=2, v=2, x=1, y=1)
  990. img = g.image_for_client_id(1)
  991. self.assertEqual(img['extra_frames'], ({'gap': 77, 'id': 2, 'data': expand(4433, 4553, 3553)},))
  992. t(payload='3' * 36, r=2)
  993. img = g.image_for_client_id(1)
  994. self.assertEqual(img['extra_frames'], ({'gap': 77, 'id': 2, 'data': b'3' * 36},))
  995. # test loading from previous frame
  996. t(payload='4' * 12, c=2, s=2, v=2, z=101, frame_number=3)
  997. img = g.image_for_client_id(1)
  998. self.assertEqual(img['extra_frames'], (
  999. {'gap': 77, 'id': 2, 'data': b'3' * 36},
  1000. {'gap': 101, 'id': 3, 'data': b'444444333333444444333333333333333333'},
  1001. ))
  1002. # test changing gaps
  1003. img = g.image_for_client_id(1)
  1004. self.assertEqual(img['root_frame_gap'], 0)
  1005. self.assertIsNone(li(a='a', i=1, r=1, z=13))
  1006. img = g.image_for_client_id(1)
  1007. self.assertEqual(img['root_frame_gap'], 13)
  1008. self.assertIsNone(li(a='a', i=1, r=2, z=43))
  1009. img = g.image_for_client_id(1)
  1010. self.assertEqual(img['extra_frames'][0]['gap'], 43)
  1011. # test changing current frame
  1012. img = g.image_for_client_id(1)
  1013. self.assertEqual(img['current_frame_index'], 0)
  1014. self.assertIsNone(li(a='a', i=1, c=2))
  1015. img = g.image_for_client_id(1)
  1016. self.assertEqual(img['current_frame_index'], 1)
  1017. # test delete of frames
  1018. t(payload='5' * 36, frame_number=4)
  1019. img = g.image_for_client_id(1)
  1020. self.assertEqual(img['extra_frames'], (
  1021. {'gap': 43, 'id': 2, 'data': b'3' * 36},
  1022. {'gap': 101, 'id': 3, 'data': b'444444333333444444333333333333333333'},
  1023. {'gap': 40, 'id': 4, 'data': b'5' * 36},
  1024. ))
  1025. self.assertEqual(img['current_frame_index'], 1)
  1026. self.assertIsNone(li(a='d', d='f', i=1, r=1))
  1027. img = g.image_for_client_id(1)
  1028. self.assertEqual(img['current_frame_index'], 0)
  1029. self.assertEqual(img['data'], b'3' * 36)
  1030. self.assertEqual(img['extra_frames'], (
  1031. {'gap': 101, 'id': 3, 'data': b'444444333333444444333333333333333333'},
  1032. {'gap': 40, 'id': 4, 'data': b'5' * 36},
  1033. ))
  1034. self.assertIsNone(li(a='a', i=1, c=3))
  1035. img = g.image_for_client_id(1)
  1036. self.assertEqual(img['current_frame_index'], 2)
  1037. self.assertIsNone(li(a='d', d='f', i=1, r=2))
  1038. img = g.image_for_client_id(1)
  1039. self.assertEqual(img['current_frame_index'], 1)
  1040. self.assertEqual(img['data'], b'3' * 36)
  1041. self.assertEqual(img['extra_frames'], (
  1042. {'gap': 40, 'id': 4, 'data': b'5' * 36},
  1043. ))
  1044. self.assertIsNone(li(a='d', d='f', i=1))
  1045. img = g.image_for_client_id(1)
  1046. self.assertEqual(img['current_frame_index'], 0)
  1047. self.assertEqual(img['data'], b'5' * 36)
  1048. self.assertFalse(img['extra_frames'])
  1049. self.assertIsNone(li(a='d', d='f', i=1))
  1050. img = g.image_for_client_id(1)
  1051. self.assertEqual(img['data'], b'5' * 36)
  1052. self.assertIsNone(li(a='d', d='F', i=1))
  1053. self.ae(g.image_count, 0)
  1054. self.assertEqual(g.disk_cache.total_size, 0)
  1055. # test frame composition
  1056. self.assertEqual(li(a='t').code, 'OK')
  1057. self.assertEqual(g.disk_cache.total_size, 36)
  1058. t(payload='2' * 36)
  1059. t(payload='3' * 36, frame_number=3)
  1060. img = g.image_for_client_id(1)
  1061. self.assertEqual(img['extra_frames'], (
  1062. {'gap': 40, 'id': 2, 'data': b'2' * 36},
  1063. {'gap': 40, 'id': 3, 'data': b'3' * 36},
  1064. ))
  1065. self.assertEqual(li(a='c', i=11).code, 'ENOENT')
  1066. self.assertEqual(li(a='c', i=1, r=1, c=2).code, 'OK')
  1067. img = g.image_for_client_id(1)
  1068. self.assertEqual(img['extra_frames'], (
  1069. {'gap': 40, 'id': 2, 'data': b'abcdefghijkl'*3},
  1070. {'gap': 40, 'id': 3, 'data': b'3' * 36},
  1071. ))
  1072. self.assertEqual(li(a='c', i=1, r=2, c=3, w=1, h=2, x=1, y=1).code, 'OK')
  1073. img = g.image_for_client_id(1)
  1074. self.assertEqual(img['extra_frames'], (
  1075. {'gap': 40, 'id': 2, 'data': b'abcdefghijkl'*3},
  1076. {'gap': 40, 'id': 3, 'data': b'3' * 12 + (b'333abc' + b'3' * 6) * 2},
  1077. ))
  1078. def test_graphics_quota_enforcement(self):
  1079. s = self.create_screen()
  1080. g = s.grman
  1081. g.storage_limit = 36*2
  1082. li = make_send_command(s)
  1083. # test quota for simple images
  1084. self.assertEqual(li(a='T').code, 'OK')
  1085. self.assertEqual(li(a='T', i=2).code, 'OK')
  1086. self.assertEqual(g.disk_cache.total_size, g.storage_limit)
  1087. self.assertEqual(g.image_count, 2)
  1088. self.assertEqual(li(a='T', i=3).code, 'OK')
  1089. self.assertEqual(g.disk_cache.total_size, g.storage_limit)
  1090. self.assertEqual(g.image_count, 2)
  1091. # test quota for frames
  1092. for i in range(8):
  1093. self.assertEqual(li(payload=f'{i}' * 36, i=2).code, 'OK')
  1094. self.assertEqual(li(payload='x' * 36, i=2).code, 'ENOSPC')
  1095. # test editing should not trigger quota
  1096. self.assertEqual(li(payload='4' * 12, r=2, s=2, v=2, i=2).code, 'OK')
  1097. s.reset()
  1098. self.ae(g.image_count, 0)
  1099. self.assertEqual(g.disk_cache.total_size, 0)